0a97324f1b
R56–R59: agent.ts 2731→1529줄 god-file 분해 (25 modules) · attrParsers + LLM 메서드 8개 (callNonStreaming, streamChatOnce 등) · executeActions 415줄 → 8 handler 그룹 (file/run/list/brain/calendar/sheets/tasks) · handlePrompt 1100줄 → 7 phase 모듈 (system prompt + budget + autoContinue 등) R50–R55: extension.ts 1145→349줄 (telegram/settings/provider commands 분리) Stocks feature 신규: /stocks slash command (v2.2.152~158) · .astra/stocks.json 저장소 + Yahoo Finance 현재가 갱신 · 8 키워드 필터 (ROE/성장성/유동성/수익성/영업효율/기술력/안정성/PBR) · Naver 시가총액 페이지 JSON API (m.stock.naver.com) 발굴 · LLM Top 5 매력도 분석 + Telegram 자동 보고서 · KST 09:00/15:00 watcher 자동 모니터링 대화 연속성 (v2.2.150~157): · [PRIOR TURN CONCLUSION] block 으로 직전 결론 anchor · thin follow-up 분류 → boilerplate 헤더 suppression · slash 명령 결과 chatHistory mirror (capture wrapper) · echo/parrot 금지 system prompt rule 기타: /stocks 슬래시 자동완성 dropdown UI, Naver JSON API 전환 (cheerio 제거) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
122 lines
5.7 KiB
TypeScript
122 lines
5.7 KiB
TypeScript
import { listActiveAgentsByCategory, resolveAgent } from './companyConfig';
|
|
import type {
|
|
AgentRoleCategory,
|
|
AgentTurnOutput,
|
|
CompanyState,
|
|
PipelineStage,
|
|
} from './types';
|
|
|
|
/**
|
|
* `dispatcher.ts` 의 6개 stateless helper 모음. dispatcher 본 흐름에서 떼어내
|
|
* (a) 단위 테스트 가능, (b) parsing 정책 변경 시 한 곳만 수정.
|
|
*
|
|
* - resolveInspector(reviewWith, state) — `inspector` / `role:<cat>` / `agent:<id>` 라우팅
|
|
* - parseInspectorVerdict(text) — pass / revise / unclear
|
|
* - parseCeoVerdict(text) — pass / revise / abort / unclear
|
|
* - renderStageInstruction(stage, ...) — instruction 템플릿 토큰 치환
|
|
* - hasActionTag(text) — action-tag 존재 cheap pre-check
|
|
* - claimsFileCreation(text) — past-tense 파일 생성 narration 검출
|
|
*/
|
|
|
|
/**
|
|
* 검수자 (또는 직군) 를 stage.reviewWith 값에 따라 한 명 결정:
|
|
* - 'inspector' → inspector 직군 활성 후보 중 첫 번째
|
|
* - 'role:<cat>' → 해당 직군 활성 후보 중 첫 번째
|
|
* - 'agent:<id>' → 그 에이전트 (활성/비활성 무관)
|
|
* 후보 없으면 null — 호출자가 검수 사이클 skip.
|
|
*/
|
|
export function resolveInspector(reviewWith: string, state: CompanyState): { agentId: string } | null {
|
|
if (reviewWith === 'inspector') {
|
|
const list = listActiveAgentsByCategory(state)['inspector'] ?? [];
|
|
return list[0] ? { agentId: list[0].id } : null;
|
|
}
|
|
if (reviewWith.startsWith('role:')) {
|
|
const cat = reviewWith.slice(5) as AgentRoleCategory;
|
|
const list = listActiveAgentsByCategory(state)[cat] ?? [];
|
|
return list[0] ? { agentId: list[0].id } : null;
|
|
}
|
|
if (reviewWith.startsWith('agent:')) {
|
|
const id = reviewWith.slice(6);
|
|
return resolveAgent(state, id) ? { agentId: id } : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 검수자 응답의 verdict 추출. 작은 모델이 라벨 흐트러뜨릴 수 있어 키워드 매칭으로
|
|
* 관대하게. 못 잡으면 'unclear' — 호출자가 안전한 쪽 (보통 'revise') 으로 폴백.
|
|
*/
|
|
export function parseInspectorVerdict(text: string): 'pass' | 'revise' | 'unclear' {
|
|
const head = (text || '').split(/\n/, 1)[0] ?? '';
|
|
if (/^\s*(?:✅|통과|승인|pass|approve|ok)/i.test(head)) return 'pass';
|
|
if (/^\s*(?:❌|보완|재작업|revise|reject|fail|보완 필요)/i.test(head)) return 'revise';
|
|
// 본문에 명확한 신호가 있으면 잡아냄 — 작은 모델이 머리말을 빠뜨리는 경우.
|
|
if (/✅\s*통과|모든 케이스 통과/.test(text)) return 'pass';
|
|
if (/❌|보완 필요|재작업/.test(text)) return 'revise';
|
|
return 'unclear';
|
|
}
|
|
|
|
/**
|
|
* CEO 메타-판단 verdict. pass / revise / abort / unclear. 검수자 verdict 와 같은
|
|
* 키워드-우선 휴리스틱이지만 abort 옵션이 추가됨 (의도적으로 중단).
|
|
*/
|
|
export function parseCeoVerdict(text: string): 'pass' | 'revise' | 'abort' | 'unclear' {
|
|
const head = (text || '').split(/\n/, 1)[0] ?? '';
|
|
if (/^\s*(?:✅|통과|approve|pass|최종\s*ok|진행)/i.test(head)) return 'pass';
|
|
if (/^\s*(?:🔁|보완|한 번 더|revise|다시)/i.test(head)) return 'revise';
|
|
if (/^\s*(?:🛑|중단|stop|abort|그만)/i.test(head)) return 'abort';
|
|
if (/✅\s*통과/.test(text)) return 'pass';
|
|
if (/🛑|중단/.test(text)) return 'abort';
|
|
if (/🔁|보완|한 번 더/.test(text)) return 'revise';
|
|
return 'unclear';
|
|
}
|
|
|
|
/**
|
|
* Stage instruction 의 템플릿 토큰 치환. 템플릿 비어 있으면 raw user prompt 그대로
|
|
* 폴백 — 사용자가 매 stage 마다 긴 템플릿을 채울 필요 없게.
|
|
*
|
|
* 지원 토큰:
|
|
* - {{userPrompt}} — 원본 사용자 prompt
|
|
* - {{brief}} — 플래너가 생성한 brief
|
|
* - {{stage.<sid>}} — 다른 stage 의 가장 최근 response (미실행 시 placeholder)
|
|
*/
|
|
export function renderStageInstruction(
|
|
stage: PipelineStage,
|
|
userPrompt: string,
|
|
brief: string,
|
|
latestByStage: Record<string, AgentTurnOutput>,
|
|
): string {
|
|
const tpl = (stage.instructionTemplate || '').trim();
|
|
if (!tpl) return userPrompt;
|
|
return tpl
|
|
.replace(/\{\{\s*userPrompt\s*\}\}/g, userPrompt)
|
|
.replace(/\{\{\s*brief\s*\}\}/g, brief)
|
|
.replace(/\{\{\s*stage\.([a-zA-Z0-9_-]+)\s*\}\}/g, (_m, sid) => {
|
|
const o = latestByStage[sid];
|
|
return o?.response ?? `[stage:${sid} 아직 실행되지 않음]`;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Cheap pre-check — text 에 어떤 action-tag 라도 있으면 true. action-tag executor
|
|
* 를 매 specialist 응답마다 띄우지 않게 가드.
|
|
*/
|
|
export function hasActionTag(text: string): boolean {
|
|
return /<\s*(?:create_file|edit_file|delete_file|read_file|list_files|list_brain|run_command|read_brain|reveal_in_explorer|open_file|glob|grep)\b/i.test(text);
|
|
}
|
|
|
|
/**
|
|
* Heuristic: response 가 *narrate* "파일 생성했음" 했는지 (action-tag 없이).
|
|
*
|
|
* 과거형 / 완료 표현 + 파일/폴더 mention 둘 다 있어야 true. plan ("다음에 X 를 만들어야")
|
|
* 같은 미래형은 의도적으로 안 잡음. agent 가 `<create_file>` 태그 안 쓰고 "foo.py
|
|
* 파일을 생성했습니다" 환각하는 패턴 검출 → dispatcher 가 CEO 와 사용자에게 flag.
|
|
*/
|
|
export function claimsFileCreation(text: string): boolean {
|
|
const claimRe = /(?:생성했|만들었|작성했|저장했|구현했|created|wrote|saved|built|generated)/i;
|
|
if (!claimRe.test(text)) return false;
|
|
const fileLike = /\b[\w\-./]+\.(?:py|js|ts|tsx|jsx|md|json|html|css|sh|yaml|yml|sql|java|go|rs|c|cpp|rb|php)\b/i.test(text);
|
|
const folderLike = /(?:폴더|디렉토리|directory|folder)/i.test(text);
|
|
return fileLike || folderLike;
|
|
}
|