397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
56 lines
3.7 KiB
JavaScript
56 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* router-emerge — лёгкий узкий per-ответ проход «всплывшая нужда» (spec 2026-06-05
|
|
* §3): после КАЖДОГО ответа роутер смотрит ТОЛЬКО последний ответ + план и ищет
|
|
* озвученную нужду, которой в плане нет (не полный пересчёт — дёшево). При батче
|
|
* ответов мелочь тонет; отдельный взгляд на каждый ответ даёт чистый шанс поймать.
|
|
* РАССУЖДЕНИЕ — у модели (llmCall мокается); проверки вокруг — механические.
|
|
*/
|
|
import { parseRouterResponse } from './router-engine.mjs';
|
|
|
|
function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; }
|
|
|
|
/** Построитель промпта лёгкого прохода (чистый, {system,user}). Узкий: только последний ответ + план. */
|
|
export function buildEmergePrompt({ lastAnswer = '', plan = null } = {}) {
|
|
const planText = plan == null ? '(плана пока нет)' : (typeof plan === 'string' ? plan : JSON.stringify(plan));
|
|
const system = [
|
|
'Ты — РОУТЕР-НАСТАВНИК, ЛЁГКИЙ УЗКИЙ ПРОХОД (всплывшая нужда). Смотри ТОЛЬКО последний ответ владельца ниже.',
|
|
'Вопрос один: есть ли в ЭТОМ последнем ответе озвученная нужда/просьба (напр. «сделай отступ»), которой НЕТ в плане? НЕ пересчитывай весь план — только этот ответ.',
|
|
'Ничего нового нет → emerged пустой. Это нормально.',
|
|
'ВЫВОД — строго JSON: {"emerged":[{"item":"…","detail":"…"}]}. item — непустой.',
|
|
].join('\n');
|
|
const user = [
|
|
'--- ПОСЛЕДНИЙ ОТВЕТ ВЛАДЕЛЬЦА ---',
|
|
lastAnswer,
|
|
'--- ТЕКУЩИЙ ПЛАН ---',
|
|
planText,
|
|
].join('\n');
|
|
return { system, user };
|
|
}
|
|
|
|
/** Механический валидатор: emerged — массив (может быть пуст); пункт {item непустой, detail?}. */
|
|
export function validateEmergeTrace(trace) {
|
|
const badItems = [];
|
|
if (!trace || typeof trace !== 'object' || !Array.isArray(trace.emerged))
|
|
return { ok: false, badItems, reason: 'нет массива emerged' };
|
|
trace.emerged.forEach((it, i) => {
|
|
if (!it || typeof it !== 'object' || !isNonEmptyString(it.item)) badItems.push(`emerged[${i}]`);
|
|
});
|
|
return { ok: badItems.length === 0, badItems };
|
|
}
|
|
|
|
/**
|
|
* Оркестратор лёгкого прохода: построить → llmCall (инъекция) → распарсить →
|
|
* провалидировать форму → вернуть emerged. hasEmerged считает КОД из массива.
|
|
*/
|
|
export async function runEmergeCheck({ lastAnswer = '', plan = null, llmCall }) {
|
|
let trace;
|
|
try { trace = await llmCall({ buildPrompt: () => buildEmergePrompt({ lastAnswer, plan }) }); }
|
|
catch { return { ok: false, reason: 'сбой вызова модели', trace: null }; }
|
|
if (typeof trace === 'string') trace = parseRouterResponse(trace);
|
|
if (!trace) return { ok: false, reason: 'пустой/неразборный ответ', trace: null };
|
|
const v = validateEmergeTrace(trace);
|
|
if (!v.ok) return { ok: false, reason: `невалидный emerge: ${v.reason || ''} пункты [${v.badItems.join(', ')}]`, trace };
|
|
return { ok: true, emerged: trace.emerged, hasEmerged: trace.emerged.length > 0, trace };
|
|
}
|