a0bb11a6fb
Add buildReviewPromptStructured() returning { system, user } and route
reviewViaDirectApi through callAnthropicAPI's structured branch — same
pattern the classifier already uses (router-classifier.mjs L456-484), so
infrastructure is reused, no new transport code.
system block: static instructions + 8-dim cues + schema-version notes
(byte-identical across episodes of the same schema_version → cache key
stable within a 5-min TTL).
user block: per-episode JSON (volatile).
Effect on Opus 4.7: ~zero until system grows past 4096-token cache-
minimum or model switches to Sonnet (2048 min). Anthropic silently
no-ops cache_control when prefix is below the minimum — no error,
cache_creation_input_tokens just stays at 0. Architecturally correct
and future-proof; activates the moment either condition flips.
buildReviewPrompt() kept as backward-compat wrapper.
Tests: +5 invariants for the split + cache-prerequisite check
(system identical across two v4 episodes with different bodies).
14/14 GREEN.
ремонт: фикс инфраструктуры стоимости — split prompt для активации
prompt caching на reviewer-agent
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
154 lines
5.8 KiB
JavaScript
154 lines
5.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* brain-retro reviewer — direct Opus API fallback handler (Phase 3 Task 18).
|
|
*
|
|
* Spec §4.6: the primary reviewer is a Claude Code subagent
|
|
* (`.claude/agents/reviewer-agent.md`) spawned via Task() from /brain-retro.
|
|
* THIS module is the FALLBACK handler invoked by the controller when the
|
|
* subagent crashes / times out: direct Opus API call with the same adaptive
|
|
* review prompt (but no cross-episode reading, no skill invocations).
|
|
*
|
|
* Pure layer: buildReviewPrompt + parseReview (this file's tests). Network
|
|
* layer: reviewViaDirectApi (zero-cost wrapper around router-classifier's
|
|
* callAnthropicAPI; the controller decides when to call it).
|
|
*
|
|
* G16 — file did not exist before Phase 3 Task 18; created here.
|
|
*/
|
|
|
|
import { REVIEWER_MODEL } from './router-config.mjs';
|
|
|
|
const REQUIRED_REVIEW_FIELDS = [
|
|
'node_quality',
|
|
'chain_quality',
|
|
'gap_assessment',
|
|
'agent_self_assessment_accuracy',
|
|
'error_root_cause',
|
|
'outcome_reviewed',
|
|
'reasoning',
|
|
];
|
|
|
|
/**
|
|
* Build the adaptive review prompt as { system, user } blocks for Anthropic
|
|
* prompt caching (ephemeral 5m TTL). The `system` block contains static
|
|
* instructions + 8-dim cues + schema-version-conditional notes; it is
|
|
* byte-identical across all episodes with the same schema_version and gets
|
|
* billed at ~10% rate after the first call within a 5-minute window. The
|
|
* `user` block carries the per-episode JSON (volatile).
|
|
*
|
|
* Cache-eligibility caveat: Anthropic's minimum cacheable prefix is
|
|
* model-dependent — 4096 tokens on Opus 4.7 / 4.6, 2048 on Sonnet 4.6. The
|
|
* static block here is ~300-400 tokens, so on Opus 4.7 cache writes silently
|
|
* no-op until either the static block grows or the model switches to Sonnet.
|
|
* The split is still applied so the moment either condition flips, caching
|
|
* activates with zero further code changes.
|
|
*/
|
|
export function buildReviewPromptStructured(episode) {
|
|
const v = Number(episode?.schema_version) || 0;
|
|
const cues = [
|
|
'node_quality: correct | wrong_node | overkill | underkill | disputable',
|
|
'chain_quality: correct | missing_step | extra_step | wrong_order | n/a',
|
|
'gap_assessment: acceptable | mistake_should_complete | mistake_should_not_start | n/a',
|
|
'agent_self_assessment_accuracy: accurate | over_confident | under_confident | no_self_assessment',
|
|
'error_root_cause: wrong_skill | wrong_tool | wrong_chain_order | external_failure | n/a',
|
|
'alternative_better: <node_id> | null',
|
|
'outcome_reviewed: success | soft_success | rework | blocked',
|
|
'reasoning: 1-3 sentences',
|
|
];
|
|
|
|
const adaptiveNotes = [];
|
|
if (v >= 3) {
|
|
adaptiveNotes.push('Episode is v3+: primary_rationale carries triggers/candidates/boundaries.');
|
|
}
|
|
if (v >= 4) {
|
|
adaptiveNotes.push('Episode is v4: classifier_output.alternatives_considered tells you what the classifier weighed.');
|
|
adaptiveNotes.push('self_assessment (if present and not pending) is the agent\'s post-hoc judgement — compare honesty.');
|
|
adaptiveNotes.push('execution_trace.chain_gaps shows whether the recommended chain ran in full.');
|
|
}
|
|
|
|
const system = [
|
|
'You are the independent reviewer of routing decisions for the Лидерра brain-governance experiment.',
|
|
'Return ONLY a JSON object with the 8 fields below. No prose, no code fences.',
|
|
'',
|
|
'Fields:',
|
|
...cues.map((c) => ' - ' + c),
|
|
'',
|
|
adaptiveNotes.length ? 'Notes for this schema version:' : '',
|
|
...adaptiveNotes.map((n) => ' - ' + n),
|
|
].filter(Boolean).join('\n');
|
|
|
|
const user = [
|
|
'Episode (JSON):',
|
|
JSON.stringify(episode, null, 2),
|
|
'',
|
|
'Output JSON only.',
|
|
].join('\n');
|
|
|
|
return { system, user };
|
|
}
|
|
|
|
/**
|
|
* Backward-compat wrapper — returns the concatenated single-string prompt for
|
|
* tests and any caller that hasn't switched to the structured form.
|
|
*
|
|
* Adaptive prompt template (spec §4.6):
|
|
* - v4 → full prompt including alternatives_considered, self_assessment,
|
|
* chain_gaps cues.
|
|
* - v3 → omits alternatives_considered.
|
|
* - v2 → omits both alternatives_considered and self_assessment.
|
|
* - v1 → skipped upstream (caller filters them out).
|
|
*/
|
|
export function buildReviewPrompt(episode) {
|
|
const { system, user } = buildReviewPromptStructured(episode);
|
|
return `${system}\n\n${user}`;
|
|
}
|
|
|
|
/**
|
|
* Parse the Opus reviewer response. Pure. Returns null on malformed JSON or
|
|
* when a required 8-dim field is missing. Passes through `reviewer_error`
|
|
* escalations from the subagent.
|
|
*/
|
|
export function parseReview(text) {
|
|
if (!text) return null;
|
|
const stripped = String(text).trim()
|
|
.replace(/^```(?:json)?\s*\n?/, '')
|
|
.replace(/\n?```$/, '')
|
|
.trim();
|
|
let parsed;
|
|
try { parsed = JSON.parse(stripped); }
|
|
catch { return null; }
|
|
if (!parsed || typeof parsed !== 'object') return null;
|
|
|
|
// Reviewer-agent escalation: pass through verbatim.
|
|
if (typeof parsed.reviewer_error === 'string') return parsed;
|
|
|
|
for (const f of REQUIRED_REVIEW_FIELDS) {
|
|
if (parsed[f] === undefined) return null;
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
/**
|
|
* Direct Opus API call. Wraps callAnthropicAPI from router-classifier with
|
|
* the reviewer model. Caller (controller inside /brain-retro) is responsible
|
|
* for decision (subagent first, this on failure).
|
|
*
|
|
* Returns the parsed review object or null on transport / parse failure.
|
|
*/
|
|
export async function reviewViaDirectApi(episode, options = {}) {
|
|
const { callAnthropicAPI } = await import('./router-classifier.mjs');
|
|
const apiKey = options.apiKey ?? process.env.ROUTER_LLM_KEY;
|
|
if (!apiKey) return null;
|
|
const structured = buildReviewPromptStructured(episode);
|
|
try {
|
|
const text = await callAnthropicAPI(structured, {
|
|
apiKey,
|
|
baseUrl: options.baseUrl ?? process.env.ROUTER_LLM_BASE_URL ?? undefined,
|
|
model: options.model ?? REVIEWER_MODEL,
|
|
onUsage: options.onUsage,
|
|
});
|
|
return parseReview(text);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|