@contextrail/code-review-agent 0.1.1-alpha.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 (84) hide show
  1. package/LICENSE +26 -0
  2. package/MODEL_RECOMMENDATIONS.md +178 -0
  3. package/README.md +177 -0
  4. package/dist/config/defaults.d.ts +72 -0
  5. package/dist/config/defaults.js +113 -0
  6. package/dist/config/index.d.ts +34 -0
  7. package/dist/config/index.js +89 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +603 -0
  10. package/dist/llm/factory.d.ts +21 -0
  11. package/dist/llm/factory.js +50 -0
  12. package/dist/llm/index.d.ts +3 -0
  13. package/dist/llm/index.js +2 -0
  14. package/dist/llm/service.d.ts +38 -0
  15. package/dist/llm/service.js +191 -0
  16. package/dist/llm/types.d.ts +119 -0
  17. package/dist/llm/types.js +1 -0
  18. package/dist/logging/logger.d.ts +9 -0
  19. package/dist/logging/logger.js +52 -0
  20. package/dist/mcp/client.d.ts +429 -0
  21. package/dist/mcp/client.js +173 -0
  22. package/dist/mcp/mcp-tools.d.ts +292 -0
  23. package/dist/mcp/mcp-tools.js +40 -0
  24. package/dist/mcp/token-validation.d.ts +31 -0
  25. package/dist/mcp/token-validation.js +57 -0
  26. package/dist/mcp/tools-provider.d.ts +18 -0
  27. package/dist/mcp/tools-provider.js +24 -0
  28. package/dist/observability/index.d.ts +2 -0
  29. package/dist/observability/index.js +1 -0
  30. package/dist/observability/metrics.d.ts +48 -0
  31. package/dist/observability/metrics.js +86 -0
  32. package/dist/orchestrator/agentic-orchestrator.d.ts +29 -0
  33. package/dist/orchestrator/agentic-orchestrator.js +136 -0
  34. package/dist/orchestrator/prompts.d.ts +25 -0
  35. package/dist/orchestrator/prompts.js +98 -0
  36. package/dist/orchestrator/validation.d.ts +2 -0
  37. package/dist/orchestrator/validation.js +7 -0
  38. package/dist/orchestrator/writer.d.ts +4 -0
  39. package/dist/orchestrator/writer.js +17 -0
  40. package/dist/output/aggregator.d.ts +30 -0
  41. package/dist/output/aggregator.js +132 -0
  42. package/dist/output/prompts.d.ts +32 -0
  43. package/dist/output/prompts.js +153 -0
  44. package/dist/output/schema.d.ts +1515 -0
  45. package/dist/output/schema.js +224 -0
  46. package/dist/output/writer.d.ts +31 -0
  47. package/dist/output/writer.js +120 -0
  48. package/dist/review-inputs/chunking.d.ts +29 -0
  49. package/dist/review-inputs/chunking.js +113 -0
  50. package/dist/review-inputs/diff-summary.d.ts +52 -0
  51. package/dist/review-inputs/diff-summary.js +83 -0
  52. package/dist/review-inputs/file-patterns.d.ts +40 -0
  53. package/dist/review-inputs/file-patterns.js +182 -0
  54. package/dist/review-inputs/filtering.d.ts +31 -0
  55. package/dist/review-inputs/filtering.js +53 -0
  56. package/dist/review-inputs/git-diff-provider.d.ts +2 -0
  57. package/dist/review-inputs/git-diff-provider.js +42 -0
  58. package/dist/review-inputs/index.d.ts +46 -0
  59. package/dist/review-inputs/index.js +122 -0
  60. package/dist/review-inputs/path-validation.d.ts +10 -0
  61. package/dist/review-inputs/path-validation.js +37 -0
  62. package/dist/review-inputs/surrounding-context.d.ts +35 -0
  63. package/dist/review-inputs/surrounding-context.js +180 -0
  64. package/dist/review-inputs/triage.d.ts +57 -0
  65. package/dist/review-inputs/triage.js +81 -0
  66. package/dist/reviewers/executor.d.ts +41 -0
  67. package/dist/reviewers/executor.js +357 -0
  68. package/dist/reviewers/findings-merge.d.ts +9 -0
  69. package/dist/reviewers/findings-merge.js +131 -0
  70. package/dist/reviewers/iteration.d.ts +17 -0
  71. package/dist/reviewers/iteration.js +95 -0
  72. package/dist/reviewers/persistence.d.ts +17 -0
  73. package/dist/reviewers/persistence.js +55 -0
  74. package/dist/reviewers/progress-tracker.d.ts +115 -0
  75. package/dist/reviewers/progress-tracker.js +194 -0
  76. package/dist/reviewers/prompt.d.ts +42 -0
  77. package/dist/reviewers/prompt.js +246 -0
  78. package/dist/reviewers/tool-call-tracker.d.ts +18 -0
  79. package/dist/reviewers/tool-call-tracker.js +40 -0
  80. package/dist/reviewers/types.d.ts +12 -0
  81. package/dist/reviewers/types.js +1 -0
  82. package/dist/reviewers/validation-rules.d.ts +27 -0
  83. package/dist/reviewers/validation-rules.js +189 -0
  84. package/package.json +79 -0
@@ -0,0 +1,2 @@
1
+ export { LlmService } from './service.js';
2
+ export { createLlmService, createOpenRouterModelProvider } from './factory.js';
@@ -0,0 +1,38 @@
1
+ import { type ModelMessage } from 'ai';
2
+ import type { z } from 'zod';
3
+ import type { LlmCallResult, LlmCallConfig, ModelProvider, ToolsProvider, LlmObservabilityHooks } from './types.js';
4
+ import type { Logger } from '../logging/logger.js';
5
+ /**
6
+ * LLM Service abstraction layer.
7
+ * Provides a predictable contract for LLM calls with built-in observability.
8
+ */
9
+ export declare class LlmService {
10
+ private modelProvider;
11
+ private toolsProvider?;
12
+ private hooks?;
13
+ private logger?;
14
+ constructor(config: {
15
+ modelProvider: ModelProvider;
16
+ toolsProvider?: ToolsProvider;
17
+ hooks?: LlmObservabilityHooks;
18
+ logger?: Logger;
19
+ });
20
+ /**
21
+ * Execute an LLM call with structured output.
22
+ *
23
+ * @param messages - Conversation messages
24
+ * @param schema - Zod schema for structured output
25
+ * @param config - LLM call configuration
26
+ * @returns Structured output with usage metrics and metadata
27
+ */
28
+ generateStructuredOutput<T extends z.ZodType>(messages: ModelMessage[], schema: T, config: LlmCallConfig): Promise<LlmCallResult<z.infer<T>>>;
29
+ /**
30
+ * Create a new LLM service instance with updated configuration.
31
+ * Useful for creating service instances with different tools or hooks.
32
+ */
33
+ withConfig(config: {
34
+ toolsProvider?: ToolsProvider;
35
+ hooks?: LlmObservabilityHooks;
36
+ logger?: Logger;
37
+ }): LlmService;
38
+ }
@@ -0,0 +1,191 @@
1
+ import { generateText, Output, stepCountIs } from 'ai';
2
+ import { DEFAULT_TEMPERATURE } from '../config/defaults.js';
3
+ /**
4
+ * LLM Service abstraction layer.
5
+ * Provides a predictable contract for LLM calls with built-in observability.
6
+ */
7
+ export class LlmService {
8
+ modelProvider;
9
+ toolsProvider;
10
+ hooks;
11
+ logger;
12
+ constructor(config) {
13
+ this.modelProvider = config.modelProvider;
14
+ this.toolsProvider = config.toolsProvider;
15
+ this.hooks = config.hooks;
16
+ this.logger = config.logger;
17
+ }
18
+ /**
19
+ * Execute an LLM call with structured output.
20
+ *
21
+ * @param messages - Conversation messages
22
+ * @param schema - Zod schema for structured output
23
+ * @param config - LLM call configuration
24
+ * @returns Structured output with usage metrics and metadata
25
+ */
26
+ async generateStructuredOutput(messages, schema, config) {
27
+ const { model, maxSteps = 10, metadata, temperature = DEFAULT_TEMPERATURE, topK, topP, toolChoice, activeTools,
28
+ // maxTokens and timeout are available in config but handled by provider/OpenRouter
29
+ // They're included for future extensibility and documentation
30
+ } = config;
31
+ // Get tools if provider is available
32
+ let tools = this.toolsProvider?.();
33
+ // Filter tools if activeTools is specified
34
+ if (tools && activeTools && activeTools.length > 0) {
35
+ // Filter tool set to only include specified tools
36
+ const filteredTools = {};
37
+ for (const toolName of activeTools) {
38
+ if (tools && toolName in tools) {
39
+ filteredTools[toolName] = tools[toolName];
40
+ }
41
+ }
42
+ const filteredKeys = Object.keys(filteredTools);
43
+ if (filteredKeys.length > 0) {
44
+ // Type assertion needed because we're filtering the tool set
45
+ tools = filteredTools;
46
+ this.logger?.debug(`[LLM] Filtered tools to: ${filteredKeys.join(', ')}`);
47
+ }
48
+ else {
49
+ // No matching tools found, disable tools
50
+ tools = undefined;
51
+ this.logger?.warn(`[LLM] No matching tools found for activeTools: ${activeTools.join(', ')}`);
52
+ }
53
+ }
54
+ // Call observability hook
55
+ this.hooks?.onCallStart?.(metadata);
56
+ const debugParams = [`model: ${model}`, `temperature: ${temperature}`];
57
+ if (topK !== undefined)
58
+ debugParams.push(`topK: ${topK}`);
59
+ if (topP !== undefined)
60
+ debugParams.push(`topP: ${topP}`);
61
+ if (toolChoice) {
62
+ debugParams.push(`toolChoice: ${typeof toolChoice === 'string' ? toolChoice : toolChoice.toolName}`);
63
+ }
64
+ this.logger?.debug(`[LLM] Starting call: ${metadata.operation} (${debugParams.join(', ')})`);
65
+ try {
66
+ // Build generateText options with reliability settings for structured output
67
+ // Lower temperature (0-0.3) improves consistency and reliability for structured output
68
+ const outputConfig = Output.object({ schema });
69
+ // Debug: Log schema info (for Azure compatibility debugging)
70
+ // Note: The AI SDK converts Zod to JSON Schema internally and sends it to the provider
71
+ // Azure's validator may be strict about optional fields in the JSON Schema
72
+ this.logger?.debug(`[LLM] Using structured output schema for ${metadata.operation}`);
73
+ this.logger?.debug(`[LLM] Schema type: ${schema._def?.typeName ?? 'unknown'}`);
74
+ const generateOptions = {
75
+ model: this.modelProvider(model),
76
+ messages,
77
+ output: outputConfig,
78
+ stopWhen: stepCountIs(maxSteps),
79
+ temperature, // Lower temperature for more reliable structured output
80
+ ...(tools ? { tools } : {}), // Only include tools if they exist
81
+ };
82
+ // Add optional sampling parameters (alternative/complement to temperature)
83
+ if (topK !== undefined) {
84
+ generateOptions.topK = topK;
85
+ }
86
+ if (topP !== undefined) {
87
+ generateOptions.topP = topP;
88
+ }
89
+ // Add tool choice if specified
90
+ if (toolChoice) {
91
+ if (toolChoice === 'auto' || toolChoice === 'none' || toolChoice === 'required') {
92
+ generateOptions.toolChoice = toolChoice;
93
+ }
94
+ else if (toolChoice.type === 'tool') {
95
+ generateOptions.toolChoice = { type: 'tool', toolName: toolChoice.toolName };
96
+ }
97
+ }
98
+ const result = await generateText(generateOptions);
99
+ if (!result.output) {
100
+ // Enhanced debug logging to understand what the model actually returned
101
+ this.logger?.debug(`[LLM] No structured output received for ${metadata.operation}`);
102
+ this.logger?.debug(`[LLM] Model: ${model}, Operation: ${metadata.operation}`);
103
+ this.logger?.debug(`[LLM] Finish reason: ${result.finishReason ?? 'unknown'}`);
104
+ this.logger?.debug(`[LLM] Raw text response:`, result.text ?? '(no text)');
105
+ this.logger?.debug(`[LLM] Token usage:`, result.usage);
106
+ this.logger?.debug(`[LLM] Tool calls made:`, result.toolCalls?.length ?? 0);
107
+ if (result.toolCalls && result.toolCalls.length > 0) {
108
+ this.logger?.debug(`[LLM] Tool calls details:`, result.toolCalls.map((call) => ({
109
+ toolName: call.toolName,
110
+ input: call.input,
111
+ })));
112
+ }
113
+ // Log schema description if available
114
+ const schemaDescription = schema.description ?? 'No description';
115
+ this.logger?.debug(`[LLM] Expected schema description: ${schemaDescription}`);
116
+ this.logger?.debug(`[LLM] Full result object (sanitized):`, {
117
+ hasOutput: !!result.output,
118
+ hasText: !!result.text,
119
+ finishReason: result.finishReason,
120
+ usage: result.usage,
121
+ toolCallsCount: result.toolCalls?.length ?? 0,
122
+ warnings: result.warnings ?? [],
123
+ });
124
+ throw new Error(`[LLM] Failed to generate structured output for ${metadata.operation}`);
125
+ }
126
+ // Parse and validate output
127
+ // Note: result.output should exist here due to check above, but TypeScript needs assertion
128
+ if (!result.output) {
129
+ // This should never happen due to check above, but handle defensively
130
+ throw new Error(`[LLM] Structured output validation failed: output is null/undefined`);
131
+ }
132
+ const parsed = schema.parse(result.output);
133
+ // Extract token usage
134
+ const usage = result.usage
135
+ ? {
136
+ promptTokens: result.usage.inputTokens ?? 0,
137
+ completionTokens: result.usage.outputTokens ?? 0,
138
+ totalTokens: result.usage.totalTokens ??
139
+ (result.usage.inputTokens ?? 0) + (result.usage.outputTokens ?? 0),
140
+ }
141
+ : undefined;
142
+ // Extract tool calls
143
+ const toolCalls = Array.isArray(result.toolCalls)
144
+ ? result.toolCalls.map((call) => ({
145
+ toolName: call.toolName,
146
+ input: call.input,
147
+ }))
148
+ : undefined;
149
+ // Call observability hooks
150
+ this.hooks?.onCallComplete?.(metadata, usage);
151
+ if (toolCalls) {
152
+ for (const call of toolCalls) {
153
+ this.hooks?.onToolCall?.(metadata, call.toolName, call.input);
154
+ }
155
+ }
156
+ this.logger?.debug(`[LLM] Completed call: ${metadata.operation} (tokens: ${usage?.totalTokens ?? 'unknown'})`);
157
+ return {
158
+ output: parsed,
159
+ usage,
160
+ toolCalls,
161
+ metadata,
162
+ };
163
+ }
164
+ catch (error) {
165
+ const err = error instanceof Error ? error : new Error(String(error));
166
+ // Enhanced error logging for debugging
167
+ if (error && typeof error === 'object' && 'text' in error) {
168
+ const errorText = error.text;
169
+ this.logger?.error(`[LLM] Model returned empty or invalid response for ${metadata.operation}`);
170
+ this.logger?.error(`[LLM] Model: ${model}`);
171
+ this.logger?.error(`[LLM] Response text: ${errorText || '(empty)'}`);
172
+ this.logger?.error(`[LLM] This may indicate the model doesn't support structured output well. Consider trying a different model.`);
173
+ }
174
+ this.hooks?.onCallError?.(metadata, err);
175
+ this.logger?.error(`[LLM] Call failed: ${metadata.operation}`, err);
176
+ throw err;
177
+ }
178
+ }
179
+ /**
180
+ * Create a new LLM service instance with updated configuration.
181
+ * Useful for creating service instances with different tools or hooks.
182
+ */
183
+ withConfig(config) {
184
+ return new LlmService({
185
+ modelProvider: this.modelProvider,
186
+ toolsProvider: config.toolsProvider ?? this.toolsProvider,
187
+ hooks: config.hooks ?? this.hooks,
188
+ logger: config.logger ?? this.logger,
189
+ });
190
+ }
191
+ }
@@ -0,0 +1,119 @@
1
+ import type { LanguageModel, ToolSet } from 'ai';
2
+ /**
3
+ * Token usage metrics for a single LLM call.
4
+ */
5
+ export type TokenUsage = {
6
+ promptTokens: number;
7
+ completionTokens: number;
8
+ totalTokens: number;
9
+ };
10
+ /**
11
+ * Metadata for an LLM call operation.
12
+ */
13
+ export type LlmCallMetadata = {
14
+ operation: string;
15
+ model: string;
16
+ iteration?: number;
17
+ reviewer?: string;
18
+ [key: string]: unknown;
19
+ };
20
+ /**
21
+ * Result from an LLM call with structured output.
22
+ */
23
+ export type LlmCallResult<T> = {
24
+ output: T;
25
+ usage?: TokenUsage;
26
+ toolCalls?: Array<{
27
+ toolName: string;
28
+ input?: unknown;
29
+ }>;
30
+ metadata: LlmCallMetadata;
31
+ };
32
+ /**
33
+ * Tool choice strategy for tool calling.
34
+ */
35
+ export type ToolChoice = 'auto' | 'none' | 'required' | {
36
+ type: 'tool';
37
+ toolName: string;
38
+ };
39
+ /**
40
+ * Configuration for an LLM call.
41
+ */
42
+ export type LlmCallConfig = {
43
+ model: string;
44
+ maxSteps?: number;
45
+ metadata: LlmCallMetadata;
46
+ /**
47
+ * Temperature for generation (0-2).
48
+ * Lower values (0-0.3) make output more deterministic and reliable for structured output.
49
+ * Default: 0.2 for structured output reliability.
50
+ */
51
+ temperature?: number;
52
+ /**
53
+ * Top-K sampling: consider only top K most likely tokens.
54
+ * Lower values (1-10) make output more deterministic.
55
+ * Use with topP for nucleus sampling, or alone as alternative to temperature.
56
+ */
57
+ topK?: number;
58
+ /**
59
+ * Top-P (nucleus) sampling: consider tokens with cumulative probability up to P.
60
+ * Lower values (0.1-0.5) make output more deterministic.
61
+ * Often used with temperature for fine-grained control.
62
+ */
63
+ topP?: number;
64
+ /**
65
+ * Tool choice strategy.
66
+ * - 'auto': Model decides (default)
67
+ * - 'required': Force tool use (useful when tools are needed for structured output)
68
+ * - 'none': Don't use tools
69
+ * - { type: 'tool', toolName: string }: Force specific tool
70
+ */
71
+ toolChoice?: ToolChoice;
72
+ /**
73
+ * Limit which tools are available for this call.
74
+ * If not set, all tools from toolsProvider are available.
75
+ * Useful for restricting tool scope or debugging.
76
+ */
77
+ activeTools?: string[];
78
+ /**
79
+ * Maximum tokens to generate.
80
+ * If not set, uses model default. For structured output, ensure this is high enough.
81
+ */
82
+ maxTokens?: number;
83
+ /**
84
+ * Request timeout in milliseconds.
85
+ * Default: 120000 (2 minutes) for reliability.
86
+ */
87
+ timeout?: number;
88
+ };
89
+ /**
90
+ * Model provider factory function.
91
+ * Takes a model name and returns a LanguageModel instance.
92
+ */
93
+ export type ModelProvider = (modelName: string) => LanguageModel;
94
+ /**
95
+ * Tools provider factory function.
96
+ * Returns a ToolSet for the LLM call.
97
+ */
98
+ export type ToolsProvider = () => ToolSet | undefined;
99
+ /**
100
+ * Observability hooks for LLM calls.
101
+ */
102
+ export type LlmObservabilityHooks = {
103
+ /**
104
+ * Called before an LLM call starts.
105
+ */
106
+ onCallStart?: (metadata: LlmCallMetadata) => void;
107
+ /**
108
+ * Called after an LLM call completes successfully.
109
+ */
110
+ onCallComplete?: (metadata: LlmCallMetadata, usage?: TokenUsage) => void;
111
+ /**
112
+ * Called when an LLM call fails.
113
+ */
114
+ onCallError?: (metadata: LlmCallMetadata, error: Error) => void;
115
+ /**
116
+ * Called when tool calls are made during an LLM call.
117
+ */
118
+ onToolCall?: (metadata: LlmCallMetadata, toolName: string, input?: unknown) => void;
119
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
2
+ export type Logger = {
3
+ debug: (...args: unknown[]) => void;
4
+ info: (...args: unknown[]) => void;
5
+ warn: (...args: unknown[]) => void;
6
+ error: (...args: unknown[]) => void;
7
+ };
8
+ export declare const parseLogLevel: (value?: string) => LogLevel;
9
+ export declare const createLogger: (level: LogLevel) => Logger;
@@ -0,0 +1,52 @@
1
+ const LEVEL_ORDER = {
2
+ debug: 10,
3
+ info: 20,
4
+ warn: 30,
5
+ error: 40,
6
+ silent: 50,
7
+ };
8
+ export const parseLogLevel = (value) => {
9
+ if (!value) {
10
+ return 'info';
11
+ }
12
+ const normalized = value.trim().toLowerCase();
13
+ if (normalized === 'true') {
14
+ return 'debug';
15
+ }
16
+ if (normalized === 'false') {
17
+ return 'info';
18
+ }
19
+ if (normalized === 'debug' || normalized === 'info' || normalized === 'warn' || normalized === 'error') {
20
+ return normalized;
21
+ }
22
+ if (normalized === 'silent') {
23
+ return 'silent';
24
+ }
25
+ return 'info';
26
+ };
27
+ export const createLogger = (level) => {
28
+ const threshold = LEVEL_ORDER[level];
29
+ const shouldLog = (messageLevel) => LEVEL_ORDER[messageLevel] >= threshold;
30
+ return {
31
+ debug: (...args) => {
32
+ if (shouldLog('debug')) {
33
+ console.debug(...args);
34
+ }
35
+ },
36
+ info: (...args) => {
37
+ if (shouldLog('info')) {
38
+ console.info(...args);
39
+ }
40
+ },
41
+ warn: (...args) => {
42
+ if (shouldLog('warn')) {
43
+ console.warn(...args);
44
+ }
45
+ },
46
+ error: (...args) => {
47
+ if (shouldLog('error')) {
48
+ console.error(...args);
49
+ }
50
+ },
51
+ };
52
+ };