@j0hanz/code-assistant 0.9.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.
Files changed (54) hide show
  1. package/README.md +437 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +72 -0
  4. package/dist/lib/concurrency.d.ts +13 -0
  5. package/dist/lib/concurrency.js +77 -0
  6. package/dist/lib/config.d.ts +43 -0
  7. package/dist/lib/config.js +87 -0
  8. package/dist/lib/diff.d.ts +49 -0
  9. package/dist/lib/diff.js +241 -0
  10. package/dist/lib/errors.d.ts +8 -0
  11. package/dist/lib/errors.js +69 -0
  12. package/dist/lib/format.d.ts +14 -0
  13. package/dist/lib/format.js +33 -0
  14. package/dist/lib/gemini.d.ts +45 -0
  15. package/dist/lib/gemini.js +833 -0
  16. package/dist/lib/progress.d.ts +72 -0
  17. package/dist/lib/progress.js +204 -0
  18. package/dist/lib/tools.d.ts +274 -0
  19. package/dist/lib/tools.js +646 -0
  20. package/dist/prompts/index.d.ts +11 -0
  21. package/dist/prompts/index.js +96 -0
  22. package/dist/resources/index.d.ts +12 -0
  23. package/dist/resources/index.js +115 -0
  24. package/dist/resources/instructions.d.ts +1 -0
  25. package/dist/resources/instructions.js +71 -0
  26. package/dist/resources/server-config.d.ts +1 -0
  27. package/dist/resources/server-config.js +75 -0
  28. package/dist/resources/tool-catalog.d.ts +1 -0
  29. package/dist/resources/tool-catalog.js +30 -0
  30. package/dist/resources/tool-info.d.ts +5 -0
  31. package/dist/resources/tool-info.js +105 -0
  32. package/dist/resources/workflows.d.ts +1 -0
  33. package/dist/resources/workflows.js +59 -0
  34. package/dist/schemas/inputs.d.ts +21 -0
  35. package/dist/schemas/inputs.js +46 -0
  36. package/dist/schemas/outputs.d.ts +121 -0
  37. package/dist/schemas/outputs.js +162 -0
  38. package/dist/server.d.ts +6 -0
  39. package/dist/server.js +88 -0
  40. package/dist/tools/analyze-complexity.d.ts +2 -0
  41. package/dist/tools/analyze-complexity.js +50 -0
  42. package/dist/tools/analyze-pr-impact.d.ts +2 -0
  43. package/dist/tools/analyze-pr-impact.js +62 -0
  44. package/dist/tools/detect-api-breaking.d.ts +2 -0
  45. package/dist/tools/detect-api-breaking.js +49 -0
  46. package/dist/tools/generate-diff.d.ts +2 -0
  47. package/dist/tools/generate-diff.js +140 -0
  48. package/dist/tools/generate-review-summary.d.ts +2 -0
  49. package/dist/tools/generate-review-summary.js +71 -0
  50. package/dist/tools/generate-test-plan.d.ts +2 -0
  51. package/dist/tools/generate-test-plan.js +67 -0
  52. package/dist/tools/index.d.ts +2 -0
  53. package/dist/tools/index.js +19 -0
  54. package/package.json +79 -0
@@ -0,0 +1,646 @@
1
+ import { z } from 'zod';
2
+ import { DefaultOutputSchema } from '../schemas/outputs.js';
3
+ import { ANALYSIS_TEMPERATURE, CREATIVE_TEMPERATURE, FLASH_API_BREAKING_MAX_OUTPUT_TOKENS, FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS, FLASH_MODEL, FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS, FLASH_THINKING_LEVEL, FLASH_TRIAGE_MAX_OUTPUT_TOKENS, FLASH_TRIAGE_THINKING_LEVEL, TRIAGE_TEMPERATURE, } from './config.js';
4
+ import { createCachedEnvInt } from './config.js';
5
+ import { createNoDiffError, diffStaleWarningMs, getDiff, } from './diff.js';
6
+ import { validateDiffBudget } from './diff.js';
7
+ import { EMPTY_DIFF_STATS } from './diff.js';
8
+ import { classifyErrorMeta } from './errors.js';
9
+ import { getErrorMessage } from './errors.js';
10
+ import { stripJsonSchemaConstraints } from './gemini.js';
11
+ import { generateStructuredJson } from './gemini.js';
12
+ import { createFailureStatusMessage, DEFAULT_PROGRESS_CONTEXT, extractValidationMessage, getOrCreateProgressReporter, normalizeProgressContext, RunReporter, sendSingleStepProgress, STEP_BUILDING_PROMPT, STEP_CALLING_MODEL, STEP_FINALIZING, STEP_STARTING, STEP_VALIDATING, STEP_VALIDATING_RESPONSE, } from './progress.js';
13
+ function appendErrorMeta(error, meta) {
14
+ if (meta?.retryable !== undefined) {
15
+ error.retryable = meta.retryable;
16
+ }
17
+ if (meta?.kind !== undefined) {
18
+ error.kind = meta.kind;
19
+ }
20
+ }
21
+ function createToolError(code, message, meta) {
22
+ const error = { code, message };
23
+ appendErrorMeta(error, meta);
24
+ return error;
25
+ }
26
+ function toTextContent(structured, textContent) {
27
+ const text = textContent ?? JSON.stringify(structured);
28
+ return [{ type: 'text', text }];
29
+ }
30
+ function createErrorStructuredContent(code, message, result, meta) {
31
+ const error = createToolError(code, message, meta);
32
+ if (result === undefined) {
33
+ return { ok: false, error };
34
+ }
35
+ return { ok: false, error, result };
36
+ }
37
+ export function createToolResponse(structured, textContent) {
38
+ return {
39
+ content: toTextContent(structured, textContent),
40
+ structuredContent: structured,
41
+ };
42
+ }
43
+ export function createErrorToolResponse(code, message, result, meta) {
44
+ const structured = createErrorStructuredContent(code, message, result, meta);
45
+ return {
46
+ content: toTextContent(structured),
47
+ isError: true,
48
+ };
49
+ }
50
+ const DEFAULT_TIMEOUT_FLASH_MS = 90_000;
51
+ export const INSPECTION_FOCUS_AREAS = [
52
+ 'security',
53
+ 'correctness',
54
+ 'performance',
55
+ 'regressions',
56
+ 'tests',
57
+ 'maintainability',
58
+ 'concurrency',
59
+ ];
60
+ export function buildStructuredToolRuntimeOptions(contract) {
61
+ return {
62
+ ...(contract.thinkingLevel !== undefined
63
+ ? { thinkingLevel: contract.thinkingLevel }
64
+ : {}),
65
+ ...(contract.temperature !== undefined
66
+ ? { temperature: contract.temperature }
67
+ : {}),
68
+ ...(contract.deterministicJson !== undefined
69
+ ? { deterministicJson: contract.deterministicJson }
70
+ : {}),
71
+ };
72
+ }
73
+ export function buildStructuredToolExecutionOptions(contract) {
74
+ return {
75
+ timeoutMs: contract.timeoutMs,
76
+ maxOutputTokens: contract.maxOutputTokens,
77
+ ...buildStructuredToolRuntimeOptions(contract),
78
+ };
79
+ }
80
+ function createParam(name, type, required, constraints, description) {
81
+ return { name, type, required, constraints, description };
82
+ }
83
+ function cloneParams(...params) {
84
+ return params.map((param) => ({ ...param }));
85
+ }
86
+ const MODE_PARAM = createParam('mode', 'string', true, "'unstaged' | 'staged'", "'unstaged': working tree changes not yet staged. 'staged': changes added to the index (git add).");
87
+ const REPOSITORY_PARAM = createParam('repository', 'string', true, '1-200 chars', 'Repository identifier (org/repo).');
88
+ const LANGUAGE_PARAM = createParam('language', 'string', false, '2-32 chars', 'Primary language hint.');
89
+ const TEST_FRAMEWORK_PARAM = createParam('testFramework', 'string', false, '1-50 chars', 'Framework hint (jest, vitest, pytest, node:test).');
90
+ const MAX_TEST_CASES_PARAM = createParam('maxTestCases', 'number', false, '1-30', 'Post-generation cap applied to test cases.');
91
+ export const TOOL_CONTRACTS = [
92
+ {
93
+ name: 'generate_diff',
94
+ purpose: 'Generate a diff of current changes and cache it server-side. MUST be called before any other tool. Uses git to capture unstaged or staged changes in the current working directory.',
95
+ model: 'none',
96
+ timeoutMs: 0,
97
+ maxOutputTokens: 0,
98
+ params: cloneParams(MODE_PARAM),
99
+ outputShape: '{ok, result: {diffRef, stats{files, added, deleted}, generatedAt, mode, message}}',
100
+ gotchas: [
101
+ 'Must be called first — all other tools return E_NO_DIFF if no diff is cached.',
102
+ 'Noisy files (lock files, dist/, build/, minified assets) are excluded automatically.',
103
+ 'Empty diff (no changes) returns E_NO_CHANGES.',
104
+ ],
105
+ crossToolFlow: [
106
+ 'Caches diff at diff://current — consumed automatically by all review tools.',
107
+ ],
108
+ },
109
+ {
110
+ name: 'analyze_pr_impact',
111
+ purpose: 'Assess severity, categories, breaking changes, and rollback complexity.',
112
+ model: FLASH_MODEL,
113
+ timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
114
+ thinkingLevel: FLASH_TRIAGE_THINKING_LEVEL,
115
+ maxOutputTokens: FLASH_TRIAGE_MAX_OUTPUT_TOKENS,
116
+ temperature: TRIAGE_TEMPERATURE,
117
+ deterministicJson: true,
118
+ params: cloneParams(REPOSITORY_PARAM, LANGUAGE_PARAM),
119
+ outputShape: '{severity, categories[], summary, breakingChanges[], affectedAreas[], rollbackComplexity}',
120
+ gotchas: [
121
+ 'Requires generate_diff to be called first.',
122
+ 'Flash triage tool optimized for speed.',
123
+ ],
124
+ crossToolFlow: [
125
+ 'severity/categories feed triage and merge-gate decisions.',
126
+ ],
127
+ },
128
+ {
129
+ name: 'generate_review_summary',
130
+ purpose: 'Produce PR summary, risk rating, and merge recommendation.',
131
+ model: FLASH_MODEL,
132
+ timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
133
+ thinkingLevel: FLASH_TRIAGE_THINKING_LEVEL,
134
+ maxOutputTokens: FLASH_TRIAGE_MAX_OUTPUT_TOKENS,
135
+ temperature: TRIAGE_TEMPERATURE,
136
+ deterministicJson: true,
137
+ params: cloneParams(REPOSITORY_PARAM, LANGUAGE_PARAM),
138
+ outputShape: '{summary, overallRisk, keyChanges[], recommendation, stats{filesChanged, linesAdded, linesRemoved}}',
139
+ gotchas: [
140
+ 'Requires generate_diff to be called first.',
141
+ 'stats are computed locally from the diff.',
142
+ ],
143
+ crossToolFlow: [
144
+ 'Use before deep review to decide whether Pro analysis is needed.',
145
+ ],
146
+ },
147
+ {
148
+ name: 'generate_test_plan',
149
+ purpose: 'Generate prioritized test cases and coverage guidance.',
150
+ model: FLASH_MODEL,
151
+ timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
152
+ thinkingLevel: FLASH_THINKING_LEVEL,
153
+ maxOutputTokens: FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS,
154
+ temperature: CREATIVE_TEMPERATURE,
155
+ deterministicJson: true,
156
+ params: cloneParams(REPOSITORY_PARAM, LANGUAGE_PARAM, TEST_FRAMEWORK_PARAM, MAX_TEST_CASES_PARAM),
157
+ outputShape: '{summary, testCases[], coverageSummary}',
158
+ gotchas: [
159
+ 'Requires generate_diff to be called first.',
160
+ 'maxTestCases caps output after generation.',
161
+ ],
162
+ crossToolFlow: ['Pair with review tools to validate high-risk paths.'],
163
+ },
164
+ {
165
+ name: 'analyze_time_space_complexity',
166
+ purpose: 'Analyze Big-O complexity and detect degradations in changed code.',
167
+ model: FLASH_MODEL,
168
+ timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
169
+ thinkingLevel: FLASH_THINKING_LEVEL,
170
+ maxOutputTokens: FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS,
171
+ temperature: ANALYSIS_TEMPERATURE,
172
+ deterministicJson: true,
173
+ params: cloneParams(LANGUAGE_PARAM),
174
+ outputShape: '{timeComplexity, spaceComplexity, explanation, potentialBottlenecks[], isDegradation}',
175
+ gotchas: [
176
+ 'Requires generate_diff to be called first.',
177
+ 'Analyzes only changed code visible in the diff.',
178
+ ],
179
+ crossToolFlow: ['Use for algorithmic/performance-sensitive changes.'],
180
+ },
181
+ {
182
+ name: 'detect_api_breaking_changes',
183
+ purpose: 'Detect breaking API/interface changes in a diff.',
184
+ model: FLASH_MODEL,
185
+ timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
186
+ thinkingLevel: FLASH_TRIAGE_THINKING_LEVEL,
187
+ maxOutputTokens: FLASH_API_BREAKING_MAX_OUTPUT_TOKENS,
188
+ temperature: TRIAGE_TEMPERATURE,
189
+ deterministicJson: true,
190
+ params: cloneParams(LANGUAGE_PARAM),
191
+ outputShape: '{hasBreakingChanges, breakingChanges[]}',
192
+ gotchas: [
193
+ 'Requires generate_diff to be called first.',
194
+ 'Targets public API contracts over internal refactors.',
195
+ ],
196
+ crossToolFlow: ['Run before merge for API-surface-sensitive changes.'],
197
+ },
198
+ ];
199
+ const TOOL_CONTRACTS_BY_NAME = new Map(TOOL_CONTRACTS.map((contract) => [contract.name, contract]));
200
+ export function getToolContracts() {
201
+ return TOOL_CONTRACTS;
202
+ }
203
+ export function getToolContract(toolName) {
204
+ return TOOL_CONTRACTS_BY_NAME.get(toolName);
205
+ }
206
+ export function requireToolContract(toolName) {
207
+ const contract = getToolContract(toolName);
208
+ if (contract) {
209
+ return contract;
210
+ }
211
+ throw new Error(`Unknown tool contract: ${toolName}`);
212
+ }
213
+ export function getToolContractNames() {
214
+ return TOOL_CONTRACTS.map((contract) => contract.name);
215
+ }
216
+ const DEFAULT_SCHEMA_RETRIES = 1;
217
+ const geminiSchemaRetriesConfig = createCachedEnvInt('GEMINI_SCHEMA_RETRIES', DEFAULT_SCHEMA_RETRIES);
218
+ const DEFAULT_SCHEMA_RETRY_ERROR_CHARS = 1_500;
219
+ const schemaRetryErrorCharsConfig = createCachedEnvInt('MAX_SCHEMA_RETRY_ERROR_CHARS', DEFAULT_SCHEMA_RETRY_ERROR_CHARS);
220
+ const DEFAULT_TASK_TTL_MS = 300_000;
221
+ const taskTtlMsConfig = createCachedEnvInt('TASK_TTL_MS', DEFAULT_TASK_TTL_MS);
222
+ const DETERMINISTIC_JSON_RETRY_NOTE = 'Deterministic JSON mode: keep key names exactly as schema-defined and preserve stable field ordering.';
223
+ const COMPLETED_STATUS_PREFIX = 'completed: ';
224
+ const STALE_DIFF_WARNING_PREFIX = '\n\nWarning: The analyzed diff is over ';
225
+ const STALE_DIFF_WARNING_SUFFIX = ' minutes old. If you have made recent changes, please run generate_diff again.';
226
+ const JSON_PARSE_ERROR_PATTERN = /model produced invalid json/i;
227
+ const responseSchemaCache = new WeakMap();
228
+ function buildToolAnnotations(annotations) {
229
+ if (!annotations) {
230
+ return {
231
+ readOnlyHint: true,
232
+ idempotentHint: true,
233
+ openWorldHint: true,
234
+ };
235
+ }
236
+ const { destructiveHint, ...annotationOverrides } = annotations;
237
+ return {
238
+ readOnlyHint: !destructiveHint,
239
+ idempotentHint: !destructiveHint,
240
+ openWorldHint: true,
241
+ ...annotationOverrides,
242
+ };
243
+ }
244
+ function createGeminiResponseSchema(config) {
245
+ const sourceSchema = config.geminiSchema ?? config.resultSchema;
246
+ return stripJsonSchemaConstraints(z.toJSONSchema(sourceSchema));
247
+ }
248
+ function getCachedGeminiResponseSchema(config) {
249
+ const cached = responseSchemaCache.get(config);
250
+ if (cached) {
251
+ return cached;
252
+ }
253
+ const responseSchema = createGeminiResponseSchema({
254
+ geminiSchema: config.geminiSchema,
255
+ resultSchema: config.resultSchema,
256
+ });
257
+ responseSchemaCache.set(config, responseSchema);
258
+ return responseSchema;
259
+ }
260
+ function parseToolInput(input, fullInputSchema) {
261
+ return fullInputSchema.parse(input);
262
+ }
263
+ function extractResponseKeyOrdering(responseSchema) {
264
+ const schemaType = responseSchema.type;
265
+ if (schemaType !== 'object') {
266
+ return undefined;
267
+ }
268
+ const { properties } = responseSchema;
269
+ if (typeof properties !== 'object' || properties === null) {
270
+ return undefined;
271
+ }
272
+ return Object.keys(properties);
273
+ }
274
+ export function summarizeSchemaValidationErrorForRetry(errorMessage) {
275
+ const maxChars = Math.max(200, schemaRetryErrorCharsConfig.get());
276
+ const compact = errorMessage.replace(/\s+/g, ' ').trim();
277
+ if (compact.length <= maxChars) {
278
+ return compact;
279
+ }
280
+ return `${compact.slice(0, maxChars - 3)}...`;
281
+ }
282
+ function createSchemaRetryPrompt(prompt, errorMessage, deterministicJson) {
283
+ const summarizedError = summarizeSchemaValidationErrorForRetry(errorMessage);
284
+ const deterministicNote = deterministicJson
285
+ ? `\n${DETERMINISTIC_JSON_RETRY_NOTE}`
286
+ : '';
287
+ return {
288
+ summarizedError,
289
+ prompt: `CRITICAL: The previous response failed schema validation. Error: ${summarizedError}${deterministicNote}\n\n${prompt}`,
290
+ };
291
+ }
292
+ function isRetryableSchemaError(error) {
293
+ const isZodError = error instanceof z.ZodError;
294
+ return isZodError || JSON_PARSE_ERROR_PATTERN.test(getErrorMessage(error));
295
+ }
296
+ function createGenerationRequest(config, promptParts, responseSchema, onLog, signal) {
297
+ const request = {
298
+ systemInstruction: promptParts.systemInstruction,
299
+ prompt: promptParts.prompt,
300
+ responseSchema,
301
+ onLog,
302
+ ...(config.thinkingLevel !== undefined
303
+ ? { thinkingLevel: config.thinkingLevel }
304
+ : {}),
305
+ ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
306
+ ...(config.maxOutputTokens !== undefined
307
+ ? { maxOutputTokens: config.maxOutputTokens }
308
+ : {}),
309
+ ...(config.temperature !== undefined
310
+ ? { temperature: config.temperature }
311
+ : {}),
312
+ ...(config.includeThoughts !== undefined
313
+ ? { includeThoughts: config.includeThoughts }
314
+ : {}),
315
+ ...(config.batchMode !== undefined ? { batchMode: config.batchMode } : {}),
316
+ ...(signal !== undefined ? { signal } : {}),
317
+ };
318
+ if (config.deterministicJson) {
319
+ const responseKeyOrdering = extractResponseKeyOrdering(responseSchema);
320
+ if (responseKeyOrdering !== undefined) {
321
+ request.responseKeyOrdering = responseKeyOrdering;
322
+ }
323
+ }
324
+ return request;
325
+ }
326
+ function appendStaleDiffWarning(textContent, diffSlot) {
327
+ if (!diffSlot) {
328
+ return textContent;
329
+ }
330
+ const ageMs = Date.now() - new Date(diffSlot.generatedAt).getTime();
331
+ if (ageMs <= diffStaleWarningMs.get()) {
332
+ return textContent;
333
+ }
334
+ const ageMinutes = Math.round(ageMs / 60_000);
335
+ const warning = `${STALE_DIFF_WARNING_PREFIX}${ageMinutes}${STALE_DIFF_WARNING_SUFFIX}`;
336
+ return textContent ? textContent + warning : warning;
337
+ }
338
+ function toLoggingLevel(level) {
339
+ switch (level) {
340
+ case 'debug':
341
+ case 'info':
342
+ case 'notice':
343
+ case 'warning':
344
+ case 'error':
345
+ case 'critical':
346
+ case 'alert':
347
+ case 'emergency':
348
+ return level;
349
+ default:
350
+ return 'error';
351
+ }
352
+ }
353
+ function asObjectRecord(value) {
354
+ if (typeof value === 'object' && value !== null) {
355
+ return value;
356
+ }
357
+ return { payload: value };
358
+ }
359
+ async function safeSendProgress(extra, toolName, context, current, state) {
360
+ try {
361
+ await sendSingleStepProgress(extra, toolName, context, current, state);
362
+ }
363
+ catch {
364
+ // Progress is best-effort; tool execution must not fail on notification errors.
365
+ }
366
+ }
367
+ export function wrapToolHandler(options, handler) {
368
+ return async (input, extra) => {
369
+ const context = normalizeProgressContext(options.progressContext?.(input));
370
+ await safeSendProgress(extra, options.toolName, context, 0, 'starting');
371
+ try {
372
+ const result = await handler(input, extra);
373
+ const outcome = result.isError ? 'failed' : 'completed';
374
+ await safeSendProgress(extra, options.toolName, context, 1, outcome);
375
+ return result;
376
+ }
377
+ catch (error) {
378
+ const errorMessage = getErrorMessage(error);
379
+ const failureMeta = classifyErrorMeta(error, errorMessage);
380
+ const outcome = failureMeta.kind === 'cancelled' ? 'cancelled' : 'failed';
381
+ await safeSendProgress(extra, options.toolName, context, 1, outcome);
382
+ throw error;
383
+ }
384
+ };
385
+ }
386
+ async function validateRequest(config, inputRecord, ctx) {
387
+ if (config.requiresDiff) {
388
+ if (!ctx.diffSlot) {
389
+ return createNoDiffError();
390
+ }
391
+ const budgetError = validateDiffBudget(ctx.diffSlot.diff);
392
+ if (budgetError) {
393
+ return budgetError;
394
+ }
395
+ }
396
+ if (config.validateInput) {
397
+ return await config.validateInput(inputRecord, ctx);
398
+ }
399
+ return undefined;
400
+ }
401
+ export class ToolExecutionRunner {
402
+ config;
403
+ signal;
404
+ diffSlotSnapshot;
405
+ hasSnapshot = false;
406
+ responseSchema;
407
+ onLog;
408
+ reporter;
409
+ constructor(config, dependencies, signal) {
410
+ this.config = config;
411
+ this.signal = signal;
412
+ this.responseSchema = getCachedGeminiResponseSchema(config);
413
+ // Initialize reporter with placeholder context; updated in run()
414
+ this.reporter = new RunReporter(config.name, dependencies.reportProgress, dependencies.statusReporter, DEFAULT_PROGRESS_CONTEXT);
415
+ this.onLog = async (level, data) => {
416
+ try {
417
+ await dependencies.onLog(level, data);
418
+ }
419
+ catch {
420
+ // Ignore logging failures
421
+ }
422
+ await this.handleInternalLog(data);
423
+ };
424
+ }
425
+ async handleInternalLog(data) {
426
+ const record = asObjectRecord(data);
427
+ if (record.event === 'gemini_retry') {
428
+ const details = asObjectRecord(record.details);
429
+ const { attempt } = details;
430
+ const msg = `Network error. Retrying (attempt ${String(attempt)})...`;
431
+ await this.reporter.reportStep(STEP_CALLING_MODEL, msg);
432
+ }
433
+ else if (record.event === 'gemini_queue_acquired') {
434
+ const msg = 'Model queue acquired, generating response...';
435
+ await this.reporter.reportStep(STEP_CALLING_MODEL, msg);
436
+ }
437
+ }
438
+ setResponseSchemaOverride(responseSchema) {
439
+ this.responseSchema = responseSchema;
440
+ responseSchemaCache.set(this.config, responseSchema);
441
+ }
442
+ setDiffSlotSnapshot(diffSlotSnapshot) {
443
+ this.diffSlotSnapshot = diffSlotSnapshot;
444
+ this.hasSnapshot = true;
445
+ }
446
+ async executeValidation(inputRecord, ctx) {
447
+ const validationError = await validateRequest(this.config, inputRecord, ctx);
448
+ if (validationError) {
449
+ const validationMessage = extractValidationMessage(validationError);
450
+ await this.reporter.updateStatus(validationMessage);
451
+ await this.reporter.reportCompletion('rejected');
452
+ await this.reporter.storeResultSafely('completed', validationError, this.onLog);
453
+ return validationError;
454
+ }
455
+ return undefined;
456
+ }
457
+ async executeModelCallAttempt(systemInstruction, prompt, attempt) {
458
+ const raw = await generateStructuredJson(createGenerationRequest(this.config, { systemInstruction, prompt }, this.responseSchema, this.onLog, this.signal));
459
+ if (attempt === 0) {
460
+ await this.reporter.reportStep(STEP_VALIDATING_RESPONSE, 'Verifying output structure...');
461
+ }
462
+ return this.config.resultSchema.parse(raw);
463
+ }
464
+ async executeModelCall(systemInstruction, prompt) {
465
+ let retryPrompt = prompt;
466
+ const maxRetries = this.config.schemaRetries ?? geminiSchemaRetriesConfig.get();
467
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
468
+ try {
469
+ return await this.executeModelCallAttempt(systemInstruction, retryPrompt, attempt);
470
+ }
471
+ catch (error) {
472
+ if (attempt >= maxRetries || !isRetryableSchemaError(error)) {
473
+ throw error;
474
+ }
475
+ const errorMessage = getErrorMessage(error);
476
+ const schemaRetryPrompt = createSchemaRetryPrompt(prompt, errorMessage, this.config.deterministicJson === true);
477
+ await this.onLog('warning', {
478
+ event: 'schema_validation_failed',
479
+ details: {
480
+ attempt,
481
+ error: schemaRetryPrompt.summarizedError,
482
+ originalChars: errorMessage.length,
483
+ },
484
+ });
485
+ await this.reporter.reportSchemaRetry(attempt + 1, maxRetries);
486
+ retryPrompt = schemaRetryPrompt.prompt;
487
+ }
488
+ }
489
+ throw new Error('Unexpected state: execution loop exhausted');
490
+ }
491
+ createExecutionContext() {
492
+ return {
493
+ diffSlot: this.hasSnapshot ? this.diffSlotSnapshot : getDiff(),
494
+ };
495
+ }
496
+ applyResultTransform(inputRecord, parsed, ctx) {
497
+ return (this.config.transformResult
498
+ ? this.config.transformResult(inputRecord, parsed, ctx)
499
+ : parsed);
500
+ }
501
+ formatResultText(finalResult, ctx) {
502
+ const textContent = this.config.formatOutput
503
+ ? this.config.formatOutput(finalResult)
504
+ : undefined;
505
+ return appendStaleDiffWarning(textContent, ctx.diffSlot);
506
+ }
507
+ async finalizeSuccessfulRun(finalResult, textContent) {
508
+ const outcome = this.config.formatOutcome?.(finalResult) ?? 'completed';
509
+ await this.reporter.reportCompletion(outcome);
510
+ await this.reporter.updateStatus(`${COMPLETED_STATUS_PREFIX}${outcome}`);
511
+ const successResponse = createToolResponse({
512
+ ok: true,
513
+ result: finalResult,
514
+ }, textContent);
515
+ await this.reporter.storeResultSafely('completed', successResponse, this.onLog);
516
+ return successResponse;
517
+ }
518
+ async handleRunFailure(error) {
519
+ const errorMessage = getErrorMessage(error);
520
+ const errorMeta = classifyErrorMeta(error, errorMessage);
521
+ const outcome = errorMeta.kind === 'cancelled' ? 'cancelled' : 'failed';
522
+ await this.reporter.updateStatus(createFailureStatusMessage(outcome, errorMessage));
523
+ const errorResponse = createErrorToolResponse(this.config.errorCode, errorMessage, undefined, errorMeta);
524
+ await this.reporter.storeResultSafely('failed', errorResponse, this.onLog);
525
+ await this.reporter.reportCompletion(outcome);
526
+ return errorResponse;
527
+ }
528
+ async run(input) {
529
+ try {
530
+ await this.reporter.reportStep(STEP_STARTING, 'Initializing...');
531
+ const inputRecord = parseToolInput(input, this.config.fullInputSchema);
532
+ const newContext = normalizeProgressContext(this.config.progressContext?.(inputRecord));
533
+ this.reporter.updateContext(newContext);
534
+ const ctx = this.createExecutionContext();
535
+ await this.reporter.reportStep(STEP_VALIDATING, 'Validating request parameters...');
536
+ const validationError = await this.executeValidation(inputRecord, ctx);
537
+ if (validationError) {
538
+ return validationError;
539
+ }
540
+ await this.reporter.reportStep(STEP_BUILDING_PROMPT, 'Constructing analysis context...');
541
+ const promptParts = this.config.buildPrompt(inputRecord, ctx);
542
+ const { prompt, systemInstruction } = promptParts;
543
+ await this.reporter.reportStep(STEP_CALLING_MODEL, 'Querying Gemini model...');
544
+ const parsed = await this.executeModelCall(systemInstruction, prompt);
545
+ await this.reporter.reportStep(STEP_FINALIZING, 'Processing results...');
546
+ const finalResult = this.applyResultTransform(inputRecord, parsed, ctx);
547
+ const textContent = this.formatResultText(finalResult, ctx);
548
+ return await this.finalizeSuccessfulRun(finalResult, textContent);
549
+ }
550
+ catch (error) {
551
+ return await this.handleRunFailure(error);
552
+ }
553
+ }
554
+ }
555
+ function createGeminiLogger(server) {
556
+ return async (level, data) => {
557
+ try {
558
+ await server.sendLoggingMessage({
559
+ level: toLoggingLevel(level),
560
+ logger: 'gemini',
561
+ data: asObjectRecord(data),
562
+ });
563
+ }
564
+ catch {
565
+ // Fallback if logging fails
566
+ }
567
+ };
568
+ }
569
+ function createTaskStatusReporter(taskId, extra, extendedStore) {
570
+ return {
571
+ updateStatus: async (message) => {
572
+ await extendedStore.updateTaskStatus(taskId, 'working', message);
573
+ },
574
+ storeResult: async (status, result) => {
575
+ await extra.taskStore.storeTaskResult(taskId, status, result);
576
+ },
577
+ };
578
+ }
579
+ function runToolTaskInBackground(runner, input, taskId, extendedStore, signal) {
580
+ runner.run(input).catch(async (error) => {
581
+ const isAbort = error != null &&
582
+ typeof error === 'object' &&
583
+ 'name' in error &&
584
+ error.name === 'AbortError';
585
+ const isCancelled = (signal?.aborted ?? false) || isAbort;
586
+ try {
587
+ await extendedStore.updateTaskStatus(taskId, isCancelled ? 'cancelled' : 'failed', getErrorMessage(error));
588
+ }
589
+ catch {
590
+ // Status update failed — nothing more we can do
591
+ }
592
+ });
593
+ }
594
+ export function registerStructuredToolTask(server, config) {
595
+ const responseSchema = createGeminiResponseSchema({
596
+ geminiSchema: config.geminiSchema,
597
+ resultSchema: config.resultSchema,
598
+ });
599
+ responseSchemaCache.set(config, responseSchema);
600
+ server.experimental.tasks.registerToolTask(config.name, {
601
+ title: config.title,
602
+ description: config.description,
603
+ inputSchema: config.inputSchema,
604
+ outputSchema: DefaultOutputSchema,
605
+ annotations: buildToolAnnotations(config.annotations),
606
+ execution: {
607
+ taskSupport: 'optional',
608
+ },
609
+ }, {
610
+ createTask: async (input, extra) => {
611
+ const task = await extra.taskStore.createTask({
612
+ ttl: taskTtlMsConfig.get(),
613
+ });
614
+ const extendedStore = extra.taskStore;
615
+ const runner = new ToolExecutionRunner(config, {
616
+ onLog: createGeminiLogger(server),
617
+ reportProgress: getOrCreateProgressReporter(extra),
618
+ statusReporter: createTaskStatusReporter(task.taskId, extra, extendedStore),
619
+ }, extra.signal);
620
+ runToolTaskInBackground(runner, input, task.taskId, extendedStore, extra.signal);
621
+ return { task };
622
+ },
623
+ getTask: async (input, extra) => {
624
+ return await extra.taskStore.getTask(extra.taskId);
625
+ },
626
+ getTaskResult: async (input, extra) => {
627
+ return (await extra.taskStore.getTaskResult(extra.taskId));
628
+ },
629
+ });
630
+ }
631
+ const EMPTY_PARSED_FILES = [];
632
+ export function getDiffContextSnapshot(ctx) {
633
+ const slot = ctx.diffSlot;
634
+ if (!slot) {
635
+ return {
636
+ diff: '',
637
+ parsedFiles: EMPTY_PARSED_FILES,
638
+ stats: EMPTY_DIFF_STATS,
639
+ };
640
+ }
641
+ return {
642
+ diff: slot.diff,
643
+ parsedFiles: slot.parsedFiles,
644
+ stats: slot.stats,
645
+ };
646
+ }
@@ -0,0 +1,11 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare const PROMPT_DEFINITIONS: readonly [{
3
+ readonly name: "get-help";
4
+ readonly title: "Get Help";
5
+ readonly description: "Server instructions.";
6
+ }, {
7
+ readonly name: "review-guide";
8
+ readonly title: "Review Guide";
9
+ readonly description: "Workflow guide for tool/focus area.";
10
+ }];
11
+ export declare function registerAllPrompts(server: McpServer, instructions: string): void;