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:
@@ -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,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
parseTranscript,
|
||||
extractEnvironment,
|
||||
@@ -1655,3 +1658,59 @@ describe('parseTranscript v3 fields', () => {
|
||||
expect(typeof hookEvent.scripts).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTranscript — router-state enrichment (Task 3)', () => {
|
||||
function makeTranscript(sessionId) {
|
||||
return [
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
message: { role: 'user', content: 'добавь новый endpoint /api/bar' },
|
||||
timestamp: '2026-05-24T10:00:00Z',
|
||||
uuid: 'u-t3-1',
|
||||
sessionId,
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'assistant',
|
||||
message: { role: 'assistant', content: [{ type: 'text', text: 'делаю' }] },
|
||||
timestamp: '2026-05-24T10:00:01Z',
|
||||
uuid: 'u-t3-2',
|
||||
sessionId,
|
||||
}),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
it('enriches primary_rationale from router-state file when present', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'router-state-test-'));
|
||||
const sessionId = 'test-session-t3-enrich';
|
||||
const state = {
|
||||
classification: { recommendedNode: '#42', recommendedChain: 'L13' },
|
||||
chainProgress: ['step-a', 'step-b'],
|
||||
chainCompleted: false,
|
||||
};
|
||||
writeFileSync(join(dir, `router-state-${sessionId}.json`), JSON.stringify(state));
|
||||
try {
|
||||
const ep = parseTranscript(makeTranscript(sessionId), sessionId, { routerStateBaseDir: dir });
|
||||
expect(ep.primary_rationale.recommended_node).toBe('#42');
|
||||
expect(ep.primary_rationale.recommended_chain).toBe('L13');
|
||||
expect(ep.primary_rationale.chain_progress).toEqual(['step-a', 'step-b']);
|
||||
expect(ep.primary_rationale.chain_completed).toBe(false);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('falls back gracefully when router-state file is absent', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'router-state-test-'));
|
||||
const sessionId = 'test-session-t3-missing';
|
||||
try {
|
||||
const ep = parseTranscript(makeTranscript(sessionId), sessionId, { routerStateBaseDir: dir });
|
||||
// recommended_node falls back to classification-map result (direct episode → feature → #19)
|
||||
expect(ep.primary_rationale.recommended_node).toBe('#19');
|
||||
expect(ep.primary_rationale.recommended_chain).toBeNull();
|
||||
expect(ep.primary_rationale.chain_progress).toEqual([]);
|
||||
expect(ep.primary_rationale.chain_completed).toBe(false);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user