This commit is contained in:
2026-06-01 20:47:26 -05:00
parent 108b8243a9
commit eb7de776e4
5 changed files with 201 additions and 49 deletions
+36 -8
View File
@@ -1,6 +1,12 @@
import { Ollama } from "ollama";
import type { Message } from "ollama";
import type { DiffChunk, ChunkReview, ReviewResult, AggregatedReview, Finding } from "./types.ts";
import type {
DiffChunk,
ChunkReview,
ReviewResult,
AggregatedReview,
Finding,
} from "./types.ts";
import type { ToolServer } from "./mcp.ts";
const ollama = new Ollama({ host: process.env.OLLAMA_HOST });
@@ -21,7 +27,10 @@ When you are done gathering context and ready to report your findings, you MUST
Do not include any text outside the JSON object.`;
export function buildReviewPrompt(chunk: DiffChunk, userPrompt: string): string {
export function buildReviewPrompt(
chunk: DiffChunk,
userPrompt: string,
): string {
return `${userPrompt}
File: ${chunk.filename} (${chunk.language})
@@ -38,7 +47,8 @@ function parseReviewResult(content: string): ReviewResult | null {
.trim();
try {
const parsed = JSON.parse(stripped);
if (!Array.isArray(parsed.issues) || typeof parsed.summary !== "string") return null;
if (!Array.isArray(parsed.issues) || typeof parsed.summary !== "string")
return null;
return parsed as ReviewResult;
} catch {
return null;
@@ -52,6 +62,7 @@ export async function reviewChunk(
contextWindow: number,
mcpServer: ToolServer,
): Promise<ChunkReview> {
console.log(`Reviewing chunk from ${chunk.filename} with model ${model}...`);
mcpServer.resetCallCount();
const messages: Message[] = [
@@ -69,6 +80,7 @@ export async function reviewChunk(
tools: forceConclusion ? undefined : mcpServer.tools,
format: "json",
stream: false,
keep_alive: -1,
options: { num_ctx: contextWindow, temperature: 0.2 },
});
@@ -76,21 +88,30 @@ export async function reviewChunk(
// Model called one or more tools
if (msg.tool_calls && msg.tool_calls.length > 0) {
messages.push({ role: "assistant", content: msg.content ?? "", tool_calls: msg.tool_calls });
messages.push({
role: "assistant",
content: msg.content ?? "",
tool_calls: msg.tool_calls,
});
for (const call of msg.tool_calls) {
const result = await mcpServer.execute(
call.function.name,
call.function.arguments as Record<string, unknown>,
);
messages.push({ role: "tool", content: result, tool_name: call.function.name });
messages.push({
role: "tool",
content: result,
tool_name: call.function.name,
});
}
if (mcpServer.atLimit) {
forceConclusion = true;
messages.push({
role: "user",
content: "You have reached the tool call limit. Respond now with your final JSON review.",
content:
"You have reached the tool call limit. Respond now with your final JSON review.",
});
}
continue;
@@ -121,16 +142,23 @@ export async function reviewChunk(
// Second failure — return raw content as summary rather than crash
return {
chunk,
result: { issues: [], summary: `[parse error] ${content.slice(0, 500)}` },
result: {
issues: [],
summary: `[parse error] ${content.slice(0, 500)}`,
},
};
}
// Empty response — shouldn't happen, but bail rather than loop forever
return { chunk, result: { issues: [], summary: "[empty response from model]" } };
return {
chunk,
result: { issues: [], summary: "[empty response from model]" },
};
}
}
export function aggregateFindings(results: ChunkReview[]): AggregatedReview {
console.log(`Aggregating findings from ${results.length} reviewed chunks...`);
const findings: Finding[] = results.flatMap(({ chunk, result }) =>
result.issues.map((issue) => ({ ...issue, filename: chunk.filename })),
);