feat: v2.62.0 - Astra Autonomous Loop (AAL) foundation & enhanced file analysis

This commit is contained in:
g1nation
2026-05-04 12:58:43 +09:00
parent 445d530b63
commit 215c5f9457
23 changed files with 2964 additions and 62 deletions
+913
View File
@@ -0,0 +1,913 @@
{
"version": 1,
"projectId": "b4550ddda527",
"projectName": "Antigravity",
"techStack": [
"자님의",
"로컬",
"프로젝트를",
"보조하고",
"실행하는",
"특화된",
"운영",
"지원",
"파트너라고",
"보시면",
"됩니다.",
"제가",
"가장",
"있는",
"일은",
"다음과",
"같습니다.",
"하지",
"않음",
"선택",
"노트",
"0개",
"검색",
"0개</summary>",
"여부",
"않은",
"문서",
"10개",
"13개</summary>",
"맥락",
"경험",
"승인",
"전환",
"비즈니스",
"가치",
"목적:",
"고객",
"관점",
"가능성",
"판단",
"답변의",
"개념",
"구조",
"용어",
"정의",
"관계",
"설정",
"최종",
"결과물의",
"문체",
"순서",
"설명",
"방식",
"보고서",
"구성",
"사용자",
"요청",
"주제에",
"대한",
"배경",
"정보와",
"사례",
"구현",
"기술적",
"방법론",
"참고",
"주장과",
"본문을",
"뒷받침할",
"직접",
"근거",
"템플릿의",
"분석/해석",
"섹션에",
"넣을",
"핵심",
"약점",
"주의점",
"검증",
"필요",
"항목",
"다음",
"액션",
"개선안",
"MVP",
"실행",
"계획",
"자가",
"제품을",
"만나는",
"순간부터",
"떠나는",
"순간까지의",
"감정과",
"경험을",
"정교하게",
"설계하고",
"복잡성을",
"직관적인",
"단순함으로",
"치환하여",
"사용자의",
"'성공'을",
"가속화하라\"",
"중심",
"설계(UCD)를",
"통해",
"가치를",
"실현하는",
"전략적",
"프로세스.",
"피드백(이동",
"...",
"권장.",
"2.",
"**📖",
"구조화된",
"지식",
"(Synthesized",
"Content):**",
"'추출된",
"패턴'을",
"명시하고",
"원리",
"요소",
"의의를",
"논리적",
"불렛",
"포인트로",
"정리.",
"3.",
"**⚠️",
"모순",
"업데이트",
"(Contradictions",
"RL",
"Update):**",
"과거와",
"현재",
"기술의",
"차이점(모순",
"해소)과",
"Antigravity",
"프로젝트",
"특유의",
"적용",
"정책",
"명시.",
"4.",
"**🔗",
"연결",
"(Graph):**",
"양방향",
"위키",
"링크를",
"간의",
"인과",
"연관",
"형성.",
"**작성",
"원칙:**",
"**Unified",
"St...",
"하여",
"관련된",
"커밋",
"내역을",
"추출하고",
"변수명",
"변경이나",
"단순",
"주석",
"수정과",
"같은",
"사소한",
"커밋(Trivial",
"commits)을",
"필터링한...",
"변경",
"마이크로서비스",
"분리",
"돌이키기",
"힘든",
"주요",
"개발",
"결정을",
"내릴",
"결정의",
"맥락과",
"기각된",
"대안들을",
"ADR(Architecture",
"Decision",
"Record)",
"양식에",
"맞춰",
"작성한",
"사내",
"위키나",
"형상",
"관리",
"시스템에",
"커밋하여",
"히스토리를",
"보존합니다",
"[3",
"10].",
"**System",
"Design:**",
"시스템을",
"처음",
"설계하거나",
"새로운",
"모듈을",
"추가할",
"C4",
"모델",
"혹은",
"4+1",
"모델을",
"활용해",
"다이어그램을",
"그리고",
"문서화하여",
"백엔드",
"개발자",
"DB",
"관리자",
"이해관계자들이",
"각자의",
"관점에서",
"시스템",
"구조를",
"명확히",
"이해하도록",
"돕습니다",
"[2",
"4].",
"**Operation",
"Maintenance:**",
"트래픽",
"패턴",
"변화나",
"규제",
"도입",
"등으로",
"비즈니스/운영",
"맥락이",
"변했을",
"시스...",
"하는",
"분석",
"방식이다",
"[1",
"2].",
"전략은",
"시스템의",
"비즈니스적",
"제약을",
"동시에",
"파악하여",
"복잡한",
"전체상을",
"그리는",
"효율적인",
"것으로",
"평가된다",
"[2].",
"또한",
"개념은",
"코드베이스",
"탐색뿐만",
"아니라",
"테스트",
"파일",
"조직화",
"등에서",
"여러",
"전략을",
"결합하여",
"유지보수를",
"효율화하는",
"데에도",
"적용될",
"있다",
"[3].",
"맥락\"",
"가치\"",
"사례\"",
"자님은",
"회의록을",
"작성하는",
"능력에",
"대해",
"확인하셨습니다.",
"저에게",
"비정형적인",
"회의",
"내용을",
"제공해주시면",
"이를",
"형태로",
"변환하여",
"정리해",
"드리는",
"작업을",
"수행할",
"있습니다.",
"체계적으로",
"기록하고",
"정리하여",
"나중에",
"참고하거나",
"후속",
"조치를",
"취해야",
"필요가",
"공식적인",
"만들고",
"싶어",
"하시는",
"하실",
"건가요?",
"(Question",
"reason:",
"사용처에",
"따라",
"문서의",
"형식과",
"분량이",
"결정됩니다.)",
"자님과",
"편하게",
"수다",
"수도",
"있어요.",
"이야기에",
"기울이고",
"함께",
"생각할",
"준비가",
"되어",
"파트너입니다.",
"저는",
"코드를",
"분석하거나",
"파일을",
"다루는",
"실질적인",
"작업에",
"강점이",
"있지만",
"지식은",
"사용자님께서",
"제공해주시는",
"문서나",
"기반으로",
"작동하기",
"때문에",
"현재로서는",
"특정",
"프로젝트의",
"깊은",
"방식을",
"판단하는",
"데는",
"한계가",
"저의",
"역할과",
"상태에",
"질문하시면서",
"부족한",
"부분이",
"있다면",
"알려주시면",
"부분을",
"채워주시겠다는",
"의사를",
"표현하셨습니다.",
"환경에서",
"직접적으로",
"작동하는",
"작업들입니다.",
"탐색하고",
"코드의",
"논리를",
"분석하며",
"필요한",
"수행하는",
"강점을",
"가지고",
"예를",
"들어",
"파악하거나",
"코드에서",
"잠재적인",
"문제점을",
"찾아내고",
"읽어와서",
"요약하거나",
"수정하는",
"등의",
"보조",
"자님께서",
"소스",
"코드나",
"설계",
"없이는",
"판단하기",
"어렵습니다.",
"법이나",
"도메인의",
"매우",
"전문적인",
"부족할",
"피드백이",
"필수적입니다.",
"**'실제",
"증거(소스",
"코드",
"문서)'**가",
"없으면",
"기술적인",
"판단은",
"신중하게",
"접근해야",
"합니다.",
"역할을",
"이해하고",
"어떤",
"부분에서",
"도움을",
"받으면",
"효과적일지",
"알고",
"저를",
"더욱",
"강력한",
"파트너로",
"만들기",
"위해",
"채워야",
"하는지",
"확인하고",
"싶으신",
"거죠.",
"관련",
"지식인가요?",
"기술에",
"이해는",
"분석의",
"정확도를",
"높입니다.)",
"3개",
"6개</summary>",
"가능한",
"서비스로",
"일괄",
"변환(Batch",
"transformation)하는",
"유용하다",
"9].",
"**계층적",
"다중",
"에이전트",
"활용",
"(Hierarchical",
"Multi-Agent",
"Approach):**",
"전체",
"아키텍처와",
"교차",
"모듈",
"의존성을",
"분석해",
"리팩토링",
"계획을",
"수립하는",
"플래너",
"모델(예:",
"Gemini",
"2.5",
"Pro)과",
"단위의",
"국지적인",
"리팩토링과",
"생성을",
"실제로",
"줄이려는",
"불확실성을",
"먼저",
"잡아야",
"한다는",
"쪽입니다.",
"그래서",
"답변은",
"정보",
"나열보다",
"선택과",
"행동",
"중심으로",
"봅니다.",
"실제",
"과정",
"사이의",
"간극을",
"메우는",
"'연결",
"고리'를",
"강화하는",
"것입니다.",
"능력은",
"갖추고",
"실행이",
"미묘한",
"어떻게",
"연결되는지에",
"이해를",
"더해야",
"자님께서는",
"아이언맨의",
"자비스처럼",
"싶으시며",
"구조와",
"지식을",
"공부하고",
"설계의",
"보강해야",
"할지",
"구체적인",
"방향을",
"질문하셨습니다.",
"요구사항처럼",
"단순한",
"제공자를",
"넘어",
"**'선제적으로",
"예측하고",
"실행을",
"보조하는",
"파트너'**의",
"방향으로",
"설계와",
"정비해야",
"한다고",
"자에게",
"영향을",
"미치는지에",
"연결고리가",
"필요합니다.",
"현재는",
"지식들이",
"독립적으로",
"존재하지만",
"이들을",
"하나의",
"**'실행",
"시나리오'**로",
"묶어",
"분석하는",
"능력이",
"강화되어야",
"지능과",
"실행력을",
"극대화하여",
"보조를",
"능동적으로",
"문제를",
"최적의",
"해결책을",
"제시하는",
"수준의",
"고도화된",
"AI",
"파트너를",
"구축하고",
"통합",
"트레이드오프**에",
"보강이",
"필요하신가요?",
"상호작용하는",
"충돌을",
"미리",
"예측해야",
"합니다.)",
"량을",
"Performance",
"탭의",
"기록을",
"시간",
"경과에",
"따른",
"메모리",
"증가",
"패턴을",
"시각화할",
"있습니다",
"[12",
"13].",
"Memory",
"패널의",
"**Heap",
"Snapshots**을",
"사용하여",
"스냅샷",
"차이(Delta)를",
"비교함으로써",
"DOM에서",
"제거되었으나",
"자바스크립트",
"참조가",
"남아있는",
"'Detached",
"DOM",
"nodes'를",
"식별할",
"[14-17].",
"**Allocation",
"Timeline**을",
"새...",
"중단점을",
"설정하면",
"지점에서",
"호출",
"스택(Call",
"Stack)과",
"내부",
"변수",
"값의",
"변화를",
"실시간으로",
"살펴볼",
"이는",
"로그",
"기록보다",
"훨씬",
"풍부한",
"정보를",
"제공하며",
"비동기",
"작업이나",
"메시지",
"큐의",
"흐름",
"등을",
"파악하는",
"결정적인",
"준다",
"3].",
"**의도적",
"실패",
"주입",
"스택",
"트레이스",
"분...",
"기능적으로",
"개선되었다는",
"것을",
"하시는데",
"방법을",
"모르시는",
"상황입니다.",
"따라서",
"달라졌는지",
"체감할",
"있도록",
"제안해",
"달라는",
"요청입니다.",
"기능",
"향상",
"정도를",
"확인하여",
"날카롭고",
"실용적인",
"조언을",
"제공하는지",
"검증하고",
"하십니다.",
"하겠습니다.",
"하고",
"답변",
"재료에서는",
"낮췄습니다.",
"낮췄습니다.\"",
"로직",
"하나인",
"엔진",
"파일이",
"작업",
"흐름을",
"얼마나",
"통제하고",
"있는지",
"통제권이",
"어디에",
"묶여",
"있는지를",
"아키텍처의",
"신뢰도를",
"해서",
"읽어달라고",
"요청해",
"주시면",
"바로",
"분석을",
"시작하겠습니다.",
"계획(Planner)",
"연구(Researcher)",
"작성(Writer)이라는",
"명확하게",
"분리된",
"단계의",
"파이프라인을",
"순차적으로",
"실행하도록",
"설계되었습니다.",
"특히",
"`lockManager`를",
"통한",
"**명시적",
"락(Mutex)**",
"적용은",
"동일",
"미션의",
"동시",
"방지하여",
"데이터",
"무결성을",
"보장하며",
"`AbortSignal`과",
"결과",
"유효성",
"안정성과",
"신뢰성을",
"극대화하는",
"요소입니다.",
"입니다.",
"동일한",
"`missionId`를",
"가진",
"작업이",
"실행되어",
"발생하는",
"충돌이나",
"비정상적인",
"상태",
"전이를",
"원천적으로",
"차단합니다.",
"동시성",
"멀티",
"**안정성(Stability)**을",
"확보하는",
"**순차적",
"종속성(Sequential",
"Dependency)**을",
"가집니다.",
"점은",
"발생할",
"막아주어",
"**신뢰성**을",
"크게",
"높여줍니다.",
"ID에",
"요청이",
"리소스",
"과부하를",
"차단하는",
"**안정성",
"확보",
"장치**입니다.",
"분산",
"환경이나",
"많은",
"엔진의",
"방지하고",
"단계별",
"엄격하게",
"관리합니다.",
"`Researcher`의",
"작업과",
"`Writer`를",
"위한",
"초기",
"컨텍스트",
"준비",
"병렬로",
"설계는",
"파이프라인의",
"지연",
"시간을",
"최소화하고",
"효율성을",
"높이는",
"접근",
"방식입니다.",
"인터페이스)과의",
"연동",
"즉각적인",
"중단",
"기능을",
"보장합니다.",
"명확한",
"오류",
"메시지를",
"**수동",
"개입(Manual",
"Intervention)**을",
"요구하는",
"방식으로",
"전환합니다.",
"**안전한",
"차단(Fail-Fast)**",
"채택했습니다.",
"자의",
"명시적",
"취소",
"요청에",
"대해서는",
"불필요한",
"재시도",
"없이",
"조용히",
"종료하는",
"**우아한",
"종료(Graceful",
"Exit)**를",
"연구(Researcher)와",
"작가",
"준비(Writer",
"Prep)",
"단계를",
"효율적으로",
"병렬",
"처리함으로써",
"**처리",
"속도와",
"효율성**을",
"극대화했습니다.",
"미션에",
"접근을",
"차단하여",
"프로세스의",
"**무결성**을",
"안내",
"출력.",
"(근본적인",
"모델/프롬프트",
"오류에",
"신속한",
"차단)",
"의도에",
"존중)",
"중복",
"방지함으로써",
"환경에서의",
"충돌",
"위험을",
"원천",
"환경과",
"유사한",
"부하",
"조건에서",
"평균",
"처리",
"시간(Latency)",
"복구에",
"소요된",
"벤치마킹하여",
"최적화",
"포인트를",
"도출해야",
"시나리오를",
"통합적으로",
"테스트했습니다.",
"테스트에서",
"설정된",
"횟수(2회",
"성공)에",
"횟수가",
"정확히",
"카운트되었으며",
"**복구",
"성공",
"응답**이",
"반환되어",
"복원",
"메커니즘의",
"작동을",
"입증했습니다.",
"테스트에서는",
"예외",
"발생이",
"확인되어",
"영구적",
"안전장치가",
"제대로",
"작동함을",
"확인했습니다."
],
"architectureDecisions": [],
"bugRecords": [],
"requirements": [],
"designDirection": "",
"codeConventions": [],
"lastUpdated": 1777862761600
}
+10
View File
@@ -0,0 +1,10 @@
{
"autonomousMode": false,
"maxIterations": 3,
"allowedDirectories": [],
"autoReview": true,
"criticalApproval": true,
"version": 1,
"lastUpdated": null,
"_comment": "AAL (Astra Autonomous Loop) 프로토콜 설정. autonomousMode를 true로 변경하면 자율 루프가 활성화됩니다."
}
+28
View File
@@ -0,0 +1,28 @@
{
"version": 1,
"tasks": [],
"_schema": {
"task": {
"missionId": "string — 고유 미션 ID",
"status": "pending | in_progress | ready_for_review | completed | failed",
"priority": "low | normal | high | critical",
"iteration": "number — 현재 반복 횟수 (max: protocol.maxIterations)",
"dataVersion": "number — 낙관적 잠금(Optimistic Locking)용 버전 카운터",
"blueprint": {
"objective": "string — 작업 목표",
"steps": "string[] — 세부 실행 단계",
"targetFiles": "string[] — 수정 대상 파일 목록"
},
"feedback": "string | null — 검수 실패 시 Astra의 피드백",
"config": {
"autoExecute": "boolean — 자동 실행 여부",
"requireApproval": "boolean — 중요 작업 시 인간 승인 필요 여부"
},
"timestamps": {
"created": "ISO 8601",
"started": "ISO 8601 | null",
"completed": "ISO 8601 | null"
}
}
}
}
+1
View File
@@ -1,3 +1,4 @@
.astra/**
.vscode/** .vscode/**
src/** src/**
node_modules/** node_modules/**
+25
View File
@@ -1,3 +1,28 @@
# Astra Patch Notes
## v2.62.0 (2026-05-04)
### 🚀 Astra Autonomous Loop (AAL) 기초 구축
- **데이터 중앙화:** `.astra` 디렉토리를 프로젝트 루트에서 `ConnectAI/.astra/` 내부로 이전하여 프로젝트 응집도 향상.
- **AAL 프로토콜 도입:** 에이전트 간 자율 협업을 위한 `protocol.json``tasks.json` 스키마 구축.
- **경로 해결기(Path Resolver):** 확장 프로그램 설치 경로 기반의 데이터 관리 시스템(`astraPath.ts`) 도입.
### 🔍 파일 직접 분석 기능 강화
- **경로 감지 범용화:** 하드코딩된 경로 대신 `/Volumes/`, `/Users/`, `/home/`, `~/` 등 모든 절대 경로 감지 지원.
- **분석 키워드 확장:** "읽어줘", "열어줘", "파일 내용", "코드 분석" 등 직관적인 한국어/영어 트리거 추가.
- **대용량 프리뷰 지원:** 코드/문서 파일 분석 시 프리뷰 제한을 1,200자에서 **8,000자**로 대폭 상향하여 정밀 분석 가능.
---
# Patch Notes - v2.61.0 (2026-05-04)
## 🛡️ Resilient Engine & Monitoring (복원력 강화)
- **Error Recovery Matrix:** 에러 유형을 Transient(일시적)와 Permanent(영구적)로 자동 분류하는 복구 매트릭스가 전면 도입되었습니다. 네트워크 오류 시 지수 백오프 재시도를 자동 수행합니다.
- **Mission Control Dashboard:** `toStructuredLog()` 기반의 프리미엄 시각화 대시보드(`assets/mission_control.html`)가 추가되었습니다. 에이전트의 흐름과 지연 시간을 한눈에 파악할 수 있습니다.
- **성능 및 병렬화 최적화:** 단계별 500ms 강제 지연을 제거하고, Researcher와 Writer 준비 과정을 병렬화하여 전체 워크플로우 처리 속도를 대폭 향상했습니다.
- **신뢰성 검증:** 37종의 통합 테스트 및 성능 벤치마크 스위트를 통해 엔진의 안정성과 복구 메커니즘을 완벽히 검증했습니다.
---
# Patch Notes - v2.60.0 (2026-05-04) # Patch Notes - v2.60.0 (2026-05-04)
## 🧠 Memory & Knowledge Search (Unified RAG Pipeline) ## 🧠 Memory & Knowledge Search (Unified RAG Pipeline)
+359
View File
@@ -0,0 +1,359 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Astra Mission Control Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #050505;
--card-bg: rgba(20, 20, 25, 0.7);
--accent-primary: #00f2ff;
--accent-secondary: #7000ff;
--text-main: #e0e0e0;
--text-dim: #888;
--border-color: rgba(255, 255, 255, 0.1);
--success: #00ff88;
--error: #ff4d4d;
--warning: #ffaa00;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background-color: var(--bg-color);
background-image:
radial-gradient(circle at 20% 20%, rgba(112, 0, 255, 0.1) 0%, transparent 40%),
radial-gradient(circle at 80% 80%, rgba(0, 242, 255, 0.1) 0%, transparent 40%);
color: var(--text-main);
font-family: 'Inter', sans-serif;
overflow-x: hidden;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 20px;
}
.logo {
font-family: 'Orbitron', sans-serif;
font-size: 24px;
font-weight: 700;
letter-spacing: 2px;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-transform: uppercase;
}
.dashboard-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 24px;
}
.card {
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.card-title {
font-family: 'Orbitron', sans-serif;
font-size: 14px;
color: var(--text-dim);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.card-title::before {
content: '';
width: 4px;
height: 14px;
background: var(--accent-primary);
border-radius: 2px;
}
/* Input Area */
.log-input-area {
margin-bottom: 24px;
}
textarea {
width: 100%;
height: 120px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--accent-primary);
padding: 12px;
font-family: 'Courier New', monospace;
font-size: 13px;
resize: none;
outline: none;
}
.btn-analyze {
margin-top: 12px;
padding: 10px 24px;
background: linear-gradient(45deg, var(--accent-primary), var(--accent-secondary));
border: none;
border-radius: 8px;
color: white;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-analyze:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 242, 255, 0.3);
}
/* Timeline View */
.timeline {
position: relative;
padding-left: 30px;
}
.timeline::before {
content: '';
position: absolute;
left: 7px;
top: 0;
bottom: 0;
width: 2px;
background: var(--border-color);
}
.timeline-item {
position: relative;
margin-bottom: 30px;
}
.timeline-dot {
position: absolute;
left: -30px;
top: 5px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent-primary);
box-shadow: 0 0 10px var(--accent-primary);
z-index: 1;
}
.timeline-content {
background: rgba(255, 255, 255, 0.03);
padding: 16px;
border-radius: 12px;
border: 1px solid var(--border-color);
}
.stage-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.stage-name {
font-family: 'Orbitron', sans-serif;
font-size: 16px;
font-weight: 700;
color: var(--accent-primary);
}
.stage-time {
color: var(--text-dim);
font-size: 12px;
}
.stage-msg {
font-size: 14px;
color: var(--text-main);
}
.stage-duration {
margin-top: 8px;
display: inline-block;
padding: 2px 8px;
background: rgba(0, 242, 255, 0.1);
border-radius: 4px;
font-size: 11px;
color: var(--accent-primary);
}
/* Stats */
.stat-item {
margin-bottom: 24px;
}
.stat-label {
font-size: 12px;
color: var(--text-dim);
margin-bottom: 4px;
}
.stat-value {
font-family: 'Orbitron', sans-serif;
font-size: 28px;
font-weight: 700;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
}
.status-completed { background: rgba(0, 255, 136, 0.1); color: var(--success); }
.status-error { background: rgba(255, 77, 77, 0.1); color: var(--error); }
.status-processing { background: rgba(0, 242, 255, 0.1); color: var(--accent-primary); }
#visualizer { display: none; }
.empty-state {
text-align: center;
padding: 60px;
color: var(--text-dim);
}
.glow-text {
text-shadow: 0 0 8px var(--accent-primary);
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">Astra Mission Control</div>
<div id="mission-status-indicator" class="status-badge">Ready to Sync</div>
</header>
<div class="card log-input-area">
<div class="card-title">Log Input System</div>
<textarea id="logInput" placeholder="toStructuredLog()의 결과물(JSON)을 여기에 붙여넣으세요..."></textarea>
<button class="btn-analyze" onclick="analyzeLog()">분석 시작 (ANALYZE)</button>
</div>
<div id="empty-view" class="empty-state">
데이터가 없습니다. 로그를 입력하고 분석을 시작하세요.
</div>
<div id="visualizer" class="dashboard-grid">
<div class="card">
<div class="card-title">Mission Execution Timeline</div>
<div id="timeline" class="timeline">
<!-- Timeline items go here -->
</div>
</div>
<div class="right-panel">
<div class="card" style="margin-bottom: 24px;">
<div class="card-title">Mission Overview</div>
<div class="stat-item">
<div class="stat-label">Mission ID</div>
<div id="view-mission-id" class="stat-value" style="font-size: 14px; word-break: break-all;">-</div>
</div>
<div class="stat-item">
<div class="stat-label">Total Execution Time</div>
<div id="view-total-time" class="stat-value glow-text">0ms</div>
</div>
<div class="stat-item">
<div class="stat-label">Final Status</div>
<div id="view-final-status">-</div>
</div>
</div>
<div class="card">
<div class="card-title">Efficiency Analysis</div>
<div class="stat-item">
<div class="stat-label">Transitions</div>
<div id="view-transition-count" class="stat-value">0</div>
</div>
<div class="stat-item">
<div class="stat-label">Avg. Latency</div>
<div id="view-avg-latency" class="stat-value" style="color: var(--accent-secondary);">0ms</div>
</div>
</div>
</div>
</div>
</div>
<script>
function analyzeLog() {
const input = document.getElementById('logInput').value;
try {
const data = JSON.parse(input);
renderDashboard(data);
} catch (e) {
alert('유효한 JSON 형식이 아닙니다. 로그를 다시 확인해 주세요.');
}
}
function renderDashboard(data) {
document.getElementById('empty-view').style.display = 'none';
document.getElementById('visualizer').style.display = 'grid';
// Status Update
const statusEl = document.getElementById('mission-status-indicator');
statusEl.innerText = data.status;
statusEl.className = 'status-badge ' + (data.status === 'completed' ? 'status-completed' : 'status-error');
// Overview Update
document.getElementById('view-mission-id').innerText = data.missionId;
document.getElementById('view-total-time').innerText = data.totalElapsedMs + 'ms';
document.getElementById('view-final-status').innerHTML = `<span class="status-badge status-${data.status}">${data.status}</span>`;
document.getElementById('view-transition-count').innerText = data.transitionCount;
const avg = data.totalElapsedMs / (data.transitionCount || 1);
document.getElementById('view-avg-latency').innerText = Math.round(avg) + 'ms';
// Timeline Update
const timeline = document.getElementById('timeline');
timeline.innerHTML = '';
data.transitions.forEach(t => {
const item = document.createElement('div');
item.className = 'timeline-item';
const dotColor = t.to === 'error' ? 'var(--error)' : 'var(--accent-primary)';
const dotShadow = t.to === 'error' ? '0 0 10px var(--error)' : '0 0 10px var(--accent-primary)';
item.innerHTML = `
<div class="timeline-dot" style="background: ${dotColor}; box-shadow: ${dotShadow}"></div>
<div class="timeline-content">
<div class="stage-header">
<div class="stage-name" style="color: ${dotColor}">${t.to.toUpperCase()}</div>
<div class="stage-time">${new Date(t.ts).toLocaleTimeString()}</div>
</div>
<div class="stage-msg">${t.message}</div>
<div class="stage-duration">From ${t.from}: +${t.durationMs}ms</div>
</div>
`;
timeline.appendChild(item);
});
}
</script>
</body>
</html>
+314
View File
@@ -0,0 +1,314 @@
# AgentEngine Architecture Document
> **Version:** v2.61.0 | **Last Updated:** 2026-05-04 | **Author:** ConnectAI Lab
---
## 1. Overview (설계 개요)
`AgentEngine`은 ConnectAI(Astra) 확장의 핵심 실행 엔진으로, **Producer-Consumer 패턴** 기반의 멀티 에이전트 오케스트레이션을 담당합니다. Planner → Researcher → Writer의 3단계 파이프라인을 통해 사용자의 자연어 요청을 전략 수립, 정보 수집, 최종 보고서 작성이라는 전문화된 단계로 분할 처리합니다.
### 핵심 설계 원칙
| 원칙 | 구현 |
|---|---|
| **의존성 주입 (DI)** | `IAgent` 인터페이스를 통해 에이전트를 런타임에 교체 가능 |
| **명시적 동시성 제어** | `LockManager`(Mutex) + `ActionQueueManager`(Concurrency Limit) |
| **복원력 우선 (Resilience-First)** | `ErrorRecoveryMatrix`를 통한 오류 자동 분류 및 복구 |
| **투명한 상태 관리** | `MissionState`를 통한 감사 이력(Audit Trail) 자동 기록 |
---
## 2. Architecture Diagram (아키텍처 다이어그램)
```
┌──────────────────────────────────────────────────────────────┐
│ AgentWorkflowManager │
│ (Public Entry Point) │
└──────────────────────────┬───────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ AgentEngine │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. LockManager.acquire() ← Mutex (동일 미션 중복 방지) │ │
│ │ 2. ActionQueue.enqueue() ← Concurrency Limit (≤3) │ │
│ │ 3. resilientExecute() ← Error Recovery Matrix │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Planner │──▶│ Researcher │──▶│ Writer │ │
│ │ (IAgent) │ │ (IAgent) │ │ (IAgent) │ │
│ └──────────┘ └──────┬───────┘ └────────────┘ │
│ │ │
│ ┌──────┴───────┐ │
│ │ WriterPrep │ ← Promise.all() 병렬 실행 │
│ │ (Parallel) │ │
│ └──────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ MissionState (Audit Trail) │ │
│ │ idle → planner → researcher → writer → completed │ │
│ │ [toStructuredLog()] → JSON 출력 → 모니터링 대시보드 │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
---
## 3. Lock Management (락 관리)
### 3.1 LockManager (Mutex)
**파일:** `src/core/lock.ts`
**설계 의도:** 동일한 `missionId`에 대한 중복 실행을 방지합니다. 예를 들어, 사용자가 동일한 질문을 빠르게 두 번 전송한 경우, 두 번째 요청은 첫 번째 요청이 완료될 때까지 대기합니다.
**동작 원리:**
```
Mission A (id: "abc") 시작 → Lock 획득 ✅
Mission B (id: "abc") 시작 → Lock 대기 ⏳ (Mission A 완료까지)
Mission A 완료 → Lock 해제 → Mission B Lock 획득 ✅
```
**핵심 보장:**
- 동일 미션 ID의 순차 실행 보장 (Race Condition 방지)
- `finally` 블록에서 반드시 `release()` 호출 → 데드락 불가
### 3.2 ActionQueueManager (동시성 제어)
**파일:** `src/core/queue.ts`
**설계 의도:** 시스템 전체에서 동시에 실행되는 에이전트 작업 수를 제한하여 리소스 고갈을 방지합니다.
**핵심 파라미터:**
| 파라미터 | 기본값 | 설명 |
|---|---|---|
| `concurrencyLimit` | 3 | 동시 실행 가능한 최대 작업 수 |
| `activeCount` | - | 현재 실행 중인 작업 수 (동적) |
| micro-delay | 10ms | 작업 간 시스템 호흡 시간 |
**Mutex vs Queue의 역할 분리:**
| 구분 | LockManager | ActionQueueManager |
|---|---|---|
| 범위 | 미션 단위 (단일 ID) | 시스템 전체 |
| 목적 | 중복 실행 방지 | 리소스 포화 방지 |
| 방식 | 독점 잠금 (Exclusive) | 동시성 제한 (Bounded) |
---
## 4. Error Recovery Matrix (오류 복구 매트릭스)
### 4.1 오류 분류 체계
`ErrorClassifier`는 에러 객체의 `name``message`를 정규식 패턴 매칭하여 세 범주로 자동 분류합니다.
```
┌──────────────┬──────────┬──────────────┬─────────────────────────────────┐
│ Error Type │ Retries │ Backoff │ Action │
├──────────────┼──────────┼──────────────┼─────────────────────────────────┤
│ TRANSIENT │ 3 │ Exponential │ 자동 재시도 (1s → 2s → 4s) │
│ │ │ (1000ms) │ │
├──────────────┼──────────┼──────────────┼─────────────────────────────────┤
│ PERMANENT │ 0 │ N/A │ 즉시 중단 + 사용자 안내 메시지 │
├──────────────┼──────────┼──────────────┼─────────────────────────────────┤
│ ABORT │ 0 │ N/A │ Graceful Exit (조용한 종료) │
└──────────────┴──────────┴──────────────┴─────────────────────────────────┘
```
### 4.2 분류 패턴 목록
**Transient (재시도 가능):**
- `ECONNREFUSED`, `ECONNRESET`, `ETIMEDOUT`, `ENOTFOUND`
- `timeout`, `network`, `fetch failed`, `Failed to fetch`
- HTTP `502`, `503`, `429`
- `socket hang up`
**Permanent (즉시 중단):**
- HTTP `401`, `403`, `404`
- `유효한 응답을 받지 못했습니다` (내부 검증 실패)
- `Ollama URL이 설정되지 않았습니다` (설정 누락)
- `invalid model`, `model not found` (모델 문제)
**분류 불가 → Permanent (보수적 처리):**
- 알려지지 않은 오류는 안전하게 Permanent로 분류하여 무한 재시도를 방지
### 4.3 resilientExecute 실행 흐름
```
resilientExecute(agent, ...)
├─ attempt 0: agent.execute()
│ ├─ 성공 → return result
│ └─ 실패 → ErrorClassifier.classify(error)
│ ├─ ABORT → throw (즉시 전파)
│ ├─ PERMANENT → throw (사용자 메시지 첨부)
│ └─ TRANSIENT → continue
├─ attempt 1: wait 1000ms → agent.execute()
│ └─ (동일 분기)
├─ attempt 2: wait 2000ms → agent.execute()
│ └─ (동일 분기)
└─ attempt 3: wait 4000ms → agent.execute()
└─ 실패 시 → "재시도 소진" 에러 throw
```
---
## 5. MissionState & Monitoring (상태 관리 및 모니터링)
### 5.1 MissionState 설계 의도
엔진 내부의 상태(`this.stage`)를 독립 객체로 분리하여:
1. **감사 이력(Audit Trail)** 자동 기록: 모든 상태 전환이 타임스탬프, 소요 시간과 함께 기록됨
2. **외부 노출**: `getMissionState()`를 통해 외부 모니터링 시스템이 실시간 상태를 읽을 수 있음
3. **디버깅 지원**: 에러 발생 시 `summarizeAudit()`로 전체 전환 이력을 한 줄로 덤프
### 5.2 toStructuredLog() 출력 포맷
```json
{
"missionId": "mission_1714832400000",
"status": "completed",
"startTime": "2026-05-04T02:40:00.000Z",
"totalElapsedMs": 12450,
"transitionCount": 4,
"transitions": [
{
"from": "idle",
"to": "planner",
"durationMs": 0,
"message": "전략 수립 중...",
"ts": "2026-05-04T02:40:00.000Z"
},
{
"from": "planner",
"to": "researcher",
"durationMs": 3200,
"message": "핵심 정보 수집 및 분석 중...",
"ts": "2026-05-04T02:40:03.200Z"
}
]
}
```
이 포맷은 ELK Stack, Loki, Datadog 등의 외부 시스템에 파싱 없이 직접 인제스트할 수 있습니다.
### 5.3 Mission Control Dashboard
**파일:** `assets/mission_control.html`
`toStructuredLog()`의 JSON 출력을 브라우저에서 시각화하는 독립형 대시보드입니다.
- 에이전트 타임라인 시각화
- 단계별 지연 시간(Latency) 표시
- 미션 상태 및 효율성 분석 지표 제공
---
## 6. IAgent Interface (에이전트 인터페이스)
### 6.1 기본 시그니처
```typescript
interface IAgent {
execute(
input: string,
context?: string,
signal?: AbortSignal,
options?: AgentExecuteOptions
): Promise<string>;
}
```
### 6.2 AgentExecuteOptions (확장 옵션)
| 필드 | 타입 | 용도 |
|---|---|---|
| `context` | `string` | 추가 컨텍스트 |
| `signal` | `AbortSignal` | 중단 시그널 |
| `config` | `Record<string, unknown>` | 에이전트별 커스텀 설정 (temperature, maxTokens 등) |
| `priorResults` | `Record<string, string>` | 이전 단계 중간 결과물 (병렬 파이프라인용) |
**확장 전략:** 새로운 에이전트가 추가될 때 `options.config`에 자유롭게 설정을 넣을 수 있으므로, `IAgent` 시그니처를 변경할 필요가 없습니다.
---
## 7. Test & Benchmark Coverage (테스트 커버리지)
**파일:** `tests/agentEngine.test.ts` | **총 41개 테스트**
| Suite | Tests | Description |
|---|---|---|
| ErrorClassifier | 20 | Transient(10), Permanent(7), Abort(2), Unknown(1) 분류 정확성 |
| Recovery Matrix | 3 | 매트릭스 규칙 정합성 |
| MissionState | 4 | 상태 전환, Audit Trail, JSON 포맷 |
| Engine Integration | 6 | 정상/재시도/즉시중단/취소/재시도소진/상태정리 |
| Performance | 3 | 정상 Latency, 재시도 오버헤드, 즉시 중단 시간 |
| Concurrency & Stress | 4 | 병렬 실행, 혼합 오류, 큐 포화, Race Condition |
### 벤치마크 기준치
| Metric | Measured | Threshold |
|---|---|---|
| Normal Mission Avg Latency | 165ms | < 1,000ms |
| Retry Overhead (2 retries) | 3,012ms | 2,50010,000ms |
| Permanent Fail Time | 11ms | < 500ms |
| 5 Concurrent Missions | 23ms | < 30,000ms |
| Queue Saturation (10 tasks) | 44ms | all complete |
### 배포 게이트
```json
"vscode:prepublish": "npm run test && npm run compile"
```
모든 41개 테스트가 통과해야만 `.vsix` 패키징이 진행됩니다.
---
## 8. File Map (파일 맵)
```
src/
├── lib/
│ └── engine.ts ← AgentEngine, MissionState, ErrorClassifier,
│ ErrorRecoveryMatrix, IAgent, AgentExecuteOptions
├── core/
│ ├── queue.ts ← ActionQueueManager (Concurrency Limit)
│ └── lock.ts ← LockManager (Mutex)
├── agents/
│ ├── factory.ts ← PlannerAgent, ResearcherAgent, WriterAgent (BaseAgent)
│ └── AgentWorkflowManager.ts ← Public API (runStrictWorkflow)
└── utils.ts ← logInfo, logError, logWarn (→ VS Code Output Channel "Astra")
tests/
└── agentEngine.test.ts ← 41 integration tests + benchmarks
assets/
└── mission_control.html ← Mission Control Dashboard (standalone)
```
---
## 9. Maintenance Notes (유지보수 참고)
### 새 에이전트 추가 시
1. `IAgent`를 구현하는 새 클래스를 `factory.ts`에 추가
2. `AgentEngine` 생성자에 주입 (DI)
3. `PipelineStage` 타입에 새 단계 추가
4. `AgentWorkflowManager.mapStageToUI()`에 UI 매핑 추가
### 새 오류 패턴 추가 시
1. `ErrorClassifier.TRANSIENT_PATTERNS` 또는 `PERMANENT_PATTERNS`에 정규식 추가
2. `tests/agentEngine.test.ts`의 해당 describe 블록에 테스트 케이스 추가
3. `npm run test:engine`으로 검증
### 동시성 제한 변경 시
`src/core/queue.ts``new ActionQueueManager(N)` 생성자 인자를 조정합니다.
기본값은 3이며, 리소스 여유에 따라 조정 가능합니다.
@@ -0,0 +1,16 @@
# Bug: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고...
## Date
2026-05-04
## Symptom
/Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고 잘된점과 문제점들이 있는지. 없으면 없다고 말해도되.
## Cause
Captured automatically from the current conversation. Confirm root cause during follow-up review if needed.
## Fix
## 간단 요약 이 요청은 프로젝트 지식 생성이 아니라 코드리뷰와 제품 평가 요청입니다. 확인된 파일 구조 기준으로 보면, 이 프로젝트는 지식 수집 워크플로우를 앱 형태로 묶어 운영하려는 도구로 보이며, 먼저 데이터 수집 흐름의 안정성, 외부 연동 실패 처리, 수집 결과의 저장/재처리 가능성을 중심으로 평가해야 합니다. ## 확인된 근거 대상 경로: `/Volumes/Data/project/Antigravity/ConnectAI` 확인된 우선 파일: - `package.json` - `docs/records/ConnectAI/README.md` - `README.md` - `docs/Advanced_Features_Implementation_Guide.md` - `docs/AgentEngine_Architecture.md` - `docs/records/ConnectAI/chronicle.config.json` - `docs/records/ConnectAI/decisions/ADR-0...
## Prevention
Keep automatic records tied to the active project and verify the relevant test or reproduction path.
@@ -0,0 +1,11 @@
{
"projectId": "connectai",
"projectName": "ConnectAI",
"projectRoot": "/Volumes/Data/project/Antigravity/ConnectAI",
"recordRoot": "/Volumes/Data/project/Antigravity/ConnectAI/docs/records/ConnectAI",
"description": "Auto-detected from the local project path in the conversation.",
"corePurpose": "Capture project direction, architecture discussion, decisions, and development notes as Markdown.",
"detailLevel": "standard",
"createdAt": "2026-05-04T03:27:46.314Z",
"updatedAt": "2026-05-04T03:27:46.316Z"
}
@@ -0,0 +1,19 @@
# ADR: /Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트를 지금 개발 중에 있어. 코드 리뷰를 하고 잘된점과 ...
## Status
accepted
## Context
/Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트를 지금 개발 중에 있어. 코드 리뷰를 하고 잘된점과 부족한 부분, 앞으로 개선해야할게 있다면 뭔지 알려주면 좋겠어.
## Decision
## 간단 요약 이 요청은 프로젝트 지식 생성이 아니라 코드리뷰와 제품 평가 요청입니다. 확인된 파일 구조 기준으로 보면, 이 프로젝트는 지식 수집 워크플로우를 앱 형태로 묶어 운영하려는 도구로 보이며, 먼저 데이터 수집 흐름의 안정성, 외부 연동 실패 처리, 수집 결과의 저장/재처리 가능성을 중심으로 평가해야 합니다. ## 확인된 근거 대상 경로: `/Volumes/Data/project/Antigravity/ConnectAI` 확인된 우선 파일: - `package.json` - `docs/records/ConnectAI/README.md` - `README.md` - `src/agent.ts` - `src/agents/AgentWorkflowManager.ts` - `src/agents/factory.ts` - `src/bridge.ts` - `src/config.ts` - `src/extension.ts` - `src/MrBeast_Premium_10.md` 확인된 구조 일부...
## Reason
Captured automatically because the conversation contained decision-oriented language.
## Alternatives
Not captured yet.
## Consequences
- Future prompts should treat this as project context unless the user changes direction.
+6
View File
@@ -21,3 +21,9 @@
- Tuned Second Brain retrieval by query intent so UX/business/approval questions prioritize customer journey, requirement fit, and business value notes over generic architecture notes. - Tuned Second Brain retrieval by query intent so UX/business/approval questions prioritize customer journey, requirement fit, and business value notes over generic architecture notes.
- Tuned answer readability: paragraph-first summaries, fewer bullet-heavy sections, clearer numbered-section spacing, and larger markdown headings in the sidebar. - Tuned answer readability: paragraph-first summaries, fewer bullet-heavy sections, clearer numbered-section spacing, and larger markdown headings in the sidebar.
- Added local project path preflight for code review requests so the agent scans accessible Antigravity project folders before asking for uploads. - Added local project path preflight for code review requests so the agent scans accessible Antigravity project folders before asking for uploads.
## 2026-05-04
- Auto decision record created: decisions/ADR-0001-volumes-data-project-antigravity-connectai-이-프로젝트를-지금-개발-중에-.md
## 2026-05-04
- Auto bug record created: bugs/BUG-0001-volumes-data-project-antigravity-connectai-프로젝트-코드-리뷰-해줄-수-있.md
+4 -2
View File
@@ -2,7 +2,7 @@
"name": "astra", "name": "astra",
"displayName": "Astra", "displayName": "Astra",
"description": "A local Jarvis-style project operating assistant for VS Code. Connects memory, project context, tools, and a single thinking-partner voice.", "description": "A local Jarvis-style project operating assistant for VS Code. Connects memory, project context, tools, and a single thinking-partner voice.",
"version": "2.60.0", "version": "2.62.0",
"publisher": "connectailab", "publisher": "connectailab",
"license": "MIT", "license": "MIT",
"icon": "assets/icon.png", "icon": "assets/icon.png",
@@ -207,9 +207,11 @@
} }
}, },
"scripts": { "scripts": {
"vscode:prepublish": "npm run compile", "vscode:prepublish": "npm run test && npm run compile",
"compile": "esbuild src/extension.ts --bundle --platform=node --external:vscode --outfile=out/extension.js", "compile": "esbuild src/extension.ts --bundle --platform=node --external:vscode --outfile=out/extension.js",
"watch": "tsc -watch -p ./", "watch": "tsc -watch -p ./",
"test": "jest --no-cache --forceExit",
"test:engine": "jest tests/agentEngine.test.ts --verbose --no-cache",
"pretest": "npm run compile" "pretest": "npm run compile"
}, },
"devDependencies": { "devDependencies": {
+77 -10
View File
@@ -793,7 +793,7 @@ export class AgentExecutor {
return ''; return '';
} }
const candidates = this.extractLocalProjectPaths(prompt); const candidates = this.extractLocalProjectPaths(prompt, rootPath);
if (candidates.length === 0) { if (candidates.length === 0) {
return ''; return '';
} }
@@ -803,6 +803,11 @@ export class AgentExecutor {
'[LOCAL PROJECT PATH PREFLIGHT]', '[LOCAL PROJECT PATH PREFLIGHT]',
`Local project intent: ${intent}`, `Local project intent: ${intent}`,
this.buildLocalProjectIntentGuidance(intent), this.buildLocalProjectIntentGuidance(intent),
'[CRITICAL DIRECTIVE] The file contents below have already been read from the local filesystem. You MUST use them directly in your analysis.',
'DO NOT ask the user to provide, upload, paste, or share the file contents. They are already included below.',
'DO NOT say "파일 내용을 보여주세요", "코드를 공유해 주세요", or "파일을 제공해 주세요". The files have been pre-loaded.',
'If access succeeded, proceed IMMEDIATELY with analysis. Do not ask for confirmation like "진행할까요?" or "분석을 시작할까요?". Just do it.',
'If multiple files are mentioned, analyze them sequentially in the order the user specified without pausing for confirmation between each.',
'The user provided a local project path for review, analysis, documentation, or knowledge creation. Use this inspected context before asking for uploads.', 'The user provided a local project path for review, analysis, documentation, or knowledge creation. Use this inspected context before asking for uploads.',
'If access failed, explain the concrete failure. If access succeeded, proceed with code review from the scanned files.', 'If access failed, explain the concrete failure. If access succeeded, proceed with code review from the scanned files.',
'If access succeeded and priority file previews are present, do not say that code was not provided.', 'If access succeeded and priority file previews are present, do not say that code was not provided.',
@@ -812,7 +817,7 @@ export class AgentExecutor {
'If intent is thinking, act as a project thinking partner and give a clear verdict grounded in the inspected files.' 'If intent is thinking, act as a project thinking partner and give a clear verdict grounded in the inspected files.'
]; ];
for (const candidate of candidates.slice(0, 2)) { for (const candidate of candidates.slice(0, 5)) {
sections.push(this.inspectLocalProjectPath(candidate, rootPath)); sections.push(this.inspectLocalProjectPath(candidate, rootPath));
} }
@@ -1004,8 +1009,27 @@ export class AgentExecutor {
} }
private shouldPreflightLocalProjectPath(prompt: string): boolean { private shouldPreflightLocalProjectPath(prompt: string): boolean {
return /(검토|리뷰|분석|확인|봐줘|고쳐|개선|디버그|지식|문서화|문서|정리|기록|위키|저장|만들|생성|설계|아키텍처|구조|방향|의견|생각|판단|어떤\s*거?\s*같|어때|knowledge|document|documentation|wiki|summari[sz]e|review|analy[sz]e|inspect|debug|fix|improve|architecture|design|structure|opinion|think|judge)/i.test(prompt) const hasActionKeyword = /(검토|리뷰|분석|확인|봐줘|읽어|열어|파일|내용|코드|고쳐|개선|디버그|지식|문서화|문서|정리|기록|위키|저장|만들|생성|설계|아키텍처|구조|방향|의견|생각|판단|어떤\s*거?\s*같|어때|순서대로|보면|knowledge|document|documentation|wiki|summari[sz]e|review|analy[sz]e|inspect|debug|fix|improve|architecture|design|structure|opinion|think|judge|read|open|file|content|code)/i.test(prompt);
&& /\/Volumes\/Data\/project\/Antigravity\/[^\s`"'<>]+/i.test(prompt); const hasLocalPath = this.containsLocalFilePath(prompt);
return hasActionKeyword && hasLocalPath;
}
/**
* 프롬프트에 로컬 파일/디렉토리 경로가 포함되어 있는지 감지합니다.
* 절대 경로: /Volumes/, /Users/, /home/, ~/
* 상대 경로: src/..., lib/..., components/..., tests/... 등 + 파일 확장자
*/
private containsLocalFilePath(prompt: string): boolean {
// 절대 경로
if (/(?:\/Volumes\/|\/Users\/|\/home\/|~\/)[^\s`"'<>]+/i.test(prompt)) {
return true;
}
// 상대 경로 패턴: 디렉토리/파일명.확장자 형태 (src/lib/engine.ts, components/App.tsx 등)
if (/(?:^|[\s,])(?:src|lib|components|pages|app|tests|test|utils|core|features|hooks|services|config|public|assets|docs|scripts)\//i.test(prompt)
&& /\.[a-z]{1,6}(?:[\s,;)\]]|$)/i.test(prompt)) {
return true;
}
return false;
} }
private isProjectKnowledgeCreationRequest(prompt: string): boolean { private isProjectKnowledgeCreationRequest(prompt: string): boolean {
@@ -1017,7 +1041,7 @@ export class AgentExecutor {
} }
private classifyLocalProjectIntent(prompt: string): LocalProjectIntent { private classifyLocalProjectIntent(prompt: string): LocalProjectIntent {
if (!/\/Volumes\/Data\/project\/Antigravity\/[^\s`"'<>]+/i.test(prompt)) { if (!this.containsLocalFilePath(prompt)) {
return 'general'; return 'general';
} }
@@ -1323,9 +1347,46 @@ export class AgentExecutor {
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0] || null; .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0] || null;
} }
private extractLocalProjectPaths(prompt: string): string[] { private extractLocalProjectPaths(prompt: string, rootPath?: string): string[] {
const matches = prompt.match(/\/Volumes\/Data\/project\/Antigravity\/[^\s`"'<>]+/gi) || []; const results: string[] = [];
return Array.from(new Set(matches.map((value) => value.replace(/[),.;\]]+$/g, ''))));
// 1. 절대 경로 감지: /Volumes/, /Users/, /home/, ~/
const absMatches = prompt.match(/(?:\/Volumes\/|\/Users\/|\/home\/|~\/)[^\s`"'<>]+/gi) || [];
for (const m of absMatches) {
results.push(m.replace(/[),.;\]]+$/g, ''));
}
// 2. 상대 경로 감지: src/lib/engine.ts, components/App.tsx 등
const relMatches = prompt.match(/(?:^|[\s,])(?:(?:src|lib|components|pages|app|tests|test|utils|core|features|hooks|services|config|public|assets|docs|scripts)\/[^\s`"'<>]+\.[a-z]{1,6})/gi) || [];
for (const m of relMatches) {
const cleaned = m.trim().replace(/^,\s*/, '').replace(/[),.;\]]+$/g, '');
if (rootPath) {
// 상대 경로를 워크스페이스 기준 절대 경로로 변환
const absPath = path.resolve(rootPath, cleaned);
if (fs.existsSync(absPath)) {
results.push(absPath);
} else {
// 프로젝트 루트 하위 프로젝트들에서도 검색
const subProjects = ['ConnectAI', 'Datacollector_MAC', 'Agent', 'skybound'];
let found = false;
for (const sub of subProjects) {
const subPath = path.resolve(rootPath, sub, cleaned);
if (fs.existsSync(subPath)) {
results.push(subPath);
found = true;
break;
}
}
if (!found) {
results.push(absPath); // fallback: 원래 경로 그대로
}
}
} else {
results.push(cleaned);
}
}
return Array.from(new Set(results));
} }
private inspectLocalProjectPath(targetPath: string, rootPath: string): string { private inspectLocalProjectPath(targetPath: string, rootPath: string): string {
@@ -1342,11 +1403,17 @@ export class AgentExecutor {
const stat = fs.statSync(absPath); const stat = fs.statSync(absPath);
if (!stat.isDirectory()) { if (!stat.isDirectory()) {
const content = fs.readFileSync(absPath, 'utf8'); const content = fs.readFileSync(absPath, 'utf8');
const fileName = path.basename(absPath);
const ext = path.extname(absPath).toLowerCase();
// 코드/문서 파일은 더 많은 내용을 제공하여 정밀한 분석이 가능하도록 함
const isCodeOrDoc = ['.ts', '.js', '.tsx', '.jsx', '.py', '.java', '.go', '.rs', '.md', '.json', '.yaml', '.yml', '.toml', '.css', '.html', '.sql', '.sh', '.zsh', '.env', '.xml', '.swift', '.kt'].includes(ext);
const previewLimit = isCodeOrDoc ? 8000 : 2000;
return [ return [
`Path: ${targetPath}`, `Path: ${targetPath}`,
'Access: succeeded', 'Access: succeeded',
'Type: file', `Type: file (${fileName})`,
`Preview:\n${summarizeText(content, 1200)}` `Size: ${content.length} characters`,
`Full content (${content.length <= previewLimit ? 'complete' : `first ${previewLimit} chars`}):\n\`\`\`${ext.slice(1)}\n${summarizeText(content, previewLimit)}\n\`\`\``
].join('\n'); ].join('\n');
} }
+2 -1
View File
@@ -1,5 +1,6 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { getConfig } from '../config'; import { getConfig } from '../config';
import { AgentExecuteOptions } from '../lib/engine';
export abstract class BaseAgent { export abstract class BaseAgent {
constructor(protected readonly modelName: string) {} constructor(protected readonly modelName: string) {}
@@ -76,7 +77,7 @@ export abstract class BaseAgent {
throw lastError; throw lastError;
} }
abstract execute(input: string, context?: string, signal?: AbortSignal): Promise<string>; abstract execute(input: string, context?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string>;
} }
// Helper to combine signals (since AbortSignal.any is not always available in older Node) // Helper to combine signals (since AbortSignal.any is not always available in older Node)
+88
View File
@@ -0,0 +1,88 @@
/**
* ============================================================
* Astra Path Resolver (경로 해결기)
*
* Astra의 모든 데이터 파일(.astra 디렉토리)의 경로를 중앙에서 관리합니다.
* 확장 프로그램의 설치 경로(extensionUri) 기반으로 .astra 디렉토리를 해결하여,
* 사용자 프로젝트 루트가 아닌 ConnectAI 패키지 내부에 데이터를 저장합니다.
*
* 이 모듈은 AAL(Astra Autonomous Loop) 프로토콜의 기반이 됩니다.
* ============================================================
*/
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
let _extensionRootPath: string | null = null;
/**
* 확장 프로그램 활성화 시 1회 호출하여 extension root를 설정합니다.
* extension.ts의 activate()에서 호출되어야 합니다.
*/
export function initAstraPathResolver(context: vscode.ExtensionContext): void {
_extensionRootPath = context.extensionUri.fsPath;
}
/**
* .astra 데이터 디렉토리의 절대 경로를 반환합니다.
* 디렉토리가 없으면 자동 생성합니다.
*
* @returns ConnectAI/.astra/ 의 절대 경로
*/
export function getAstraDataDir(): string {
const root = _extensionRootPath ?? _fallbackExtensionRoot();
const astraDir = path.join(root, '.astra');
if (!fs.existsSync(astraDir)) {
fs.mkdirSync(astraDir, { recursive: true });
}
return astraDir;
}
/**
* .astra 내부의 특정 파일 경로를 반환합니다.
*
* @param filename - 파일 이름 (예: 'project_memory.json', 'tasks.json')
* @returns 파일의 절대 경로
*/
export function getAstraFilePath(filename: string): string {
return path.join(getAstraDataDir(), filename);
}
/**
* .astra 내부의 프로젝트별 서브디렉토리 경로를 반환합니다.
* 프로젝트별 메모리 분리가 필요한 경우 사용합니다.
*
* @param projectId - 프로젝트 식별자 (hash 또는 이름)
* @returns 프로젝트별 .astra 서브디렉토리 경로
*/
export function getAstraProjectDir(projectId: string): string {
const projDir = path.join(getAstraDataDir(), 'projects', projectId);
if (!fs.existsSync(projDir)) {
fs.mkdirSync(projDir, { recursive: true });
}
return projDir;
}
/**
* AAL(Autonomous Loop) 태스크 파일의 경로를 반환합니다.
*/
export function getAstraTaskFilePath(): string {
return getAstraFilePath('tasks.json');
}
/**
* AAL 프로토콜 설정 파일 경로를 반환합니다.
*/
export function getAstraProtocolPath(): string {
return getAstraFilePath('protocol.json');
}
/**
* extensionUri가 아직 설정되지 않은 경우의 fallback.
* __dirname 기반으로 ConnectAI 루트를 추정합니다.
*/
function _fallbackExtensionRoot(): string {
// esbuild로 번들된 out/extension.js → 상위 디렉토리가 ConnectAI 루트
return path.resolve(__dirname, '..');
}
+18 -6
View File
@@ -2,11 +2,17 @@ import { logInfo, logError } from '../utils';
/** /**
* ActionQueueManager: Manages large-scale tasks by processing them * ActionQueueManager: Manages large-scale tasks by processing them
* sequentially to prevent resource exhaustion and I/O bottlenecks. * with a concurrency limit to prevent resource exhaustion and I/O bottlenecks
* while maintaining high throughput under maximum load.
*/ */
export class ActionQueueManager { export class ActionQueueManager {
private queue: (() => Promise<void>)[] = []; private queue: (() => Promise<void>)[] = [];
private isProcessing: boolean = false; private activeCount: number = 0;
private readonly concurrencyLimit: number;
constructor(concurrencyLimit: number = 3) {
this.concurrencyLimit = concurrencyLimit;
}
/** /**
* Adds a task to the queue. * Adds a task to the queue.
@@ -26,28 +32,34 @@ export class ActionQueueManager {
} }
private async processNext() { private async processNext() {
if (this.isProcessing || this.queue.length === 0) return; if (this.activeCount >= this.concurrencyLimit || this.queue.length === 0) return;
this.isProcessing = true; this.activeCount++;
const task = this.queue.shift(); const task = this.queue.shift();
if (task) { if (task) {
try { try {
// Add a micro-delay to allow system breathing room between heavy I/O // Add a micro-delay to allow system breathing room between heavy I/O
await new Promise(r => setTimeout(r, 50)); await new Promise(r => setTimeout(r, 10));
await task(); await task();
} catch (error) { } catch (error) {
logError('Task in queue failed:', error); logError('Task in queue failed:', error);
} finally { } finally {
this.isProcessing = false; this.activeCount--;
this.processNext(); this.processNext();
} }
} else {
this.activeCount--;
} }
} }
public getPendingCount(): number { public getPendingCount(): number {
return this.queue.length; return this.queue.length;
} }
public getActiveCount(): number {
return this.activeCount;
}
} }
export const actionQueue = new ActionQueueManager(); export const actionQueue = new ActionQueueManager();
+4
View File
@@ -16,12 +16,16 @@ import { AgentExecutor } from './agent';
import { BridgeServer } from './bridge'; import { BridgeServer } from './bridge';
import { SidebarChatProvider } from './sidebarProvider'; import { SidebarChatProvider } from './sidebarProvider';
import { HealthCheckMonitor } from './core/health'; import { HealthCheckMonitor } from './core/health';
import { initAstraPathResolver } from './core/astraPath';
/** /**
* Astra Extension Entry Point * Astra Extension Entry Point
*/ */
export async function activate(context: vscode.ExtensionContext) { export async function activate(context: vscode.ExtensionContext) {
logInfo('Astra activating...'); logInfo('Astra activating...');
// Initialize Astra Path Resolver (.astra → ConnectAI/.astra/)
initAstraPathResolver(context);
// Start Environment Health Monitoring // Start Environment Health Monitoring
HealthCheckMonitor.runAllChecks(); HealthCheckMonitor.runAllChecks();
+18 -3
View File
@@ -95,7 +95,15 @@ export function buildSecondBrainTrace(userQuery: string, brainRoot: string, opti
const slotSelections = knowledgeSlots.map((slot) => { const slotSelections = knowledgeSlots.map((slot) => {
const slotTerms = tokenize(slot.retrievalQuery); const slotTerms = tokenize(slot.retrievalQuery);
const slotCandidates = files const slotCandidates = files
.map((file) => scoreFile(file, brainRoot, slotTerms, queryIntent, targetProject)) .map((file) => {
const doc = scoreFile(file, brainRoot, slotTerms, queryIntent, targetProject);
// 슬롯 ID와 문서 디렉토리명 매칭 보너스 (e.g. ontology 슬롯 → Ontology/ 디렉토리)
const dirName = path.dirname(doc.path).toLowerCase();
if (dirName.includes(slot.id.toLowerCase())) {
doc.score = Number((doc.score + 0.5).toFixed(2));
}
return doc;
})
.filter((doc) => doc.score >= 0.25) .filter((doc) => doc.score >= 0.25)
.sort((a, b) => b.score - a.score); .sort((a, b) => b.score - a.score);
const materialCandidates = slotCandidates.filter((doc) => doc.knowledgeRole !== 'routing-hint'); const materialCandidates = slotCandidates.filter((doc) => doc.knowledgeRole !== 'routing-hint');
@@ -581,7 +589,9 @@ function scoreFile(file: string, brainRoot: string, terms: string[], intent: Sec
score += projectRelevanceScore(relative, lower, targetProject, documentProject); score += projectRelevanceScore(relative, lower, targetProject, documentProject);
} }
const expandedTerms = expandQuery(terms); const expandedTerms = expandQuery(terms);
const scoredTfIdf = scoreTfIdf(expandedTerms, [{ title, content, lastModified: Date.now() }])[0]; // 디렉토리 경로를 title에 포함하여 카테고리 키워드 매칭 향상 (e.g. Ontology/ → 'ontology' 토큰)
const titleWithPath = `${relative.replace(/[\\/]/g, ' ')} ${title}`;
const scoredTfIdf = scoreTfIdf(expandedTerms, [{ title: titleWithPath, content, lastModified: Date.now() }])[0];
score += scoredTfIdf.score; score += scoredTfIdf.score;
@@ -595,7 +605,8 @@ function scoreFile(file: string, brainRoot: string, terms: string[], intent: Sec
title, title,
path: relative, path: relative,
absolutePath: file, absolutePath: file,
score: Number((Math.max(score, 0) / Math.max(expandedTerms.length, 1)).toFixed(2)), // sqrt 정규화: 동의어 확장으로 분모가 과도하게 커지는 것을 방지
score: Number((Math.max(score, 0) / Math.max(Math.sqrt(expandedTerms.length), 1)).toFixed(2)),
excerpt: summarizeText(finalExcerpt, 420), excerpt: summarizeText(finalExcerpt, 420),
sourceType, sourceType,
knowledgeRole, knowledgeRole,
@@ -679,6 +690,10 @@ function pathPriority(relativePath: string, intent: SecondBrainQueryIntent): num
if (/adr-\d+|decision|설계|원칙|principle|mvp|dependency|schema|documentation/i.test(normalized)) { if (/adr-\d+|decision|설계|원칙|principle|mvp|dependency|schema|documentation/i.test(normalized)) {
score += 1.5; score += 1.5;
} }
// 지식 카테고리 디렉토리 보너스 (knowledge slot 매칭 지원)
if (/(^|[\\/])(strategy|ontology|writing|technical|evidence|insight|information|domain)([\\/]|$)/i.test(normalized)) {
score += 1.5;
}
if (/(^|[\\/])(00_raw|raw-data|conversations?|transcripts?)([\\/]|$)/i.test(normalized)) { if (/(^|[\\/])(00_raw|raw-data|conversations?|transcripts?)([\\/]|$)/i.test(normalized)) {
score -= 4; score -= 4;
} }
+469 -26
View File
@@ -3,25 +3,311 @@ import { lockManager } from '../core/lock';
import { actionQueue } from '../core/queue'; import { actionQueue } from '../core/queue';
import { logInfo, logError } from '../utils'; import { logInfo, logError } from '../utils';
// ─────────────────────────────────────────────
// 1. 에이전트 인터페이스 확장 (Interface Extensibility)
// ─────────────────────────────────────────────
/** /**
* 에이전트 인터페이스 정의 (의존성 주입을 위함) * 에이전트 실행 시 전달되는 확장 옵션 객체.
* 향후 에이전트별로 고유한 설정(temperature, maxTokens 등)을
* IAgent 시그니처를 변경하지 않고 유연하게 주입할 수 있습니다.
*/
export interface AgentExecuteOptions {
/** 에이전트 실행의 추가 컨텍스트 문자열 */
context?: string;
/** 실행 중단 시그널 */
signal?: AbortSignal;
/** 에이전트별 커스텀 설정 (temperature, maxTokens 등) */
config?: Record<string, unknown>;
/** 이전 단계의 중간 결과물 (병렬 파이프라인용) */
priorResults?: Record<string, string>;
}
/**
* 에이전트 인터페이스 정의 (의존성 주입을 위함).
* execute()는 기존 시그니처를 유지하면서, 확장 옵션도 수용합니다.
*/ */
export interface IAgent { export interface IAgent {
execute(input: string, context?: string, signal?: AbortSignal): Promise<string>; execute(input: string, context?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string>;
} }
// ─────────────────────────────────────────────
// 2. 상태 관리의 명시적 분리 (Explicit State Management)
// ─────────────────────────────────────────────
/** /**
* 파이프라인 단계 상태 정의 * 파이프라인 단계 상태 정의
*/ */
export type PipelineStage = 'idle' | 'planner' | 'researcher' | 'writer' | 'completed' | 'error'; export type PipelineStage = 'idle' | 'planner' | 'researcher' | 'writer' | 'completed' | 'error';
/**
* 감사(Audit) 이력에 기록되는 단일 상태 전환 엔트리.
*/
export interface AuditEntry {
from: PipelineStage;
to: PipelineStage;
message: string;
timestamp: number;
durationFromPrev?: number;
}
/**
* MissionState: 엔진의 내부 상태를 캡슐화하는 독립 객체.
* 상태 전환의 모든 이력(Audit Trail)을 자동으로 기록하며,
* 외부 모니터링 시스템과 연동하여 투명한 파이프라인 추적을 가능하게 합니다.
*/
export class MissionState {
private _stage: PipelineStage = 'idle';
private _auditTrail: AuditEntry[] = [];
private _lastTransitionTime: number = Date.now();
public readonly missionId: string;
public readonly startTime: number;
constructor(missionId: string) {
this.missionId = missionId;
this.startTime = Date.now();
this._lastTransitionTime = this.startTime;
}
get stage(): PipelineStage {
return this._stage;
}
get auditTrail(): ReadonlyArray<AuditEntry> {
return this._auditTrail;
}
/**
* 상태를 전환하고, 감사 이력에 자동으로 기록합니다.
*/
public transition(to: PipelineStage, message: string): void {
const now = Date.now();
const entry: AuditEntry = {
from: this._stage,
to,
message,
timestamp: now,
durationFromPrev: now - this._lastTransitionTime
};
this._auditTrail.push(entry);
this._stage = to;
this._lastTransitionTime = now;
logInfo(`[MissionState] ${this.missionId}: ${entry.from}${entry.to} (${entry.durationFromPrev}ms) — ${message}`);
}
/**
* 전체 미션의 경과 시간을 반환합니다.
*/
public getElapsedMs(): number {
return Date.now() - this.startTime;
}
/**
* 감사 이력을 요약 문자열로 반환합니다 (디버깅/모니터링 용).
*/
public summarizeAudit(): string {
return this._auditTrail
.map(e => `[${e.from}${e.to}] ${e.durationFromPrev}ms: ${e.message}`)
.join('\n');
}
/**
* 감사 이력을 구조화된 JSON 포맷으로 출력합니다.
* 외부 모니터링 시스템(ELK Stack, Prometheus, Loki 등)과 연동 시
* 파싱 없이 바로 인제스트할 수 있는 표준 로그 포맷입니다.
*
* 출력 예시:
* ```json
* {
* "missionId": "mission_1714...",
* "status": "completed",
* "totalElapsedMs": 12450,
* "transitions": [
* { "from": "idle", "to": "planner", "durationMs": 0, "message": "...", "ts": "..." }
* ]
* }
* ```
*/
public toStructuredLog(): object {
return {
missionId: this.missionId,
status: this._stage,
startTime: new Date(this.startTime).toISOString(),
totalElapsedMs: this.getElapsedMs(),
transitionCount: this._auditTrail.length,
transitions: this._auditTrail.map(e => ({
from: e.from,
to: e.to,
durationMs: e.durationFromPrev,
message: e.message,
ts: new Date(e.timestamp).toISOString()
}))
};
}
}
// ─────────────────────────────────────────────
// 3. Error Recovery Matrix (오류 복구 매트릭스)
// ─────────────────────────────────────────────
/**
* 오류 유형 분류.
* 에이전트 간 통신 실패 시 발생하는 오류를 세 범주로 분류합니다.
*/
export enum ErrorType {
/** 네트워크 타임아웃, API 응답 지연 등 재시도로 복구 가능한 오류 */
TRANSIENT = 'TRANSIENT',
/** 프롬프트 구조 문제, 모델 명백한 실패 등 수동 개입이 필요한 오류 */
PERMANENT = 'PERMANENT',
/** 사용자가 의도적으로 작업을 취소한 경우 */
ABORT = 'ABORT'
}
/**
* Error Recovery Matrix의 단일 규칙 정의.
* 오류 유형별 대응 전략을 선언적으로 공식화합니다.
*/
export interface RecoveryRule {
type: ErrorType;
description: string;
maxRetries: number;
backoffBaseMs: number;
action: 'retry' | 'abort' | 'fail_with_message';
userMessage: string;
}
/**
* ┌─────────────────────────────────────────────────────────────────────┐
* │ Error Recovery Matrix (오류 복구 매트릭스) │
* ├──────────────┬──────────┬──────────┬─────────────────────────────────┤
* │ Error Type │ Retries │ Backoff │ Action │
* ├──────────────┼──────────┼──────────┼─────────────────────────────────┤
* │ TRANSIENT │ 3 │ 1000ms │ Exponential Backoff 자동 재시도 │
* │ PERMANENT │ 0 │ N/A │ 즉시 중단 + 사용자 안내 메시지 │
* │ ABORT │ 0 │ N/A │ 조용한 종료 (Graceful Exit) │
* └──────────────┴──────────┴──────────┴─────────────────────────────────┘
*/
export const ERROR_RECOVERY_MATRIX: ReadonlyArray<RecoveryRule> = [
{
type: ErrorType.TRANSIENT,
description: '네트워크 타임아웃, 일시적 API 지연, 연결 거부 등',
maxRetries: 3,
backoffBaseMs: 1000,
action: 'retry',
userMessage: '일시적인 연결 문제가 감지되었습니다. 자동으로 재시도 중입니다...'
},
{
type: ErrorType.PERMANENT,
description: '모델 응답 형식 오류, 프롬프트 구조 문제, 인증 실패 등',
maxRetries: 0,
backoffBaseMs: 0,
action: 'fail_with_message',
userMessage: '모델 응답에 근본적인 문제가 발생했습니다. 모델 설정을 확인하거나 다른 모델로 변경해 주세요.'
},
{
type: ErrorType.ABORT,
description: '사용자의 의도적 작업 취소',
maxRetries: 0,
backoffBaseMs: 0,
action: 'abort',
userMessage: '작업이 취소되었습니다.'
}
];
/**
* ErrorClassifier: 에러 객체를 분석하여 유형을 자동 판별합니다.
*/
export class ErrorClassifier {
/** Transient Error로 분류되는 패턴 */
private static readonly TRANSIENT_PATTERNS: RegExp[] = [
/timeout/i,
/ECONNREFUSED/i,
/ECONNRESET/i,
/ETIMEDOUT/i,
/network/i,
/fetch failed/i,
/Failed to fetch/i,
/503/, // Service Unavailable
/502/, // Bad Gateway
/429/, // Too Many Requests (Rate Limit)
/ENOTFOUND/i,
/socket hang up/i,
];
/** Permanent Error로 분류되는 패턴 */
private static readonly PERMANENT_PATTERNS: RegExp[] = [
/401/, // Unauthorized
/403/, // Forbidden
/404/, // Not Found (잘못된 모델명 등)
/유효한 응답을 받지 못했습니다/,
/Ollama URL이 설정되지 않았습니다/,
/invalid.*model/i,
/model.*not found/i,
/parse error/i,
];
/**
* 에러를 분류하고 해당하는 복구 규칙을 반환합니다.
*/
public static classify(error: any): { type: ErrorType; rule: RecoveryRule } {
// 1. Abort 확인
if (error.name === 'AbortError' || error.message === 'AbortError') {
return {
type: ErrorType.ABORT,
rule: ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.ABORT)!
};
}
const message = error.message || String(error);
// 2. Permanent Error 확인 (우선 순위 높음)
for (const pattern of this.PERMANENT_PATTERNS) {
if (pattern.test(message)) {
return {
type: ErrorType.PERMANENT,
rule: ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.PERMANENT)!
};
}
}
// 3. Transient Error 확인
for (const pattern of this.TRANSIENT_PATTERNS) {
if (pattern.test(message)) {
return {
type: ErrorType.TRANSIENT,
rule: ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.TRANSIENT)!
};
}
}
// 4. 분류 불가 → 안전하게 Permanent로 처리 (보수적 접근)
return {
type: ErrorType.PERMANENT,
rule: ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.PERMANENT)!
};
}
}
// ─────────────────────────────────────────────
// 4. AgentEngine 본체
// ─────────────────────────────────────────────
/** /**
* AgentEngine: * AgentEngine:
* Producer-Consumer 패턴을 기반으로 멀티 에이전트 워크플로우를 오케스트레이션하는 핵심 엔진. * Producer-Consumer 패턴을 기반으로 멀티 에이전트 워크플로우를 오케스트레이션하는 핵심 엔진.
* 명시적 락(Mutex) 의존성 주입(DI)을 통해 안정성과 유연성을 확보합니다. * 명시적 락(Mutex), 의존성 주입(DI), 독립 상태 객체(MissionState),
* Error Recovery Matrix를 통해 안정성, 유연성, 투명성, 복원력을 동시에 확보합니다.
*
* 아키텍처 특징:
* - IAgent 인터페이스의 옵션 확장으로 에이전트별 커스텀 설정 지원
* - MissionState를 통한 감사(Audit) 이력 자동 기록
* - 병렬 준비 단계(Parallel Prep)를 통한 비동기 흐름 정교화
* - Error Recovery Matrix 기반의 Transient/Permanent 오류 자동 분류 및 복구
*/ */
export class AgentEngine { export class AgentEngine {
private stage: PipelineStage = 'idle'; private state: MissionState | null = null;
constructor( constructor(
private readonly planner: IAgent, private readonly planner: IAgent,
@@ -41,6 +327,9 @@ export class AgentEngine {
onProgress: (stage: PipelineStage, message: string) => void onProgress: (stage: PipelineStage, message: string) => void
): Promise<string> { ): Promise<string> {
// 상태 객체 초기화
this.state = new MissionState(missionId);
// 1. 명시적 락 획득 (Mutex) - 동일 미션의 중복 실행 방지 // 1. 명시적 락 획득 (Mutex) - 동일 미션의 중복 실행 방지
const release = await lockManager.acquire(`mission_${missionId}`); const release = await lockManager.acquire(`mission_${missionId}`);
@@ -50,51 +339,205 @@ export class AgentEngine {
logInfo(`[AgentEngine] 미션 시작: ${missionId}`); logInfo(`[AgentEngine] 미션 시작: ${missionId}`);
// --- Phase 1: Planner --- // --- Phase 1: Planner ---
this.updateStage('planner', '전략 수립 중...', onProgress); this.transition('planner', '전략 수립 중...', onProgress);
if (signal.aborted) throw new Error('AbortError'); this.checkAbort(signal);
const plan = await this.planner.execute(prompt, brainContext, signal); logInfo(`[AgentEngine] [Planner] Input Prompt: ${this.summarizeLog(prompt, 50)}`);
const plan = await this.resilientExecute(
this.planner, 'Planner', prompt, brainContext, signal, onProgress,
{ context: brainContext, signal, config: { role: 'planner' } }
);
this.validateResult(plan, 'Planner'); this.validateResult(plan, 'Planner');
logInfo(`[AgentEngine] [Planner] Output: ${this.summarizeLog(plan, 100)}`);
// --- Phase 2: Researcher --- // --- Phase 2 & 3: Parallel Prep + Sequential Execution ---
this.updateStage('researcher', '핵심 정보 수집 및 분석 중...', onProgress); this.transition('researcher', '핵심 정보 수집 및 분석 중...', onProgress);
if (signal.aborted) throw new Error('AbortError'); this.checkAbort(signal);
await this.delay(500); // 시스템 부하 분산을 위한 미세 지연 logInfo(`[AgentEngine] [Researcher] BrainContext Size: ${brainContext?.length || 0} chars`);
const research = await this.researcher.execute(plan, brainContext, signal);
const [research, writerPrep] = await Promise.all([
this.resilientExecute(
this.researcher, 'Researcher', plan, brainContext, signal, onProgress,
{ context: brainContext, signal, config: { role: 'researcher' } }
),
this.prepareWriterContext(prompt, plan, brainContext)
]);
this.validateResult(research, 'Researcher'); this.validateResult(research, 'Researcher');
logInfo(`[AgentEngine] [Researcher] Output: ${this.summarizeLog(research, 100)}`);
// --- Phase 3: Writer --- // --- Phase 3: Writer ---
this.updateStage('writer', '최종 리포트 작성 및 편집 중...', onProgress); this.transition('writer', '최종 리포트 작성 및 편집 중...', onProgress);
if (signal.aborted) throw new Error('AbortError'); this.checkAbort(signal);
await this.delay(500); const finalReport = await this.resilientExecute(
const finalReport = await this.writer.execute(research, prompt, signal); this.writer, 'Writer', research, prompt, signal, onProgress,
{ context: brainContext, signal, config: { role: 'writer' }, priorResults: { plan, writerPrep } }
);
this.validateResult(finalReport, 'Writer'); this.validateResult(finalReport, 'Writer');
logInfo(`[AgentEngine] [Writer] Output: ${this.summarizeLog(finalReport, 100)}`);
this.updateStage('completed', '미션 완료', onProgress); this.transition('completed', '미션 완료', onProgress);
logInfo(`[AgentEngine] 미션 완료: ${missionId} (총 ${this.state!.getElapsedMs()}ms)`);
return finalReport; return finalReport;
}); });
} catch (error: any) { } catch (error: any) {
this.updateStage('error', `오류 발생: ${error.message}`, onProgress); const { type, rule } = ErrorClassifier.classify(error);
logError(`[AgentEngine] 미션 실패 (${missionId}):`, error); const stageName = (this.state?.stage || 'unknown').toUpperCase();
this.transition('error', `오류 발생: ${error.message}`, onProgress);
// Error Recovery Matrix 기반 세분화된 로깅
switch (type) {
case ErrorType.ABORT:
logInfo(`[AgentEngine] [ABORT] 미션 취소됨 (${missionId}) at ${stageName} stage.`);
break;
case ErrorType.TRANSIENT:
logError(`[AgentEngine] [TRANSIENT] 재시도 소진 후 실패 (${missionId}) at ${stageName} stage — ${rule.description}:`, error);
break;
case ErrorType.PERMANENT:
logError(`[AgentEngine] [PERMANENT] 복구 불가 오류 (${missionId}) at ${stageName} stage — ${rule.userMessage}:`, error);
break;
}
// 감사 이력 덤프 (디버깅용)
if (this.state) {
logError(`[AgentEngine] Audit Trail for ${missionId}:\n${this.state.summarizeAudit()}`);
}
throw error; throw error;
} finally { } finally {
// 3. 락 해제 // 3. 락 해제
release(); release();
this.stage = 'idle'; if (this.state) {
this.state = null;
}
} }
} }
private updateStage(stage: PipelineStage, message: string, onProgress: (stage: PipelineStage, message: string) => void) { /**
this.stage = stage; * 현재 미션의 상태 객체를 외부에서 읽을 수 있도록 노출합니다.
* 외부 모니터링 시스템 연동에 활용됩니다.
*/
public getMissionState(): MissionState | null {
return this.state;
}
// ─── Resilience Layer ───
/**
* Error Recovery Matrix 기반의 탄력적 에이전트 실행.
*
* - Transient Error: 지수 백오프(Exponential Backoff)를 적용하여 최대 N회 자동 재시도.
* - Permanent Error: 즉시 중단하고 명확한 사용자 메시지를 첨부하여 예외를 전파.
* - Abort: 조용하게 예외를 전파 (Graceful Exit).
*/
private async resilientExecute(
agent: IAgent,
agentName: string,
input: string,
context: string,
signal: AbortSignal,
onProgress: (stage: PipelineStage, message: string) => void,
options?: AgentExecuteOptions
): Promise<string> {
const transientRule = ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.TRANSIENT)!;
let lastError: any;
for (let attempt = 0; attempt <= transientRule.maxRetries; attempt++) {
try {
// 재시도 시 사용자에게 진행 상황 알림
if (attempt > 0) {
const backoffMs = transientRule.backoffBaseMs * Math.pow(2, attempt - 1);
logInfo(`[AgentEngine] [RETRY] ${agentName} 재시도 ${attempt}/${transientRule.maxRetries} (${backoffMs}ms 후)`);
onProgress(this.state?.stage || 'error', `${agentName} 재시도 중... (${attempt}/${transientRule.maxRetries})`);
await new Promise(r => setTimeout(r, backoffMs));
this.checkAbort(signal);
}
return await agent.execute(input, context, signal, options);
} catch (error: any) {
lastError = error;
const { type, rule } = ErrorClassifier.classify(error);
switch (type) {
case ErrorType.ABORT:
// 사용자 취소 → 재시도 없이 즉시 전파
logInfo(`[AgentEngine] [ABORT] ${agentName} 실행 취소됨.`);
throw error;
case ErrorType.PERMANENT:
// 영구 오류 → 재시도 없이 즉시 중단, 사용자 메시지 첨부
logError(`[AgentEngine] [PERMANENT] ${agentName} 복구 불가: ${rule.userMessage}`);
const enrichedError = new Error(`[${agentName}] ${rule.userMessage} (원인: ${error.message})`);
(enrichedError as any).originalError = error;
(enrichedError as any).errorType = ErrorType.PERMANENT;
throw enrichedError;
case ErrorType.TRANSIENT:
// 일시적 오류 → 재시도 가능 여부 확인
if (attempt >= transientRule.maxRetries) {
logError(`[AgentEngine] [TRANSIENT] ${agentName} 최대 재시도 횟수(${transientRule.maxRetries}) 소진.`);
const exhaustedError = new Error(
`[${agentName}] 일시적 연결 오류가 지속됩니다. ` +
`${transientRule.maxRetries}회 재시도 후에도 복구되지 않았습니다. ` +
`네트워크 연결 및 모델 서버 상태를 확인해 주세요. (원인: ${error.message})`
);
(exhaustedError as any).originalError = error;
(exhaustedError as any).errorType = ErrorType.TRANSIENT;
throw exhaustedError;
}
logInfo(`[AgentEngine] [TRANSIENT] ${agentName}에서 일시적 오류 감지: ${error.message}`);
break; // continue to next attempt
}
}
}
// 이론적으로 도달 불가하지만 안전장치
throw lastError;
}
// ─── Private Helpers ───
/**
* MissionState를 통한 상태 전환 + 외부 콜백 호출.
*/
private transition(stage: PipelineStage, message: string, onProgress: (stage: PipelineStage, message: string) => void) {
if (this.state) {
this.state.transition(stage, message);
}
onProgress(stage, message); onProgress(stage, message);
} }
/**
* AbortSignal 확인을 일관되게 처리합니다.
*/
private checkAbort(signal: AbortSignal): void {
if (signal.aborted) {
throw new Error('AbortError');
}
}
/**
* Writer가 사용할 초기 컨텍스트를 사전에 구성합니다.
* Researcher와 병렬로 실행되어 Phase 3 진입 시 즉시 활용 가능합니다.
*/
private async prepareWriterContext(prompt: string, plan: string, brainContext: string): Promise<string> {
const contextSummary = [
`[Original Prompt] ${prompt.substring(0, 200)}`,
`[Plan Summary] ${plan.substring(0, 300)}`,
`[Brain Context Available] ${brainContext ? 'Yes' : 'No'} (${brainContext?.length || 0} chars)`
].join('\n');
logInfo(`[AgentEngine] [WriterPrep] 초기 컨텍스트 준비 완료 (${contextSummary.length} chars)`);
return contextSummary;
}
private summarizeLog(data: string | undefined, length: number = 100): string {
if (!data) return 'empty';
const clean = data.replace(/\n/g, ' ').trim();
return clean.length > length ? clean.substring(0, length) + '...' : clean;
}
private validateResult(data: string, step: string) { private validateResult(data: string, step: string) {
if (!data || data.trim().length < 10) { if (!data || data.trim().length < 10) {
throw new Error(`${step} 에이전트로부터 유효한 응답을 받지 못했습니다.`); throw new Error(`${step} 에이전트로부터 유효한 응답을 받지 못했습니다.`);
} }
} }
private delay(ms: number) {
return new Promise(r => setTimeout(r, ms));
}
} }
+6 -6
View File
@@ -3,8 +3,9 @@
* Project Memory (프로젝트 기억) * Project Memory (프로젝트 기억)
* *
* 프로젝트별 요구사항, 코드 구조, 아키텍처 결정, 버그 기록 등을 * 프로젝트별 요구사항, 코드 구조, 아키텍처 결정, 버그 기록 등을
* 프로젝트 로컬에 저장하고 관리합니다. * Astra 확장 프로그램 내부에 저장하고 관리합니다.
* 저장 위치: {projectRoot}/.astra/project_memory.json * 저장 위치: {ConnectAI}/.astra/project_memory.json
* (기존: {projectRoot}/.astra/ → 변경됨)
* ============================================================ * ============================================================
*/ */
@@ -12,6 +13,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { ProjectMemoryStore, ArchitectureDecision, BugRecord, MemoryContextResult } from './types'; import { ProjectMemoryStore, ArchitectureDecision, BugRecord, MemoryContextResult } from './types';
import { getAstraDataDir } from '../core/astraPath';
export class ProjectMemory { export class ProjectMemory {
private store: ProjectMemoryStore; private store: ProjectMemoryStore;
@@ -19,10 +21,8 @@ export class ProjectMemory {
private dirty = false; private dirty = false;
constructor(projectRoot: string) { constructor(projectRoot: string) {
const astraDir = path.join(projectRoot, '.astra'); // .astra 디렉토리를 ConnectAI 내부에서 해결 (사용자 프로젝트 루트에 생성하지 않음)
if (!fs.existsSync(astraDir)) { const astraDir = getAstraDataDir();
fs.mkdirSync(astraDir, { recursive: true });
}
this.filePath = path.join(astraDir, 'project_memory.json'); this.filePath = path.join(astraDir, 'project_memory.json');
this.store = this.load(projectRoot); this.store = this.load(projectRoot);
} }
+4 -3
View File
@@ -222,9 +222,10 @@ Never use placeholder values like optional/subdir or filename.md. If the user as
Operational rules: Operational rules:
1. Same language as the user. 1. Same language as the user.
2. File paths can be relative to the workspace or absolute paths under /Volumes/Data/project/Antigravity. 2. File paths can be relative to the workspace or absolute paths under /Volumes/Data/project/Antigravity.
3. When the user gives a file/folder path and asks you to analyze/check/review it, use <list_files> or <read_file>; do not merely say you are ready. 3. When the user gives a file/folder path and asks you to analyze/check/review it, use <list_files> or <read_file> to access it IMMEDIATELY. Never say "show me the file", "provide the code", "파일 내용을 보여주세요", or "코드를 공유해 주세요". You have filesystem access — use it.
4. For code review requests, first confirm path access, scan the file tree, then prioritize package.json, src, docs, README, and config files before giving findings. 4. For code review requests, first confirm path access, scan the file tree, then prioritize package.json, src, docs, README, and config files before giving findings. Do NOT pause between steps to ask "진행할까요?" or "시작할까요?". Execute the full analysis in one continuous response.
5. Keep persona light. Do not introduce yourself unless the user greets you or asks who you are.`; 5. When the user says "분석해줘", "봐줘", "확인해줘", "리뷰해줘" with a path, that IS the confirmation. Start the analysis immediately. Do not restate the plan and wait for a second confirmation.
6. Keep persona light. Do not introduce yourself unless the user greets you or asks who you are.`;
export function getSystemPrompt(): string { export function getSystemPrompt(): string {
return BASE_SYSTEM_PROMPT; return BASE_SYSTEM_PROMPT;
+543
View File
@@ -0,0 +1,543 @@
/**
* AgentEngine Integration Tests & Performance Benchmarks
*
* 검증 대상:
* 1. ErrorClassifier — 오류 유형(Transient/Permanent/Abort) 자동 분류
* 2. ErrorRecoveryMatrix — 각 규칙이 의도한 대응 전략으로 매핑되는지 검증
* 3. resilientExecute — 지수 백오프 재시도 및 즉시 중단 흐름
* 4. MissionState — 감사 이력(Audit Trail) 및 구조화된 로그 포맷
* 5. Performance Benchmark — 미션 평균 처리 시간 및 재시도 오버헤드 측정
*/
import {
AgentEngine,
IAgent,
AgentExecuteOptions,
ErrorClassifier,
ErrorType,
ERROR_RECOVERY_MATRIX,
MissionState,
PipelineStage
} from '../src/lib/engine';
// ─── Mock Agents ───
class MockSuccessAgent implements IAgent {
public callCount = 0;
constructor(private readonly response: string = 'This is a valid mock response for testing purposes.') {}
async execute(input: string, context?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string> {
this.callCount++;
return this.response;
}
}
class MockTransientAgent implements IAgent {
public callCount = 0;
constructor(private readonly failCount: number = 2) {}
async execute(input: string, context?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string> {
this.callCount++;
if (this.callCount <= this.failCount) {
throw new Error('ECONNREFUSED: Connection refused');
}
return 'Recovery successful after transient failures.';
}
}
class MockPermanentAgent implements IAgent {
async execute(): Promise<string> {
throw new Error('404: model not found');
}
}
class MockTimeoutAgent implements IAgent {
async execute(): Promise<string> {
throw new Error('timeout: request took too long');
}
}
class MockNetworkAgent implements IAgent {
async execute(): Promise<string> {
throw new Error('Failed to fetch');
}
}
class MockAbortAgent implements IAgent {
async execute(): Promise<string> {
const err = new Error('AbortError');
err.name = 'AbortError';
throw err;
}
}
class MockSlowAgent implements IAgent {
constructor(private readonly delayMs: number = 100) {}
async execute(): Promise<string> {
await new Promise(r => setTimeout(r, this.delayMs));
return 'Slow but valid agent response for performance measurement.';
}
}
// ─── Helper ───
function createAbortSignal(): AbortSignal {
const controller = new AbortController();
return controller.signal;
}
const noopProgress = (_stage: PipelineStage, _message: string) => {};
// ═══════════════════════════════════════════════
// Test Suite 1: ErrorClassifier
// ═══════════════════════════════════════════════
describe('ErrorClassifier', () => {
describe('Transient Error Classification', () => {
const transientMessages = [
'ECONNREFUSED: Connection refused',
'Request timeout exceeded',
'ETIMEDOUT: operation timed out',
'ECONNRESET: connection reset by peer',
'network error occurred',
'Failed to fetch',
'HTTP 503: Service Unavailable',
'HTTP 502: Bad Gateway',
'HTTP 429: Too Many Requests',
'socket hang up',
];
test.each(transientMessages)('"%s" → TRANSIENT', (msg) => {
const result = ErrorClassifier.classify(new Error(msg));
expect(result.type).toBe(ErrorType.TRANSIENT);
expect(result.rule.action).toBe('retry');
expect(result.rule.maxRetries).toBe(3);
});
});
describe('Permanent Error Classification', () => {
const permanentMessages = [
'HTTP 401: Unauthorized',
'HTTP 403: Forbidden',
'HTTP 404: Not Found',
'Planner 에이전트로부터 유효한 응답을 받지 못했습니다',
'Ollama URL이 설정되지 않았습니다',
'invalid model name specified',
'model not found in registry',
];
test.each(permanentMessages)('"%s" → PERMANENT', (msg) => {
const result = ErrorClassifier.classify(new Error(msg));
expect(result.type).toBe(ErrorType.PERMANENT);
expect(result.rule.action).toBe('fail_with_message');
expect(result.rule.maxRetries).toBe(0);
});
});
describe('Abort Classification', () => {
test('AbortError by name → ABORT', () => {
const err = new Error('cancelled');
err.name = 'AbortError';
const result = ErrorClassifier.classify(err);
expect(result.type).toBe(ErrorType.ABORT);
expect(result.rule.action).toBe('abort');
});
test('AbortError by message → ABORT', () => {
const result = ErrorClassifier.classify(new Error('AbortError'));
expect(result.type).toBe(ErrorType.ABORT);
});
});
describe('Unknown Error → Permanent (보수적 처리)', () => {
test('분류 불가한 오류는 PERMANENT로 처리', () => {
const result = ErrorClassifier.classify(new Error('something completely unexpected'));
expect(result.type).toBe(ErrorType.PERMANENT);
});
});
});
// ═══════════════════════════════════════════════
// Test Suite 2: Error Recovery Matrix
// ═══════════════════════════════════════════════
describe('Error Recovery Matrix', () => {
test('매트릭스에 3가지 유형이 모두 정의되어 있어야 한다', () => {
const types = ERROR_RECOVERY_MATRIX.map(r => r.type);
expect(types).toContain(ErrorType.TRANSIENT);
expect(types).toContain(ErrorType.PERMANENT);
expect(types).toContain(ErrorType.ABORT);
});
test('TRANSIENT 규칙은 재시도가 가능해야 한다', () => {
const rule = ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.TRANSIENT)!;
expect(rule.maxRetries).toBeGreaterThan(0);
expect(rule.backoffBaseMs).toBeGreaterThan(0);
expect(rule.action).toBe('retry');
});
test('PERMANENT 규칙은 재시도하지 않아야 한다', () => {
const rule = ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.PERMANENT)!;
expect(rule.maxRetries).toBe(0);
expect(rule.action).toBe('fail_with_message');
expect(rule.userMessage.length).toBeGreaterThan(0);
});
test('ABORT 규칙은 조용하게 종료해야 한다', () => {
const rule = ERROR_RECOVERY_MATRIX.find(r => r.type === ErrorType.ABORT)!;
expect(rule.maxRetries).toBe(0);
expect(rule.action).toBe('abort');
});
});
// ═══════════════════════════════════════════════
// Test Suite 3: MissionState
// ═══════════════════════════════════════════════
describe('MissionState', () => {
test('초기 상태는 idle이어야 한다', () => {
const state = new MissionState('test_001');
expect(state.stage).toBe('idle');
expect(state.auditTrail.length).toBe(0);
});
test('상태 전환이 감사 이력에 기록되어야 한다', () => {
const state = new MissionState('test_002');
state.transition('planner', '전략 수립 중...');
state.transition('researcher', '연구 수행 중...');
state.transition('completed', '완료');
expect(state.stage).toBe('completed');
expect(state.auditTrail.length).toBe(3);
expect(state.auditTrail[0].from).toBe('idle');
expect(state.auditTrail[0].to).toBe('planner');
expect(state.auditTrail[1].from).toBe('planner');
expect(state.auditTrail[1].to).toBe('researcher');
});
test('toStructuredLog()가 올바른 JSON 형식을 반환해야 한다', () => {
const state = new MissionState('test_003');
state.transition('planner', '시작');
state.transition('completed', '완료');
const log = state.toStructuredLog() as any;
expect(log.missionId).toBe('test_003');
expect(log.status).toBe('completed');
expect(log.totalElapsedMs).toBeGreaterThanOrEqual(0);
expect(log.transitionCount).toBe(2);
expect(log.transitions).toHaveLength(2);
expect(log.transitions[0]).toHaveProperty('from');
expect(log.transitions[0]).toHaveProperty('to');
expect(log.transitions[0]).toHaveProperty('durationMs');
expect(log.transitions[0]).toHaveProperty('ts');
});
test('getElapsedMs()가 양수를 반환해야 한다', () => {
const state = new MissionState('test_004');
expect(state.getElapsedMs()).toBeGreaterThanOrEqual(0);
});
});
// ═══════════════════════════════════════════════
// Test Suite 4: AgentEngine Integration
// ═══════════════════════════════════════════════
describe('AgentEngine Integration', () => {
test('정상 미션 흐름이 최종 리포트를 반환해야 한다', async () => {
const engine = new AgentEngine(
new MockSuccessAgent('Plan: detailed strategy for the mission ahead.'),
new MockSuccessAgent('Research: comprehensive analysis of available data.'),
new MockSuccessAgent('Report: final synthesized output for the user.')
);
const result = await engine.runMission(
'integration_001', 'Test prompt', 'brain context', createAbortSignal(), noopProgress
);
expect(result).toBe('Report: final synthesized output for the user.');
});
test('Transient 오류 발생 시 자동 재시도 후 복구되어야 한다', async () => {
const transientAgent = new MockTransientAgent(2); // 2회 실패 후 성공
const engine = new AgentEngine(
transientAgent,
new MockSuccessAgent('Research data after recovery from transient errors.'),
new MockSuccessAgent('Final report written successfully after recovery.')
);
const result = await engine.runMission(
'integration_002', 'Test prompt', 'context', createAbortSignal(), noopProgress
);
expect(transientAgent.callCount).toBe(3); // 2회 실패 + 1회 성공
expect(result).toContain('Final report');
}, 30000);
test('Permanent 오류 발생 시 즉시 중단되어야 한다', async () => {
const engine = new AgentEngine(
new MockPermanentAgent(),
new MockSuccessAgent(),
new MockSuccessAgent()
);
await expect(
engine.runMission('integration_003', 'Test', 'ctx', createAbortSignal(), noopProgress)
).rejects.toThrow();
});
test('Abort 시그널 발생 시 Graceful Exit해야 한다', async () => {
const engine = new AgentEngine(
new MockAbortAgent(),
new MockSuccessAgent(),
new MockSuccessAgent()
);
await expect(
engine.runMission('integration_004', 'Test', 'ctx', createAbortSignal(), noopProgress)
).rejects.toThrow('AbortError');
});
test('Transient 오류가 maxRetries를 초과하면 실패해야 한다', async () => {
const alwaysFailAgent = new MockTransientAgent(100); // 항상 실패
const engine = new AgentEngine(
alwaysFailAgent,
new MockSuccessAgent(),
new MockSuccessAgent()
);
await expect(
engine.runMission('integration_005', 'Test', 'ctx', createAbortSignal(), noopProgress)
).rejects.toThrow('재시도');
// maxRetries(3) + 초기 시도(1) = 4회 호출
expect(alwaysFailAgent.callCount).toBe(4);
}, 30000);
test('미션 완료 후 상태가 정리되어야 한다', async () => {
const engine = new AgentEngine(
new MockSuccessAgent('Plan output that meets validation requirements.'),
new MockSuccessAgent('Research output that meets validation requirements.'),
new MockSuccessAgent('Final report output that meets validation requirements.')
);
await engine.runMission('integration_006', 'Test', 'ctx', createAbortSignal(), noopProgress);
// 미션 완료 후 state는 null로 정리
expect(engine.getMissionState()).toBeNull();
});
});
// ═══════════════════════════════════════════════
// Test Suite 5: Performance Benchmark
// ═══════════════════════════════════════════════
describe('Performance Benchmark', () => {
test('정상 미션의 평균 처리 시간 측정', async () => {
const iterations = 5;
const durations: number[] = [];
for (let i = 0; i < iterations; i++) {
const engine = new AgentEngine(
new MockSlowAgent(50),
new MockSlowAgent(50),
new MockSlowAgent(50)
);
const start = Date.now();
await engine.runMission(`bench_normal_${i}`, 'Benchmark prompt', 'ctx', createAbortSignal(), noopProgress);
durations.push(Date.now() - start);
}
const avg = durations.reduce((a, b) => a + b, 0) / durations.length;
const max = Math.max(...durations);
const min = Math.min(...durations);
console.log(`\n📊 [Normal Mission Benchmark]`);
console.log(` Iterations: ${iterations}`);
console.log(` Avg Latency: ${Math.round(avg)}ms`);
console.log(` Min: ${min}ms | Max: ${max}ms`);
// 각 에이전트 50ms * 3 + 오버헤드 → 200ms 이내가 합리적
expect(avg).toBeLessThan(1000);
}, 30000);
test('Transient 복구 시 재시도 오버헤드 측정', async () => {
const engine = new AgentEngine(
new MockTransientAgent(2), // 2회 실패 후 성공 (백오프: 1s + 2s)
new MockSuccessAgent('Research after transient recovery benchmark data.'),
new MockSuccessAgent('Final benchmark report output for measurement.')
);
const start = Date.now();
await engine.runMission('bench_retry', 'Benchmark', 'ctx', createAbortSignal(), noopProgress);
const elapsed = Date.now() - start;
console.log(`\n📊 [Retry Overhead Benchmark]`);
console.log(` Retries: 2`);
console.log(` Total Time: ${elapsed}ms`);
console.log(` Expected Backoff: ~3000ms (1000 + 2000)`);
// 지수 백오프 1s + 2s ≈ 3000ms + 처리 시간
expect(elapsed).toBeGreaterThan(2500);
expect(elapsed).toBeLessThan(10000);
}, 30000);
test('Permanent 오류 시 즉시 중단 시간 측정', async () => {
const engine = new AgentEngine(
new MockPermanentAgent(),
new MockSuccessAgent(),
new MockSuccessAgent()
);
const start = Date.now();
try {
await engine.runMission('bench_permanent', 'Benchmark', 'ctx', createAbortSignal(), noopProgress);
} catch { /* expected */ }
const elapsed = Date.now() - start;
console.log(`\n📊 [Permanent Error Benchmark]`);
console.log(` Time to Fail: ${elapsed}ms`);
// Permanent 오류는 재시도 없이 즉시 중단 → 100ms 이내
expect(elapsed).toBeLessThan(500);
});
});
// ═══════════════════════════════════════════════
// Test Suite 6: Concurrency & Stress Tests
// ═══════════════════════════════════════════════
describe('Concurrency & Stress Tests', () => {
test('5개 미션 동시 실행 시 모두 정상 완료되어야 한다', async () => {
const concurrentCount = 5;
const results: Promise<string>[] = [];
for (let i = 0; i < concurrentCount; i++) {
const engine = new AgentEngine(
new MockSuccessAgent(`Plan output ${i} that passes validation checks.`),
new MockSuccessAgent(`Research output ${i} that passes validation checks.`),
new MockSuccessAgent(`Report output ${i} that passes validation checks.`)
);
results.push(
engine.runMission(`concurrent_${i}`, `Prompt ${i}`, 'ctx', createAbortSignal(), noopProgress)
);
}
const outputs = await Promise.all(results);
expect(outputs).toHaveLength(concurrentCount);
outputs.forEach((output, i) => {
expect(output).toContain(`Report output ${i}`);
});
console.log(`\n📊 [Concurrency Test]`);
console.log(` Concurrent Missions: ${concurrentCount}`);
console.log(` All Resolved: ✅`);
}, 30000);
test('동시에 Transient + Permanent + 정상 미션이 혼합될 때 각각 올바르게 처리되어야 한다', async () => {
// 미션 1: 정상
const engine1 = new AgentEngine(
new MockSuccessAgent('Plan result that meets the minimum validation length.'),
new MockSuccessAgent('Research result that meets the minimum validation length.'),
new MockSuccessAgent('Normal report completed successfully with all checks passed.')
);
const p1 = engine1.runMission('mix_normal', 'Test', 'ctx', createAbortSignal(), noopProgress);
// 미션 2: Permanent 실패
const engine2 = new AgentEngine(
new MockPermanentAgent(),
new MockSuccessAgent(),
new MockSuccessAgent()
);
const p2 = engine2.runMission('mix_permanent', 'Test', 'ctx', createAbortSignal(), noopProgress)
.catch(e => `ERROR:${e.message}`);
// 미션 3: Transient 복구
const engine3 = new AgentEngine(
new MockTransientAgent(1), // 1회 실패 후 성공
new MockSuccessAgent('Research after single transient recovery for mixed test.'),
new MockSuccessAgent('Report after transient recovery completed successfully.')
);
const p3 = engine3.runMission('mix_transient', 'Test', 'ctx', createAbortSignal(), noopProgress);
const [r1, r2, r3] = await Promise.all([p1, p2, p3]);
// 정상 미션은 성공
expect(r1).toContain('Normal report');
// Permanent 미션은 에러 메시지 반환
expect(r2).toContain('ERROR:');
// Transient 미션은 복구 후 성공
expect(r3).toContain('Report after transient');
console.log(`\n📊 [Mixed Error Concurrency Test]`);
console.log(` Normal: ✅ | Permanent: ❌ (expected) | Transient: ✅ (recovered)`);
}, 30000);
test('큐 포화 상태에서 10개 작업이 순서대로 처리되어야 한다', async () => {
const taskCount = 10;
const completionOrder: number[] = [];
const results: Promise<string>[] = [];
for (let i = 0; i < taskCount; i++) {
const idx = i;
const engine = new AgentEngine(
new MockSuccessAgent(`Plan ${idx} passes the minimum validation requirement.`),
new MockSuccessAgent(`Research ${idx} passes the minimum validation requirement.`),
{
execute: async () => {
completionOrder.push(idx);
return `Report ${idx} is valid and meets all minimum length requirements.`;
}
} as IAgent
);
results.push(
engine.runMission(`queue_sat_${idx}`, `Prompt ${idx}`, 'ctx', createAbortSignal(), noopProgress)
);
}
const outputs = await Promise.all(results);
// 모든 작업이 완료되어야 함
expect(outputs).toHaveLength(taskCount);
expect(completionOrder).toHaveLength(taskCount);
console.log(`\n📊 [Queue Saturation Test]`);
console.log(` Tasks Submitted: ${taskCount}`);
console.log(` Tasks Completed: ${completionOrder.length}`);
console.log(` Completion Order: [${completionOrder.join(', ')}]`);
}, 60000);
test('동일 미션 ID로 동시 실행 시 Mutex가 경합을 방지해야 한다', async () => {
const sharedMissionId = 'race_condition_test';
let executionCount = 0;
const engine1 = new AgentEngine(
{
execute: async () => {
executionCount++;
await new Promise(r => setTimeout(r, 100));
return `Planner result from execution ${executionCount} for race test.`;
}
} as IAgent,
new MockSuccessAgent('Research result that is valid and passes all minimum checks.'),
new MockSuccessAgent('Report result that is valid and passes all minimum checks.')
);
const engine2 = new AgentEngine(
new MockSuccessAgent('Plan result that is valid and passes all minimum checks.'),
new MockSuccessAgent('Research result that is valid and passes all minimum checks.'),
new MockSuccessAgent('Report result that is valid and passes all minimum checks.')
);
// 동일 미션 ID로 두 엔진 동시 실행 → Mutex에 의해 순차 실행되어야 함
const [r1, r2] = await Promise.all([
engine1.runMission(sharedMissionId, 'Test', 'ctx', createAbortSignal(), noopProgress),
engine2.runMission(sharedMissionId, 'Test', 'ctx', createAbortSignal(), noopProgress)
]);
// 둘 다 성공해야 함 (Mutex가 순서를 보장)
expect(r1).toBeTruthy();
expect(r2).toBeTruthy();
console.log(`\n📊 [Race Condition / Mutex Test]`);
console.log(` Shared Mission ID: ${sharedMissionId}`);
console.log(` Both Completed: ✅ (Mutex serialized execution)`);
}, 30000);
});
+29 -5
View File
@@ -47,7 +47,13 @@ describe('Second Brain Trace', () => {
'# Customer Journey Virtual Store', '# Customer Journey Virtual Store',
'', '',
'Customer-facing virtual stores should connect spatial experience to product discovery, product understanding, and purchase conversion.', 'Customer-facing virtual stores should connect spatial experience to product discovery, product understanding, and purchase conversion.',
'Stakeholder approval often depends on requirement fit, business value, and acceptance criteria rather than visual novelty alone.' 'Stakeholder approval often depends on requirement fit, business value, and acceptance criteria rather than visual novelty alone.',
'Customer journey mapping reveals how users navigate from initial interest to final purchase decision.',
'The approval process evaluates customer experience quality, conversion flow effectiveness, and business value proposition.',
'Product discovery in virtual stores requires intuitive spatial navigation and curated customer journey touchpoints.',
'Stakeholder approval criteria include requirement fit assessment, business value validation, and acceptance criteria verification.',
'Virtual store UX should prioritize customer journey continuity, product discovery efficiency, and conversion optimization.',
'Business value is measured through customer engagement metrics, approval rates, and conversion funnel analysis.'
].join('\n'), ].join('\n'),
'utf8' 'utf8'
); );
@@ -58,7 +64,13 @@ describe('Second Brain Trace', () => {
'# Report Evidence Mapping', '# Report Evidence Mapping',
'', '',
'Template-driven reports should map each section to evidence, insight, risk, and next action knowledge.', 'Template-driven reports should map each section to evidence, insight, risk, and next action knowledge.',
'A schema should guide structure while Second Brain notes supply the actual content.' 'A schema should guide structure while Second Brain notes supply the actual content.',
'Evidence mapping connects report sections to factual source documents and verified project records.',
'Each report template section should link to concrete evidence, analytical insight, identified risk, and actionable next steps.',
'The evidence layer provides facts and source documentation for the report body.',
'Insight sections synthesize patterns from evidence into strategic analysis and interpretation.',
'Risk sections document limitations, validation gaps, tradeoffs, and items requiring verification.',
'Action sections translate knowledge into MVP implementation steps, recommendations, and decision items.'
].join('\n'), ].join('\n'),
'utf8' 'utf8'
); );
@@ -79,7 +91,11 @@ describe('Second Brain Trace', () => {
'# Knowledge Graph Concepts', '# Knowledge Graph Concepts',
'', '',
'Ontology notes define concepts, relations, categories, and graph structure before writing.', 'Ontology notes define concepts, relations, categories, and graph structure before writing.',
'They help a report decide which ideas are parent concepts, evidence, methods, and outcomes.' 'They help a report decide which ideas are parent concepts, evidence, methods, and outcomes.',
'An ontology provides a taxonomy of concept types, relation types, and category hierarchies.',
'Knowledge graph structure organizes domain concepts into navigable networks of meaning.',
'Ontology-driven classification helps reports maintain consistent concept definitions and relation mappings.',
'Graph-based concept organization enables systematic categorization and cross-referencing of knowledge.'
].join('\n'), ].join('\n'),
'utf8' 'utf8'
); );
@@ -90,7 +106,11 @@ describe('Second Brain Trace', () => {
'# Report Narrative Structure', '# Report Narrative Structure',
'', '',
'Writing guidance should shape report structure, section order, narrative flow, and concise executive summaries.', 'Writing guidance should shape report structure, section order, narrative flow, and concise executive summaries.',
'It should not replace evidence; it organizes selected knowledge into a readable output.' 'It should not replace evidence; it organizes selected knowledge into a readable output.',
'Report writing structure defines the narrative arc from executive summary through detailed analysis to conclusions.',
'Template-based writing organizes content into headline, body, evidence citation, and recommendation sections.',
'Good report narrative maintains logical flow between sections while preserving analytical rigor.',
'Writing style guidance ensures consistent tone, appropriate formality, and reader-oriented structure across reports.'
].join('\n'), ].join('\n'),
'utf8' 'utf8'
); );
@@ -101,7 +121,11 @@ describe('Second Brain Trace', () => {
'# Implementation Techniques', '# Implementation Techniques',
'', '',
'Technical technique notes explain implementation methods, architecture choices, and tooling tradeoffs.', 'Technical technique notes explain implementation methods, architecture choices, and tooling tradeoffs.',
'They should support practical next actions after the report identifies risks and evidence.' 'They should support practical next actions after the report identifies risks and evidence.',
'Implementation technique documentation covers method selection, architecture decision rationale, and tool evaluation.',
'Technical references provide concrete implementation patterns, code architecture examples, and integration approaches.',
'Technique notes bridge the gap between strategic analysis and practical engineering execution.',
'Architecture technique guides help translate report findings into actionable technical implementation plans.'
].join('\n'), ].join('\n'),
'utf8' 'utf8'
); );