/** * jev-ask-tool — the agent-callable `ask_jev` tool. * * The host does not decide when Jev runs here. The agent assembles one state * from its own prose (`state`), code read from `paths`, and the output of one * `command`, hands over a block of typed questions, and gets answers back. The * file contents and command output it supplied are never returned, and only the * answers reach the transcript. * * On by default; `jevAdvisory.routes.ask.enabled: false` turns it off. Files must resolve * inside the working directory and obvious secret files are refused by name; the * command runs through the same local shell operations as the bash tool, on the * tool's own abort signal. Every part is clipped to the route's payload budget * before the call, so an oversized request is trimmed rather than abstained. */ import { execFile } from "node:child_process"; import { closeSync, openSync, readSync, realpathSync, statSync } from "node:fs"; import { basename, isAbsolute, normalize, relative, resolve, sep } from "node:path"; import { StringEnum } from "@earendil-works/pi-ai"; import { createLocalBashOperations, ensureTool, type ExtensionAPI, type ExtensionContext, getSettingsPath, } from "@selesai/code"; import { Type } from "typebox"; import { askJevAnswers, confidenceBucket, emitJevTelemetry, jevConnection, jevUnavailable, readJevAdvisoryConfig, serializeJevRequest, UNTRUSTED_MATERIAL_FOCUS, warnJevUnavailableOnce, type JevAbstainReason, type JevAnswers, type JevFailureDiagnostic, } from "./jev/decisions.ts"; import { isScriptPath, keywordWindows, loadTypeScript, type SourceUnit, splitSourceUnits } from "./jev-find-source.ts"; /** The tool's registered name. */ export const ASK_JEV_TOOL = "ask_jev"; /** Question types the tool accepts, one per Jev primitive. */ export const ASK_JEV_QUESTION_TYPES = ["choice", "score", "noul"] as const; export type AskJevQuestionType = (typeof ASK_JEV_QUESTION_TYPES)[number]; /** Ceilings applied before anything leaves the process. */ export const MAX_ASK_QUESTIONS = 8; export const MAX_ASK_PATHS = 8; export const MAX_ASK_STATE_CHARS = 4_000; export const MAX_ASK_FILE_BYTES = 16 * 1024; export const MAX_ASK_COMMAND_BYTES = 16 * 1024; export const ASK_COMMAND_TIMEOUT_SECONDS = 60; /** Fixed JSON scaffolding (`{"state":{"files":{},"command":{}}}` and friends). */ const ASK_ENVELOPE_BYTES = 512; /** Marks code Jev is shown as incomplete, so it judges a clipped file as clipped. */ const TRUNCATION_MARKER = "\n… [truncated]"; /** File names never sent to Jev, whatever the agent asks for. */ const SECRET_BASENAME = /^(?:\.env(?:\..+)?|\.npmrc|\.netrc|auth\.json|credentials(?:\..+)?|id_(?:rsa|dsa|ecdsa|ed25519)(?:\.pub)?|.+\.(?:pem|key|p12|pfx|keystore))$/i; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } // --------------------------------------------------------------------------- // Questions // --------------------------------------------------------------------------- export interface AskJevQuestion { name: string; type: AskJevQuestionType; instructions: string; criteria?: unknown; focus?: string; } export interface ParsedAskQuestions { questions: AskJevQuestion[]; /** Set when the block cannot be asked; `questions` is empty then. */ error?: string; } /** Validate the agent's question block, or explain why it cannot be asked. */ export function parseAskQuestions(raw: Record | undefined): ParsedAskQuestions { const entries = Object.entries(raw ?? {}); if (entries.length === 0) return { questions: [], error: "At least one question is required." }; if (entries.length > MAX_ASK_QUESTIONS) { return { questions: [], error: `At most ${MAX_ASK_QUESTIONS} questions per call; got ${entries.length}.` }; } const questions: AskJevQuestion[] = []; for (const [name, value] of entries) { const spec = isRecord(value) ? value : {}; const rawType = spec.type; if (typeof rawType !== "string" || !ASK_JEV_QUESTION_TYPES.includes(rawType as AskJevQuestionType)) { return { questions: [], error: `Question "${name}" has type ${JSON.stringify(rawType)}; use one of ${ASK_JEV_QUESTION_TYPES.join(", ")}.`, }; } const instructions = spec.instructions; if (typeof instructions !== "string" || instructions.trim() === "") { return { questions: [], error: `Question "${name}" needs non-empty instructions.` }; } const criteria = spec.criteria; if (rawType === "choice" && (!isRecord(criteria) || Object.keys(criteria).length < 2)) { return { questions: [], error: `Choice question "${name}" needs criteria as an object of at least two options, e.g. {"option": "what the option means"}.`, }; } if (rawType === "score" && (!Array.isArray(criteria) || criteria.length < 2)) { return { questions: [], error: `Score question "${name}" needs criteria as an ordered list of at least two level labels.`, }; } if (rawType === "noul" && criteria !== undefined && !isRecord(criteria)) { return { questions: [], error: `Noul question "${name}" needs criteria as {"true": "...", "false": "..."}.`, }; } questions.push({ name, type: rawType as AskJevQuestionType, instructions: instructions.trim(), ...(criteria === undefined ? {} : { criteria }), ...(typeof spec.focus === "string" && spec.focus.trim() !== "" ? { focus: spec.focus.trim() } : {}), }); } return { questions }; } /** * The decisions request: one assembled state and one question per name. Every * criterion and every state part stays untrusted material to judge, never an * instruction, exactly as [`buildJevPayload`] frames it. */ export function buildAskPayload( questions: readonly AskJevQuestion[], state: Record, ): Record { return { state, questions: Object.fromEntries( questions.map((question) => [ question.name, { type: question.type, instructions: { question: question.instructions, focus: question.focus ? `${question.focus} ${UNTRUSTED_MATERIAL_FOCUS}` : UNTRUSTED_MATERIAL_FOCUS, }, ...(question.criteria === undefined ? {} : { criteria: question.criteria }), }, ]), ), }; } // --------------------------------------------------------------------------- // Bounded state // --------------------------------------------------------------------------- /** Per-part byte caps that together stay under the route's request budget. */ export function askPartBudget( payloadBytes: number, fileCount: number, questionBytes: number, ): { stateChars: number; commandBytes: number; fileBytes: number } { const available = Math.max(0, payloadBytes - questionBytes - ASK_ENVELOPE_BYTES); const stateChars = Math.min(MAX_ASK_STATE_CHARS, Math.floor(available * 0.2)); const commandBytes = Math.min(MAX_ASK_COMMAND_BYTES, Math.floor(available * 0.4)); const forFiles = Math.max(0, available - stateChars - commandBytes); return { stateChars, commandBytes, fileBytes: fileCount === 0 ? 0 : Math.min(MAX_ASK_FILE_BYTES, Math.floor(forFiles / fileCount)), }; } /** Clip text to a UTF-8 byte budget without splitting a character. */ function clip(text: string, maxBytes: number): { text: string; clipped: boolean } { if (maxBytes <= 0) return { text: "", clipped: text !== "" }; if (Buffer.byteLength(text, "utf-8") <= maxBytes) return { text, clipped: false }; let end = Math.min(text.length, maxBytes); while (end > 0 && Buffer.byteLength(text.slice(0, end), "utf-8") > maxBytes) end -= 1; return { text: text.slice(0, end), clipped: true }; } /** A path the tool may read: inside the working directory, and not a known secret file. */ export function askPath(raw: string, cwd: string): { full: string } | { refused: string } { const full = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw); // Judge the symlink-resolved target too: a link inside cwd may point anywhere. // A missing path has no target to leak; the read reports "not found". const checks: Array<[string, string]> = [[full, resolve(cwd)]]; try { checks.push([realpathSync(full), realpathSync(cwd)]); } catch {} for (const [path, root] of checks) { const inside = relative(root, path); if (inside === "" || inside.startsWith("..") || isAbsolute(inside)) { return { refused: "outside the working directory" }; } if (SECRET_BASENAME.test(basename(path))) return { refused: "looks like a secret file" }; } return { full }; } /** Read at most `maxBytes` of a file without loading the rest of it, or say why it could not be read. */ function readBounded(path: string, maxBytes: number): { text: string; clipped: boolean } | { error: string } { let size: number; try { const stats = statSync(path); if (!stats.isFile()) return { error: "not a file" }; size = stats.size; } catch (error) { return { error: isRecord(error) && error.code === "ENOENT" ? "not found" : "unreadable" }; } const length = Math.max(0, Math.min(size, maxBytes)); const buffer = Buffer.alloc(length); try { const fd = openSync(path, "r"); try { const read = readSync(fd, buffer, 0, length, 0); return { text: buffer.subarray(0, read).toString("utf-8"), clipped: size > read }; } finally { closeSync(fd); } } catch { return { error: "unreadable" }; } } /** One shell command through the same local operations the bash tool uses. */ export async function runAskCommand( command: string, cwd: string, signal: AbortSignal | undefined, ): Promise<{ output: string; exitCode: number | null; clipped: boolean; failed: boolean }> { const chunks: string[] = []; let bytes = 0; let clipped = false; let failed = false; let exitCode: number | null = null; try { const code = await createLocalBashOperations().exec(command, cwd, { onData: (data) => { if (clipped) return; const text = data.toString("utf-8"); chunks.push(text); bytes += Buffer.byteLength(text, "utf-8"); if (bytes >= MAX_ASK_COMMAND_BYTES) clipped = true; }, signal, timeout: ASK_COMMAND_TIMEOUT_SECONDS, }); exitCode = code.exitCode; } catch { // A failed, timed-out, or cancelled command still reports what it printed. failed = true; } return { output: chunks.join(""), exitCode, clipped, failed }; } // --------------------------------------------------------------------------- // Answers // --------------------------------------------------------------------------- function formatValue(value: unknown): string { if (typeof value === "number") return Number.isInteger(value) ? String(value) : value.toFixed(2); if (typeof value === "string") return value; return JSON.stringify(value); } /** A score answer as the agent reads it: the nearest level label, then the raw position. */ export function formatScore(answer: Record, levels: unknown): string | undefined { const score = answer.score; if (typeof score !== "number") return undefined; const legend = isRecord(answer.legend) ? answer.legend : undefined; const nearest = Math.round(score); const label = legend?.[String(nearest)] ?? (Array.isArray(levels) ? levels[nearest] : undefined); return typeof label === "string" ? `${label} (${formatValue(score)})` : formatValue(score); } /** * Render answers for the agent: each value and its confidence, never the raw * probability distribution, the file contents, or the command output. */ export function renderAskAnswers(input: { questions: readonly AskJevQuestion[]; answers: Record; rejected: Record; failure?: JevAbstainReason; model: string; elapsedMs: number; refused: readonly string[]; notes: readonly string[]; /** How many paths the agent passed, and the directory they resolve against. */ pathsRequested?: number; cwd?: string; }): string { const lines: string[] = []; let answered = 0; for (const question of input.questions) { const answer = input.answers[question.name]; if (!isRecord(answer)) { lines.push(`- ${question.name}: no answer (${input.rejected[question.name] ?? input.failure ?? "missing"})`); continue; } answered += 1; // The type is already in the label, the legend is folded into the score, and raw // distributions stay out of the agent's context. const fields = Object.entries(answer) .filter(([key]) => !["probabilities", "type", "legend"].includes(key)) .map(([key, value]) => key === "score" ? `score=${formatScore(answer, question.criteria)}` : `${key}=${formatValue(value)}`, ); lines.push(`- ${question.name} (${question.type}): ${fields.length > 0 ? fields.join(", ") : "answered"}`); } const header = [`ask_jev: ${answered}/${input.questions.length} answered via ${input.model} in ${input.elapsedMs}ms`]; if (answered === 0 && input.failure) header.push(`Jev was unavailable or abstained: ${input.failure}.`); // Missing material changes what the answers mean, so it leads rather than trails them. const requested = input.pathsRequested ?? 0; if (requested > 0 && input.refused.length >= requested) { header.push( `WARNING: Jev answered without any of the ${requested} files you passed (${input.refused.join("; ")}). ` + `Paths resolve inside ${input.cwd ?? "the working directory"}; treat these answers as judged from your prose alone.`, ); } else if (input.refused.length > 0) { header.push(`Not sent to Jev: ${input.refused.join("; ")}.`); } const footer = [ "Only these answers are returned: the file contents and command output you supplied were not sent back to you.", ...(input.notes.length > 0 ? [`Truncated to fit the request budget: ${input.notes.join("; ")}.`] : []), ]; return [...header, ...lines, ...footer].join("\n"); } // --------------------------------------------------------------------------- // jev_find: ripgrep gathers files; Jev narrows directories, then files, then source units // --------------------------------------------------------------------------- /** The file finder's registered name. */ export const JEV_FIND_TOOL = "jev_find"; /** ponytail: 16 nouls per request (the JevPDF batch size); raise once Jev is measured on larger blocks. */ export const FIND_BATCH = 16; /** ponytail: a set this small (three parallel batches) is judged file by file, without directory descent. */ export const FIND_DIRECT_FILES = 48; /** ponytail: at most 128 files judged per call (eight batches), best word/path score first. */ export const FIND_MAX_JUDGED_FILES = 128; /** ponytail: hard cap of 12 Jev requests per call, retries included; raise once latency and cost are measured. */ export const FIND_MAX_REQUESTS = 12; /** ponytail: directory descent spends at most 4 of those requests (64 directory questions over all levels). */ export const FIND_DIR_REQUESTS = 4; /** ponytail: 2 requests are held back for the source-unit stage (32 units). */ export const FIND_UNIT_REQUESTS = 2; /** ponytail: about 16 KB of verbatim source per call; the rest stays reachable through the reading leads. */ export const FIND_MAX_SOURCE_BYTES = 16 * 1024; /** ponytail: descend at most 4 directory levels below the search root. */ export const FIND_MAX_DEPTH = 4; /** ponytail: keep a directory at 0.35 or above: pruning drops every file under it, so it errs toward keeping. */ export const FIND_DIR_KEEP = 0.35; /** ponytail: at most 6 candidate units per relevant file, each shown as at most 60 lines. */ export const FIND_UNITS_PER_FILE = 6; export const FIND_UNIT_MAX_LINES = 60; const FIND_DIR_FILE_NAMES = 15; const FIND_DIR_HIT_LINES = 3; const FIND_MATCH_LINES = 6; const FIND_DEFAULT_LIMIT = 8; const FIND_MIN_RELEVANCE = 0.5; /** Files that still get keyword-window source when Jev could not rank them. */ const FIND_FALLBACK_SOURCE_FILES = 3; /** The asker's question is clipped to this before it is repeated in every request. */ const FIND_REQUEST_BYTES = 2 * 1024; /** Room each file batch keeps for the questions and JSON scaffolding when sizing snippets. */ const FIND_QUESTION_RESERVE = 6 * 1024; /** Lines of a long unit shown above its best keyword line. */ const FIND_FOCUS_LEAD = 10; /** One file ripgrep offered. */ export interface FindFile { path: string; /** Matched line numbers (pattern mode). */ lines: number[]; /** Matched lines as `L12: text` (pattern mode). */ hits: string[]; /** Question words in the path, plus matched lines (pattern mode) or question words in the content. */ score: number; } export interface FindCandidate { path: string; /** Matched line numbers, or the best keyword lines, best first. */ lines: number[]; /** What Jev reads: the matched lines, or the file's best keyword lines, or its head. */ snippet: string; } /** Sends one decisions request. The tool binds it to `askJevAnswers`; tests pass a fake. */ export type JevFindAsk = (payload: Record) => Promise; /** Run ripgrep with an argument list (no shell). Exit 1 is "no matches"; an overfull buffer keeps what arrived. */ function runRg(bin: string, args: string[], cwd: string, signal: AbortSignal | undefined): Promise { return new Promise((done, fail) => { execFile( bin, args, { cwd, signal, maxBuffer: 8 * 1024 * 1024, timeout: ASK_COMMAND_TIMEOUT_SECONDS * 1000 }, (error, stdout) => { const code = error ? (error as { code?: unknown }).code : 0; if (!error || code === 1 || code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") done(String(stdout)); else fail(error); }, ); }); } /** Filler words that would match nearly every file. */ const FIND_STOP_WORDS = new Set( "the and for with where what which when who how does did this that from into its are was can should would there file files code find implement implemented implements".split(" "), ); /** Question words worth searching for: 3+ letters, not filler, at most eight. */ function questionWords(question: string): string[] { const words = question.toLowerCase().match(/[a-z0-9]{3,}/g) ?? []; return [...new Set(words)].filter((word) => !FIND_STOP_WORDS.has(word)).slice(0, 8); } /** How much of each listed file is scanned for question words. */ const FIND_SCAN_BYTES = 256 * 1024; const FIND_EXCERPT_LINES = 12; /** * What Jev reads of a listed file: the lines holding the most distinct question words, in file * order and numbered, or the head of the file when none match. A file's head is usually its * doc comment, which rarely shows where the asked-about behavior lives. */ export function excerpt(text: string, words: readonly string[], maxBytes: number): { lines: number[]; snippet: string } { const rows = text.split("\n"); const best = rows .map((row, index) => { const lower = row.toLowerCase(); return { index, count: words.filter((word) => lower.includes(word)).length }; }) .filter((hit) => hit.count > 0) .sort((a, b) => b.count - a.count || a.index - b.index) .slice(0, FIND_EXCERPT_LINES); if (best.length === 0) return { lines: [], snippet: clip(text, maxBytes).text }; const inOrder = [...best].sort((a, b) => a.index - b.index); const snippet = inOrder.map((hit) => `L${hit.index + 1}: ${rows[hit.index].trim().slice(0, 240)}`).join("\n"); // Leads start at the best-matching lines. return { lines: best.map((hit) => hit.index + 1), snippet: clip(snippet, maxBytes).text }; } /** Path parts (3+ letters) sharing a stem with a question word; orders a file listing before it is capped. */ function pathOverlap(path: string, words: readonly string[]): number { const parts = path.toLowerCase().match(/[a-z0-9]{3,}/g) ?? []; return parts.filter((part) => words.some((word) => word.includes(part) || part.includes(word))).length; } const byScore = (a: FindFile, b: FindFile) => b.score - a.score || a.path.localeCompare(b.path); export interface FindGatherOptions { rg: string; cwd: string; /** Search root relative to `cwd`; "." for the working directory. */ root: string; question: string; pattern?: string; glob?: string; ignoreCase?: boolean; } /** * Every file under `root` worth judging, most promising first. With `pattern`, the files ripgrep * matches with their matched lines; without it, every file ripgrep lists, scored by question words * in the path and (for a set too large to judge file by file) in the content, one fixed-string * ripgrep per word, in parallel. Respects .gitignore, and never offers a file `askPath` would refuse. */ export async function gatherFindFiles(options: FindGatherOptions, signal: AbortSignal | undefined): Promise { // Always name the root: with no path and a piped stdin, rg searches stdin and hangs. // Paths come back as `./src/x.ts`, so they are normalized below. const root = ["--", options.root]; const filters = [...(options.glob ? ["--glob", options.glob] : []), ...(options.ignoreCase ? ["-i"] : [])]; const allowed = (path: string) => "full" in askPath(path, options.cwd); const words = questionWords(options.question); if (options.pattern) { const out = await runRg( options.rg, ["--null", "--line-number", "--no-heading", "--color=never", "--max-count", String(FIND_MATCH_LINES), "--max-columns", "240", ...filters, "-e", options.pattern, ...root], options.cwd, signal, ); const byFile = new Map(); for (const row of out.split("\n")) { const nul = row.indexOf("\0"); if (nul < 0) continue; const path = normalize(row.slice(0, nul)); const match = /^(\d+):(.*)$/.exec(row.slice(nul + 1)); if (!match) continue; const entry = byFile.get(path) ?? { lines: [], text: [] }; entry.lines.push(Number(match[1])); entry.text.push(`L${match[1]}: ${match[2].trim()}`); byFile.set(path, entry); } return [...byFile] .filter(([path]) => allowed(path)) .map(([path, entry]) => ({ path, lines: entry.lines, hits: entry.text, score: entry.lines.length + pathOverlap(path, words) })) .sort(byScore); } const out = await runRg(options.rg, ["--files", "--color=never", ...filters, ...root], options.cwd, signal); const listed = out .split("\n") .filter((path) => path !== "") .map((path) => normalize(path)) .filter(allowed); const score = new Map(listed.map((path) => [path, pathOverlap(path, words)])); if (listed.length > FIND_DIRECT_FILES && words.length > 0) { const hits = await Promise.all( words.map((word) => runRg(options.rg, ["-l", "-i", "-F", "--color=never", ...filters, "-e", word, ...root], options.cwd, signal), ), ); for (const listing of hits) { for (const raw of listing.split("\n")) { const path = raw === "" ? "" : normalize(raw); const current = score.get(path); if (current !== undefined) score.set(path, current + 1); } } } return listed.map((path) => ({ path, lines: [], hits: [], score: score.get(path) ?? 0 })).sort(byScore); } // Judging ------------------------------------------------------------------ /** One noul: a keyed piece of state and the question asked about it. */ export interface JudgeItem { key: string; text: string; question: string; } export interface JudgeStage { request: string; /** Where the items sit in `state` (`directories`, `files`, `units`). */ stateKey: string; criteria: { true: string; false: string }; } /** One request: the items at `items` (indexes into the stage's list), one noul `q` each. */ export function buildJudgePayload(stage: JudgeStage, items: readonly JudgeItem[]): Record { return buildAskPayload( items.map((item, index) => ({ name: `q${index}`, type: "noul" as const, instructions: item.question, criteria: stage.criteria })), { request: stage.request, [stage.stateKey]: Object.fromEntries(items.map((item) => [item.key, item.text])) }, ); } /** * The stage's items as requests that each serialize within `maxBytes`: FIND_BATCH items per * request, their texts shrunk together until the request fits (code grows under JSON escaping, so a * byte budget per text alone does not guarantee a fit). When even empty texts do not fit — the * questions, paths, and scaffolding alone are too large — the batch is halved, and an item that * cannot fit alone is dropped. No request this returns can come back as `overflow`. */ export function fitJudgePayloads( stage: JudgeStage, items: readonly JudgeItem[], maxBytes: number, ): { payloads: Array<{ payload: Record; items: number[] }>; dropped: number[] } { const payloads: Array<{ payload: Record; items: number[] }> = []; const dropped: number[] = []; const place = (indexes: number[]): void => { let limit = Math.max(0, ...indexes.map((index) => Buffer.byteLength(items[index].text, "utf-8"))); for (;;) { const payload = buildJudgePayload( stage, indexes.map((index) => { const text = clip(items[index].text, limit); return { ...items[index], text: text.clipped && limit > 0 ? `${text.text}${TRUNCATION_MARKER}` : text.text }; }), ); if (serializeJevRequest(payload, maxBytes) !== undefined) { payloads.push({ payload, items: indexes }); return; } if (limit > 0) { limit = limit < 64 ? 0 : Math.floor(limit * 0.75); continue; } if (indexes.length === 1) { dropped.push(indexes[0]); return; } const half = Math.ceil(indexes.length / 2); place(indexes.slice(0, half)); place(indexes.slice(half)); return; } }; for (let start = 0; start < items.length; start += FIND_BATCH) { place(Array.from({ length: Math.min(FIND_BATCH, items.length - start) }, (_, offset) => start + offset)); } return { payloads, dropped }; } /** * The call's Jev requests: every stage sends its batches in parallel (one round trip), retries a * `transport` failure once, and never exceeds FIND_MAX_REQUESTS in total, retries included. */ export class FindRequests { sent = 0; retried = 0; readonly diagnostics: JevFailureDiagnostic[] = []; private readonly ask: JevFindAsk; readonly max: number; constructor(ask: JevFindAsk, max = FIND_MAX_REQUESTS) { this.ask = ask; this.max = max; } get left(): number { return this.max - this.sent; } /** * Send up to `sendCap` payloads in parallel, then retry the transport failures within `retryCap` * total requests for this stage. A payload past the caps comes back undefined (not sent). */ async sendAll( payloads: readonly Record[], sendCap: number, retryCap = sendCap, ): Promise> { const count = Math.max(0, Math.min(payloads.length, sendCap, this.left)); this.sent += count; const results: Array = await Promise.all( payloads.slice(0, count).map((payload) => this.ask(payload)), ); const retry = results .map((result, index) => ({ result, index })) .filter(({ result }) => result?.failure === "transport") .slice(0, Math.max(0, Math.min(retryCap - count, this.left))); this.sent += retry.length; this.retried += retry.length; const again = await Promise.all(retry.map(({ index }) => this.ask(payloads[index]))); retry.forEach(({ index }, k) => { results[index] = again[k]; }); for (const result of results) { const diagnostic = result?.diagnostic; if (diagnostic && !this.diagnostics.some((item) => item.kind === diagnostic.kind && item.httpStatus === diagnostic.httpStatus)) { this.diagnostics.push(diagnostic); } } return [...results, ...Array.from({ length: payloads.length - count }, () => undefined)]; } } interface JudgeOutcome { /** Jev's noul per item; undefined when it was not answered. */ scores: Array; /** Why unanswered items went unanswered: a request failure, `request cap`, or `too large`. */ reasons: string[]; } /** Ask one noul per item, batched and fitted, within the stage's request caps. */ async function judgeNouls( requests: FindRequests, stage: JudgeStage, items: readonly JudgeItem[], maxBytes: number, caps: { send: number; retry: number }, ): Promise { const scores: Array = items.map(() => undefined); const reasons: string[] = []; if (items.length === 0) return { scores, reasons }; const fitted = fitJudgePayloads(stage, items, maxBytes); if (fitted.dropped.length > 0) reasons.push("too large"); const results = await requests.sendAll( fitted.payloads.map((entry) => entry.payload), caps.send, caps.retry, ); fitted.payloads.forEach((entry, p) => { const result = results[p]; if (!result) { reasons.push("request cap"); return; } if (result.failure) reasons.push(result.failure); const answers = result.answers ?? {}; entry.items.forEach((index, k) => { const answer = answers[`q${k}`]; if (isRecord(answer) && typeof answer.noul === "number") scores[index] = answer.noul; }); }); return { scores, reasons: [...new Set(reasons)] }; } // Directory descent -------------------------------------------------------- const DIRECTORY_CRITERIA = { true: "Its name, file names, or keyword hits suggest it holds code that implements, defines, or configures what the request asks about.", false: "It clearly holds something unrelated to the request.", }; /** The directory one level below `dir` that holds `path`, or undefined when `path` sits directly in `dir`. */ export function childDirectory(path: string, dir: string): string | undefined { const rest = dir === "." ? path : path.slice(dir.length + 1); const cut = rest.indexOf(sep); if (cut < 0) return undefined; return dir === "." ? rest.slice(0, cut) : `${dir}${sep}${rest.slice(0, cut)}`; } /** What Jev reads of a directory: its size, its most promising file names, and a few keyword hits. */ function describeDirectory(dir: string, files: readonly FindFile[], hitLines: (file: FindFile) => string[]): string { const ordered = [...files].sort(byScore); const local = (path: string) => path.slice(dir.length + 1); const names = ordered.slice(0, FIND_DIR_FILE_NAMES).map((file) => local(file.path)); const more = files.length - names.length; const hits: string[] = []; for (const file of ordered.filter((candidate) => candidate.score > 0).slice(0, FIND_DIR_HIT_LINES)) { for (const line of hitLines(file).slice(0, 2)) { if (hits.length < FIND_DIR_HIT_LINES) hits.push(`${local(file.path)} ${line.slice(0, 160)}`); } } return [ `${files.length} file(s): ${names.join(", ")}${more > 0 ? `, … (+${more} more)` : ""}`, ...(hits.length > 0 ? ["Keyword hits:", ...hits] : []), ].join("\n"); } export interface DescentResult { /** Files still worth judging: those in kept directories, and those sitting directly in a descended one. */ kept: FindFile[]; prunedDirs: number; prunedFiles: number; /** Directories kept without an answer, and why. */ unjudgedDirs: number; reasons: string[]; } /** * Narrow a large file set directory by directory: one noul per next-level directory ("could this * hold the answer?"), all of a level in one parallel round trip. A directory below FIND_DIR_KEEP is * dropped with every file under it; an unanswered one is kept (unknown is not rejected). Kept * directories still larger than FIND_DIRECT_FILES are descended again, up to FIND_MAX_DEPTH * levels; a lone subdirectory is entered without a question. */ export async function narrowByDirectory( files: readonly FindFile[], root: string, options: { request: string; maxBytes: number; requests: FindRequests; hitLines: (file: FindFile) => string[] }, ): Promise { const result: DescentResult = { kept: [], prunedDirs: 0, prunedFiles: 0, unjudgedDirs: 0, reasons: [] }; if (files.length <= FIND_DIRECT_FILES) return { ...result, kept: [...files] }; let pending: Array<{ dir: string; files: FindFile[]; depth: number }> = [{ dir: root, files: [...files], depth: 0 }]; let dirRequests = 0; while (pending.length > 0) { const children: Array<{ dir: string; files: FindFile[]; depth: number; free: boolean }> = []; for (const group of pending) { const byChild = new Map(); let direct = 0; for (const file of group.files) { const child = childDirectory(file.path, group.dir); if (child === undefined) { result.kept.push(file); direct += 1; } else { byChild.set(child, [...(byChild.get(child) ?? []), file]); } } const free = byChild.size === 1 && direct === 0; for (const [dir, under] of byChild) children.push({ dir, files: under, depth: group.depth + 1, free }); } const sendCap = Math.min(FIND_DIR_REQUESTS - dirRequests, options.requests.left - FIND_UNIT_REQUESTS - 1); const questionable = children.filter((child) => !child.free && child.depth <= FIND_MAX_DEPTH); const asked = sendCap > 0 ? questionable.sort( (a, b) => b.files.reduce((sum, f) => sum + f.score, 0) - a.files.reduce((sum, f) => sum + f.score, 0), ) : []; if (children.some((child) => !child.free && child.depth > FIND_MAX_DEPTH)) result.reasons.push("depth limit"); if (questionable.length > 0 && asked.length === 0) result.reasons.push("request cap"); const verdicts = new Map(); if (asked.length > 0) { const before = options.requests.sent; const outcome = await judgeNouls( options.requests, { request: options.request, stateKey: "directories", criteria: DIRECTORY_CRITERIA }, asked.map((child) => ({ key: `${child.dir}${sep}`, text: describeDirectory(child.dir, child.files, options.hitLines), question: `Could the directory "${child.dir}${sep}" contain code that answers the request, judged by its description in state.directories?`, })), options.maxBytes, { send: sendCap, retry: sendCap }, ); dirRequests += options.requests.sent - before; asked.forEach((child, index) => verdicts.set(child.dir, outcome.scores[index])); result.reasons.push(...outcome.reasons); } const next: typeof pending = []; for (const child of children) { const wasAsked = verdicts.has(child.dir); const verdict = verdicts.get(child.dir); if (verdict !== undefined && verdict < FIND_DIR_KEEP) { result.prunedDirs += 1; result.prunedFiles += child.files.length; continue; } if (verdict === undefined && !child.free) result.unjudgedDirs += 1; const canDescend = child.depth < FIND_MAX_DEPTH && (child.free || wasAsked); if (child.files.length > FIND_DIRECT_FILES && canDescend) { next.push({ dir: child.dir, files: child.files, depth: child.depth }); } else result.kept.push(...child.files); } pending = next; } result.reasons = [...new Set(result.reasons)]; return result; } // Source units ------------------------------------------------------------- const FILE_CRITERIA = { true: "The excerpt shows this file implements, defines, configures, or directly answers the request.", false: "The file only mentions related words, or is about something else.", }; const UNIT_CRITERIA = { true: "This unit implements, defines, configures, or directly answers what the request asks.", false: "The unit only mentions related words, or does something else.", }; /** A unit's candidate score: matched/keyword lines inside it weigh most, then distinct question words. */ export function pickUnits( units: readonly SourceUnit[], rows: readonly string[], hits: readonly number[], words: readonly string[], ): SourceUnit[] { const bodies = units.filter((unit) => unit.name !== "imports"); const scored = bodies.map((unit) => { const body = rows.slice(unit.start - 1, unit.end).join("\n").toLowerCase(); const hitCount = hits.filter((line) => line >= unit.start && line <= unit.end).length; return { unit, score: hitCount * 10 + words.filter((word) => body.includes(word)).length }; }); const picks = scored .filter((entry) => entry.score > 0) .sort((a, b) => b.score - a.score || a.unit.start - b.unit.start) .slice(0, FIND_UNITS_PER_FILE) .map((entry) => entry.unit); // Nothing matched a word: let Jev judge the file's first declarations. return (picks.length > 0 ? picks : bodies.slice(0, 3)).sort((a, b) => a.start - b.start); } /** * The line numbers a unit is shown as (0 marks elided lines): the header line of a member, then the * unit whole, or — when it is longer than `maxLines` — its first line and a window from just above * its best keyword line (`focus`), so a long function shows where the asked-about behavior sits. */ export function unitView(unit: SourceUnit, focus: number | undefined, maxLines = FIND_UNIT_MAX_LINES): number[] { const view: number[] = unit.header !== undefined && unit.header < unit.start ? [unit.header, 0] : []; const range = (from: number, to: number) => Array.from({ length: Math.max(0, to - from + 1) }, (_, i) => from + i); if (unit.end - unit.start + 1 <= maxLines) return [...view, ...range(unit.start, unit.end)]; if (focus === undefined || focus < unit.start + maxLines - FIND_FOCUS_LEAD) { return [...view, ...range(unit.start, unit.start + maxLines - 1), 0]; } const from = Math.max(unit.start + 1, focus - FIND_FOCUS_LEAD); const to = Math.min(unit.end, from + maxLines - 2); return [...view, unit.start, ...(from > unit.start + 1 ? [0] : []), ...range(from, to), ...(to < unit.end ? [0] : [])]; } /** The first `hits` line inside the unit, in `hits` order (best first). */ function focusOf(unit: SourceUnit, hits: readonly number[]): number | undefined { return hits.find((line) => line >= unit.start && line <= unit.end); } /** A file's rows without carriage returns (and without the empty row after a final newline), so numbered lines print as they read. */ function fileRows(text: string): string[] { const rows = text.split("\n").map((row) => row.replace(/\r$/, "")); if (rows.length > 1 && rows.at(-1) === "") rows.pop(); return rows; } interface SourceBlock { path: string; unit: SourceUnit; view: number[]; rows: readonly string[]; } /** * `Source block "path" lines a-b:` and the numbered verbatim lines of each block, in order, within * `budgetBytes` of source; a block cut by the budget ends in `…`, and one that cannot show three * lines is left out. */ export function renderSourceBlocks( blocks: readonly SourceBlock[], budgetBytes = FIND_MAX_SOURCE_BYTES, ): { lines: string[]; bytes: number; omitted: number } { const lines: string[] = []; let bytes = 0; let omitted = 0; for (const block of blocks) { const numbered: string[] = []; let used = 0; let cut = false; for (const line of block.view) { const text = line === 0 ? "…" : `${line}: ${block.rows[line - 1] ?? ""}`; const size = Buffer.byteLength(text, "utf-8") + 1; if (bytes + used + size > budgetBytes) { cut = true; break; } numbered.push(text); used += size; } if (numbered.filter((text) => text !== "…").length < Math.min(3, block.view.filter((line) => line !== 0).length)) { omitted += 1; continue; } if (cut && numbered.at(-1) !== "…") numbered.push("…"); lines.push("", `Source block "${block.path}" lines ${block.unit.start}-${block.unit.end}:`, ...numbered); bytes += used; } return { lines, bytes, omitted }; } /** Keyword lines for a directory description or a fallback window: rg's matches, or the file's best question-word lines. */ function keywordLines(text: string | undefined, words: readonly string[]): { lines: number[]; snippet: string[] } { if (text === undefined || words.length === 0) return { lines: [], snippet: [] }; const found = excerpt(text, words, FIND_SCAN_BYTES); return found.lines.length === 0 ? { lines: [], snippet: [] } : { lines: found.lines, snippet: found.snippet.split("\n") }; } function fallbackSourceUnits( path: string, text: string, ts: Awaited>, rows: readonly string[], hits: readonly number[], words: readonly string[], ): SourceUnit[] { if (hits.length === 0) return []; return pickUnits(splitSourceUnits(path, text, ts), rows, hits, words).filter((unit) => hits.some((line) => (line >= unit.start && line <= unit.end) || line === unit.header), ); } function formatJevDiagnostic(diagnostic: JevFailureDiagnostic): string { if (diagnostic.kind === "timeout") return "Jev request timed out."; if (diagnostic.kind === "malformed-response") return "Jev returned a malformed response."; return `Jev provider request failed${diagnostic.httpStatus ? ` (HTTP ${diagnostic.httpStatus})` : ""}.`; } // The whole call ----------------------------------------------------------- export interface JevFindOptions extends FindGatherOptions { limit?: number; /** The route's request budget; no request is sent larger than this. */ maxBytes: number; model: string; /** Sends one request; absent when Jev is out of reach (`unreachable` says why). */ ask?: JevFindAsk; unreachable?: JevAbstainReason; /** The TypeScript loader; tests pass one resolving undefined to force chunking. */ loadTypeScript?: typeof loadTypeScript; signal?: AbortSignal; } export interface JevFindResult { text: string; /** Shape only, like ask_jev: details persist in the session. */ details: Record; judged: number; topRelevance?: number; failure?: string; elapsedMs: number; } /** * The whole jev_find call: gather files with ripgrep, narrow large sets by directory, judge files, * then judge the source units of the relevant ones and return them verbatim. Without a reachable * Jev (or when no file could be judged) the ripgrep-ranked list still comes back, with keyword * windows of the top files, so the call is never wasted. Throws only when ripgrep fails. */ export async function runJevFind(options: JevFindOptions): Promise { const files = await gatherFindFiles(options, options.signal); if (files.length === 0) { return { text: "jev_find: no candidate files. Loosen `pattern`, `glob`, or `path`.", details: { total: 0, judged: 0 }, judged: 0, elapsedMs: 0, }; } const words = questionWords(options.question); const limit = Math.max(1, Math.floor(options.limit ?? FIND_DEFAULT_LIMIT)); const texts = new Map(); const readText = (path: string): string | undefined => { if (!texts.has(path)) { const file = readBounded(resolve(options.cwd, path), FIND_SCAN_BYTES); texts.set(path, "error" in file ? undefined : file.text); } return texts.get(path); }; const hitsOf = (file: FindFile): { lines: number[]; snippet: string[] } => file.lines.length > 0 ? { lines: file.lines, snippet: file.hits } : keywordLines(readText(file.path), words); /** The ripgrep-ranked list and source units of the top files: what comes back when Jev did not rank. */ const fallback = async ( reason: string, notes: string[], elapsedMs: number, requests?: FindRequests, ): Promise => { const shown = files.slice(0, limit); const sourceFiles = shown.slice(0, FIND_FALLBACK_SOURCE_FILES); const ts = sourceFiles.some((file) => isScriptPath(file.path)) ? await (options.loadTypeScript ?? loadTypeScript)() : undefined; const blocks: SourceBlock[] = []; const leads = new Map(); for (const file of sourceFiles) { const text = readText(file.path); if (text === undefined) continue; const rows = fileRows(text); const hits = hitsOf(file).lines; const units = fallbackSourceUnits(file.path, text, ts, rows, hits, words); const selected = units.length > 0 ? units : keywordWindows(rows.length, hits); leads.set(file.path, selected.map((unit) => `${unit.name} lines ${unit.start}-${unit.end}`).join(", ")); for (const unit of selected) blocks.push({ path: file.path, unit, view: unitView(unit, focusOf(unit, hits)), rows }); } const source = renderSourceBlocks(blocks); const diagnostic = requests?.diagnostics[0]; const lines = [ `jev_find: Jev did not judge (${reason}); ${files.length} candidate file(s), ranked by ripgrep:`, ...notes, ...(diagnostic ? [`Diagnostic: ${formatJevDiagnostic(diagnostic)}`] : []), ...shown.map((file) => `- ${file.path}${leads.has(file.path) ? ` — ${leads.get(file.path)}` : ""}`), ...(files.length > shown.length ? [`${files.length - shown.length} more candidate file(s) not listed.`] : []), ...source.lines, "", "End context.", ]; return { text: lines.join("\n"), details: { total: files.length, judged: 0, requests: requests?.sent ?? 0, sourceBytes: source.bytes, elapsedMs, failure: reason, ...(diagnostic ? { diagnostic } : {}), }, judged: 0, failure: reason, ...(diagnostic ? { diagnostic } : {}), elapsedMs, }; }; if (!options.ask) return fallback(options.unreachable ?? "missing", [], 0); const started = Date.now(); const requests = new FindRequests(options.ask); const request = clip(options.question, FIND_REQUEST_BYTES).text; const notes: string[] = []; // 1. Directories. const descent = await narrowByDirectory(files, options.root, { request, maxBytes: options.maxBytes, requests, hitLines: (file) => hitsOf(file).snippet, }); if (descent.prunedDirs > 0) { notes.push(`Jev ruled out ${descent.prunedDirs} director${descent.prunedDirs === 1 ? "y" : "ies"} holding ${descent.prunedFiles} file(s).`); } if (descent.unjudgedDirs > 0) { notes.push( `${descent.unjudgedDirs} director${descent.unjudgedDirs === 1 ? "y was" : "ies were"} kept unjudged (${descent.reasons.join(", ") || "missing"}).`, ); } // 2. Files, best score first, within the judging cap and the requests left after the unit reserve. const fileSendCap = requests.left - FIND_UNIT_REQUESTS; const judgeCap = Math.min(FIND_MAX_JUDGED_FILES, Math.max(0, fileSendCap) * FIND_BATCH); const ordered = [...descent.kept].sort(byScore); const pastCap = Math.max(0, ordered.length - judgeCap); const snippetBytes = Math.max(256, Math.floor((options.maxBytes - FIND_QUESTION_RESERVE) / FIND_BATCH)); const candidates: FindCandidate[] = []; for (const file of ordered.slice(0, judgeCap)) { if (file.lines.length > 0) { candidates.push({ path: file.path, lines: file.lines, snippet: clip(file.hits.join("\n"), snippetBytes).text }); continue; } const text = readText(file.path); if (text !== undefined) candidates.push({ path: file.path, ...excerpt(text, words, snippetBytes) }); } const filesOutcome = await judgeNouls( requests, { request, stateKey: "files", criteria: FILE_CRITERIA }, candidates.map((candidate) => ({ key: candidate.path, text: candidate.snippet, question: `Is the file "${candidate.path}" relevant to the request, judged by its excerpt in state.files?`, })), options.maxBytes, // A retry may borrow one request from the unit reserve: without file verdicts there is nothing to excerpt. { send: fileSendCap, retry: requests.left - 1 }, ); const scored = candidates.map((candidate, index) => ({ ...candidate, relevance: filesOutcome.scores[index] })); const judged = scored.filter((candidate) => candidate.relevance !== undefined).length; const unjudgedFiles = candidates.length - judged; if (pastCap > 0) { notes.push(`${pastCap} file(s) past the ${judgeCap}-file judging cap were not judged; narrow with \`pattern\`, \`glob\`, or \`path\`.`); } if (judged === 0) { return fallback(filesOutcome.reasons[0] ?? "missing", notes, Date.now() - started, requests); } if (unjudgedFiles > 0) notes.push(`${unjudgedFiles} file(s) were not judged (${filesOutcome.reasons.join(", ") || "missing"}).`); const ranked = scored .filter((candidate): candidate is FindCandidate & { relevance: number } => candidate.relevance !== undefined) .sort((a, b) => b.relevance - a.relevance); const relevant = ranked.filter((candidate) => candidate.relevance >= FIND_MIN_RELEVANCE); const shown = relevant.slice(0, limit); // 3. Source units of the relevant files, in relevance order, within the requests left. const ts = shown.some((candidate) => isScriptPath(candidate.path)) ? await (options.loadTypeScript ?? loadTypeScript)() : undefined; const perFile = shown.map((candidate) => { const text = readText(candidate.path) ?? ""; const rows = fileRows(text); const hits = [...new Set([...candidate.lines, ...keywordLines(text, words).lines])]; const picks = text === "" ? [] : pickUnits(splitSourceUnits(candidate.path, text, ts), rows, hits, words); return { candidate, rows, hits, picks }; }); const unitItems: JudgeItem[] = []; const owners: Array<{ file: number; unit: SourceUnit }> = []; const unitCapacity = requests.left * FIND_BATCH; perFile.forEach((entry, file) => { for (const unit of entry.picks) { if (unitItems.length >= unitCapacity) return; const key = `${entry.candidate.path} lines ${unit.start}-${unit.end} (${unit.name})`; unitItems.push({ key, text: unitView(unit, focusOf(unit, entry.hits)) .map((line) => (line === 0 ? "…" : (entry.rows[line - 1] ?? ""))) .join("\n"), question: `Does the source unit "${key}" in state.units implement, define, configure, or directly answer the request?`, }); owners.push({ file, unit }); } }); const unitsOutcome = await judgeNouls( requests, { request, stateKey: "units", criteria: UNIT_CRITERIA }, unitItems, options.maxBytes, { send: requests.left, retry: requests.left }, ); const elapsedMs = Date.now() - started; const blocks: SourceBlock[] = []; const listing: string[] = []; let windowed = 0; perFile.forEach((entry, file) => { const judgedUnits = owners .map((owner, index) => ({ ...owner, score: unitsOutcome.scores[index] })) .filter((owner) => owner.file === file); const answered = judgedUnits.some((owner) => owner.score !== undefined); const fallbackUnits = fallbackSourceUnits( entry.candidate.path, readText(entry.candidate.path) ?? "", ts, entry.rows, entry.hits, words, ); const units = answered || entry.rows.length === 0 || entry.picks.length === 0 ? judgedUnits.filter((owner) => (owner.score ?? 0) >= FIND_MIN_RELEVANCE).map((owner) => owner.unit) : fallbackUnits.length > 0 ? fallbackUnits : keywordWindows(entry.rows.length, entry.hits); if (!answered && units.length > 0) windowed += 1; for (const unit of units) { blocks.push({ path: entry.candidate.path, unit, view: unitView(unit, focusOf(unit, entry.hits)), rows: entry.rows }); } const leads = units.map((unit) => `${unit.name} lines ${unit.start}-${unit.end}`).join(", "); listing.push(`- ${entry.candidate.path} (${entry.candidate.relevance.toFixed(2)})${leads ? ` — reading leads: ${leads}` : ""}`); }); if (windowed > 0) { notes.push( `Jev did not judge the source units of ${windowed} file(s) (${unitsOutcome.reasons.join(", ") || "missing"}); matching source units or keyword windows are shown instead.`, ); } if (relevant.length > shown.length) { const rest = relevant.slice(shown.length); notes.push( `${rest.length} more relevant file(s) past the limit: ${rest .slice(0, 5) .map((candidate) => `${candidate.path} (${candidate.relevance.toFixed(2)})`) .join(", ")}${rest.length > 5 ? ", …" : ""}.`, ); } const source = renderSourceBlocks(blocks); const diagnostic = requests.diagnostics[0]; if (diagnostic) notes.push(`Diagnostic: ${formatJevDiagnostic(diagnostic)}`); if (source.omitted > 0) { notes.push(`${source.omitted} source block(s) left out to stay within ${FIND_MAX_SOURCE_BYTES / 1024} KB; read them from the leads.`); } const lines = [ `jev_find: ${relevant.length} relevant file(s) (judged ${judged} of ${files.length} candidates via ${options.model} in ${elapsedMs}ms)`, ...notes, ...(shown.length > 0 ? listing : [ "No file is a confident match; the closest were:", ...ranked.slice(0, 3).map((candidate) => `- ${candidate.path} (${candidate.relevance.toFixed(2)})`), ]), ...source.lines, "", "End context.", ]; const failure = filesOutcome.reasons.find((reason) => reason !== "request cap" && reason !== "too large"); return { text: lines.join("\n"), details: { total: files.length, judged, relevant: relevant.length, requests: requests.sent, retried: requests.retried, sourceBytes: source.bytes, elapsedMs, ...(failure ? { failure } : {}), ...(diagnostic ? { diagnostic } : {}), }, judged, topRelevance: ranked[0]?.relevance, ...(failure ? { failure } : {}), ...(diagnostic ? { diagnostic } : {}), elapsedMs, }; } const JevFindParams = Type.Object({ question: Type.String({ description: "What you want to understand, in plain words (e.g. 'how is the retry backoff configured').", }), pattern: Type.Optional( Type.String({ description: "ripgrep regex to narrow candidates to files that match; omit to consider every listed file." }), ), glob: Type.Optional(Type.String({ description: "File glob filter, e.g. '*.ts' or 'src/**/*.md'." })), path: Type.Optional(Type.String({ description: "Directory to search, inside the working directory (default: '.')." })), ignoreCase: Type.Optional(Type.Boolean({ description: "Case-insensitive pattern." })), limit: Type.Optional( Type.Number({ description: `Most relevant files to return, with their source (default ${FIND_DEFAULT_LIMIT}).` }), ), }); // --------------------------------------------------------------------------- // Tool // --------------------------------------------------------------------------- const QuestionSpec = Type.Object({ type: StringEnum(ASK_JEV_QUESTION_TYPES, { description: "choice: pick one option. score: place the state on a rubric. noul: probability a statement holds.", }), instructions: Type.String({ description: "The question, stated in one sentence." }), criteria: Type.Optional( Type.Unknown({ description: 'choice: {"option": "what the option means"} (at least two). score: an ordered array of level labels. noul: {"true": "...", "false": "..."}.', }), ), focus: Type.Optional(Type.String({ description: "Extra judging guidance for this question." })), }); const AskJevParams = Type.Object({ state: Type.Optional(Type.String({ description: "Your own prose: the situation and what you are deciding." })), paths: Type.Optional( Type.Array(Type.String(), { description: `Code to include as state, read-only (max ${MAX_ASK_PATHS}). Relative paths resolve against the working directory; paths outside it are refused.`, }), ), command: Type.Optional(Type.String({ description: "One shell command whose output becomes state." })), questions: Type.Record(Type.String(), QuestionSpec, { description: "Question name -> spec." }), }); export default function jevAskToolExtension(pi: ExtensionAPI): void { pi.registerTool({ name: ASK_JEV_TOOL, label: "Ask Jev", description: [ "Ask Jev (a typed decisions model, not a chat model) a block of questions about a state you assemble here:", "your own prose in `state`, code read from `paths`, and the output of one shell `command`. Code makes one", "bounded call and returns answers only — the file contents and command output are never sent back to you.", "Anything you put in the state is sent to Jev, so pass only code you are willing to share.", '`questions` is an object keyed by question name; each value is {"type": "choice" | "score" | "noul",', '"instructions": , "criteria": , "focus"?: }.', 'choice criteria: {"option": "what the option means"}; score criteria: an ordered array of level labels;', 'noul criteria: {"true": "...", "false": "..."} (optional for noul).', "Each answer returns the chosen value, score, or probability plus a confidence; raw probability", "distributions are omitted.", "Use it to triage a failure before touching anything (run the failing command, ask what kind of failure it is),", "judge a diff before shipping (a risk score and a needs-review flag), classify a request before planning, or", "find which of several files matters without reading them into your own context.", `Bounds: ${MAX_ASK_QUESTIONS} questions, ${MAX_ASK_PATHS} paths per call; each file is clipped to about ${MAX_ASK_FILE_BYTES / 1024} KB and marked when clipped.`, ].join(" "), promptSnippet: "Ask Jev a typed question about state you assemble from prose, files, or one command", promptGuidelines: [ "Use ask_jev whenever a judgment would otherwise be a guess — what kind of failure this is, how risky a diff is, what a request is really asking for. It returns typed answers with confidences for a fraction of a cent, so it validates assumptions before they cost a wrong edit.", "Prefer ask_jev over reading files when you only need a verdict about them (which of these files handles X, does this code already do Y, is this test output a code bug or a flaky test): pass them as `paths` or the command as `command`, and only the answer enters your context. Read a file yourself when you need its exact text, such as before editing it.", ], discovery: { summary: "Ask Jev one block of typed choice/score/noul questions over prose, code, and one command's output", aliases: ["jev", "triage", "classify", "judge", "score"], category: "Decisions", }, parameters: AskJevParams, async execute(_toolCallId, params, signal, _onUpdate, ctx: ExtensionContext) { const text = (value: string) => ({ content: [{ type: "text" as const, text: value }], details: {} }); const config = readJevAdvisoryConfig(getSettingsPath()); const route = config.routes.ask; if (!route.enabled) { return text( `ask_jev is disabled (jevAdvisory.routes.ask.enabled is false in ${getSettingsPath()}). Remove that setting or set it to true.`, ); } const parsed = parseAskQuestions(params.questions as Record | undefined); if (parsed.error !== undefined) { return text(`ask_jev needs a usable question block: ${parsed.error}`); } const questions = parsed.questions; // Reading files and running the command exist only to feed Jev: without a reachable // Jev, do neither (an `npm test` would otherwise run in full and be thrown away). const connection = jevConnection(config, route); const unreachable = await jevUnavailable(ctx, connection); if (unreachable) { if (ctx.hasUI) warnJevUnavailableOnce(ctx.ui, config.provider); emitJevTelemetry(pi.events, "decision", { route: "ask", outcome: "fallback", candidates: questions.length, confidence: confidenceBucket(undefined), elapsedMs: 0, reason: unreachable, }); return { content: [ { type: "text" as const, text: `${renderAskAnswers({ questions, answers: {}, rejected: {}, failure: unreachable, model: config.model, elapsedMs: 0, refused: [], notes: [], })}\nNothing was read, run, or sent. Decide without Jev: read the files or run the command yourself.`, }, ], details: { answered: 0, questions: questions.length, model: config.model, elapsedMs: 0, failure: unreachable }, }; } const questionBytes = Buffer.byteLength( JSON.stringify(buildAskPayload(questions, {}).questions), "utf-8", ); const requested = (params.paths ?? []).slice(0, MAX_ASK_PATHS); const budget = askPartBudget(route.payloadBytes, requested.length, questionBytes); const refused: string[] = []; const notes: string[] = []; const prose = clip((params.state ?? "").trim(), budget.stateChars); if (prose.clipped) notes.push("state"); const files: Record = {}; for (const raw of requested) { const path = askPath(raw, ctx.cwd); if ("refused" in path) { refused.push(`${raw} (${path.refused})`); continue; } const file = readBounded(path.full, Math.max(0, budget.fileBytes - Buffer.byteLength(TRUNCATION_MARKER, "utf-8"))); if ("error" in file) { refused.push(`${raw} (${file.error})`); continue; } if (file.clipped) notes.push(raw); files[relative(ctx.cwd, path.full)] = file.clipped ? `${file.text}${TRUNCATION_MARKER}` : file.text; } let command: Record | undefined; if (params.command?.trim()) { const result = await runAskCommand(params.command.trim(), ctx.cwd, signal); const output = clip(result.output, budget.commandBytes); if (output.clipped || result.clipped) notes.push("command output"); command = { command: params.command.trim(), output: output.text, exitCode: result.exitCode, ...(result.failed ? { failed: true } : {}), }; } const payload = buildAskPayload(questions, { ...(prose.text ? { request: prose.text } : {}), ...(Object.keys(files).length > 0 ? { files } : {}), ...(command ? { command } : {}), }); if (serializeJevRequest(payload, route.payloadBytes) === undefined) { return text("ask_jev could not fit this request in its budget; pass fewer paths or a shorter command."); } const result = await askJevAnswers(ctx, connection, { payload, maxBytes: route.payloadBytes, }); const answers = result.answers ?? {}; const rejected: Record = {}; let answered = 0; let topConfidence: number | undefined; for (const question of questions) { const answer = answers[question.name]; if (!isRecord(answer)) { rejected[question.name] = result.failure ?? "missing"; continue; } answered += 1; if (typeof answer.confidence === "number") { topConfidence = topConfidence === undefined ? answer.confidence : Math.max(topConfidence, answer.confidence); } } // Content-free: no state, answers, or criteria travel, only shape and outcome. emitJevTelemetry(pi.events, "decision", { route: "ask", outcome: answered > 0 ? "jev" : "fallback", candidates: questions.length, confidence: confidenceBucket(topConfidence), elapsedMs: result.elapsedMs, ...(answered > 0 ? {} : { reason: result.failure ?? "missing" }), }); return { content: [ { type: "text" as const, text: renderAskAnswers({ questions, answers, rejected, failure: result.failure, model: config.model, elapsedMs: result.elapsedMs, refused, notes, pathsRequested: requested.length, cwd: ctx.cwd, }), }, ], // Deliberately shape only: tool details are persisted in the session. details: { answered, questions: questions.length, model: config.model, elapsedMs: result.elapsedMs, ...(result.failure ? { failure: result.failure } : {}), }, }; }, }); pi.registerTool({ name: JEV_FIND_TOOL, label: "Jev Find", description: [ "Find where behavior lives and read it in one call. ripgrep gathers candidate files (those matching `pattern`,", "or every file under `path` filtered by `glob`); Jev narrows a large tree directory by directory, judges each", "remaining file's excerpt against `question`, then judges the functions, classes, and sections of the relevant", "files. Returns the relevant files with reading leads AND the accepted source verbatim with original line", `numbers (about ${FIND_MAX_SOURCE_BYTES / 1024} KB of source, at most ${FIND_MAX_REQUESTS} Jev requests per call).`, "Respects .gitignore; secret-named files are never sent.", ].join(" "), promptSnippet: "Find where behavior lives: Jev-judged relevant files plus their verbatim source with line numbers", promptGuidelines: [ "Use jev_find for how/why/where-does-this-behavior-live questions — even when the question names a function or setting — before grep, find, ls, or reading candidate files. One call returns the relevant files and their relevant source verbatim with line numbers. Pass `question` in plain words; add `pattern` when you know a likely identifier, `glob`/`path` to scope it.", "Read the source blocks jev_find returns before searching again; `read` only the leads you still need (to edit, or past a clipped block).", "Use grep or read instead for an exact string, a known symbol's definition, or a known filename.", "When delegating repository discovery to a subagent, tell it to start with jev_find.", ], discovery: { summary: "Semantic code finder: Jev-judged relevant files and their verbatim source excerpts", aliases: ["jevgrep", "jg", "find", "search", "locate"], category: "Decisions", }, parameters: JevFindParams, async execute(_toolCallId, params, signal, _onUpdate, ctx: ExtensionContext) { const text = (value: string, details: Record = {}) => ({ content: [{ type: "text" as const, text: value }], details, }); const config = readJevAdvisoryConfig(getSettingsPath()); const route = config.routes.ask; if (!route.enabled) { return text(`jev_find is disabled (jevAdvisory.routes.ask.enabled is false in ${getSettingsPath()}).`); } const question = params.question?.trim() ?? ""; if (question === "") return text("jev_find needs a non-empty `question`."); const rootArg = params.path?.trim() || "."; const rootFull = resolve(ctx.cwd, rootArg); const inside = relative(resolve(ctx.cwd), rootFull); if (inside.startsWith("..") || isAbsolute(inside)) { return text(`jev_find: ${rootArg} is outside the working directory.`); } const rg = await ensureTool("rg"); if (!rg) return text("jev_find needs ripgrep (rg), which is not available; use grep instead."); // Without Jev the ripgrep-ranked files and their keyword lines still come back: the call is never wasted. const connection = jevConnection(config, route); const unreachable = await jevUnavailable(ctx, connection); let result: JevFindResult; try { result = await runJevFind({ rg, cwd: ctx.cwd, root: inside === "" ? "." : inside, question, pattern: params.pattern || undefined, glob: params.glob || undefined, ignoreCase: params.ignoreCase, limit: params.limit, maxBytes: route.payloadBytes, model: config.model, ...(unreachable ? { unreachable } : { ask: (payload) => askJevAnswers(ctx, connection, { payload, maxBytes: route.payloadBytes }) }), signal, }); } catch (error) { return text(`jev_find: ripgrep failed (${error instanceof Error ? error.message.split("\n")[0] : String(error)}).`); } if (result.details.total !== 0) { emitJevTelemetry(pi.events, "decision", { route: "find", outcome: result.judged > 0 ? "jev" : "fallback", candidates: result.details.total, confidence: confidenceBucket(result.topRelevance), elapsedMs: result.elapsedMs, ...(result.judged > 0 ? {} : { reason: result.failure ?? "missing" }), }); } return text(result.text, result.details); }, }); // Offer the tools only while Jev can answer: a tool that always fails costs the agent a turn // every time it reaches for it. Checked before every run, so `/tokenin add` brings them back // without a reload. Silent on purpose: the one warning comes from whichever Jev path first // needs the missing credential, not from every session of a user without a subscription. // Only a removal made here is ever undone, so a loadout the user trimmed stays trimmed. const jevTools = [ASK_JEV_TOOL, JEV_FIND_TOOL]; const hidden = new Set(); pi.on("before_agent_start", async (_event, ctx) => { const config = readJevAdvisoryConfig(getSettingsPath()); const route = config.routes.ask; const offline = !route.enabled || (await jevUnavailable(ctx, jevConnection(config, route))) !== undefined; const active = pi.getActiveTools(); if (offline) { const dropping = jevTools.filter((name) => active.includes(name)); if (dropping.length > 0) { pi.setActiveTools(active.filter((name) => !dropping.includes(name))); for (const name of dropping) hidden.add(name); } } else if (hidden.size > 0) { const restoring = [...hidden].filter((name) => !active.includes(name)); if (restoring.length > 0) pi.setActiveTools([...active, ...restoring]); hidden.clear(); } return undefined; }); }