{"version":3,"file":"trigger-judge.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/trigger-judge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAkB,KAAK,KAAK,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AA+B7F;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CACnC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,WAAW,EAAE,MAAM,GACjB,mBAAmB,EAAE,CAgCvB;AAED,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,qDAAqD;AACrD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,gBAAgB,GAAG,YAAY,CAgC1E","sourcesContent":["/**\n * The model call G4 was designed around and never had.\n *\n * `trigger-eval.ts` takes its judge as a parameter so scoring stays testable\n * without a model, and nothing in the tree ever passed one — so every G4 run\n * has reported `not-run` since it was written. This is that judge.\n *\n * It is deliberately in its own module rather than inside `trigger-eval.ts`:\n * the eval is pure and testable, this reaches the network, and the same\n * separation lets the agent-selection eval reuse the judge without pulling in\n * the plugin gate machinery.\n *\n * ## Why one call for all prompts\n *\n * The judge sees every candidate and every prompt at once. Per-prompt calls\n * would be cleaner to reason about, but the question being scored is\n * comparative — \"which of these fires\" — and batching keeps the candidate list\n * identical across prompts, which is the thing that must not vary. It also\n * makes the cost proportional to the corpus rather than to the case count.\n */\n\nimport { completeSimple, type Model } from \"@kolisachint/hoocode-ai\";\nimport type { TriggerCandidate, TriggerJudge, TriggerJudgeVerdict } from \"./trigger-eval.js\";\n\n/** Description characters per candidate. The opening states the trigger; past that it is filler. */\nconst DESCRIPTION_CHARS = 600;\nconst MAX_RESPONSE_TOKENS = 4_000;\n\nconst JUDGE_SYSTEM_PROMPT = `You simulate how a coding agent picks a capability.\n\nYou are given numbered CAPABILITIES (name and description) and numbered PROMPTS. For each prompt, answer with the ONE capability an agent would reach for, or null when none of them fits and the agent should just do the work itself.\n\nRules:\n- Judge ONLY from the descriptions. Do not use knowledge about what these names usually mean elsewhere.\n- Pick the single best fit. When two fit, pick the one whose description names the prompt's situation more specifically.\n- Answer null when no description covers the prompt. Do not stretch a description to make it fit; a wrong pick and a null are both wrong, but pretending coverage hides the real failure.\n- Answer every prompt exactly once, in the order given.\n\nOutput STRICT JSON, no markdown fence, no prose:\n{\"verdicts\":[{\"prompt\":0,\"capability\":\"explore\"},{\"prompt\":1,\"capability\":null}]}`;\n\nfunction buildPrompt(candidates: readonly TriggerCandidate[], prompts: readonly string[]): string {\n\tconst lines: string[] = [\"CAPABILITIES:\"];\n\tfor (const [i, candidate] of candidates.entries()) {\n\t\tlines.push(`${i}. ${candidate.name} — ${candidate.description.slice(0, DESCRIPTION_CHARS)}`);\n\t}\n\tlines.push(\"\", \"PROMPTS:\");\n\tfor (const [i, prompt] of prompts.entries()) {\n\t\tlines.push(`${i}. ${prompt}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Read the verdict list back, positionally.\n *\n * Returns one entry per prompt no matter what came back: a missing or\n * unparseable row becomes null (read as \"nothing fired\") rather than shifting\n * every later verdict onto the wrong prompt. `runTriggerEval` rejects a\n * length mismatch outright, so the alternative to filling the gaps is\n * discarding the whole run — and a hallucinated capability name is a real\n * signal about the candidate list, not a reason to throw the batch away.\n */\nexport function parseTriggerVerdicts(\n\tresponse: string,\n\tcandidates: readonly TriggerCandidate[],\n\tpromptCount: number,\n): TriggerJudgeVerdict[] {\n\tconst verdicts: TriggerJudgeVerdict[] = new Array(promptCount).fill(null);\n\tconst start = response.indexOf(\"{\");\n\tconst end = response.lastIndexOf(\"}\");\n\tif (start < 0 || end <= start) return verdicts;\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(response.slice(start, end + 1));\n\t} catch {\n\t\treturn verdicts;\n\t}\n\n\tconst rows = (parsed as { verdicts?: unknown })?.verdicts;\n\tif (!Array.isArray(rows)) return verdicts;\n\n\tconst known = new Set(candidates.map((c) => c.name));\n\tfor (const row of rows) {\n\t\tif (!row || typeof row !== \"object\") continue;\n\t\tconst entry = row as Record<string, unknown>;\n\t\tconst index = typeof entry.prompt === \"number\" ? entry.prompt : Number.NaN;\n\t\tif (!Number.isInteger(index) || index < 0 || index >= promptCount) continue;\n\t\tconst capability = entry.capability;\n\t\tif (capability === null || capability === undefined) {\n\t\t\tverdicts[index] = null;\n\t\t\tcontinue;\n\t\t}\n\t\t// A name that is not on the candidate list is a hallucination. Recording it\n\t\t// as null keeps it wrong without letting it masquerade as a real pick.\n\t\tverdicts[index] = typeof capability === \"string\" && known.has(capability) ? capability : null;\n\t}\n\treturn verdicts;\n}\n\nexport interface TriggerJudgeDeps {\n\tmodel: Model<any>;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tsignal?: AbortSignal;\n}\n\n/** A {@link TriggerJudge} backed by a real model. */\nexport function createLlmTriggerJudge(deps: TriggerJudgeDeps): TriggerJudge {\n\treturn async ({ candidates, prompts }) => {\n\t\tif (candidates.length === 0 || prompts.length === 0) return prompts.map(() => null);\n\n\t\tconst response = await completeSimple(\n\t\t\tdeps.model,\n\t\t\t{\n\t\t\t\tsystemPrompt: JUDGE_SYSTEM_PROMPT,\n\t\t\t\tmessages: [\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: buildPrompt(candidates, prompts) }],\n\t\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\t{ maxTokens: MAX_RESPONSE_TOKENS, signal: deps.signal, apiKey: deps.apiKey, headers: deps.headers },\n\t\t);\n\n\t\tif (response.stopReason === \"error\") {\n\t\t\t// Thrown, not swallowed: runTriggerEval turns this into `not-run` with\n\t\t\t// the reason attached, which is the honest outcome. Returning nulls\n\t\t\t// would score every case as \"nothing fired\" and read like a real result.\n\t\t\tthrow new Error(response.errorMessage || \"trigger judge call failed\");\n\t\t}\n\n\t\tconst text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\\n\");\n\t\treturn parseTriggerVerdicts(text, candidates, prompts.length);\n\t};\n}\n"]}