272 lines
17 KiB
Markdown
272 lines
17 KiB
Markdown
# Deep-Research 하네스 → Codex / Antigravity CLI 이식 설계
|
|
|
|
- **상태**: design (구현 전)
|
|
- **작성일**: 2026-06-09
|
|
- **원본 하네스**: Claude Code `Workflow` 도구 기반 JS 스크립트
|
|
`~/.claude/projects/-home-donghyeon-dev-llm-wiki-private/88afa9ca-1e45-4353-9ff0-6361812d9053/workflows/scripts/deep-research-wf_aecef33f-4cc.js`
|
|
- **목표**: claude code의 deep-research를 **codex-cli와 antigravity-cli 각각이 그대로 수행**할 수 있도록 단일 외부 Python 드라이버로 이식. 완전 충실(원본 동작·상수·퇴화 경로 1:1).
|
|
|
|
---
|
|
|
|
## 0. 한 줄 요약
|
|
|
|
원본 deep-research는 Claude `Workflow` 도구 위에서 도는 **결정론적 JS 오케스트레이터**다. Codex/Antigravity는 이 도구가 없고 오케스트레이션이 모델 주도이므로, **결정론 제어 로직을 외부 Python 드라이버 1벌**에 이식하고, 각 플랫폼은 `run_agent()` **backend 어댑터**로 흡수한다. 에이전트 실행기는 **각 CLI의 headless 모드**(`codex exec`, `agy -p`)이며, 웹 조사는 **각 CLI의 네이티브 web_search·webfetch 도구**가 수행한다(별도 검색 API·비용 없음). 포팅 = 프롬프트 3벌 복제가 아니라 **backend flag 하나로 분기하는 1벌 도구**.
|
|
|
|
---
|
|
|
|
## 1. 배경 / 문제
|
|
|
|
### 1.1 원본 하네스의 2층 구조
|
|
|
|
| 층 | 내용 | 이식 난이도 |
|
|
|---|---|---|
|
|
| **층1 — 결정론 제어** (JS 코드, 모델이 못 건드림) | URL 정규화·dedup, fetch 예산 회계, claim 랭킹, 3-vote 정족수 산식, barrier 동기화, stats | 플랫폼 무관 — 코드로 1:1 이식 |
|
|
| **층2 — 에이전트 프롬프트 + 스키마 + 페이즈** | Scope→Search→Fetch→Verify→Synthesize 6단계 프롬프트, 5개 JSON 스키마, 3-vote adversarial 패턴 | 플랫폼 무관 콘텐츠 — 그대로 이식 |
|
|
|
|
### 1.2 플랫폼 능력 검증 결과 (공식 소스 기반)
|
|
|
|
| 능력 | Claude Code | Codex CLI (`codex exec`) | Antigravity CLI (`agy`) |
|
|
|---|---|---|---|
|
|
| 결정론적 코드 제어 | Workflow 네이티브 | ❌ (모델 주도) → **외부 드라이버 필요** | ❌ (모델 주도) → **외부 드라이버 필요** |
|
|
| 병렬 실행 | `parallel()` | subprocess 병렬 | Async Subagent / subprocess 병렬 |
|
|
| headless 단발 실행 | — | `codex exec` | `agy -p "prompt"` (Command Mode) |
|
|
| **네이티브 웹검색** | WebSearch | ✅ `codex exec --json`에 web_search 항목 | ✅ harness 내 web/research 도구 |
|
|
| **네이티브 웹페치** | WebFetch | ✅ | ✅ webfetch |
|
|
| **스키마 강제 출력** | tool-layer 검증+재시도 | ✅ `--output-schema <jsonschema>` | ⚠️ 미확인 → 드라이버 검증·재시도로 보강 |
|
|
| 설정 포맷 | JS | `.toml` agent / CLI flag | `agent.json` / CLI flag |
|
|
|
|
> **검증 메모**: 초기 조사 때 "Codex/Antigravity 서브에이전트는 웹·스키마 없음"이라 판단했으나, 그건 *interactive subagent* 문서만 본 오판이었다. `codex exec` **headless 모드**에는 `--json`(web_search 이벤트 포함) + `--output-schema`가 있고, `agy -p`도 동일 harness의 웹 도구를 쓴다. 따라서 별도 검색 API(Tavily 등)·bare SDK는 불필요하며, **사용자가 이미 구독으로 쓰는 CLI 네이티브 도구**를 그대로 활용한다.
|
|
|
|
출처:
|
|
- Codex non-interactive(`--json`/`--output-schema`/web_search): https://developers.openai.com/codex/noninteractive
|
|
- Codex CLI 웹검색 설정: https://codex.danielvaughan.com/2026/05/09/codex-cli-web-search-configuration-cached-live-domain-allow-lists-prompt-injection-defence/
|
|
- Antigravity CLI features: https://antigravity.google/docs/cli-features
|
|
- OpenAI Agents SDK 언어(Py/TS): https://openai.github.io/openai-agents-js/
|
|
- Antigravity Python SDK: https://github.com/google-antigravity/antigravity-sdk-python
|
|
|
|
### 1.3 왜 SDK가 아니라 CLI headless인가
|
|
|
|
- **비용**: CLI는 구독제 → 호출당 추가 과금 없음. bare SDK + 검색 API는 중복 비용.
|
|
- **충실도**: 원본은 에이전트가 *직접* WebSearch/WebFetch를 호출한다. CLI headless도 에이전트가 네이티브 웹도구를 호출 → 동일 의미.
|
|
- **스키마**: Codex `--output-schema`가 tool-layer 검증을 대체. Antigravity는 드라이버가 Pydantic 검증·재시도로 보강.
|
|
|
|
---
|
|
|
|
## 2. 목표 / 비목표
|
|
|
|
### 목표
|
|
1. 원본 6페이즈·상수·퇴화 경로를 **1:1 충실** 이식.
|
|
2. `python -m deep_research --backend {codex|antigravity} "<질문>"` 단일 진입점으로 두 플랫폼에서 동일 동작.
|
|
3. 결정론 제어 로직을 **순수 함수**로 분리 → 단위테스트로 원본 동작 회귀 검증.
|
|
4. 웹·스키마를 각 CLI 네이티브 기능으로 충족(추가 비용 0).
|
|
|
|
### 비목표
|
|
- Claude를 backend로 추가하지 않음(원본 그 자체이므로 불필요).
|
|
- repo의 `.claude/.codex/.agents` 프롬프트 미러 체계에 편입하지 않음(이 드라이버는 그 밖의 새 카테고리 — 1벌 도구).
|
|
- 원본 상수·페이즈 구조 변경/최적화 없음(충실 우선).
|
|
- GUI·서버·스케줄러 없음(단발 CLI 실행만).
|
|
|
|
---
|
|
|
|
## 3. 아키텍처
|
|
|
|
```
|
|
scripts/deep-research/
|
|
├── pyproject.toml # deps: pydantic>=2, (stdlib asyncio/subprocess/json)
|
|
├── README.md # 사용법, 인증 전제, backend별 주의
|
|
├── deep_research/
|
|
│ ├── __init__.py
|
|
│ ├── __main__.py # CLI 진입점, argparse, .env/키 점검, report 출력
|
|
│ ├── config.py # 상수 (원본과 동일): VOTES_PER_CLAIM 등 + 동시성 cap
|
|
│ ├── schemas.py # 5 Pydantic v2 모델 + JSON Schema export 헬퍼
|
|
│ ├── core.py # 결정론 순수함수 (JS 1:1): dedup/budget/rank/tally/assemble
|
|
│ ├── pipeline.py # asyncio 오케스트레이션 (무배리어 pipeline / verify 배리어)
|
|
│ ├── prompts.py # 6단계 프롬프트 (원본 문자열 이식)
|
|
│ ├── backends/
|
|
│ │ ├── base.py # AgentBackend ABC: async run_agent(prompt, schema) -> BaseModel
|
|
│ │ ├── codex.py # codex exec --json --output-schema 호출·파싱
|
|
│ │ ├── antigravity.py # agy -p 호출·파싱 + Pydantic 검증·재시도
|
|
│ │ └── mock.py # 테스트 더블 (고정 응답 주입)
|
|
│ └── report.py # Report → markdown + JSON dump
|
|
└── tests/
|
|
├── test_core.py # dedup/budget/tally/rank 단위테스트
|
|
└── test_pipeline_mock.py # MockBackend 통합 스모크 + 퇴화 경로 3종
|
|
```
|
|
|
|
### 3.1 모듈 책임 (단일 책임 / 격리)
|
|
|
|
- **core.py** — 네트워크·LLM·플랫폼 의존 0. 입력 dict/list → 출력 dict/list 순수 변환. 단위테스트 100% 가능. *충실도의 심장.*
|
|
- **backends/** — 유일하게 플랫폼을 아는 곳. interface `run_agent(prompt, schema) -> 검증된 Pydantic 객체`. 한 backend 교체가 core·pipeline에 무영향.
|
|
- **pipeline.py** — core 함수 + backend 호출을 asyncio로 엮는 곳. 원본 `pipeline()`(무배리어) / `parallel()`(배리어) 의미를 asyncio로 재현.
|
|
- **web 계층 없음** — 웹은 backend가 호출하는 CLI의 네이티브 도구가 수행. 드라이버는 URL 문자열만 다룬다(정규화·dedup).
|
|
|
|
### 3.2 backend 인터페이스 계약
|
|
|
|
```python
|
|
class AgentBackend(ABC):
|
|
@abstractmethod
|
|
async def run_agent(self, prompt: str, schema: type[BaseModel],
|
|
*, label: str) -> BaseModel | None:
|
|
"""프롬프트를 1개 CLI headless 에이전트로 실행, schema로 검증된 객체 반환.
|
|
실패/사용자-skip → None (원본 .filter(Boolean) 의미)."""
|
|
```
|
|
|
|
- **codex.py**: `codex exec --json --output-schema <tmp.json> --cd <neutral_dir> -s read-only "<prompt>"` subprocess. JSONL stdout에서 최종 메시지 파싱 → Pydantic 검증. 웹은 codex 네이티브 web_search.
|
|
- **antigravity.py**: `agy -p "<prompt>" ...` subprocess. 출력 파싱 → Pydantic 검증, 실패 시 "스키마에 맞춰 JSON만" 재프롬프트 N회(기본 2). 웹은 agy 네이티브 도구.
|
|
- **mock.py**: 생성자에 `{label_prefix: 응답객체}` 주입. 네트워크·subprocess 없이 즉답. 테스트 전용.
|
|
|
|
> **중립 작업 디렉터리**: CLI executor는 repo가 아닌 임시/중립 `--cd`에서 호출 → 코드베이스 컨텍스트(AGENTS.md/CLAUDE.md)를 안 물고 순수 리서치 에이전트로 동작. read-only 샌드박스.
|
|
|
|
---
|
|
|
|
## 4. 상수 (원본과 동일 — `config.py`)
|
|
|
|
```python
|
|
VOTES_PER_CLAIM = 3 # claim당 verifier 수
|
|
REFUTATIONS_REQUIRED = 2 # 2/3 refute면 kill
|
|
MAX_FETCH = 15 # fetch 예산 (전체 novel URL 상한)
|
|
MAX_VERIFY_CLAIMS = 25 # 검증 대상 claim 상한
|
|
ANGLES_TARGET = 5 # scope 분해 목표(스키마 minItems3/maxItems6)
|
|
CONCURRENCY = 10 # asyncio.Semaphore (원본 Workflow cap 대응, 설정 가능)
|
|
ANTIGRAVITY_RETRIES = 2 # 스키마 검증 실패 시 재프롬프트 횟수
|
|
```
|
|
|
|
---
|
|
|
|
## 5. 스키마 (`schemas.py` — 5 Pydantic v2 모델)
|
|
|
|
원본 5 SCHEMA를 Pydantic으로 1:1. `model_json_schema()`로 Codex `--output-schema`용 JSON Schema export.
|
|
|
|
- **Scope**: `question:str`, `summary:str`, `angles: list[Angle](min 3, max 6)` / `Angle{label, query, rationale?}`
|
|
- **Search**: `results: list[SearchResult](max 6)` / `SearchResult{url, title, snippet?, relevance: Enum[high|medium|low]}`
|
|
- **Extract**: `sourceQuality: Enum[primary|secondary|blog|forum|unreliable]`, `publishDate?`, `claims: list[Claim](max 5)` / `Claim{claim, quote, importance: Enum[central|supporting|tangential]}`
|
|
- **Verdict**: `refuted:bool`, `evidence:str`, `confidence: Enum[high|medium|low]`, `counterSource?`
|
|
- **Report**: `summary`, `findings: list[Finding]` / `Finding{claim, confidence, sources:list[str], evidence, vote?}`, `caveats`, `openQuestions?: list[str]`
|
|
|
|
---
|
|
|
|
## 6. 데이터 흐름 (6 페이즈 — 원본 충실)
|
|
|
|
### 6.0 Scope (1 agent)
|
|
질문 → 5개 상보적 검색 각도(`Scope`). 빈 질문이면 즉시 에러 반환(원본과 동일).
|
|
|
|
### 6.1 Search (각도별 1 agent, 무배리어 pipeline 1단계)
|
|
- 에이전트가 **CLI 네이티브 web_search**로 검색(쿼리 정제 가능) → 원질문 기준 관련도 재랭킹·SEO 스팸 제거 → top 4-6(`Search`).
|
|
- *드라이버는 검색을 직접 하지 않음.* 에이전트의 인지 단계(관련도 판단) 보존.
|
|
|
|
### 6.2 Dedup (순수 드라이버 — `core.dedup`)
|
|
원본 로직 1:1:
|
|
- `normURL(u)`: hostname `www.` 제거 + pathname 후행 `/` 제거 + lowercase. 파싱 실패 시 `u.lower()`.
|
|
- `seen: set`, relevance 순(high<medium<low) 정렬 후 순회.
|
|
- 이미 본 키 → `dupes`에 적재, 스킵.
|
|
- `fetchSlots<=0` 이고 relevance가 medium/low(rank≥1) → `budgetDropped`에 적재, 스킵.
|
|
- 통과 시 `seen`에 추가, `fetchSlots--`.
|
|
|
|
### 6.3 Fetch + Extract (novel URL별 1 agent, pipeline 2단계 parallel)
|
|
- 에이전트가 **CLI 네이티브 webfetch**로 본문 취득 → 출처품질 평가 + falsifiable claim 2-5개(직접 인용 포함) 추출(`Extract`).
|
|
- fetch 실패/무관/페이월 → `claims:[]`, `sourceQuality:"unreliable"`(원본과 동일).
|
|
- backend가 None 반환(실패/skip) → 드롭(원본 `.filter(Boolean)`).
|
|
|
|
### 6.4 Rank (순수 — `core.rank_claims`)
|
|
`importance`(central<supporting<tangential) → `sourceQuality`(primary<…<unreliable) 순 정렬 → 상위 `MAX_VERIFY_CLAIMS=25`.
|
|
|
|
### 6.5 Verify (배리어 — claim당 3 agent 병렬)
|
|
- claim당 `VOTES_PER_CLAIM=3` verifier를 병렬, "refute 우선" 프롬프트(`Verdict`).
|
|
- **정족수 산식 (`core.tally`, 원본 1:1, abstention 엣지 포함)**:
|
|
- `valid = [v for v in verdicts if v is not None]` (None=기권)
|
|
- `refuted = sum(v.refuted for v in valid)`
|
|
- `survives = (len(valid) >= REFUTATIONS_REQUIRED) and (refuted < REFUTATIONS_REQUIRED)`
|
|
- ⚠️ **all-abstain → refuted=0 → 거짓 생존 금지**: `len(valid) >= 2` 조건이 이를 차단(원본 주석과 동일 의도). 이 엣지는 단위테스트로 고정.
|
|
|
|
### 6.6 Synthesize (1 agent)
|
|
생존 claim → 의미중복 병합·findings 그룹화·confidence 부여·3-5문장 요약·caveats·open questions(`Report`). refuted 목록은 투명성 위해 첨부.
|
|
|
|
### 6.7 퇴화 경로 3종 (원본 1:1 — 반드시 보존)
|
|
1. **claim 0개**: 검증 0건 요약 + sources/stats 반환.
|
|
2. **전건 refute**: "adversarial verification에서 전부 기각, 결론 불가" 요약 + refuted 목록 반환.
|
|
3. **synthesis 실패/skip**: 보고서 대신 생존 claim raw salvage 반환(전체 run 폐기 금지).
|
|
|
|
### 6.8 stats (원본 키 보존)
|
|
`angles, sourcesFetched, claimsExtracted, claimsVerified, confirmed, killed, afterSynthesis, urlDupes, budgetDropped, agentCalls(=1+angles+sources+voted*3+1)`.
|
|
|
|
---
|
|
|
|
## 7. 동시성 (`pipeline.py`)
|
|
|
|
- `asyncio.Semaphore(CONCURRENCY)`로 동시 CLI subprocess 수 제한.
|
|
- **무배리어 pipeline (Search→Dedup→Fetch)**: 각도별 async 체인 — A각도가 Fetch 도는 동안 B각도는 Search 가능. 단, **dedup의 `seen`/`fetchSlots`는 공유 상태**이므로 각 Search 완료 직후 dedup을 적용하는 지점에서만 갱신(원본은 pipeline 2단계 진입 시 순차 도착하며 갱신). → asyncio에서는 dedup 임계구역을 단일 코루틴/락으로 직렬화해 경쟁 제거.
|
|
- **배리어 (Verify)**: 전체 claim 풀이 모인 뒤 `asyncio.gather`로 일괄 검증(원본 의도적 barrier).
|
|
- agent 호출 1개 = CLI subprocess 1개. 콜드스타트 ~97콜/run → 구독제라 비용 무관, 속도는 동시성으로 완화.
|
|
|
|
---
|
|
|
|
## 8. 에러 처리
|
|
|
|
| 상황 | 처리 (원본 의미) |
|
|
|---|---|
|
|
| backend run_agent 실패/타임아웃 | None 반환 → 상위에서 filter |
|
|
| fetch 실패/페이월 | `unreliable` + `claims:[]` |
|
|
| verifier 기권(None) | valid에서 제외, 정족수 미달 시 미생존 |
|
|
| Codex `--output-schema` 위반 | codex 재시도; 그래도 실패 시 None |
|
|
| Antigravity 스키마 검증 실패 | 드라이버가 "JSON만" 재프롬프트 `ANTIGRAVITY_RETRIES`회 후 None |
|
|
| 빈 질문 / scope 실패 | 즉시 에러 dict 반환 |
|
|
| 키/CLI 미설치 | 시작 시 명확한 에러로 중단 |
|
|
|
|
---
|
|
|
|
## 9. 인증 / 설정 전제
|
|
|
|
- **codex backend**: `codex` CLI 설치 + 로그인(구독). 웹검색 활성(`--json` web_search).
|
|
- **antigravity backend**: `agy` CLI 설치 + 로그인(구독). 웹 도구 활성.
|
|
- 별도 검색 API 키 **불필요**.
|
|
- 시작 시 선택된 backend의 CLI 존재·로그인 점검, 미충족 시 안내 후 종료.
|
|
|
|
---
|
|
|
|
## 10. 테스트 전략
|
|
|
|
### 10.1 core 순수함수 단위테스트 (`test_core.py`)
|
|
- `normURL`: www/trailing-slash/대소문자/파싱실패 케이스.
|
|
- `dedup`: 중복 적재, budget-drop(slot 소진 시 medium/low만), high는 slot 무시 통과 여부(원본 `relRank>=1` 조건 정확 재현).
|
|
- `tally`: 3-0/2-1/1-2/0-3, **all-abstain(생존 금지)**, 1 valid+2 abstain(미달 미생존), 2 valid 0 refute(생존).
|
|
- `rank_claims`: importance·quality 2차 정렬 순서.
|
|
|
|
### 10.2 MockBackend 통합 스모크 (`test_pipeline_mock.py`)
|
|
- 고정 응답으로 6페이즈 정상 흐름 1건 + **퇴화 경로 3종** 재현. 네트워크·키·subprocess 0.
|
|
- stats 키·`agentCalls` 계산식 검증.
|
|
|
|
### 10.3 실 backend 스모크 (수동, 선택)
|
|
- 짧은 질문 1건을 `--backend codex` / `--backend antigravity`로 각 1회 실행, 보고서·stats 육안 확인.
|
|
|
|
---
|
|
|
|
## 11. 리스크 / 미해결 (구현 1차에서 확인)
|
|
|
|
| 리스크 | 영향 | 완화 |
|
|
|---|---|---|
|
|
| `agy` output-schema 플래그 유무 미확인 | Antigravity 스키마 보장 약함 | 드라이버 Pydantic 검증·재시도로 보강; 플래그 있으면 채택 |
|
|
| CLI 콜드스타트 누적 지연 | run 시간 증가 | 동시성 cap 상향, Antigravity Go 런타임, (후속) 세션 재사용/`spawn_agents_on_csv` |
|
|
| `codex exec` JSONL 최종 메시지 추출 형식 | 파싱 취약 | 이벤트 타입 기반 파서 + 스키마 검증으로 방어 |
|
|
| 무배리어 pipeline의 dedup 공유 상태 경쟁 | dedup 비결정성 | dedup 임계구역 직렬화(락/단일 코루틴) |
|
|
| repo 내 `scripts/` 부재(기존 메모) | 신규 디렉터리 | `scripts/deep-research/` 신설 — sync 생성기와 무관(별 카테고리) |
|
|
|
|
---
|
|
|
|
## 12. 거버넌스 / repo 정합
|
|
|
|
- 이 드라이버는 프롬프트 미러(`.claude`/`.codex`/`.agents`) 대상이 **아님** — backend 어댑터로 분기하는 1벌 도구.
|
|
- 위치: `scripts/deep-research/`. 구현 완료 후 CLAUDE.md에 **"외부 도구: deep-research 드라이버"** 한 줄 등재 검토(별 카테고리, 3-플랫폼 패리티 예외 — invest/project 파이프라인과 유사한 비미러 항목).
|
|
- 원본 JS 경로를 README에 명시해 충실도 대조 기준 보존.
|
|
|
|
---
|
|
|
|
## 13. 구현 순서(개요 — 상세는 plan 단계)
|
|
|
|
1. `schemas.py` + `config.py` + `prompts.py`(원본 문자열 이식).
|
|
2. `core.py` 순수함수 + `test_core.py`(원본 동작 고정).
|
|
3. `backends/base.py` + `mock.py` + `test_pipeline_mock.py`(퇴화 경로 포함) — 여기까지 **네트워크 0으로 충실도 검증 완료**.
|
|
4. `pipeline.py`(asyncio, dedup 임계구역 직렬화) — MockBackend로 통과.
|
|
5. `backends/codex.py`(`codex exec --json --output-schema`) → 실 스모크.
|
|
6. `backends/antigravity.py`(`agy -p` + 검증·재시도) → 실 스모크.
|
|
7. `report.py` + `__main__.py` + README.
|