@j0hanz/code-review-analyst-mcp 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +12 -3
- package/dist/instructions.md +4 -146
- package/dist/lib/context-budget.d.ts +2 -2
- package/dist/lib/context-budget.js +12 -6
- package/dist/lib/diff-budget.js +6 -2
- package/dist/lib/diff-parser.js +31 -36
- package/dist/lib/env-config.d.ts +1 -0
- package/dist/lib/env-config.js +9 -3
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +5 -5
- package/dist/lib/gemini-schema.js +2 -1
- package/dist/lib/gemini.js +135 -67
- package/dist/lib/model-config.d.ts +14 -2
- package/dist/lib/model-config.js +30 -6
- package/dist/lib/tool-contracts.d.ts +245 -0
- package/dist/lib/tool-contracts.js +302 -0
- package/dist/lib/tool-factory.d.ts +5 -1
- package/dist/lib/tool-factory.js +48 -54
- package/dist/lib/tool-response.js +10 -12
- package/dist/lib/types.d.ts +3 -3
- package/dist/prompts/index.js +47 -41
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.js +80 -18
- package/dist/resources/instructions.d.ts +1 -0
- package/dist/resources/instructions.js +59 -0
- package/dist/resources/server-config.d.ts +1 -0
- package/dist/resources/server-config.js +70 -0
- package/dist/resources/tool-catalog.d.ts +1 -0
- package/dist/resources/tool-catalog.js +39 -0
- package/dist/resources/tool-info.d.ts +5 -0
- package/dist/resources/tool-info.js +122 -0
- package/dist/resources/workflows.d.ts +1 -0
- package/dist/resources/workflows.js +72 -0
- package/dist/schemas/inputs.d.ts +8 -0
- package/dist/schemas/inputs.js +20 -18
- package/dist/schemas/outputs.d.ts +17 -1
- package/dist/schemas/outputs.js +84 -52
- package/dist/server.js +25 -26
- package/dist/tools/analyze-complexity.d.ts +2 -0
- package/dist/tools/analyze-complexity.js +45 -0
- package/dist/tools/analyze-pr-impact.js +30 -25
- package/dist/tools/detect-api-breaking.d.ts +2 -0
- package/dist/tools/detect-api-breaking.js +42 -0
- package/dist/tools/generate-review-summary.js +26 -20
- package/dist/tools/generate-test-plan.js +34 -28
- package/dist/tools/index.js +9 -2
- package/dist/tools/inspect-code-quality.js +46 -40
- package/dist/tools/suggest-search-replace.js +34 -27
- package/package.json +1 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { DEFAULT_TIMEOUT_PRO_MS, FLASH_API_BREAKING_MAX_OUTPUT_TOKENS, FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS, FLASH_MODEL, FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS, FLASH_THINKING_BUDGET, FLASH_TRIAGE_MAX_OUTPUT_TOKENS, PRO_MODEL, PRO_PATCH_MAX_OUTPUT_TOKENS, PRO_REVIEW_MAX_OUTPUT_TOKENS, PRO_THINKING_BUDGET, } from './model-config.js';
|
|
2
|
+
const DEFAULT_TIMEOUT_FLASH_MS = 90_000;
|
|
3
|
+
export const TOOL_CONTRACTS = [
|
|
4
|
+
{
|
|
5
|
+
name: 'analyze_pr_impact',
|
|
6
|
+
purpose: 'Assess severity, categories, breaking changes, and rollback complexity.',
|
|
7
|
+
model: FLASH_MODEL,
|
|
8
|
+
timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
|
|
9
|
+
maxOutputTokens: FLASH_TRIAGE_MAX_OUTPUT_TOKENS,
|
|
10
|
+
params: [
|
|
11
|
+
{
|
|
12
|
+
name: 'diff',
|
|
13
|
+
type: 'string',
|
|
14
|
+
required: true,
|
|
15
|
+
constraints: '10-120K chars',
|
|
16
|
+
description: 'Unified diff text.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'repository',
|
|
20
|
+
type: 'string',
|
|
21
|
+
required: true,
|
|
22
|
+
constraints: '1-200 chars',
|
|
23
|
+
description: 'Repository identifier (org/repo).',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'language',
|
|
27
|
+
type: 'string',
|
|
28
|
+
required: false,
|
|
29
|
+
constraints: '2-32 chars',
|
|
30
|
+
description: 'Primary language hint.',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
outputShape: '{severity, categories[], summary, breakingChanges[], affectedAreas[], rollbackComplexity}',
|
|
34
|
+
gotchas: [
|
|
35
|
+
'Flash triage tool optimized for speed.',
|
|
36
|
+
'Diff-only analysis (no full-file context).',
|
|
37
|
+
],
|
|
38
|
+
crossToolFlow: [
|
|
39
|
+
'severity/categories feed triage and merge-gate decisions.',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'generate_review_summary',
|
|
44
|
+
purpose: 'Produce PR summary, risk rating, and merge recommendation.',
|
|
45
|
+
model: FLASH_MODEL,
|
|
46
|
+
timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
|
|
47
|
+
maxOutputTokens: FLASH_TRIAGE_MAX_OUTPUT_TOKENS,
|
|
48
|
+
params: [
|
|
49
|
+
{
|
|
50
|
+
name: 'diff',
|
|
51
|
+
type: 'string',
|
|
52
|
+
required: true,
|
|
53
|
+
constraints: '10-120K chars',
|
|
54
|
+
description: 'Unified diff text.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'repository',
|
|
58
|
+
type: 'string',
|
|
59
|
+
required: true,
|
|
60
|
+
constraints: '1-200 chars',
|
|
61
|
+
description: 'Repository identifier (org/repo).',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'language',
|
|
65
|
+
type: 'string',
|
|
66
|
+
required: false,
|
|
67
|
+
constraints: '2-32 chars',
|
|
68
|
+
description: 'Primary language hint.',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
outputShape: '{summary, overallRisk, keyChanges[], recommendation, stats{filesChanged, linesAdded, linesRemoved}}',
|
|
72
|
+
gotchas: [
|
|
73
|
+
'stats are computed locally from the diff.',
|
|
74
|
+
'Flash triage tool optimized for speed.',
|
|
75
|
+
],
|
|
76
|
+
crossToolFlow: [
|
|
77
|
+
'Use before deep review to decide whether Pro analysis is needed.',
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'inspect_code_quality',
|
|
82
|
+
purpose: 'Deep code review with optional full-file context.',
|
|
83
|
+
model: PRO_MODEL,
|
|
84
|
+
timeoutMs: DEFAULT_TIMEOUT_PRO_MS,
|
|
85
|
+
thinkingBudget: PRO_THINKING_BUDGET,
|
|
86
|
+
maxOutputTokens: PRO_REVIEW_MAX_OUTPUT_TOKENS,
|
|
87
|
+
params: [
|
|
88
|
+
{
|
|
89
|
+
name: 'diff',
|
|
90
|
+
type: 'string',
|
|
91
|
+
required: true,
|
|
92
|
+
constraints: '10-120K chars',
|
|
93
|
+
description: 'Unified diff text.',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'repository',
|
|
97
|
+
type: 'string',
|
|
98
|
+
required: true,
|
|
99
|
+
constraints: '1-200 chars',
|
|
100
|
+
description: 'Repository identifier (org/repo).',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'language',
|
|
104
|
+
type: 'string',
|
|
105
|
+
required: false,
|
|
106
|
+
constraints: '2-32 chars',
|
|
107
|
+
description: 'Primary language hint.',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'focusAreas',
|
|
111
|
+
type: 'string[]',
|
|
112
|
+
required: false,
|
|
113
|
+
constraints: '1-12 items, 2-80 chars each',
|
|
114
|
+
description: 'Focused inspection categories.',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'maxFindings',
|
|
118
|
+
type: 'number',
|
|
119
|
+
required: false,
|
|
120
|
+
constraints: '1-25',
|
|
121
|
+
description: 'Post-generation cap applied to findings.',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'files',
|
|
125
|
+
type: 'object[]',
|
|
126
|
+
required: false,
|
|
127
|
+
constraints: '1-20 files, 100K chars/file',
|
|
128
|
+
description: 'Optional full file content context.',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
outputShape: '{summary, overallRisk, findings[], testsNeeded[], contextualInsights[], totalFindings}',
|
|
132
|
+
gotchas: [
|
|
133
|
+
'Combined diff + file context is bounded by MAX_CONTEXT_CHARS.',
|
|
134
|
+
'maxFindings caps output after generation.',
|
|
135
|
+
],
|
|
136
|
+
crossToolFlow: [
|
|
137
|
+
'findings[].title -> suggest_search_replace.findingTitle',
|
|
138
|
+
'findings[].explanation -> suggest_search_replace.findingDetails',
|
|
139
|
+
],
|
|
140
|
+
constraints: ['Context budget (diff + files) < 500K chars.'],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'suggest_search_replace',
|
|
144
|
+
purpose: 'Generate verbatim search/replace fix blocks for one finding.',
|
|
145
|
+
model: PRO_MODEL,
|
|
146
|
+
timeoutMs: DEFAULT_TIMEOUT_PRO_MS,
|
|
147
|
+
thinkingBudget: PRO_THINKING_BUDGET,
|
|
148
|
+
maxOutputTokens: PRO_PATCH_MAX_OUTPUT_TOKENS,
|
|
149
|
+
params: [
|
|
150
|
+
{
|
|
151
|
+
name: 'diff',
|
|
152
|
+
type: 'string',
|
|
153
|
+
required: true,
|
|
154
|
+
constraints: '10-120K chars',
|
|
155
|
+
description: 'Unified diff containing the target issue.',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'findingTitle',
|
|
159
|
+
type: 'string',
|
|
160
|
+
required: true,
|
|
161
|
+
constraints: '3-160 chars',
|
|
162
|
+
description: 'Short finding title.',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'findingDetails',
|
|
166
|
+
type: 'string',
|
|
167
|
+
required: true,
|
|
168
|
+
constraints: '10-3000 chars',
|
|
169
|
+
description: 'Detailed finding context.',
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
outputShape: '{summary, blocks[], validationChecklist[]}',
|
|
173
|
+
gotchas: [
|
|
174
|
+
'One finding per call to avoid mixed patch intent.',
|
|
175
|
+
'search must be exact whitespace-preserving match.',
|
|
176
|
+
],
|
|
177
|
+
crossToolFlow: [
|
|
178
|
+
'Consumes findings from inspect_code_quality for targeted fixes.',
|
|
179
|
+
],
|
|
180
|
+
constraints: ['One finding per call; verbatim search match required.'],
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'generate_test_plan',
|
|
184
|
+
purpose: 'Generate prioritized test cases and coverage guidance.',
|
|
185
|
+
model: FLASH_MODEL,
|
|
186
|
+
timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
|
|
187
|
+
thinkingBudget: FLASH_THINKING_BUDGET,
|
|
188
|
+
maxOutputTokens: FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS,
|
|
189
|
+
params: [
|
|
190
|
+
{
|
|
191
|
+
name: 'diff',
|
|
192
|
+
type: 'string',
|
|
193
|
+
required: true,
|
|
194
|
+
constraints: '10-120K chars',
|
|
195
|
+
description: 'Unified diff text.',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'repository',
|
|
199
|
+
type: 'string',
|
|
200
|
+
required: true,
|
|
201
|
+
constraints: '1-200 chars',
|
|
202
|
+
description: 'Repository identifier (org/repo).',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'language',
|
|
206
|
+
type: 'string',
|
|
207
|
+
required: false,
|
|
208
|
+
constraints: '2-32 chars',
|
|
209
|
+
description: 'Primary language hint.',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'testFramework',
|
|
213
|
+
type: 'string',
|
|
214
|
+
required: false,
|
|
215
|
+
constraints: '1-50 chars',
|
|
216
|
+
description: 'Framework hint (jest, vitest, pytest, node:test).',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'maxTestCases',
|
|
220
|
+
type: 'number',
|
|
221
|
+
required: false,
|
|
222
|
+
constraints: '1-30',
|
|
223
|
+
description: 'Post-generation cap applied to test cases.',
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
outputShape: '{summary, testCases[], coverageSummary}',
|
|
227
|
+
gotchas: ['maxTestCases caps output after generation.'],
|
|
228
|
+
crossToolFlow: [
|
|
229
|
+
'Pair with inspect_code_quality to validate high-risk paths.',
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'analyze_time_space_complexity',
|
|
234
|
+
purpose: 'Analyze Big-O complexity and detect degradations in changed code.',
|
|
235
|
+
model: FLASH_MODEL,
|
|
236
|
+
timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
|
|
237
|
+
thinkingBudget: FLASH_THINKING_BUDGET,
|
|
238
|
+
maxOutputTokens: FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS,
|
|
239
|
+
params: [
|
|
240
|
+
{
|
|
241
|
+
name: 'diff',
|
|
242
|
+
type: 'string',
|
|
243
|
+
required: true,
|
|
244
|
+
constraints: '10-120K chars',
|
|
245
|
+
description: 'Unified diff text.',
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'language',
|
|
249
|
+
type: 'string',
|
|
250
|
+
required: false,
|
|
251
|
+
constraints: '2-32 chars',
|
|
252
|
+
description: 'Primary language hint.',
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
outputShape: '{timeComplexity, spaceComplexity, explanation, potentialBottlenecks[], isDegradation}',
|
|
256
|
+
gotchas: ['Analyzes only changed code visible in the diff.'],
|
|
257
|
+
crossToolFlow: ['Use for algorithmic/performance-sensitive changes.'],
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'detect_api_breaking_changes',
|
|
261
|
+
purpose: 'Detect breaking API/interface changes in a diff.',
|
|
262
|
+
model: FLASH_MODEL,
|
|
263
|
+
timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
|
|
264
|
+
maxOutputTokens: FLASH_API_BREAKING_MAX_OUTPUT_TOKENS,
|
|
265
|
+
params: [
|
|
266
|
+
{
|
|
267
|
+
name: 'diff',
|
|
268
|
+
type: 'string',
|
|
269
|
+
required: true,
|
|
270
|
+
constraints: '10-120K chars',
|
|
271
|
+
description: 'Unified diff text.',
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'language',
|
|
275
|
+
type: 'string',
|
|
276
|
+
required: false,
|
|
277
|
+
constraints: '2-32 chars',
|
|
278
|
+
description: 'Primary language hint.',
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
outputShape: '{hasBreakingChanges, breakingChanges[]}',
|
|
282
|
+
gotchas: ['Targets public API contracts over internal refactors.'],
|
|
283
|
+
crossToolFlow: ['Run before merge for API-surface-sensitive changes.'],
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
const TOOL_CONTRACTS_BY_NAME = new Map(TOOL_CONTRACTS.map((contract) => [contract.name, contract]));
|
|
287
|
+
export function getToolContracts() {
|
|
288
|
+
return TOOL_CONTRACTS;
|
|
289
|
+
}
|
|
290
|
+
export function getToolContract(toolName) {
|
|
291
|
+
return TOOL_CONTRACTS_BY_NAME.get(toolName);
|
|
292
|
+
}
|
|
293
|
+
export function requireToolContract(toolName) {
|
|
294
|
+
const contract = getToolContract(toolName);
|
|
295
|
+
if (contract) {
|
|
296
|
+
return contract;
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`Unknown tool contract: ${toolName}`);
|
|
299
|
+
}
|
|
300
|
+
export function getToolContractNames() {
|
|
301
|
+
return TOOL_CONTRACTS.map((contract) => contract.name);
|
|
302
|
+
}
|
|
@@ -31,8 +31,12 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
|
|
|
31
31
|
model?: string;
|
|
32
32
|
/** Optional thinking budget in tokens. */
|
|
33
33
|
thinkingBudget?: number;
|
|
34
|
-
/** Optional timeout in ms for the Gemini call. Defaults to
|
|
34
|
+
/** Optional timeout in ms for the Gemini call. Defaults to 90,000 ms. Use DEFAULT_TIMEOUT_PRO_MS for Pro model calls. */
|
|
35
35
|
timeoutMs?: number;
|
|
36
|
+
/** Optional max output tokens for Gemini. */
|
|
37
|
+
maxOutputTokens?: number;
|
|
38
|
+
/** Optional opt-in to Gemini thought output. Defaults to false. */
|
|
39
|
+
includeThoughts?: boolean;
|
|
36
40
|
/** Optional formatter for human-readable text output. */
|
|
37
41
|
formatOutput?: (result: TFinal) => string;
|
|
38
42
|
/** Optional context text used in progress messages. */
|
package/dist/lib/tool-factory.js
CHANGED
|
@@ -12,6 +12,7 @@ const CANCELLED_ERROR_PATTERN = /cancelled|canceled/i;
|
|
|
12
12
|
const TIMEOUT_ERROR_PATTERN = /timed out|timeout/i;
|
|
13
13
|
const BUDGET_ERROR_PATTERN = /exceeds limit|max allowed size|input too large/i;
|
|
14
14
|
const BUSY_ERROR_PATTERN = /too many concurrent/i;
|
|
15
|
+
const MAX_SCHEMA_RETRIES = 1;
|
|
15
16
|
function createGeminiResponseSchema(config) {
|
|
16
17
|
const sourceSchema = config.geminiSchema ?? config.resultSchema;
|
|
17
18
|
return stripJsonSchemaConstraints(z.toJSONSchema(sourceSchema));
|
|
@@ -35,6 +36,12 @@ function createGenerationRequest(config, promptParts, responseSchema, onLog, sig
|
|
|
35
36
|
if (config.timeoutMs !== undefined) {
|
|
36
37
|
request.timeoutMs = config.timeoutMs;
|
|
37
38
|
}
|
|
39
|
+
if (config.maxOutputTokens !== undefined) {
|
|
40
|
+
request.maxOutputTokens = config.maxOutputTokens;
|
|
41
|
+
}
|
|
42
|
+
if (config.includeThoughts !== undefined) {
|
|
43
|
+
request.includeThoughts = config.includeThoughts;
|
|
44
|
+
}
|
|
38
45
|
if (signal !== undefined) {
|
|
39
46
|
request.signal = signal;
|
|
40
47
|
}
|
|
@@ -65,13 +72,7 @@ function classifyErrorMeta(error, message) {
|
|
|
65
72
|
retryable: false,
|
|
66
73
|
};
|
|
67
74
|
}
|
|
68
|
-
if (
|
|
69
|
-
return {
|
|
70
|
-
kind: 'upstream',
|
|
71
|
-
retryable: true,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
if (BUSY_ERROR_PATTERN.test(message)) {
|
|
75
|
+
if (isRetryableUpstreamMessage(message)) {
|
|
75
76
|
return {
|
|
76
77
|
kind: 'upstream',
|
|
77
78
|
retryable: true,
|
|
@@ -82,6 +83,10 @@ function classifyErrorMeta(error, message) {
|
|
|
82
83
|
retryable: false,
|
|
83
84
|
};
|
|
84
85
|
}
|
|
86
|
+
function isRetryableUpstreamMessage(message) {
|
|
87
|
+
return (RETRYABLE_UPSTREAM_ERROR_PATTERN.test(message) ||
|
|
88
|
+
BUSY_ERROR_PATTERN.test(message));
|
|
89
|
+
}
|
|
85
90
|
async function sendTaskProgress(extra, payload) {
|
|
86
91
|
const progressToken = extra._meta?.progressToken;
|
|
87
92
|
if (typeof progressToken !== 'string' && typeof progressToken !== 'number') {
|
|
@@ -118,11 +123,14 @@ function createProgressReporter(extra) {
|
|
|
118
123
|
const total = payload.total !== undefined
|
|
119
124
|
? Math.max(payload.total, current)
|
|
120
125
|
: undefined;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
const progressPayload = { current };
|
|
127
|
+
if (total !== undefined) {
|
|
128
|
+
progressPayload.total = total;
|
|
129
|
+
}
|
|
130
|
+
if (payload.message !== undefined) {
|
|
131
|
+
progressPayload.message = payload.message;
|
|
132
|
+
}
|
|
133
|
+
await sendTaskProgress(extra, progressPayload);
|
|
126
134
|
lastCurrent = current;
|
|
127
135
|
if (total !== undefined && total === current) {
|
|
128
136
|
didSendTerminal = true;
|
|
@@ -140,7 +148,7 @@ function normalizeProgressContext(context) {
|
|
|
140
148
|
return `${compact.slice(0, 77)}...`;
|
|
141
149
|
}
|
|
142
150
|
function formatProgressStep(toolName, context, metadata) {
|
|
143
|
-
const prefix = metadata === '
|
|
151
|
+
const prefix = metadata === 'starting' ? '▸' : '◦';
|
|
144
152
|
return `${prefix} ${toolName}: ${context} [${metadata}]`;
|
|
145
153
|
}
|
|
146
154
|
function friendlyModelName(model) {
|
|
@@ -153,9 +161,23 @@ function friendlyModelName(model) {
|
|
|
153
161
|
return 'model';
|
|
154
162
|
}
|
|
155
163
|
function formatProgressCompletion(toolName, context, outcome, success = true) {
|
|
156
|
-
const prefix = success ? '
|
|
164
|
+
const prefix = success ? '◆' : '◇';
|
|
157
165
|
return `${prefix} ${toolName}: ${context} • ${outcome}`;
|
|
158
166
|
}
|
|
167
|
+
async function reportProgressStepUpdate(reportProgress, toolName, context, current, metadata) {
|
|
168
|
+
await reportProgress({
|
|
169
|
+
current,
|
|
170
|
+
total: TASK_PROGRESS_TOTAL,
|
|
171
|
+
message: formatProgressStep(toolName, context, metadata),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function reportProgressCompletionUpdate(reportProgress, toolName, context, outcome, success = true) {
|
|
175
|
+
await reportProgress({
|
|
176
|
+
current: TASK_PROGRESS_TOTAL,
|
|
177
|
+
total: TASK_PROGRESS_TOTAL,
|
|
178
|
+
message: formatProgressCompletion(toolName, context, outcome, success),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
159
181
|
function toLoggingLevel(level) {
|
|
160
182
|
switch (level) {
|
|
161
183
|
case 'debug':
|
|
@@ -207,6 +229,7 @@ export function registerStructuredToolTask(server, config) {
|
|
|
207
229
|
outputSchema: DefaultOutputSchema,
|
|
208
230
|
annotations: {
|
|
209
231
|
readOnlyHint: true,
|
|
232
|
+
idempotentHint: true,
|
|
210
233
|
openWorldHint: true,
|
|
211
234
|
},
|
|
212
235
|
}, {
|
|
@@ -237,52 +260,31 @@ export function registerStructuredToolTask(server, config) {
|
|
|
237
260
|
const onLog = createGeminiLogger(server, task.taskId);
|
|
238
261
|
const inputRecord = parseToolInput(input, config.fullInputSchema);
|
|
239
262
|
progressContext = normalizeProgressContext(config.progressContext?.(inputRecord));
|
|
240
|
-
await reportProgress
|
|
241
|
-
current: 0,
|
|
242
|
-
total: TASK_PROGRESS_TOTAL,
|
|
243
|
-
message: formatProgressStep(config.name, progressContext, 'starting'),
|
|
244
|
-
});
|
|
263
|
+
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 0, 'starting');
|
|
245
264
|
if (config.validateInput) {
|
|
246
265
|
const validationError = await config.validateInput(inputRecord);
|
|
247
266
|
if (validationError) {
|
|
248
267
|
const validationMessage = validationError.structuredContent.error?.message ??
|
|
249
268
|
INPUT_VALIDATION_FAILED;
|
|
250
269
|
await updateStatusMessage(validationMessage);
|
|
251
|
-
await reportProgress
|
|
252
|
-
current: TASK_PROGRESS_TOTAL,
|
|
253
|
-
total: TASK_PROGRESS_TOTAL,
|
|
254
|
-
message: formatProgressCompletion(config.name, progressContext, 'rejected', false),
|
|
255
|
-
});
|
|
270
|
+
await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, 'rejected', false);
|
|
256
271
|
await storeResultSafely('completed', validationError);
|
|
257
272
|
return;
|
|
258
273
|
}
|
|
259
274
|
}
|
|
260
|
-
await reportProgress
|
|
261
|
-
current: 1,
|
|
262
|
-
total: TASK_PROGRESS_TOTAL,
|
|
263
|
-
message: formatProgressStep(config.name, progressContext, 'preparing'),
|
|
264
|
-
});
|
|
275
|
+
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 1, 'preparing');
|
|
265
276
|
const promptParts = config.buildPrompt(inputRecord);
|
|
266
|
-
|
|
277
|
+
const { prompt } = promptParts;
|
|
267
278
|
const { systemInstruction } = promptParts;
|
|
268
279
|
const modelLabel = friendlyModelName(config.model);
|
|
269
|
-
await reportProgress
|
|
270
|
-
current: 2,
|
|
271
|
-
total: TASK_PROGRESS_TOTAL,
|
|
272
|
-
message: formatProgressStep(config.name, progressContext, `awaiting ${modelLabel}`),
|
|
273
|
-
});
|
|
274
|
-
const MAX_SCHEMA_RETRIES = 1;
|
|
275
|
-
let raw;
|
|
280
|
+
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 2, modelLabel);
|
|
276
281
|
let parsed;
|
|
282
|
+
let retryPrompt = prompt;
|
|
277
283
|
for (let attempt = 0; attempt <= MAX_SCHEMA_RETRIES; attempt += 1) {
|
|
278
284
|
try {
|
|
279
|
-
raw = await generateStructuredJson(createGenerationRequest(config, { systemInstruction, prompt }, responseSchema, onLog, extra.signal));
|
|
285
|
+
const raw = await generateStructuredJson(createGenerationRequest(config, { systemInstruction, prompt: retryPrompt }, responseSchema, onLog, extra.signal));
|
|
280
286
|
if (attempt === 0) {
|
|
281
|
-
await reportProgress
|
|
282
|
-
current: 3,
|
|
283
|
-
total: TASK_PROGRESS_TOTAL,
|
|
284
|
-
message: formatProgressStep(config.name, progressContext, 'processing response'),
|
|
285
|
-
});
|
|
287
|
+
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 3, 'processing response');
|
|
286
288
|
}
|
|
287
289
|
parsed = config.resultSchema.parse(raw);
|
|
288
290
|
break;
|
|
@@ -297,7 +299,7 @@ export function registerStructuredToolTask(server, config) {
|
|
|
297
299
|
event: 'schema_validation_failed',
|
|
298
300
|
details: { attempt, error: errorMessage },
|
|
299
301
|
});
|
|
300
|
-
|
|
302
|
+
retryPrompt = `${prompt}\n\nCRITICAL: The previous response failed schema validation. Error: ${errorMessage}`;
|
|
301
303
|
}
|
|
302
304
|
}
|
|
303
305
|
if (!parsed) {
|
|
@@ -310,11 +312,7 @@ export function registerStructuredToolTask(server, config) {
|
|
|
310
312
|
? config.formatOutput(finalResult)
|
|
311
313
|
: undefined;
|
|
312
314
|
const outcome = config.formatOutcome?.(finalResult) ?? 'completed';
|
|
313
|
-
await reportProgress
|
|
314
|
-
current: TASK_PROGRESS_TOTAL,
|
|
315
|
-
total: TASK_PROGRESS_TOTAL,
|
|
316
|
-
message: formatProgressCompletion(config.name, progressContext, outcome),
|
|
317
|
-
});
|
|
315
|
+
await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, outcome);
|
|
318
316
|
await storeResultSafely('completed', createToolResponse({
|
|
319
317
|
ok: true,
|
|
320
318
|
result: finalResult,
|
|
@@ -323,11 +321,7 @@ export function registerStructuredToolTask(server, config) {
|
|
|
323
321
|
catch (error) {
|
|
324
322
|
const errorMessage = getErrorMessage(error);
|
|
325
323
|
const errorMeta = classifyErrorMeta(error, errorMessage);
|
|
326
|
-
await reportProgress
|
|
327
|
-
current: TASK_PROGRESS_TOTAL,
|
|
328
|
-
total: TASK_PROGRESS_TOTAL,
|
|
329
|
-
message: formatProgressCompletion(config.name, progressContext, 'failed', false),
|
|
330
|
-
});
|
|
324
|
+
await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, 'failed', false);
|
|
331
325
|
await updateStatusMessage(errorMessage);
|
|
332
326
|
await storeResultSafely('failed', createErrorToolResponse(config.errorCode, errorMessage, undefined, errorMeta));
|
|
333
327
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
function appendErrorMeta(error, meta) {
|
|
2
|
+
if (meta?.retryable !== undefined) {
|
|
3
|
+
error.retryable = meta.retryable;
|
|
4
|
+
}
|
|
5
|
+
if (meta?.kind !== undefined) {
|
|
6
|
+
error.kind = meta.kind;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
1
9
|
function toTextContent(structured, textContent) {
|
|
2
10
|
const text = textContent ?? JSON.stringify(structured);
|
|
3
11
|
return [{ type: 'text', text }];
|
|
@@ -13,12 +21,7 @@ function createErrorStructuredContent(code, message, result, meta) {
|
|
|
13
21
|
code,
|
|
14
22
|
message,
|
|
15
23
|
};
|
|
16
|
-
|
|
17
|
-
error.retryable = meta.retryable;
|
|
18
|
-
}
|
|
19
|
-
if (meta?.kind !== undefined) {
|
|
20
|
-
error.kind = meta.kind;
|
|
21
|
-
}
|
|
24
|
+
appendErrorMeta(error, meta);
|
|
22
25
|
if (result === undefined) {
|
|
23
26
|
return { ok: false, error };
|
|
24
27
|
}
|
|
@@ -29,10 +32,5 @@ export function createToolResponse(structured, textContent) {
|
|
|
29
32
|
}
|
|
30
33
|
export function createErrorToolResponse(code, message, result, meta) {
|
|
31
34
|
const structured = createErrorStructuredContent(code, message, result, meta);
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
content: base.content,
|
|
35
|
-
structuredContent: base.structuredContent,
|
|
36
|
-
isError: true,
|
|
37
|
-
};
|
|
35
|
+
return { ...buildToolResponse(structured), isError: true };
|
|
38
36
|
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export type JsonObject = Record<string, unknown>;
|
|
2
2
|
export type GeminiLogHandler = (level: string, data: unknown) => Promise<void>;
|
|
3
|
-
interface GeminiRequestExecutionOptions {
|
|
3
|
+
export interface GeminiRequestExecutionOptions {
|
|
4
4
|
maxRetries?: number;
|
|
5
5
|
timeoutMs?: number;
|
|
6
6
|
temperature?: number;
|
|
7
7
|
maxOutputTokens?: number;
|
|
8
8
|
thinkingBudget?: number;
|
|
9
|
+
includeThoughts?: boolean;
|
|
9
10
|
signal?: AbortSignal;
|
|
10
11
|
onLog?: GeminiLogHandler;
|
|
11
12
|
}
|
|
@@ -15,6 +16,5 @@ export interface GeminiStructuredRequestOptions extends GeminiRequestExecutionOp
|
|
|
15
16
|
export interface GeminiStructuredRequest extends GeminiStructuredRequestOptions {
|
|
16
17
|
systemInstruction?: string;
|
|
17
18
|
prompt: string;
|
|
18
|
-
responseSchema: JsonObject
|
|
19
|
+
responseSchema: Readonly<JsonObject>;
|
|
19
20
|
}
|
|
20
|
-
export {};
|