5feacb4799
v2.2.308 — Standing Rules (Correction Loop ④): 행동/스타일 교정의 세션 영속화
- 행동 지적 감지("또 ~하네", "하지 말라고 했잖아" 등 5종) → LLM 이 명령형 규칙으로
정규화 → .astra/growth/standing-rules.json 즉시 저장 → 매 턴 보호 구역 주입.
self-reflection 이 컨텍스트 창(단기 기억)에 갇혀 새 세션에서 증발하던 문제 해결.
- 중복 지적은 hits 증가(토큰 자카드), 상한 10개 초과 시 자동 은퇴, 재지적 시 부활.
- "Astra: 상시 행동 규칙" 커맨드 — 활성/은퇴 규칙 + 실제 주입 블록 열람(도달 증거).
v2.2.309 — 조사 파이프라인: 파일명만 보고 상상하는 헛조사 4중 차단
- <investigate_files path focus> 액션: 폴더의 텍스트 파일을 코드가 강제 정독해
파일별 노트 주입(Map-Reduce). 내용 해시 캐시(.astra/cache/investigate)로 재조사
시 재사용 — 온디맨드 지식화.
- 조사 증거 게이트(행동 제약) + Hollow Investigation 감지(list만 하고 read 0회로
파일 서술 시 경고 footer — continuation 경로 포함) + actionStats turn 추적.
- g1nation.investigationModel: 조사형 요청만 상위 모델(예: claude-code:sonnet) 라우팅.
v2.2.310 — LM Studio LSEP 합성 추론 구분자 처리
- 일부 모델 조합에서 추론이 여는 마커 없이 content 앞에 흐르고
__LM_STUDIO_INTERNAL_LSEP_SYNTHETIC_REASONING_END_<hex>__ 로만 경계 표시 →
생각 과정 전체가 채팅에 노출되던 문제.
- LiveReasoningFilter: 마커 감지 시 streamReplace 신호로 화면 즉시 리셋(토큰 분절
대응 holdback 포함), sanitizeAssistantContent: 마커 이전 추론 최종 제거(이중 방어).
기타: tests/wikiSave.test.ts 상대경로 테스트 Windows 호환 수정(드라이브 문자).
검증: 전체 테스트 926건 통과 (신규 39건: standingRules 15 + investigationPipeline 16 + LSEP 8).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
137 lines
6.3 KiB
TypeScript
137 lines
6.3 KiB
TypeScript
import { LiveReasoningFilter } from '../src/lib/contextBuilders/liveReasoningFilter';
|
|
|
|
/** 토큰 배열을 순서대로 밀어넣고 화면에 방출된 텍스트를 합쳐 반환. */
|
|
function run(tokens: string[]): string {
|
|
const f = new LiveReasoningFilter();
|
|
let out = '';
|
|
for (const t of tokens) out += f.push(t);
|
|
out += f.flush();
|
|
return out;
|
|
}
|
|
|
|
describe('LiveReasoningFilter — 스트리밍 중 추론 구간 실시간 차단', () => {
|
|
test('일반 텍스트는 그대로 통과한다', () => {
|
|
expect(run(['안녕', '하세요. ', '무엇을 도와드릴까요?'])).toBe('안녕하세요. 무엇을 도와드릴까요?');
|
|
});
|
|
|
|
test('<think> 블록은 통째로 숨긴다', () => {
|
|
expect(run(['<think>사용자가 인사했다</think>', '반갑습니다.'])).toBe('반갑습니다.');
|
|
});
|
|
|
|
test('토큰 경계에 걸쳐 쪼개진 <think> 도 잡는다', () => {
|
|
expect(run(['<th', 'ink>내부 ', '추론</thi', 'nk>답', '변입니다'])).toBe('답변입니다');
|
|
});
|
|
|
|
test('<thinking> 과 <analysis> 변형도 처리한다', () => {
|
|
expect(run(['<thinking>...</thinking>', 'A'])).toBe('A');
|
|
expect(run(['<analysis>...</analysis>', 'B'])).toBe('B');
|
|
});
|
|
|
|
test('본문 중간의 think 블록도 앞뒤 본문을 보존한다', () => {
|
|
expect(run(['결론은 X입니다. ', '<think>검산</think>', '근거는 Y입니다.']))
|
|
.toBe('결론은 X입니다. 근거는 Y입니다.');
|
|
});
|
|
|
|
test('Harmony thought 채널은 숨기고 final 채널 본문만 통과한다', () => {
|
|
const tokens = [
|
|
'<|channel|>thought The user said "안녕". This is a greeting.',
|
|
'<|channel|>final<|message|>',
|
|
'반갑습니다. 무엇을 도와드릴까요?',
|
|
];
|
|
expect(run(tokens)).toBe('반갑습니다. 무엇을 도와드릴까요?');
|
|
});
|
|
|
|
test('채널 마커가 토큰 경계에 걸쳐도 잡는다', () => {
|
|
const tokens = ['<|chan', 'nel|>thought 내부 추론 텍스트 ', '<|channel|>fin', 'al<|mess', 'age|>답변'];
|
|
expect(run(tokens)).toBe('답변');
|
|
});
|
|
|
|
test('thought 채널이 <|end|> 로 끝나면 이후는 visible', () => {
|
|
expect(run(['<|channel|>thought 추론<|end|>', '본문'])).toBe('본문');
|
|
});
|
|
|
|
test('닫히지 않은 think 는 아무것도 방출하지 않는다 (최종 streamReplace 가 정리)', () => {
|
|
expect(run(['<think>끝나지 않는 추론...'])).toBe('');
|
|
});
|
|
|
|
test('마커가 아닌 < 문자는 보류 후 정상 방출된다', () => {
|
|
expect(run(['a < b 이고 ', 'c > d 입니다'])).toBe('a < b 이고 c > d 입니다');
|
|
expect(run(['제네릭 Array<string> 타입'])).toBe('제네릭 Array<string> 타입');
|
|
});
|
|
|
|
test('스트림이 잠재 마커 접두사로 끝나면 flush 로 회수된다', () => {
|
|
expect(run(['답변 끝 <'])).toBe('답변 끝 <');
|
|
});
|
|
});
|
|
|
|
describe('[v2.2.310] LM Studio LSEP 합성 추론 구분자', () => {
|
|
const MARKER = '__LM_STUDIO_INTERNAL_LSEP_SYNTHETIC_REASONING_END_f4e9a8d2c6b14d0c9e5f3a7b8c1d2e6a__';
|
|
|
|
/** run + separator 신호 관찰 버전. */
|
|
function runWithSignal(tokens: string[]): { out: string; replacedAt: number[] } {
|
|
const f = new LiveReasoningFilter();
|
|
let out = '';
|
|
const replacedAt: number[] = [];
|
|
tokens.forEach((t, i) => {
|
|
const visible = f.push(t);
|
|
if (f.consumeSeparatorSignal()) {
|
|
out = visible; // streamReplace 시뮬레이션 — 화면 리셋
|
|
replacedAt.push(i);
|
|
} else {
|
|
out += visible;
|
|
}
|
|
});
|
|
out += f.flush();
|
|
return { out, replacedAt };
|
|
}
|
|
|
|
test('END 마커 뒤 텍스트만 최종 화면에 남는다 (실사례 재현)', () => {
|
|
const { out, replacedAt } = runWithSignal([
|
|
'User message: "안녕, 잘지내?" 분석... Option 1... Final choice...',
|
|
MARKER + '잘 지내고 있습니다. 별일 없으셨나요?',
|
|
]);
|
|
expect(out).toBe('잘 지내고 있습니다. 별일 없으셨나요?');
|
|
expect(replacedAt).toEqual([1]); // 마커 도착 시점에 streamReplace 신호
|
|
});
|
|
|
|
test('마커가 토큰 경계에 잘게 쪼개져도 잡는다', () => {
|
|
const { out } = runWithSignal([
|
|
'추론 텍스트 어쩌고 ',
|
|
'__LM_STUDIO_INTERNAL_LSEP',
|
|
'_SYNTHETIC_REASONING_END_f4e9a8d2c6b14d0c',
|
|
'9e5f3a7b8c1d2e6a__',
|
|
'실제 답변',
|
|
]);
|
|
expect(out).toBe('실제 답변');
|
|
});
|
|
|
|
test('START 변형이 오면 END 까지 통째로 숨긴다', () => {
|
|
const start = '__LM_STUDIO_INTERNAL_LSEP_SYNTHETIC_REASONING_START_abc123def456__';
|
|
const { out } = runWithSignal(['답변 앞부분 ', start + '숨길 추론 ' + MARKER, '답변 뒷부분']);
|
|
expect(out).toBe('답변 앞부분 답변 뒷부분');
|
|
});
|
|
|
|
test('마커 없는 일반 밑줄 텍스트는 그대로 통과 (오탐 없음)', () => {
|
|
const { out, replacedAt } = runWithSignal(['snake_case_variable 와 __init__ 메서드', ' 설명입니다']);
|
|
expect(out).toBe('snake_case_variable 와 __init__ 메서드 설명입니다');
|
|
expect(replacedAt).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('[v2.2.310] sanitizeAssistantContent — LSEP 최종 정리 (이중 방어)', () => {
|
|
const { sanitizeAssistantContent } = require('../src/lib/contextBuilders/outputSanitization');
|
|
const MARKER = '__LM_STUDIO_INTERNAL_LSEP_SYNTHETIC_REASONING_END_f4e9a8d2c6b14d0c9e5f3a7b8c1d2e6a__';
|
|
|
|
test('마커 앞 추론 전체 제거, 답변만 남긴다', () => {
|
|
const raw = 'User message 분석... Option 1... Final choice...' + MARKER + '잘 지내고 있습니다. 별일 없으셨나요?';
|
|
expect(sanitizeAssistantContent(raw)).toBe('잘 지내고 있습니다. 별일 없으셨나요?');
|
|
});
|
|
test('마커가 여러 번이면 마지막 마커 뒤만', () => {
|
|
const raw = '추론1' + MARKER + '중간' + MARKER + '최종 답변';
|
|
expect(sanitizeAssistantContent(raw)).toBe('최종 답변');
|
|
});
|
|
test('마커 없으면 기존 동작 그대로', () => {
|
|
expect(sanitizeAssistantContent('일반 답변입니다.')).toBe('일반 답변입니다.');
|
|
});
|
|
});
|