Files
brain/tools/router-emerge.mjs
T

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 };
}