Imported from jorepong/web-agent-research (
AGENTS.md). Install upstream withnpx skills add jorepong/web-agent-research. Copyright stays with the author.
AGENTS.md — AI 개발 컨텍스트
이 프로젝트에서 작업할 때 알아야 할 현재 구조와 규칙을 정리한 문서입니다.
프로젝트 구성
두 개의 패키지가 하나의 npm 워크스페이스(모노레포)에 들어 있습니다. 저장소 루트는 agentic-web-research이고, 실제 코드는 packages/ 아래에 있습니다. 의존 방향은 한쪽뿐입니다 — @awr/research가 @awr/reader를 의존하고, 변환기는 검색기를 전혀 모릅니다.
agentic-web-research/ (private 워크스페이스 루트)
package.json workspaces: ["packages/*"], scripts: build(tsc -b)/test(vitest)
tsconfig.base.json 공용 컴파일러 옵션 (composite)
tsconfig.json 솔루션 파일 (두 패키지 참조)
vitest.config.ts @awr/reader를 소스로 해소하는 별칭
packages/reader/ @awr/reader
packages/research/ @awr/research
1. @awr/reader (packages/reader)
웹 페이지를 LLM이 읽기 좋은 마크다운과 별도 레지스트리로 변환하는 라이브러리입니다. deps: playwright, linkedom. bin: llm-page.
packages/reader/src/
index.ts — 공개 API (convertPage, convertHtml, activateLink, resolveLink, openLink, clickElement)
dom-normalizer.ts — HTML → 마크다운/AST 변환 핵심 로직
link-registry.ts — 링크 ID 발급, URL 정규화, 중복 제거
element-registry.ts — 폼 요소 ID 발급 (버튼/입력창/셀렉트/텍스트에어리어)
io.ts — page.md/page.json/links.json/elements.json 저장·읽기
types.ts — 변환기 타입 정의
cli.ts — llm-page CLI 진입점
cli-utils.ts — CLI 파싱 헬퍼
2. @awr/research (packages/research)
@awr/reader 위에 구축된 에이전트 기반 웹 검색 도구입니다. 하나의 재귀 Researcher가 자기 자신을 호출하며, search/paginate/read_sections/delegate/delegate_parallel/done 행동으로 목표를 좁혀 갑니다. deps: openai, @awr/reader. bin: llm-search.
packages/research/src/
index.ts — 라이브러리 진입점 (research, runResearcher 재노출)
types.ts — 공유 타입 (ResearchOptions / BudgetLimits / ResearcherBrief / CurrentSurface, LLMMessage, LogEventKind)
config.ts — llm-search.config.json 로딩과 탐색 예산(limits) 정규화
cli.ts — llm-search CLI 진입점
cli-runner.ts — env/config 로딩과 리서처 실행 공통부
cli-utils.ts — CLI 파싱 헬퍼 (reader와 별개로 자립)
openai-client.ts — OpenAI SDK 래퍼 (로깅 + Structured Outputs)
logger.ts — ResearchLogger (researcher-*.jsonl + stderr 요약)
search-engines.ts — google/bing/naver SERP URL 빌더
json-utils.ts — LLM 응답 JSON 파싱 방어 레이어
budget.ts — SharedBudget (트리 전체 라운드·검색·위임·URL 중복 관리)
sections.ts — 긴 페이지를 heading 기반 섹션으로 나누고 필요한 섹션만 읽는 유틸
prompts.ts — 액션 스키마와 프롬프트
researcher.ts — runResearcher 재귀 본체 + research() 래퍼
llm-search.config.json — 탐색 예산 설정 (패키지 안에 위치)
@awr/research는 변환기를 @awr/reader 패키지 이름으로 import합니다(상대 경로가 아니라). 검색기 코드에서 convertPage, activateLink, ConvertResult 등이 필요하면 @awr/reader에서 가져옵니다.
기술 스택
- TypeScript strict, ES2022, NodeNext(ESM), 프로젝트 참조(
composite) 기반tsc -b빌드 - npm workspaces — 두 패키지를 한 저장소에서 관리,
@awr/reader↔@awr/research는 심링크로 연결 - Playwright — Chromium 렌더링, 스크롤 안정화, stealth 모드 (reader)
- linkedom — 경량 DOM 파싱 (reader)
- OpenAI SDK — 기본 모델
gpt-5.4-mini(research) - Vitest — 테스트 프레임워크 (루트에서 실행,
@awr/reader는 소스로 별칭 해소)
빌드: npm run build(루트, tsc -b), 테스트: npm test(루트, vitest run)
NodeNext 규칙 때문에 로컬 TS import 경로에는 .js 확장자가 필요합니다.
변환기 동작 방식
convertPage(url) 흐름:
- Playwright로 페이지를 열고
domcontentloaded와 짧은networkidle대기를 수행합니다. - 기본값으로 자동 스크롤을 수행해 동적 콘텐츠를 안정화합니다.
page.content()HTML을linkedom으로 파싱합니다.cleanupDocument가 script/style/광고/숨김 요소 등을 제거합니다.buildRegions가 navigation/main/aside/footer/footnotes 영역을 만들고, 링크와 폼 요소를 레지스트리에 등록합니다.page.md,PageAst,LinkRegistry,ElementRegistry를 반환합니다.
마크다운에서 링크는 텍스트 [L1] 형식이고, 폼 요소는 [button#B1: 텍스트] 형식입니다. renderPage는 헤더에 Page ID, Host, Links, Elements를 출력합니다.
누락 디버깅 순서:
- 렌더링된
document.body.innerText에 해당 텍스트가 있는지 확인합니다. cleanupDocument가 해당 영역을 제거했는지 확인합니다.buildRegions가 영역을 navigation/main/aside/footer/footnotes 어디에도 넣지 못했는지 확인합니다.
검색 도구 동작 방식
research(goal, options, client, logger)는 자연어 목표를 받아 자연어 답변을 반환합니다. CLI에서는 cli-runner.ts가 OpenAIClient, ResearchLogger, 설정에서 읽은 예산(limits)을 구성해 주입합니다. 탐색 예산은 packages/research/llm-search.config.json으로 조정하며, 설정 파일이 없으면 config.ts의 DEFAULT_LIMITS를 씁니다.
핵심은 하나의 runResearcher가 자기 자신을 재귀 호출한다는 점입니다. 루트, URL 없는 서브 리서처, 시작 페이지가 있는 리서처는 다른 에이전트가 아니라 서로 다른 실행 상태입니다.
행동:
search— URL 없는 서브 리서처가 SERP를 가져옵니다. 루트는 직접 search하지 않고 먼저 위임합니다.paginate— 현재 표면이 SERP일 때 같은 query의 다른 페이지를 가져옵니다.read_sections— 긴 시작 페이지에서 아직 읽지 않은 섹션을 추가로 읽습니다.delegate— 자연어 task와 선택적targetId/startUrl로 하위 리서처를 호출합니다.delegate_parallel— 독립 하위 리서처를 병렬 호출합니다.done—ANSWER / SOURCES / COVERAGE / GAPS / NEXT_CANDIDATES템플릿의 자연어 답변을 반환합니다.
CandidateRegistry로 원래 [L*] 링크를 전역 후보 ID [C*]로 다시 매핑합니다. LLM에는 현재 표면에 보이는 후보 ID만 스키마 enum으로 허용됩니다.
긴 페이지는 sections.ts에서 heading 기반 섹션 목록으로 나뉩니다. 40,000자를 넘는 페이지는 먼저 섹션 선택 LLM 호출을 거쳐 일부 섹션만 읽고, 이후 필요하면 read_sections로 추가 섹션을 읽습니다.
SharedBudget은 한 research 트리 전체에서 라운드, search/paginate, delegate/delegate_parallel, 방문 URL 중복을 관리합니다. 설정 파일 기준 현재 한도는 maxRounds=30, maxSearches=20, maxExplores=20, maxParallel=3, maxDepth=5, maxChildCallsPerAgent=30입니다.
디버그 로그
--debug는 researcher-<timestamp>.jsonl을 만듭니다. 각 줄이 유효한 JSON 객체이며, 에이전트 depth만큼 좌측 공백을 넣어 시간순으로 저장합니다. ResearchLogger.finalize()는 성공/실패 경로 모두에서 호출되어야 하며, CLI 러너가 이를 처리합니다.
주요 이벤트:
llm_request/llm_responsepage_markdownpage_sections/page_section_selectionmission_briefexploration_reportorchestrator_plan
로거는 llm_request의 messages를 얕게 복사합니다. 메시지 객체 자체는 복사되지만 거대한 payload를 깊은 스냅샷으로 보관하는 구조는 아닙니다.
코딩 컨벤션
- 기존 패턴과 모듈 경계를 우선합니다. 특히 패키지 경계(
@awr/reader→@awr/research단방향)를 지킵니다 — 변환기가 검색기를 import하지 않습니다. - 프롬프트 변경은
packages/research/src/prompts.ts에서만 합니다. - LLM 액션 응답은
OpenAIClient.complete(..., { responseSchema })로 OpenAI Structured Outputs를 사용합니다. parseJsonResponse는 mock, 예외 경로, 방어 레이어로 유지합니다.- logger, client, budget 같은 상태는 명시적으로 주입합니다. 새 싱글톤/전역 상태를 만들지 않습니다.
- 리서처 실패는 가능한 한 크래시 대신 구조화된 폴백 답변으로 바꿉니다.
로드맵
현재 로드맵과 개선 후보는 TODO.md 하나에서 관리합니다. 리서처 설계 상세는 RESEARCHER.md를 참고합니다.