feat(observer): обогащение primary_rationale из router-state (Task 3)

- parseTranscript получает третий параметр options = {}
- options.routerStateBaseDir пробрасывается в readRouterState
- recommended_node: router-state переопределяет classification-map
- новые поля: recommended_chain, chain_progress, chain_completed
- 2 новых теста (enrich + fallback), 538/538 tools GREEN

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-24 15:53:59 +03:00
parent 593f12ae6a
commit 92bbd64eed
2 changed files with 73 additions and 5 deletions
+14 -5
View File
@@ -18,6 +18,7 @@
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { readRouterState, extractRouterFields } from './observer-state-enricher.mjs';
import { homedir } from 'node:os';
import { detectChoiceProvenance, detectAskUserQuestionChoice } from './observer-choice-detector.mjs';
import { loadChainMap, chainsFor } from './observer-chain-detector.mjs';
@@ -751,13 +752,17 @@ function extractLastAssistantContent(entries, turnStartIdx) {
* @param {string|null} fallbackSessionId - Used when the transcript has no sessionId.
* @returns {object} v2 episode.
*/
export function parseTranscript(transcriptText, fallbackSessionId = null) {
export function parseTranscript(transcriptText, fallbackSessionId = null, options = {}) {
const { entries, broken, total } = parseLines(transcriptText);
const withSession = entries.find((e) => e && e.sessionId);
const sessionId =
(withSession && withSession.sessionId) || fallbackSessionId || `unknown-${Date.now()}`;
const routerStateBaseDir = options.routerStateBaseDir;
const routerState = readRouterState(sessionId, routerStateBaseDir ? { baseDir: routerStateBaseDir } : {});
const routerFields = extractRouterFields(routerState);
const start = findTurnStart(entries);
const turn = entries.slice(start);
@@ -809,6 +814,10 @@ export function parseTranscript(transcriptText, fallbackSessionId = null) {
primary_rationale: (() => {
const tag = parseReasoningTag(turn);
const merge = (heur, fromTag) => [...new Set([...heur, ...fromTag])];
const classifMapNode =
skills.length === 0
? recommendNode(classifyTask(prompt), getClassificationMap(), getDormancy())
: null;
return {
step: 1,
node_chosen: skills.length > 0 ? skills[0] : 'direct',
@@ -820,10 +829,10 @@ export function parseTranscript(transcriptText, fallbackSessionId = null) {
? { invoked: true, rules: ['Pravila §12'] }
: { invoked: false, rules: [] },
task_classification: classifyTask(prompt),
recommended_node:
skills.length === 0
? recommendNode(classifyTask(prompt), getClassificationMap(), getDormancy())
: null,
recommended_node: routerFields.recommended_node !== null ? routerFields.recommended_node : classifMapNode,
recommended_chain: routerFields.recommended_chain,
chain_progress: routerFields.chain_progress,
chain_completed: routerFields.chain_completed,
};
})(),
events,