@j0hanz/code-review-analyst-mcp 1.5.0 → 1.5.2

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.
@@ -20,7 +20,7 @@ export declare const PRO_THINKING_LEVEL: "high";
20
20
  /** Output cap for Flash API breaking-change detection. */
21
21
  export declare const FLASH_API_BREAKING_MAX_OUTPUT_TOKENS: 4096;
22
22
  /** Output cap for Flash complexity analysis. */
23
- export declare const FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS: 2048;
23
+ export declare const FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS: 4096;
24
24
  /** Output cap for Flash test-plan generation. */
25
25
  export declare const FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS: 8192;
26
26
  /** Output cap for Flash triage tools. */
@@ -28,7 +28,7 @@ const THINKING_LEVELS = {
28
28
  // Thinking budget in tokens for Flash and Pro tools. Note that these are not hard limits, but rather guidelines to encourage concise responses and manage latency/cost.
29
29
  const OUTPUT_TOKEN_BUDGET = {
30
30
  flashApiBreaking: 4_096,
31
- flashComplexity: 2_048,
31
+ flashComplexity: 4_096,
32
32
  flashTestPlan: 8_192,
33
33
  flashTriage: 4_096,
34
34
  proPatch: 8_192,
@@ -18,11 +18,11 @@ export interface ToolContract {
18
18
  maxOutputTokens: number;
19
19
  /**
20
20
  * Sampling temperature for the Gemini call.
21
- * Lower values (0.0–0.1) favour deterministic structured output;
22
- * higher values (0.2) add diversity for creative synthesis tasks.
23
- * Omit to use the global default (0.2).
21
+ * Gemini 3 recommends 1.0 for all tasks.
24
22
  */
25
23
  temperature?: number;
24
+ /** Enables deterministic JSON guidance and schema key ordering. */
25
+ deterministicJson?: boolean;
26
26
  params: readonly ToolParameterContract[];
27
27
  outputShape: string;
28
28
  gotchas: readonly string[];
@@ -53,6 +53,7 @@ export declare const TOOL_CONTRACTS: readonly [{
53
53
  readonly thinkingLevel: "minimal";
54
54
  readonly maxOutputTokens: 4096;
55
55
  readonly temperature: 1;
56
+ readonly deterministicJson: true;
56
57
  readonly params: readonly [{
57
58
  readonly name: "repository";
58
59
  readonly type: "string";
@@ -77,6 +78,7 @@ export declare const TOOL_CONTRACTS: readonly [{
77
78
  readonly thinkingLevel: "minimal";
78
79
  readonly maxOutputTokens: 4096;
79
80
  readonly temperature: 1;
81
+ readonly deterministicJson: true;
80
82
  readonly params: readonly [{
81
83
  readonly name: "repository";
82
84
  readonly type: "string";
@@ -101,6 +103,7 @@ export declare const TOOL_CONTRACTS: readonly [{
101
103
  readonly thinkingLevel: "high";
102
104
  readonly maxOutputTokens: 12288;
103
105
  readonly temperature: 1;
106
+ readonly deterministicJson: true;
104
107
  readonly params: readonly [{
105
108
  readonly name: "repository";
106
109
  readonly type: "string";
@@ -144,6 +147,7 @@ export declare const TOOL_CONTRACTS: readonly [{
144
147
  readonly thinkingLevel: "high";
145
148
  readonly maxOutputTokens: 8192;
146
149
  readonly temperature: 1;
150
+ readonly deterministicJson: true;
147
151
  readonly params: readonly [{
148
152
  readonly name: "findingTitle";
149
153
  readonly type: "string";
@@ -169,6 +173,7 @@ export declare const TOOL_CONTRACTS: readonly [{
169
173
  readonly thinkingLevel: "medium";
170
174
  readonly maxOutputTokens: 8192;
171
175
  readonly temperature: 1;
176
+ readonly deterministicJson: true;
172
177
  readonly params: readonly [{
173
178
  readonly name: "repository";
174
179
  readonly type: "string";
@@ -203,8 +208,9 @@ export declare const TOOL_CONTRACTS: readonly [{
203
208
  readonly model: "gemini-3-flash-preview";
204
209
  readonly timeoutMs: 90000;
205
210
  readonly thinkingLevel: "medium";
206
- readonly maxOutputTokens: 2048;
211
+ readonly maxOutputTokens: 4096;
207
212
  readonly temperature: 1;
213
+ readonly deterministicJson: true;
208
214
  readonly params: readonly [{
209
215
  readonly name: "language";
210
216
  readonly type: "string";
@@ -223,6 +229,7 @@ export declare const TOOL_CONTRACTS: readonly [{
223
229
  readonly thinkingLevel: "minimal";
224
230
  readonly maxOutputTokens: 4096;
225
231
  readonly temperature: 1;
232
+ readonly deterministicJson: true;
226
233
  readonly params: readonly [{
227
234
  readonly name: "language";
228
235
  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',
@@ -55,6 +55,10 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
55
55
  transformResult?: (input: TInput, result: TResult, ctx: ToolExecutionContext) => TFinal;
56
56
  /** Optional validation hook for input parameters. */
57
57
  validateInput?: (input: TInput, ctx: ToolExecutionContext) => Promise<ReturnType<typeof createErrorToolResponse> | undefined> | ReturnType<typeof createErrorToolResponse> | undefined;
58
+ /** Optional flag to enforce diff presence and budget check before tool execution. */
59
+ requiresDiff?: boolean;
60
+ /** Optional override for schema validation retries. Defaults to GEMINI_SCHEMA_RETRIES env var. */
61
+ schemaRetries?: number;
58
62
  /** Optional Gemini model to use (e.g. 'gemini-3-pro-preview'). */
59
63
  model?: string;
60
64
  /** Optional thinking level. */
@@ -65,13 +69,15 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
65
69
  maxOutputTokens?: number;
66
70
  /**
67
71
  * Optional sampling temperature for this tool's Gemini call.
68
- * Lower values (0.0–0.1) favour determinism for structured extraction;
69
- * higher values (0.2) add useful diversity for creative synthesis tasks.
70
- * Falls back to the global default (0.2) when omitted.
72
+ * Gemini 3 recommends 1.0 for all tasks.
71
73
  */
72
74
  temperature?: number;
73
75
  /** Optional opt-in to Gemini thought output. Defaults to false. */
74
76
  includeThoughts?: boolean;
77
+ /** Optional deterministic JSON mode for stricter key ordering and repair prompting. */
78
+ deterministicJson?: boolean;
79
+ /** Optional batch execution mode. Defaults to runtime setting. */
80
+ batchMode?: 'off' | 'inline';
75
81
  /** Optional formatter for human-readable text output. */
76
82
  formatOutput?: (result: TFinal) => string;
77
83
  /** Optional context text used in progress messages. */
@@ -81,6 +87,7 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
81
87
  /** Builds the system instruction and user prompt from parsed tool input. */
82
88
  buildPrompt: (input: TInput, ctx: ToolExecutionContext) => PromptParts;
83
89
  }
90
+ export declare function summarizeSchemaValidationErrorForRetry(errorMessage: string): string;
84
91
  export declare function wrapToolHandler<TInput, TResult extends CallToolResult>(options: {
85
92
  toolName: string;
86
93
  progressContext?: (input: TInput) => string;
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { DefaultOutputSchema } from '../schemas/outputs.js';
3
- import { getDiff } from './diff-store.js';
3
+ import { validateDiffBudget } from './diff-budget.js';
4
+ import { createNoDiffError, getDiff } from './diff-store.js';
5
+ import { createCachedEnvInt } from './env-config.js';
4
6
  import { getErrorMessage, RETRYABLE_UPSTREAM_ERROR_PATTERN } from './errors.js';
5
7
  import { stripJsonSchemaConstraints } from './gemini-schema.js';
6
8
  import { generateStructuredJson, getCurrentRequestId } from './gemini.js';
@@ -13,7 +15,11 @@ const CANCELLED_ERROR_PATTERN = /cancelled|canceled/i;
13
15
  const TIMEOUT_ERROR_PATTERN = /timed out|timeout/i;
14
16
  const BUDGET_ERROR_PATTERN = /exceeds limit|max allowed size|input too large/i;
15
17
  const BUSY_ERROR_PATTERN = /too many concurrent/i;
16
- const MAX_SCHEMA_RETRIES = 1;
18
+ const DEFAULT_SCHEMA_RETRIES = 1;
19
+ const geminiSchemaRetriesConfig = createCachedEnvInt('GEMINI_SCHEMA_RETRIES', DEFAULT_SCHEMA_RETRIES);
20
+ const DEFAULT_SCHEMA_RETRY_ERROR_CHARS = 1_500;
21
+ const schemaRetryErrorCharsConfig = createCachedEnvInt('MAX_SCHEMA_RETRY_ERROR_CHARS', DEFAULT_SCHEMA_RETRY_ERROR_CHARS);
22
+ const DETERMINISTIC_JSON_RETRY_NOTE = 'Deterministic JSON mode: keep key names exactly as schema-defined and preserve stable field ordering.';
17
23
  function createGeminiResponseSchema(config) {
18
24
  const sourceSchema = config.geminiSchema ?? config.resultSchema;
19
25
  return stripJsonSchemaConstraints(z.toJSONSchema(sourceSchema));
@@ -21,6 +27,35 @@ function createGeminiResponseSchema(config) {
21
27
  function parseToolInput(input, fullInputSchema) {
22
28
  return fullInputSchema.parse(input);
23
29
  }
30
+ function extractResponseKeyOrdering(responseSchema) {
31
+ const schemaType = responseSchema.type;
32
+ if (schemaType !== 'object') {
33
+ return undefined;
34
+ }
35
+ const { properties } = responseSchema;
36
+ if (typeof properties !== 'object' || properties === null) {
37
+ return undefined;
38
+ }
39
+ return Object.keys(properties);
40
+ }
41
+ export function summarizeSchemaValidationErrorForRetry(errorMessage) {
42
+ const maxChars = Math.max(200, schemaRetryErrorCharsConfig.get());
43
+ const compact = errorMessage.replace(/\s+/g, ' ').trim();
44
+ if (compact.length <= maxChars) {
45
+ return compact;
46
+ }
47
+ return `${compact.slice(0, maxChars - 3)}...`;
48
+ }
49
+ function createSchemaRetryPrompt(prompt, errorMessage, deterministicJson) {
50
+ const summarizedError = summarizeSchemaValidationErrorForRetry(errorMessage);
51
+ const deterministicNote = deterministicJson
52
+ ? `\n${DETERMINISTIC_JSON_RETRY_NOTE}`
53
+ : '';
54
+ return {
55
+ summarizedError,
56
+ prompt: `${prompt}\n\nCRITICAL: The previous response failed schema validation. Error: ${summarizedError}${deterministicNote}`,
57
+ };
58
+ }
24
59
  function createGenerationRequest(config, promptParts, responseSchema, onLog, signal) {
25
60
  const request = {
26
61
  systemInstruction: promptParts.systemInstruction,
@@ -46,13 +81,23 @@ function createGenerationRequest(config, promptParts, responseSchema, onLog, sig
46
81
  if (config.includeThoughts !== undefined) {
47
82
  request.includeThoughts = config.includeThoughts;
48
83
  }
84
+ if (config.deterministicJson) {
85
+ const responseKeyOrdering = extractResponseKeyOrdering(responseSchema);
86
+ if (responseKeyOrdering !== undefined) {
87
+ request.responseKeyOrdering = responseKeyOrdering;
88
+ }
89
+ }
90
+ if (config.batchMode !== undefined) {
91
+ request.batchMode = config.batchMode;
92
+ }
49
93
  if (signal !== undefined) {
50
94
  request.signal = signal;
51
95
  }
52
96
  return request;
53
97
  }
98
+ const VALIDATION_ERROR_PATTERN = /validation/i;
54
99
  function classifyErrorMeta(error, message) {
55
- if (error instanceof z.ZodError || /validation/i.test(message)) {
100
+ if (error instanceof z.ZodError || VALIDATION_ERROR_PATTERN.test(message)) {
56
101
  return {
57
102
  kind: 'validation',
58
103
  retryable: false,
@@ -91,30 +136,15 @@ function isRetryableUpstreamMessage(message) {
91
136
  return (RETRYABLE_UPSTREAM_ERROR_PATTERN.test(message) ||
92
137
  BUSY_ERROR_PATTERN.test(message));
93
138
  }
94
- async function sendTaskProgress(extra, payload) {
95
- const progressToken = extra._meta?.progressToken;
96
- if (typeof progressToken !== 'string' && typeof progressToken !== 'number') {
139
+ function ignoreProgressInput(value) {
140
+ if (value === null) {
97
141
  return;
98
142
  }
99
- try {
100
- const params = {
101
- progressToken,
102
- progress: payload.current,
103
- };
104
- if (payload.total !== undefined) {
105
- params.total = payload.total;
106
- }
107
- if (payload.message !== undefined) {
108
- params.message = payload.message;
109
- }
110
- await extra.sendNotification({
111
- method: 'notifications/progress',
112
- params,
113
- });
114
- }
115
- catch {
116
- // Progress is best-effort; never fail the tool call.
117
- }
143
+ }
144
+ function sendTaskProgress(extra, payload) {
145
+ ignoreProgressInput(extra);
146
+ ignoreProgressInput(payload);
147
+ return Promise.resolve();
118
148
  }
119
149
  function createProgressReporter(extra) {
120
150
  let lastCurrent = 0;
@@ -152,21 +182,26 @@ function normalizeProgressContext(context) {
152
182
  return `${compact.slice(0, 77)}...`;
153
183
  }
154
184
  function formatProgressStep(toolName, context, metadata) {
155
- const prefix = metadata === 'starting' ? '▸' : '◦';
156
- return `${prefix} ${toolName}: ${context} [${metadata}]`;
185
+ return `${toolName}: ${context} [${metadata}]`;
157
186
  }
158
187
  function friendlyModelName(model) {
159
188
  if (!model)
160
- return 'model';
161
- if (model.includes('pro'))
162
- return 'Pro';
163
- if (model.includes('flash'))
164
- return 'Flash';
165
- return 'model';
189
+ return 'calling model';
190
+ const normalized = model.toLowerCase();
191
+ if (normalized.includes('pro'))
192
+ return 'calling Pro';
193
+ if (normalized.includes('flash'))
194
+ return 'calling Flash';
195
+ return 'calling model';
166
196
  }
167
- function formatProgressCompletion(toolName, context, outcome, success = true) {
168
- const prefix = success ? '◆' : '◇';
169
- return `${prefix} ${toolName}: ${context} • ${outcome}`;
197
+ function formatProgressCompletion(toolName, context, outcome) {
198
+ return `🗒 ${toolName}: ${context} ${outcome}`;
199
+ }
200
+ function createFailureStatusMessage(outcome, errorMessage) {
201
+ if (outcome === 'cancelled') {
202
+ return `cancelled: ${errorMessage}`;
203
+ }
204
+ return errorMessage;
170
205
  }
171
206
  async function reportProgressStepUpdate(reportProgress, toolName, context, current, metadata) {
172
207
  await reportProgress({
@@ -175,13 +210,21 @@ async function reportProgressStepUpdate(reportProgress, toolName, context, curre
175
210
  message: formatProgressStep(toolName, context, metadata),
176
211
  });
177
212
  }
178
- async function reportProgressCompletionUpdate(reportProgress, toolName, context, outcome, success = true) {
213
+ async function reportProgressCompletionUpdate(reportProgress, toolName, context, outcome) {
179
214
  await reportProgress({
180
215
  current: TASK_PROGRESS_TOTAL,
181
216
  total: TASK_PROGRESS_TOTAL,
182
- message: formatProgressCompletion(toolName, context, outcome, success),
217
+ message: formatProgressCompletion(toolName, context, outcome),
183
218
  });
184
219
  }
220
+ async function reportSchemaRetryProgressBestEffort(reportProgress, toolName, context, retryCount, maxRetries) {
221
+ try {
222
+ await reportProgressStepUpdate(reportProgress, toolName, context, 3, `repairing schema retry ${retryCount}/${maxRetries}`);
223
+ }
224
+ catch {
225
+ // Progress updates are best-effort and must not interrupt retries.
226
+ }
227
+ }
185
228
  function toLoggingLevel(level) {
186
229
  switch (level) {
187
230
  case 'debug':
@@ -234,21 +277,23 @@ export function wrapToolHandler(options, handler) {
234
277
  const result = await handler(input, extra);
235
278
  // End progress (1/1)
236
279
  const outcome = result.isError ? 'failed' : 'completed';
237
- const success = !result.isError;
238
280
  await sendTaskProgress(extra, {
239
281
  current: 1,
240
282
  total: 1,
241
- message: formatProgressCompletion(options.toolName, context, outcome, success),
283
+ message: formatProgressCompletion(options.toolName, context, outcome),
242
284
  });
243
285
  return result;
244
286
  }
245
287
  catch (error) {
288
+ const errorMessage = getErrorMessage(error);
289
+ const failureMeta = classifyErrorMeta(error, errorMessage);
290
+ const outcome = failureMeta.kind === 'cancelled' ? 'cancelled' : 'failed';
246
291
  // Progress is best-effort; must never mask the original error.
247
292
  try {
248
293
  await sendTaskProgress(extra, {
249
294
  current: 1,
250
295
  total: 1,
251
- message: formatProgressCompletion(options.toolName, context, 'failed', false),
296
+ message: formatProgressCompletion(options.toolName, context, outcome),
252
297
  });
253
298
  }
254
299
  catch {
@@ -258,6 +303,21 @@ export function wrapToolHandler(options, handler) {
258
303
  }
259
304
  };
260
305
  }
306
+ async function validateRequest(config, inputRecord, ctx) {
307
+ if (config.requiresDiff) {
308
+ if (!ctx.diffSlot) {
309
+ return createNoDiffError();
310
+ }
311
+ const budgetError = validateDiffBudget(ctx.diffSlot.diff);
312
+ if (budgetError) {
313
+ return budgetError;
314
+ }
315
+ }
316
+ if (config.validateInput) {
317
+ return await config.validateInput(inputRecord, ctx);
318
+ }
319
+ return undefined;
320
+ }
261
321
  export function registerStructuredToolTask(server, config) {
262
322
  const responseSchema = createGeminiResponseSchema({
263
323
  geminiSchema: config.geminiSchema,
@@ -307,18 +367,16 @@ export function registerStructuredToolTask(server, config) {
307
367
  // could replace the slot and silently bypass the budget check.
308
368
  const ctx = { diffSlot: getDiff() };
309
369
  await reportProgressStepUpdate(reportProgress, config.name, progressContext, 0, 'starting');
310
- if (config.validateInput) {
311
- const validationError = await config.validateInput(inputRecord, ctx);
312
- if (validationError) {
313
- const validationMessage = validationError.structuredContent.error?.message ??
314
- INPUT_VALIDATION_FAILED;
315
- await updateStatusMessage(validationMessage);
316
- await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, 'rejected', false);
317
- await storeResultSafely('completed', validationError);
318
- return;
319
- }
370
+ const validationError = await validateRequest(config, inputRecord, ctx);
371
+ if (validationError) {
372
+ const validationMessage = validationError.structuredContent.error?.message ??
373
+ INPUT_VALIDATION_FAILED;
374
+ await updateStatusMessage(validationMessage);
375
+ await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, 'rejected');
376
+ await storeResultSafely('completed', validationError);
377
+ return;
320
378
  }
321
- await reportProgressStepUpdate(reportProgress, config.name, progressContext, 1, 'preparing');
379
+ await reportProgressStepUpdate(reportProgress, config.name, progressContext, 1, 'building prompt');
322
380
  const promptParts = config.buildPrompt(inputRecord, ctx);
323
381
  const { prompt } = promptParts;
324
382
  const { systemInstruction } = promptParts;
@@ -326,26 +384,33 @@ export function registerStructuredToolTask(server, config) {
326
384
  await reportProgressStepUpdate(reportProgress, config.name, progressContext, 2, modelLabel);
327
385
  let parsed;
328
386
  let retryPrompt = prompt;
329
- for (let attempt = 0; attempt <= MAX_SCHEMA_RETRIES; attempt += 1) {
387
+ const maxRetries = config.schemaRetries ?? geminiSchemaRetriesConfig.get();
388
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
330
389
  try {
331
390
  const raw = await generateStructuredJson(createGenerationRequest(config, { systemInstruction, prompt: retryPrompt }, responseSchema, onLog, extra.signal));
332
391
  if (attempt === 0) {
333
- await reportProgressStepUpdate(reportProgress, config.name, progressContext, 3, 'processing response');
392
+ await reportProgressStepUpdate(reportProgress, config.name, progressContext, 3, 'validating response');
334
393
  }
335
394
  parsed = config.resultSchema.parse(raw);
336
395
  break;
337
396
  }
338
397
  catch (error) {
339
- if (attempt >= MAX_SCHEMA_RETRIES ||
340
- !(error instanceof z.ZodError)) {
398
+ if (attempt >= maxRetries || !(error instanceof z.ZodError)) {
341
399
  throw error;
342
400
  }
343
401
  const errorMessage = getErrorMessage(error);
402
+ const schemaRetryPrompt = createSchemaRetryPrompt(prompt, errorMessage, config.deterministicJson === true);
344
403
  await onLog('warning', {
345
404
  event: 'schema_validation_failed',
346
- details: { attempt, error: errorMessage },
405
+ details: {
406
+ attempt,
407
+ error: schemaRetryPrompt.summarizedError,
408
+ originalChars: errorMessage.length,
409
+ },
347
410
  });
348
- retryPrompt = `${prompt}\n\nCRITICAL: The previous response failed schema validation. Error: ${errorMessage}`;
411
+ const retryCount = attempt + 1;
412
+ await reportSchemaRetryProgressBestEffort(reportProgress, config.name, progressContext, retryCount, maxRetries);
413
+ retryPrompt = schemaRetryPrompt.prompt;
349
414
  }
350
415
  }
351
416
  if (!parsed) {
@@ -367,9 +432,10 @@ export function registerStructuredToolTask(server, config) {
367
432
  catch (error) {
368
433
  const errorMessage = getErrorMessage(error);
369
434
  const errorMeta = classifyErrorMeta(error, errorMessage);
370
- await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, 'failed', false);
371
- await updateStatusMessage(errorMessage);
435
+ const outcome = errorMeta.kind === 'cancelled' ? 'cancelled' : 'failed';
436
+ await updateStatusMessage(createFailureStatusMessage(outcome, errorMessage));
372
437
  await storeResultSafely('failed', createErrorToolResponse(config.errorCode, errorMessage, undefined, errorMeta));
438
+ await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, outcome);
373
439
  }
374
440
  };
375
441
  queueMicrotask(() => {
@@ -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
  }
@@ -1,5 +1,3 @@
1
- import { validateDiffBudget } from '../lib/diff-budget.js';
2
- import { createNoDiffError } from '../lib/diff-store.js';
3
1
  import { requireToolContract } from '../lib/tool-contracts.js';
4
2
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
5
3
  import { AnalyzeComplexityInputSchema } from '../schemas/inputs.js';
@@ -29,12 +27,10 @@ export function registerAnalyzeComplexityTool(server) {
29
27
  ...(TOOL_CONTRACT.temperature !== undefined
30
28
  ? { temperature: TOOL_CONTRACT.temperature }
31
29
  : undefined),
32
- validateInput: (_input, ctx) => {
33
- const slot = ctx.diffSlot;
34
- if (!slot)
35
- return createNoDiffError();
36
- return validateDiffBudget(slot.diff);
37
- },
30
+ ...(TOOL_CONTRACT.deterministicJson !== undefined
31
+ ? { deterministicJson: TOOL_CONTRACT.deterministicJson }
32
+ : undefined),
33
+ requiresDiff: true,
38
34
  formatOutcome: (result) => result.isDegradation
39
35
  ? 'Performance degradation detected'
40
36
  : 'No degradation',
@@ -1,6 +1,4 @@
1
- import { validateDiffBudget } from '../lib/diff-budget.js';
2
- import { computeDiffStatsAndSummaryFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
3
- import { createNoDiffError } from '../lib/diff-store.js';
1
+ import { computeDiffStatsAndSummaryFromFiles } from '../lib/diff-parser.js';
4
2
  import { requireToolContract } from '../lib/tool-contracts.js';
5
3
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
6
4
  import { AnalyzePrImpactInputSchema } from '../schemas/inputs.js';
@@ -33,17 +31,15 @@ export function registerAnalyzePrImpactTool(server) {
33
31
  ...(TOOL_CONTRACT.temperature !== undefined
34
32
  ? { temperature: TOOL_CONTRACT.temperature }
35
33
  : undefined),
36
- validateInput: (_input, ctx) => {
37
- const slot = ctx.diffSlot;
38
- if (!slot)
39
- return createNoDiffError();
40
- return validateDiffBudget(slot.diff);
41
- },
34
+ ...(TOOL_CONTRACT.deterministicJson !== undefined
35
+ ? { deterministicJson: TOOL_CONTRACT.deterministicJson }
36
+ : undefined),
37
+ requiresDiff: true,
42
38
  formatOutcome: (result) => `severity: ${result.severity}`,
43
39
  formatOutput: (result) => `Impact Analysis (${result.severity}): ${result.summary}`,
44
40
  buildPrompt: (input, ctx) => {
45
41
  const diff = ctx.diffSlot?.diff ?? '';
46
- const files = parseDiffFiles(diff);
42
+ const files = ctx.diffSlot?.parsedFiles ?? [];
47
43
  const { stats, summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(files);
48
44
  const languageSegment = formatLanguageSegment(input.language);
49
45
  return {
@@ -1,5 +1,3 @@
1
- import { validateDiffBudget } from '../lib/diff-budget.js';
2
- import { createNoDiffError } from '../lib/diff-store.js';
3
1
  import { requireToolContract } from '../lib/tool-contracts.js';
4
2
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
5
3
  import { DetectApiBreakingInputSchema } from '../schemas/inputs.js';
@@ -29,12 +27,10 @@ export function registerDetectApiBreakingTool(server) {
29
27
  ...(TOOL_CONTRACT.temperature !== undefined
30
28
  ? { temperature: TOOL_CONTRACT.temperature }
31
29
  : undefined),
32
- validateInput: (_input, ctx) => {
33
- const slot = ctx.diffSlot;
34
- if (!slot)
35
- return createNoDiffError();
36
- return validateDiffBudget(slot.diff);
37
- },
30
+ ...(TOOL_CONTRACT.deterministicJson !== undefined
31
+ ? { deterministicJson: TOOL_CONTRACT.deterministicJson }
32
+ : undefined),
33
+ requiresDiff: true,
38
34
  formatOutcome: (result) => `${result.breakingChanges.length} breaking change(s) found`,
39
35
  formatOutput: (result) => result.hasBreakingChanges
40
36
  ? `API Breaking Changes: ${result.breakingChanges.length} found.`