@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.
- package/README.md +15 -11
- package/dist/lib/diff-parser.js +33 -13
- package/dist/lib/diff-store.d.ts +6 -4
- package/dist/lib/diff-store.js +15 -10
- package/dist/lib/gemini.d.ts +4 -0
- package/dist/lib/gemini.js +299 -26
- package/dist/lib/model-config.d.ts +1 -1
- package/dist/lib/model-config.js +1 -1
- package/dist/lib/tool-contracts.d.ts +11 -4
- package/dist/lib/tool-contracts.js +7 -0
- package/dist/lib/tool-factory.d.ts +10 -3
- package/dist/lib/tool-factory.js +126 -60
- package/dist/lib/types.d.ts +6 -0
- package/dist/resources/server-config.js +15 -0
- package/dist/tools/analyze-complexity.js +4 -8
- package/dist/tools/analyze-pr-impact.js +6 -10
- package/dist/tools/detect-api-breaking.js +4 -8
- package/dist/tools/generate-diff.js +39 -35
- package/dist/tools/generate-review-summary.js +14 -12
- package/dist/tools/generate-test-plan.js +6 -10
- package/dist/tools/inspect-code-quality.js +8 -11
- package/dist/tools/suggest-search-replace.js +6 -10
- package/package.json +1 -1
|
@@ -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:
|
|
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. */
|
package/dist/lib/model-config.js
CHANGED
|
@@ -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:
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
*
|
|
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;
|
package/dist/lib/tool-factory.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { DefaultOutputSchema } from '../schemas/outputs.js';
|
|
3
|
-
import {
|
|
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
|
|
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 ||
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
if (typeof progressToken !== 'string' && typeof progressToken !== 'number') {
|
|
139
|
+
function ignoreProgressInput(value) {
|
|
140
|
+
if (value === null) {
|
|
97
141
|
return;
|
|
98
142
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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, '
|
|
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
|
-
|
|
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, '
|
|
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 >=
|
|
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: {
|
|
405
|
+
details: {
|
|
406
|
+
attempt,
|
|
407
|
+
error: schemaRetryPrompt.summarizedError,
|
|
408
|
+
originalChars: errorMessage.length,
|
|
409
|
+
},
|
|
347
410
|
});
|
|
348
|
-
|
|
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
|
-
|
|
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(() => {
|
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
|
}
|
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 {
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 =
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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.`
|