Metadata-Version: 2.4
Name: xgen-harness
Version: 0.26.15
Summary: XGEN Harness — OpenAI enum dict 평탄화 + s06 reranker toggle 정상화 + s06 description 축약. xgen-nodes 가 options [{value,label}] 로 들고 있던 게 OpenAI enum 으로 그대로 흘러 invalid_function_parameters 400 유발하던 결함 (Tavily 등). _normalize_for_openai 가 enum 의 dict 항목을 .value 만 추출. s06 reranker 는 bool(str(False))=True 함정 회피로 toggle 파라미터 정상 OFF.
Author-email: 김진수 <wlstn010203@plateer.com>
License: MIT
Project-URL: Repository, https://github.com/jinsoo96/xgen-harness-executor
Keywords: agent,pipeline,harness,llm,xgen
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Provides-Extra: api
Requires-Dist: fastapi>=0.110; extra == "api"
Requires-Dist: uvicorn>=0.29; extra == "api"
Provides-Extra: mcp
Requires-Dist: mcp>=0.9; extra == "mcp"

<div align="center">

# xgen-harness

LLM 에이전트 실행기입니다. 워크플로우를 코드로 짜지 않고 `HarnessConfig` 한 객체로 선언하면, 13개 Stage 가 정해진 순서로 실행합니다.

[![PyPI](https://img.shields.io/pypi/v/xgen-harness?color=blue&label=PyPI)](https://pypi.org/project/xgen-harness/)
[![Python](https://img.shields.io/pypi/pyversions/xgen-harness)](https://pypi.org/project/xgen-harness/)
[![License](https://img.shields.io/pypi/l/xgen-harness)](https://pypi.org/project/xgen-harness/)

```bash
pip install xgen-harness          # 코어
pip install 'xgen-harness[mcp]'   # MCP stdio 서버 / 마켓 연동
pip install 'xgen-harness[api]'   # FastAPI 라우터 (이식측에서만 필요)
```

</div>

---

## 처음 읽는 분께 — 용어 안내

본문에서 자주 나오는 여섯 단어입니다. 이 정도만 알고 계시면 나머지는 자연스럽게 따라옵니다.

| 용어 | 뜻 | 본문에서 |
|---|---|---|
| **Stage** | 요청 처리의 한 구간 (입력 정규화 / 도구 카탈로그 / LLM 호출 / 응답 평가 ···). 13개가 정해진 순서로 실행 | s00 ~ s11 |
| **Strategy** | 한 Stage 안에서 골라 끼우는 구현체. 예: 응답 평가 = `llm_judge` 또는 `rule_based` | `active_strategies` |
| **ToolSource** | LLM 이 부를 도구를 한 곳에서 모으는 통로. MCP 서버·캔버스 노드·파이썬 함수 모두 같은 인터페이스로 들어옴 | `register_tool_source()` |
| **Capability** | "RAG 검색", "웹 검색" 같이 **무슨 능력이 필요하다** 만 선언하면 도구가 자동 매칭 | `capabilities=[...]` |
| **Orchestrator** | 13 Stage 사이클을 어떻게 반복할지의 패턴 (`linear` / `iterative` / `dag` ···) | `orchestrator_hint` |
| **Guard** | "이 도구 부르기 전엔 반드시 검색을 먼저 해라" 같은 규칙을 코드 수정 없이 데이터로 선언 | Policy Gate |

---

## 무엇을 풀어주나

| 항목 | 풀이 방법 |
|---|---|
| 여러 LLM 제공자 | provider 레지스트리 + entry_points. anthropic / openai / google / bedrock / vllm 빌트인 |
| 여러 종류 도구 | `ToolSource` 인터페이스 한 갈래로 통합 — MCP 세션 / 캔버스 노드 / 로컬 함수 / 합성 도구 |
| 여러 실행 패턴 | Orchestrator 레지스트리 — linear / iterative / react / plan_execute / dag |
| 워크플로우 배포 | `compile_workflow()` 가 `pip install` 가능한 wheel 로 만들어 MCP stdio 서버로 기동 |
| 도구 호출 정책·예산 | Policy Gate — 선언형 Guard 체인 × 4 훅 포인트 |

---

## 어떻게 돌아가는가

요청이 들어오면 13개 Stage 가 **초기화 → 에이전트 루프 → 종료** 세 그룹으로 나뉘어 순차 실행됩니다. 각 Stage 는 자기 담당 영역(도구·정책·컨텍스트·예산) 만 LLM 에게 보여주고 다음 Stage 로 `PipelineState` 객체를 넘깁니다.

```
[ ingress · 1회 ]                       [ agent loop · max_iterations 회 ]              [ egress · 1회 ]

  s00 ─ s01 ─ s02 ─ s03 ─ s04   ─▶   s05 ─ s06 ─ s07 ─ s08 ─ s09  ─┐   ─▶   s10 ─ s11
  ──────────────────────────────         ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
  LLM 핸들 owner / 입력 / 히스토리 /        계획 / 컨텍스트 / 본문호출 /     │       DB 기록 / 최종 응답
  프롬프트·citation / 도구 카탈로그         judge / 루프 결정                │
                                       ◀─── orchestrator hint 분기 ─────┘
                                            (linear · iterative · plan_execute · react · dag)
```

### 알아두면 좋은 4 가지

1. **Stage 는 "단계"가 아니라 "담당 영역"입니다.** 각 Stage 가 자기 도구·자원·정책을 LLM 에게 점진적으로 보여주고, Auto 모드에서는 Planner LLM 이 그 안에서 골라 씁니다.
2. **도구는 한 통로로 들어옵니다.** MCP 세션·캔버스 노드·파이썬 함수·합성 도구가 전부 `ToolSource` 인터페이스를 거쳐 s04 에서 LLM 카탈로그로 합쳐집니다.
3. **본문 LLM 호출의 책임은 s00 하나에 있습니다.** provider·model·max_tokens·전송 방식(streaming/batch) 결정이 `s00_harness` 한 곳에 모이고, s07_act 같은 다른 Stage 는 s00 의 dispatcher 를 거쳐 호출합니다.
4. **워크플로우 자체를 도구로 발행할 수 있습니다.** `compile_workflow()` 호출 한 번에 `pip install` 가능한 wheel · MCP stdio 서버 · 격리 검증 페이로드(NOMGraph) 가 함께 만들어집니다.

### 한 사이클의 데이터 흐름

```
HarnessConfig + user_input
    │
    ▼
PipelineState  ◀──────────  EventEmitter (17 이벤트, SSE 스트림)
    │
    │   ── ingress ──
    ├─ s00  LLM 핸들 owner / transport(streaming|batch) 선정
    ├─ s01  사용자 입력 정규화 · multimodal 추출
    ├─ s02  같은 interaction 의 이전 turn 로드
    ├─ s03  system_prompt 주입 + citation
    ├─ s04  ToolSource → tool_definitions 합성 (UI 가 selected_tools 로 필터)
    │
    │   ── agent loop (orchestrator_hint 가 결정한 횟수만큼) ──
    ├─ s05_strategy   CoT / ReAct / Capability 계획
    ├─ s05_policy     (옵션) Guard 체인 — pre_main / pre_tool / post_response / loop_boundary
    ├─ s06            RAG · 온톨로지 · DB → 컨텍스트 주입 + 압축 (microcompact / cascade / …)
    ├─ s07            본문 LLM 호출 + tool_use multi-turn   (s00 dispatcher 경유)
    ├─ s08            응답 품질 judge (llm_judge | rule_based | none)
    ├─ s09            judge 결과 → loop_decision (continue / complete)
    │
    │   ── egress ──
    ├─ s10  DB 실행 기록 (이식측 hook)
    └─ s11  최종 응답 + MetricsEvent  →  state.final_output
```

`HarnessConfig.harness_mode` 가 이 사이클의 자율도를 결정합니다 (`off` / `selected` / `autonomous`). 자세한 내용은 다음 섹션입니다.

---

## 3 모드 — 자유도 vs 안정성

| 모드 | `harness_mode` | 동작 | 언제 쓰나 |
|---|---|---|---|
| **Off** (기본) | `"off"` | 13 Stage 정해진 순서, Plan 안 만듦, 본문 LLM 1회 | 빠른 단발 Q&A |
| **Selected** | `"selected"` + `active_strategies={...}` | 사용자 핀 한 부분만 hard-pin, 나머지 Planner 자율 | 일부만 강제 |
| **Auto** | `"autonomous"` | Planner LLM 이 Stage / Strategy / 도구 / orchestrator_hint 자율 결정 | 복잡 요청 · RAG · 멀티턴 도구 |

```python
# Auto — LLM 이 입력 보고 자율 조립 (RAG 결정, 도구 선택, orchestrator 결정)
config = HarnessConfig(harness_mode="autonomous", max_iterations=5)
assert config.is_autonomous() is True   # v0.25.3 helper

# Selected — 일부만 핀
config = HarnessConfig(
    harness_mode="selected",
    active_strategies={
        "s06_context": "microcompact",   # RAG 압축 전략 핀
        "s08_judge":   "rule_based",
    },
)
```

---

## 13 Stage 표

각 Stage 는 자기 담당 영역(capability / 도구 / 리소스)만 LLM 에게 점진적으로 보여주고, Auto 모드에서는 Planner LLM 이 그 중에서 골라 씁니다.

### 초기화 그룹 (ingress · 1 회)

| # | Stage | 책임 | 주요 `stage_params` | Strategy |
|---|---|---|---|---|
| 0 | **s00_harness** | LLM 핸들 owner + 본문호출 dispatcher (모드별 책임) | `strategy`(transport), `max_tokens`, `thinking_enabled`, `thinking_budget` | `streaming`* / `batch` |
| 1 | **s01_input** ✱ | 사용자 입력 정규화, multimodal 추출 | (없음 — top-level config.user_input) | `default`* / `with_classification` |
| 2 | **s02_history** | 같은 interaction 이전 turn 로드 | `max_history` | `default`* / `embedding_search` |
| 3 | **s03_prompt** | system_prompt 주입 + citation | `system_prompt`, `prompt_id`, `include_rules`, `citation_mode`, `citation_auto_doc_tokens`, `citation_auto_prod_tokens` | `section_priority`* |
| 4 | **s04_tool** | LLM 노출 도구 카탈로그 — **ToolSource 단일 채널** | `selected_tools`, `tool_source_filters` (UI 가 주입) | `progressive_3level`* / `eager_load` / `none` |

### 에이전트 루프 그룹 (loop · `max_iterations` 회)

| # | Stage | 책임 | 주요 `stage_params` | Strategy |
|---|---|---|---|---|
| 5 | **s05_strategy** | 계획 모드 결정 (cot/react/capability) | `planning_mode`, `intent_rules` | `cot_planner`* / `react` / `capability` / `none` |
| 5 | **s05_policy** ◆ | Guard 체인 × 4 훅 포인트 (옵트인) | `guards: [{name, params}]` | — (Guard 합성) |
| 6 | **s06_context** | RAG / 온톨로지 / DB → 컨텍스트 주입 + 압축 | `rag_collections`, `folders`, `files`, `db_connections`, `ontology_collections`, `score_threshold`, `reranker`, `metadata_filter`, `rag_pd_mode`, `rag_ingestion_mode`, `strategy`(compactor), cascade/L3/L4/L5 임계 | `token_budget`* / `sliding_window` / `microcompact` / `context_collapse_overlay` / `autocompact_llm` / `cascade` |
| 7 | **s07_act** | 본문 LLM 호출 + tool_use multi-turn | `timeout`, `result_budget`, `tool_result_preview_threshold`, `tool_result_preview_size` | `default`* / `parallel_read` |
| 8 | **s08_judge** | 응답 품질 평가 (0~1) | `threshold`, `criteria` | `llm_judge`* / `rule_based` / `none` |
| 9 | **s09_decide** ✱ | judge 결과 → loop_decision | `max_retries` (max_iterations 은 top-level config) | `threshold`* / `always_pass` |

### 종료 그룹 (egress · 1 회)

| # | Stage | 책임 | 주요 `stage_params` | Strategy |
|---|---|---|---|---|
| 10 | **s10_save** | DB 실행 기록 (이식측 hook) | `save_enabled` | `default`* / `noop` |
| 11 | **s11_finalize** ✱ | 최종 응답 + MetricsEvent | `output_format` (text/markdown/json) | `default`* / `format_json` |

`✱` = `REQUIRED_STAGES` (비활성화 불가) · `◆` = 옵트인 (registry-only, role 로 호출됨) · `*` = 기본 strategy

> **최근 변경 — 한눈에**:
>
> - **v0.26.x 패치 사이클** (production 라이브 검증 → 발견 → 즉시 fix): `s06_context.files` 부활 (v0.26.1, frontend UI 와 wiring 일치) · OpenAI strict schema 호환 (v0.26.2) · `s10_save` 컬럼명 정합 (v0.26.3) · batch transport 응답 누락 fix (v0.26.4) · Anthropic thinking max_tokens 자동 보정 (v0.26.5) · DAG orchestrator init TypeError fix (v0.26.6) · `max_iter=1` + 도구 활성 빈응답 보강 (v0.26.7) · DAG sub-Pipeline `DoneEvent` forward 누수 fix (v0.26.10, 후속 노드 이벤트 누락 차단).
> - **v0.26.0 — Dead UI 정리**: 사용자 클릭이 LLM 환경에 안 박히던 stage_param 정리. `s01_input.provider` (글로벌 ConfigPanel 와 중복) · `s02_history.memory_source` (코드 미read) · `s09_decide.max_iterations` (top-level config 만 작동) 3건 제거. Label-only 라 동일 동작이던 `s03_prompt.simple` strategy 제거. `s04_tool.none` / `s10_save.noop` 는 분기 코드 신규 구현해 진짜 short-circuit. EventEmitter queue 1000→8000 + drop 카운터.
>   - ⚠ `s06_context.files` 도 v0.26.0 에선 같이 제거됐으나, frontend UI 가 잔존해 클릭 무효화되는 문제로 **v0.26.1 에서 부활** (`metadata_filter.file_name` 자동 라우팅).
> - **v0.25.0 — 도구 채널 단일화**: `s04_tool` 의 `mcp_sessions` / `custom_tools` / `node_tags` / `cli_skills` 4 개 stage_param 사라짐. 모든 도구는 이제 **ToolSource 한 채널** 로 (다음 섹션).

---

## 도구 통로 — `ToolSource` 인터페이스

하네스가 LLM 에게 노출하는 모든 도구는 이 한 인터페이스를 거쳐 들어옵니다. MCP 서버든, 캔버스 노드든, 파이썬 함수든 똑같이 취급됩니다.

```python
from xgen_harness import ToolSource, register_tool_source

class MySource:
    source_id = "my-tools"
    display_name = "My Tools"
    display_name_ko = "내 도구"
    description = "외부 API 도구 모음"
    icon = "🛠"
    category = "api"
    # 프론트가 이 Box 안에 sub-UI 자동 렌더 — 검색·필터 UI 데이터로 전달.
    filter_schema = {
        "tags": {"type": "multi_select", "options_source": "my-tags"},
    }

    async def list_tools(self, filters=None) -> list[dict]:
        # 각 dict 표준 스키마: {name, description, input_schema?, annotations?, tags?}
        return [{
            "name": "echo",
            "description": "echo back the input",
            "input_schema": {
                "type": "object",
                "properties": {"text": {"type": "string"}},
                "required": ["text"],
            },
        }]

    async def call_tool(self, name: str, args: dict) -> dict:
        # 반환: {"content": str, "is_error": bool?}
        return {"content": args["text"]}

    def has_tool(self, name: str) -> bool:
        return name == "echo"

register_tool_source(MySource())
```

**외부 패키지에서 등록하기** — 외부 패키지의 `pyproject.toml` 에 다음 한 줄을 추가하면 `pip install` 후 자동으로 잡힙니다 (엔진 / 이식측 / 프론트 코드를 건드리지 않습니다):

```toml
[project.entry-points."xgen_harness.tool_sources"]
my_source = "my_pkg:MySource"
```

설치 후 엔진을 재시작하면 s04 UI 에 "My Tools" 박스가 나타나고, LLM 이 호출할 수 있게 됩니다.

**선택 / 필터** — 사용자 설정 단계에서 도구 가시성을 좁힐 수 있습니다:

```python
config = HarnessConfig(
    stage_params={
        "s04_tool": {
            # source_id → 노출할 도구 이름 리스트 ([] 면 비활성, 키 없으면 소스 전체)
            "selected_tools": {"my-tools": ["echo"], "mcp-sessions": ["search"]},
            # source_id → list_tools 에 전달할 filter
            "tool_source_filters": {"my-tools": {"tags": ["safe"]}},
        }
    }
)
```

---

## MCP — 받기 + 내보내기

하네스는 MCP 서버의 도구를 받아쓰는 쪽도 되고, 자기 워크플로우를 MCP stdio 서버로 내보내는 쪽도 됩니다. 한 번 내보낸 워크플로우는 다시 (같은 또는 다른 하네스의) `s04` 카탈로그로 받아와 도구처럼 호출할 수 있습니다.

```
   ┌────────────────────────────────────────────────────────────────────────────┐
   │                                                                            │
   │    하네스 UI · HarnessConfig                                               │
   │           │                                                                │
   │           │  ① compile_workflow()  — 워크플로우를 도구로 패키징           │
   │           ▼                                                                │
   │    WheelBuildResult                                                        │
   │     · xgen_gallery_<name>-<ver>-py3-none-any.whl                           │
   │     · CLI 3종 (run / info / serve-mcp)                                     │
   │     · NOMGraph 단일 IR                                                     │
   │           │                                                                │
   │           │  ② MCPStdioVerifier — 격리 핸드셰이크 + SHA-256 지문            │
   │           ▼                                                                │
   │    POST /api/harness/compile/publish    ─────────►   PublishTargetRegistry│
   │                                                       ├─ mcp-station       │
   │                                                       ├─ xgen-gallery      │
   │                                                       ├─ 사내 PyPI / 폐쇄망  │
   │                                                       └─ Claude Desktop    │
   │                                                              │             │
   │                                                              ▼             │
   │                                                       ③ 발행처에서        │
   │                                                       wheel install +      │
   │                                                       MCP stdio 기동       │
   │                                                              │             │
   │           ┌─────────── ④ 다시 받아오기 (re-ingest) ──────────┘             │
   │           │                                                                │
   │           ▼                                                                │
   │    ToolSource 로 흡수                                                      │
   │     ├─ MCPSessionToolSource(station_url)   — mcp-station 세션              │
   │     ├─ discover_galleries()                — entry_points 자동 스캔        │
   │     └─ Claude Desktop / Cursor             — 외부 호스트가 직접 호출       │
   │           │                                                                │
   │           ▼                                                                │
   │    ⑤ s04_tool 카탈로그 합류 → Auto 모드 LLM 이 호출                        │
   │                                                                            │
   └────────────────────────────────────────────────────────────────────────────┘
```

| 단계 | 무엇이 | 어디서 | 핵심 산출 |
|---|---|---|---|
| ① 패키징 (wrap) | `HarnessConfig` + 캔버스 스냅샷 → wheel | 엔진 `compile_workflow()` | `WheelBuildResult` (wheel/sdist/dist_name/package_name + NOMGraph) |
| ② 검증 (verify) | wheel 격리 기동 → `initialize`/`tools/list` 왕복 | 엔진 `MCPStdioVerifier` | `VerifyResult` (`payload_hash` 발행 감사용 지문) |
| ③ 올리기 (publish) | wheel + 메타 → 발행처 등록 | 이식측 `POST /api/harness/compile/publish` | PublishTargetRegistry 항목 (mcp-station / gallery / Claude Desktop / 사내 PyPI) |
| ④ 받아오기 (ingest) | 발행된 도구를 다시 카탈로그로 흡수 | `MCPSessionToolSource` / `discover_galleries()` | `tool_definitions` 추가 |
| ⑤ 호출 (call) | LLM 이 도구로 사용 | `s04_tool` → `s07_act` (s00 dispatcher 경유) | `ToolCallEvent` / `ToolResultEvent` |

각 단계의 상세입니다:

### A. 다른 MCP 서버를 하네스 안에서 쓰기

이미 떠 있는 MCP 서버 (Claude Desktop, mcp-station, npx 로 띄운 서버 등) 의 도구를 카탈로그에 그대로 합치는 경우입니다. 이식측(xgen-workflow) 에서 `MCPSessionToolSource` 를 등록하면 하네스는 일반 `ToolSource` 와 동일하게 도구를 받아 옵니다.

```python
# 이식측 (예: xgen-workflow) 에서 한 번만 호출
from xgen_harness import register_tool_source
from harness_bridge.tool_sources.mcp_sessions import MCPSessionToolSource

register_tool_source(MCPSessionToolSource(station_url="http://mcp-station:8030"))
```

```python
# 하네스 사용자 코드
config = HarnessConfig(
    harness_mode="autonomous",
    stage_params={
        "s04_tool": {
            "selected_tools": {"mcp-sessions": ["playwright_navigate", "playwright_screenshot"]},
        }
    },
)
```

### B. 하네스 워크플로우를 MCP stdio 서버로 내보내기

워크플로우 하나를 `pip install` 가능한 wheel 로 컴파일하면, 그 wheel 자체가 MCP stdio 서버로 동작합니다. Claude Desktop, Cursor, 다른 하네스 어디서든 도구로 호출할 수 있습니다.

```python
from xgen_harness import HarnessConfig, compile_workflow

result = compile_workflow(
    harness_config=HarnessConfig(
        harness_mode="autonomous",
        system_prompt="너는 KRRA 규정 검색 전문가야.",
        capabilities=["retrieval.rag_query"],
        stage_params={"s06_context": {"rag_collections": ["krra_2024"]}},
    ),
    workflow_data={"workflow_type": "harness", "nodes": [], "edges": []},  # 캔버스 스냅샷 (옵션)
    gallery_name="krra_search",
    gallery_version="0.1.0",
    out_dir="./dist",
)

print(result.wheel_path)     # ./dist/xgen_gallery_krra_search-0.1.0-py3-none-any.whl
print(result.dist_name)      # xgen-gallery-krra_search
print(result.package_name)   # xgen_gallery_krra_search
```

설치 후 CLI 3 종이 자동 주입됩니다:

```bash
pip install ./dist/xgen_gallery_krra_search-0.1.0-py3-none-any.whl
xgen-gallery-krra_search run --input "마사회 운영 규정 알려줘"     # 일회성 호출
xgen-gallery-krra_search info                                        # 갤러리 메타
xgen-gallery-krra_search serve-mcp                                   # MCP stdio 서버로 기동
```

**Claude Desktop 에 등록** (`~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "krra-search": {
      "command": "xgen-gallery-krra_search",
      "args": ["serve-mcp"]
    }
  }
}
```

이렇게 등록하면 Claude Desktop 이 이 하네스 워크플로우를 `run_workflow(input, overrides)` 도구로 호출합니다.

### C. 발행 전 격리 검증 (Sandbox Verifier)

서버에 등록하기 전에 `initialize + tools/list` 왕복 / POSIX rlimit / SHA-256 재현성 해시로 게이팅합니다.

```python
from xgen_harness import MCPStdioVerifier, SandboxLimits

v = MCPStdioVerifier()
result = v.verify(
    command=["xgen-gallery-krra_search", "serve-mcp"],
    timeout_sec=10.0,
    limits=SandboxLimits(
        cpu_seconds=15,
        address_space_mb=1024,
        max_open_files=128,
        max_file_size_mb=16,
        no_core_dump=True,
    ),
)

assert result.ok, result.error
print(result.tool_count, result.payload_hash[:12], result.handshake_ms)
```

`VerifyResult` 에는 `ok / tool_count / tools / handshake_ms / tools_ms / payload_hash / stderr_tail / applied_limits / timed_out / error` 가 모두 담깁니다. 같은 wheel 은 항상 같은 `payload_hash` 를 내므로, 발행 감사용 지문으로 사용할 수 있습니다.

---

## Compile — wheel 한 장에 들어가는 내용

`compile_workflow()` 는 여러 단계를 거쳐 산출물을 만듭니다. 각 단계는 독립적으로도 호출할 수 있습니다.

```
HarnessConfig + workflow_data
        │
        ▼
 ① WorkflowSnapshot.from_config()       — 직렬화 가능한 스냅샷 (SNAPSHOT_VERSION 박제)
        │
        ▼
 ② scan_placeholders(snapshot)          — ${VAR} 발견 → ExternalInputSpec 자동 등록
        │
        ▼
 ③ resolve_dependencies(snapshot)       — DependencyRule 레지스트리 → pip 의존성 산출
        │
        ▼
 ④ build_wheel(snapshot)                — _write_source_tree → python -m build
        │
        ▼
 WheelBuildResult { wheel_path, sdist_path, source_dir, dist_name, package_name, snapshot }
```

### 외부 입력 (`external_inputs`) — wheel 이 런타임에 요구하는 값

```python
from xgen_harness import scan_placeholders, ExternalInputSpec, InputType

config.external_inputs = {
    "QDRANT_URL": ExternalInputSpec(
        name="QDRANT_URL", type=InputType.URL, required=True,
        description="RAG 컬렉션이 살아있는 Qdrant 인스턴스",
    ),
    "OPENAI_API_KEY": ExternalInputSpec(
        name="OPENAI_API_KEY", type=InputType.SECRET, required=True,
    ),
}
```

UI 는 이 선언을 보고 배포 전에 입력 폼을 자동으로 렌더링합니다. `${QDRANT_URL}` 같은 placeholder 가 `system_prompt` / `capability_params` / `stage_params` 안에 들어 있으면 컴파일러가 `ExternalInputSpec` 후보를 자동 등록합니다.

### 의존성 해석 — `DependencyRule` 레지스트리

```python
from xgen_harness import register_dependency_rule, DependencyRule

register_dependency_rule(DependencyRule(
    name="my-tool-deps",
    matcher=lambda snap: any(
        sid == "my-tools" for sid in snap.harness_config.stage_params
            .get("s04_tool", {}).get("selected_tools", {}).keys()
    ),
    requirements=["my-tool-package>=1.2"],
))
```

Snapshot 내용에 따라 wheel 의 `install_requires` 가 자동 산출됩니다. 외부 도구 / capability / provider 가 자기 의존성을 선언할 때 이 등록 한 번으로 충분합니다.

### 갤러리 자동 발견 — `discover_galleries()`

```python
from xgen_harness import discover_galleries, get_gallery

for g in discover_galleries():       # entry_points "xgen_harness.galleries" 자동 스캔
    print(g.dist_name, g.version, g.entry_module)

g = get_gallery("xgen-gallery-krra_search")
print(g.snapshot.harness_config["system_prompt"])
```

### NOM IR — wheel + MCP 카탈로그 + 격리 페이로드 단일 그래프

Stage / Strategy / Tool / MCP 서버 / 외부 플러그인 노드를 단일 IR (`NOMGraph`) 로 표현하고, 세 가지 변환으로 wheel · MCP · Sandbox 출력을 모두 다룹니다:

```python
from xgen_harness import NOMGraph, NOMNode, NOMKind, NOMParam, compile_nom_graph

graph = NOMGraph(nodes=[
    NOMNode(
        id="x.tools.search", kind=NOMKind.TOOL,
        description="웹 검색", entry="my_pkg.tools:search",
        inputs=[NOMParam(name="q", type="string", required=True)],
    ),
])

graph.to_mcp_schema()                      # → MCP tools/list 응답 그대로
graph.to_sandbox_payload("x.tools.search", {"q": "hello"})   # → 격리 실행 payload
compile_nom_graph(graph, gallery_name="my_tools", gallery_version="0.1.0")  # → wheel
```

런타임에 LLM 이 합성한 도구도 같은 흐름으로 wheel 화할 수 있습니다:

```python
from xgen_harness.tools.synthesis import synthesize_and_register, synthesized_tools_as_nom_graph

# synthesize_and_register(...) 로 검증·등록한 뒤
graph = synthesized_tools_as_nom_graph([slugify, camelcase, redact_pii])
result = compile_nom_graph(graph, gallery_name="my_synth_tools", gallery_version="0.1.0")
```

엔진의 현재 상태도 그대로 NOM 으로 덤프할 수 있습니다. 디버깅 / 갤러리 업로드 / 샌드박스 복원에 재사용됩니다:

```python
from xgen_harness import snapshot_current_registry_as_nom

nom = snapshot_current_registry_as_nom()
print([n.id for n in nom.nodes])
# ["xgen.stages.s00_harness", ..., "xgen.strategies.s06_context.compact.microcompact",
#  "xgen.orchestrators.iterative", "xgen.providers.anthropic", ...]
```

---

## Strategy — 디폴트 그대로 두고 변형만

```python
config = HarnessConfig(
    strategy_variants={
        "s06_context": [{
            "name":   "microcompact_strict",   # active_strategies 에서 참조할 새 이름
            "base":   "microcompact",          # 복제 원본
            "params": {"threshold": 95},       # 오버라이드
            "label":  "엄격 압축",
        }],
    },
    active_strategies={"s06_context": "microcompact_strict"},
)
```

엔진 코드 변경 없이 동작합니다. variant 레지스트리에서 base 구현 클래스를 찾아 새 인스턴스를 만들고 `configure(params)` 로 오버라이드를 주입합니다.

---

## Orchestrator — Auto 모드 5 패턴 + 외부 추가

Auto 모드에서는 Planner 가 입력을 보고 `Plan.orchestrator_hint` 를 결정하고, 그 값에 따라 루프가 분기합니다.

| hint | 동작 | 사용 케이스 |
|---|---|---|
| `linear` | 1 회 실행 후 종료 | 단발 Q&A |
| `iterative` (default) | 매 iter Plan replan + 13 Stage 1바퀴 | 멀티턴 도구 |
| `plan_execute` | 첫 Plan 고수, replan 생략, 반복 | 정형 절차 |
| `react` | 엔진 no-op, 이식측 dispatcher 위임 | 외부 ReAct 통합 |
| `dag` | 엔진 no-op, 이식측 DAG runner 위임 | 멀티 에이전트 병렬 |

**외부 패턴 추가**:

```python
from xgen_harness.core.orchestrator_registry import register_orchestrator
register_orchestrator(
    "swarm_v2",
    description="병렬 swarm + 보팅 결합",
    dispatch_key="swarm_v2",
    replan_per_iter=True,
)
```

또는 `entry_points` 그룹 `xgen_harness.orchestrators` 에 노출시키면 자동으로 합류됩니다. `OrchestratorSpec.replan_per_iter` / `max_iterations_override` 가 행동을 선언하므로 pipeline 은 이름 분기 없이 spec 속성만 읽습니다.

---

## Policy Gate — 선언형 Guard × 4 훅

"submit_result 호출 전 iterative_document_search 를 최소 1회 불러야 한다" 같은 도구 호출 선행조건 / 입출력 정책 / 예산 제한을 코드 수정 없이 데이터로 선언할 수 있습니다.

```python
config = HarnessConfig(
    stage_params={
        "s05_policy": {
            "guards": [
                {"name": "iteration"},                                          # max_iterations 도달 → 종료
                {"name": "cost_budget", "params": {"cost_budget_usd": 5.0}},    # 누적 비용 초과 → 종료
                {"name": "tool_precondition", "params": {                        # 선행조건 강제
                    "rules": [{
                        "tool": "submit_result",
                        "require_prior": [{"tool": "iterative_document_search", "min_count": 1}],
                        "message": "합격 판정 전 QA 기준을 검색하세요.",
                    }]
                }},
                {"name": "hitl"},   # destructiveHint=true 도구는 사용자 승인 후 실행
            ]
        }
    }
)
```

| 훅 | 시점 | 차단 시 동작 |
|---|---|---|
| `pre_main` | 본문 LLM 호출 직전 | `state.policy_block_reason` 설정 + `loop_decision="complete"` |
| `pre_tool` | 도구 실행 직전 (각 pending tool_call) | 해당 도구 제거 + 가짜 `tool_result(is_error=True)` 주입 → LLM 자체 교정 |
| `post_response` | LLM 응답 직후 | `policy_block_reason` 설정 + 종료 |
| `loop_boundary` | 루프 경계 | `policy_block_reason` 설정 + 종료 |

**내장 6 Guard:** `iteration` · `token_budget` · `cost_budget` · `content` · `tool_precondition` · `hitl` (HITL 승인 모달 — `ApprovalRequiredEvent` / `ApprovalDecidedEvent` 송수신).

**외부 Guard 추가:**

```toml
[project.entry-points."xgen_harness.guards"]
my_guard = "my_pkg.guards:MyGuard"
```

`pip install` 후에 UI Guard 드롭다운에서 자동으로 노출됩니다.

---

## Capability — 선언적 도구 wiring

```python
config = HarnessConfig(
    capabilities=["retrieval.web_search", "retrieval.rag_query"],
    capability_params={"retrieval.web_search": {"max_results": 10}},
)
```

`s04_tool` 이 `CapabilityRegistry` 에서 매칭한 도구를 `tool_definitions` 에 합칩니다. 외부 패키지가 `entry_points` 로 `CapabilitySpec` 을 등록하면 자동으로 발견됩니다.

---

## RAG 사용

```python
config = HarnessConfig(
    stage_params={
        "s06_context": {
            "rag_collections":  ["masahoe_v1", "voc_templates"],   # Qdrant 컬렉션 ID
            "rag_top_k":        5,
            "rag_ingestion_mode": "both",     # system_prompt + tool_only 양쪽 주입
        }
    }
)
```

`s06_context` Strategy 별 동작:
- `microcompact` (기본) — 5-Level Claude Code 압축
- `cascade` — L3 → L4 → L5 자동 압력 단계화
- `progressive_3level` — Stage 환경 슬롯 progressive disclosure
- `none` — 패스스루

---

## 외부 패키지가 끼워넣는 16 지점

엔진 코드를 건드리지 않고 외부 패키지의 `pyproject.toml` 에 entry_points 항목을 추가하는 방식으로 합류시킬 수 있습니다.

| # | 지점 | entry_points 그룹 | 등록 함수 |
|---|---|---|---|
| 1 | **Stage** | `xgen_harness.stages` | `register_stage()` |
| 2 | **Strategy** | `xgen_harness.strategies` | (registry 자동 검색) |
| 3 | **Provider** | `xgen_harness.providers` | `register_provider()` |
| 4 | **Orchestrator** | `xgen_harness.orchestrators` | `register_orchestrator()` |
| 5 | **ToolSource** | `xgen_harness.tool_sources` | `register_tool_source()` |
| 6 | **Capability** | `xgen_harness.capabilities` | `CapabilityRegistry.register()` |
| 7 | **Guard** | `xgen_harness.guards` | `register_guard()` |
| 8 | **NodeAdapter** | `xgen_harness.node_adapters` | (이식측 wiring) |
| 9 | **OptionSource** | `xgen_harness.option_sources` (이식측) | `register_option_source()` |
| 10 | **SandboxVerifier** | `xgen_harness.sandbox_verifiers` | `register_sandbox_verifier()` |
| 11 | **PublishTarget** | `xgen_harness.publish_targets` (이식측) | `register_publish_target()` |
| 12 | **Phase** | `xgen_harness.phases` | `register_phase()` |
| 13 | **NodePlugin** | `xgen_harness.node_plugins` | `register_node_plugin()` |
| 14 | **Gallery / Tools** | `xgen_harness.tools` | wheel install 시 자동 발견 |
| 15 | **FanOutStrategy** | `xgen_harness.fan_out_strategies` | `register_fan_out_strategy()` |
| 16 | **EvaluationCriterion** | `xgen_harness.evaluation_criteria` | `register_evaluation_criterion()` |

추가로 *모델 가격 등록* 도 같은 패턴: `xgen_harness.model_pricing` 그룹 + `register_model_pricing()` (사내 vLLM 모델 등 비용 추적용).

각 그룹별 빈 본 섹션이 [pyproject.toml](pyproject.toml) 에 미리 마련되어 있어, 외부 기여자가 어떤 그룹 이름이 유효한지 한눈에 확인할 수 있습니다.

---

## 이식 통합 (xgen-workflow) — 엔진과 호스트의 책임 분리

엔진은 범용 primitive 만 제공합니다. 실서비스(xgen-documents, xgen-mcp-station, postgres, SSE) 와의 결선은 호스트 측(`xgen-workflow/controller/workflow/endpoints/`) 이 소유합니다.

| 엔드포인트 | 역할 |
|---|---|
| `GET  /api/harness/stages` | 13 Stage 정의 + 설정 스키마 (icon, fields, behavior) |
| `GET  /api/harness/tool-sources` | 등록된 ToolSource 메타 + `list_tools()` 결과 |
| `GET  /api/harness/options/{source}` | 동적 옵션 (mcp-sessions / rag-collections / providers …) |
| `POST /api/harness/execute/stream` | SSE 실행 (하네스 전용 — 레거시 워크플로우와 분리) |
| `POST /api/harness/compile` | wheel 컴파일 + 바이너리 다운로드 |
| `POST /api/harness/compile/publish` | compile + PublishTarget 발행 (mcp-station / gallery) |
| `POST /api/harness/dag/execute/stream` | 멀티 하네스 DAG orchestration |

**호스트 노드 주입** — 엔진은 xgen 노드 스키마를 직접 알지 못하므로, 호스트가 Protocol 을 통해 주입합니다:

```python
from xgen_harness import register_xgen_node_resolver, XgenNodeResolver

class MyResolver:
    def list_nodes(self) -> list[dict]: ...
    def get_node_tool(self, node_id: str): ...

register_xgen_node_resolver(MyResolver())
```

엔진 자신은 라이브러리에서 제공하는 작은 FastAPI 라우터도 가지고 있어 단독으로 띄울 수 있습니다 (옵션):

```python
from fastapi import FastAPI
from xgen_harness.api.router import harness_router

app = FastAPI()
app.include_router(harness_router, prefix="/api/harness")
```

→ `GET /stages`, `GET /tool-sources`, `POST /execute`, `POST /orchestrate`, `WS /ws/{session_id}`.

---

## 공식 Public API

```python
from xgen_harness import (
    # Core
    Pipeline, PipelineState, TokenUsage, HarnessConfig,
    ALL_STAGES, REQUIRED_STAGES,
    Stage, StageDescription, StrategyInfo,
    # Builder & Session
    PipelineBuilder, HarnessSession, SessionManager,
    # Events (17종)
    EventEmitter, HarnessEvent, StageEnterEvent, StageExitEvent,
    MessageEvent, ToolCallEvent, ToolResultEvent, MetricsEvent,
    PlanningEvent, ApprovalRequiredEvent, ApprovalDecidedEvent,
    ErrorEvent, DoneEvent, MissingParamEvent, ServiceLookupEvent,
    CapabilityBindEvent, StageSubstepEvent, RetryEvent,
    # Errors
    HarnessError, ConfigError, ProviderError, ToolError,
    PipelineAbortError, RateLimitError, OverloadError,
    ContextOverflowError, ToolTimeoutError, MCPConnectionError,
    ValidationError, ErrorCategory,
    # Tools
    ToolSource, register_tool_source, get_tool_sources,
    ToolPackageSpec, GalleryTool, load_tool_package, discover_gallery_tools,
    # Policy Gate
    Guard, GuardResult, GuardChain, HookPoint, HookContext,
    available_guards, register_guard, describe_guards, build_guard_chain,
    # Orchestrator
    DAGOrchestrator, AgentNode, DAGEdge, DAGResult, DAGCycleError,
    MultiAgentExecutor,
    # Capability
    CapabilitySpec, CapabilityMatch, ParamSpec, ProviderKind,
    CapabilityRegistry, get_default_registry, set_default_registry,
    CapabilityMatcher, MatchStrategy,
    materialize_capabilities, merge_into_state, MaterializationReport,
    ParameterResolver, ResolveResult,
    # Compile
    compile, compile_workflow, build_wheel, WheelBuildResult,
    WorkflowSnapshot, SNAPSHOT_VERSION, load_snapshot,
    ExternalInputSpec, InputType, scan_placeholders, merge_scanned,
    collect_runtime_values, MissingExternalInputError,
    DependencyResolver, resolve_dependencies, register_dependency_rule, DependencyRule,
    serve_mcp, run_mcp_blocking, MCPNotInstalledError,
    InstalledGallery, discover_galleries, get_gallery,
    # Sandbox
    Sandbox, SandboxLimits, SandboxResult, run_sandboxed,
    SandboxVerifier, VerifyResult, MCPStdioVerifier,
    register_sandbox_verifier, get_sandbox_verifier, list_sandbox_verifiers,
    bootstrap_default_sandbox_verifiers, verify_mcp_stdio,
    # NOM IR
    NOMKind, NOMParam, NOMOutput, NOMNode, NOMGraph,
    snapshot_current_registry_as_nom, compile_nom_graph,
    # Host integration
    register_xgen_node_resolver, get_xgen_node_resolver, XgenNodeResolver,
    # Catalog & planning
    get_catalog, HarnessPlanner, HarnessPlan, ensure_provider,
    # Presets (legacy compat)
    PRESETS, Preset, get_preset, apply_preset, list_presets,
)
```

---

## 환경변수 치트시트

| 변수 | 의미 | 기본값 |
|---|---|---|
| `XGEN_HARNESS_DEFAULT_PROVIDER` | 기본 provider 강제 | (env → openai → anthropic → registry[0]) |
| `XGEN_HARNESS_<PROVIDER>_DEFAULT_MODEL` | 해당 provider 기본 모델 override | `PROVIDER_DEFAULT_MODEL` |
| `XGEN_HARNESS_DEFAULT_CONTEXT_LIMIT` | 등록 안 된 provider 의 컨텍스트 한도 (chars) | 500_000 |
| `XGEN_HARNESS_API_KEY_FILE_DIR` | API key 파일 폴백 디렉토리 | `/app/config` |
| `XGEN_HARNESS_PRELOAD_MANIFEST` | 시작 시 자동 로드할 LocalManifest 경로 (PATH-style) | (없음) |
| `HARNESS_SANDBOX_POLICY` | 발행 전 검증 정책 (`strict` / `advisory` / `off`) | `strict` (이식측) |
| `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY` | provider API 키 | (없음) |

---

## 버전 흐름

크게 네 단계로 정리됩니다.

```
Phase 1  13 Stage 구조      ─ Stage 디렉토리화 / s00_harness 통제탑       (v0.12 ~ v0.16)
Phase 2  발행·격리·정책      ─ MCP wheel / Sandbox / NOM / Policy Gate    (v0.17 ~ v0.21)
Phase 3  독립성 정리         ─ xgen 특화 호스트 이관 / ToolSource 통합     (v0.22 ~ v0.25)
Phase 4  라이브 검증 패치    ─ production 결함 일괄 fix                   (v0.26.x)
```

### Phase 1 — 13 Stage 구조 (v0.12 ~ v0.16)

캔버스 모델을 걷어내고 "13 Stage = 담당 영역" 구조로 전환한 시기입니다. 각 Stage 가 자기 capability·도구·리소스를 LLM 에게 점진적으로 노출하고, `s00_harness` 가 본문 LLM 호출의 단일 책임자가 되었습니다.

| 버전 | 핵심 |
|---|---|
| `v0.12.0` | 13 Stage 디렉토리화 + `s00_harness` Planner |
| `v0.13.0` | 단일 Provider + iterative planning |
| `v0.14.0` | `s00_harness` 본문 LLM 호출 owner + 3 모드 (`off`/`selected`/`autonomous`) |
| `v0.15.x` | `orchestrator_hint` + OrchestratorRegistry + fs_scanner 자동 발견 |
| `v0.16.x` | Sandbox / NOM / NodePlugin / ToolSynthesis + Pipeline Role |

### Phase 2 — 발행·격리·정책 (v0.17 ~ v0.21)

워크플로우를 wheel 로 컴파일해 MCP stdio 서버로 배포하는 흐름, 발행 전 격리 검증, 선언형 Guard 정책 시스템이 자리잡은 시기입니다. Stage / Tool / MCP / Plugin 을 단일 IR (`NOMGraph`) 로 통합했습니다.

| 버전 | 핵심 |
|---|---|
| `v0.17.0` | **Policy Gate** — 선언형 Guard × 4 훅 포인트 + `entry_points` 외부 Guard 합류 |
| `v0.18.0` | **MCP 양방향 연동** — 하네스 → wheel → MCP stdio 발행 / 마켓·Station → s04 카탈로그 흡수 |
| `v0.20.0` | **Sandbox Verifier** — `MCPStdioVerifier` + POSIX rlimit + SHA-256 재현성 해시 |
| `v0.21.0` | **NOM IR 허브** — `to_mcp_schema` / `to_sandbox_payload` / `to_wheel_snapshot` 세 변환을 단일 그래프로 |

### Phase 3 — 독립성 정리 (v0.22 ~ v0.25)

엔진이 xgen 서비스(workflow / mcp-station / documents) 와 직접 결선되어 있던 잔재를 호스트(이식측)로 옮긴 시기입니다. 도구 공급 채널도 네 갈래에서 `ToolSource` 단일 Protocol 로 정리했습니다.

| 버전 | 핵심 |
|---|---|
| `v0.22.0` | **엔진 독립성** — xgen 특화 코드 호스트 이관 + `ExternalNodeRef` Protocol + `REQUIRED_STAGES` 레지스트리 |
| `v0.23.0` | **MCP Tool Annotations** — `readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint` 를 Tool ABC 의 정식 속성으로 |
| `v0.24.0` | **HITL Guard + Agent-controlled Compact Tool** — `destructiveHint=true` 도구 사용자 승인 모달 |
| `v0.25.0` | **ToolSource 단일 공급 채널** — s04 의 `mcp_sessions` / `custom_tools` / `node_tags` / `cli_skills` 4 하드코딩 제거 + `/tool-sources` 엔드포인트 |
| `v0.25.3` | **HarnessConfig 헬퍼** — `is_autonomous()` / `is_selected()` / `is_off()` 도메인 캡슐화 (리터럴 `== "autonomous"` 비교 추적 불필요) |

### Phase 4 — 라이브 검증 패치 사이클 (v0.26.x)

프로덕션 운영에서 한 번 검증할 때마다 한두 개씩 드러나는 결함을 즉시 메우는 패치 사이클입니다. 보고서 → 라이브 재검증 → 패치 → 다음 검증에서 또 결함 발견 → 패치, 의 빠른 반복으로 진행됩니다.

| 버전 | 무엇이 깨져있었나 | 어떻게 고쳤나 |
|---|---|---|
| `v0.26.0` | UI 클릭이 LLM 환경에 안 박히는 4 stage_param + label-only strategy 1개 + EventQueue 백프레셔 부재 | Dead UI 4건 제거 (`s01.provider` / `s02.memory_source` / `s06.files` / `s09.max_iterations`), label-only 1건 제거 (`s03.simple`) + 분기 신규 2건 (`s04.none` / `s10.noop`), queue 1000→8000 + drop 카운터 |
| `v0.26.1` | v0.26.0 에서 dead 로 제거한 `s06_context.files` 가 frontend UI 엔 살아있어서 사용자 클릭이 무효화 | 엔진에 진짜 wiring 추가 (`metadata_filter.file_name` 자동 라우팅) → 필드 부활 |
| `v0.26.2` | OpenAI strict schema 가 `properties` 없는 도구를 거부 → HTTP 400 (SynthesizedToolSource 자동 등록 도구 영향) | `providers/openai.py:_convert_tools` 가 `type=object` + `properties` 누락 시 `{}` 자동 보강 |
| `v0.26.3` | `s10_save` 가 dict 컬럼 (`input_data` / `output_data`) 으로 보내지만 실 DB 는 text (`input_text` / `output_text`) — 매 실행 `inserted_id=None` 으로 graceful 종료, `/executions` 빈 채 | record 컬럼명을 실 schema 에 맞춰 직렬화 (5K / 50K 자 truncate) |
| `v0.26.4` | OpenAI batch transport (`stream=False`) 가 STOP 이벤트 `.text` 로 응답 한 번에 yield, 엔진 STOP 핸들러는 `output_tokens` 만 처리 → 응답 텍스트 사라짐 | `core/llm_call.py:_single_call` STOP 핸들러에 `event.text` 처리 + `MessageEvent` emit 추가 |
| `v0.26.5` | Anthropic `thinking` 켤 때 `thinking_budget > max_tokens` 이면 무조건 HTTP 400 — engine default 도 동일 함정 (`max_tokens=8192 < thinking_budget=10000`) | thinking 활성 시 자동 보정 `max_tokens = budget_tokens + 1024` (사용자 설정 무시 아니라 안전 보장) |
| `v0.26.6` | DAG orchestrator 가 `PipelineState(tool_definitions=...)` 로 init — v0.11.22 도메인 그룹화 후 `dag.py:255` 동기화 누락 → 모든 DAG 노드 100% TypeError | init kwarg 제거, instance 생성 후 `state.tool_definitions = ...` setter 로 박음 |
| `v0.26.7` | `max_iter=1` + 도구 활성 시 LLM 이 첫 iter 에서 도구만 호출, 답변 텍스트 만들 두 번째 iter 가 없어 `output_length=0` 빈 응답 (default `max_iter=10` 환경에선 안 드러남) | Phase B 후 빈 응답 + 도구 실행 ≥ 1 이면 `tool_definitions=[]` 로 1회 보강 `main_call` (직후 `tool_definitions` 원복 → 다음 iteration / 외부 코드 영향 0) |
| **`v0.26.10`** | DAG orchestrator 가 sub-Pipeline 의 `DoneEvent` 도 그대로 외부 emitter 로 forward → `EventEmitter.stream()` 의 자동 break (events/emitter.py:102) 가 첫 노드 끝에서 발화 → 두 번째 노드 이벤트가 외부 클라이언트에 도달 못 함 + "DAG 실행 타임아웃" 으로 끊김 | `_forward_events()` 가 sub Pipeline 의 `DoneEvent` 만 skip. DAG 전체 `DoneEvent` 는 `run()` 마지막에 별도 emit (line 229) 하므로 정상 종료 신호 유지. 다른 이벤트 (Stage / Metrics / ToolCall / Error / …) 는 그대로 forward |

이전 변경: [CHANGELOG.md](CHANGELOG.md).

---

## 사용자 매뉴얼 (UI 사용자용)

엔진을 직접 호출하지 않고 XGEN 하네스 페이지(`http://xgen.x2bee.com/harness`) 만 사용하시는 경우, [docs/confluence/harness-user-manual.md](https://github.com/jinsoo96/xgen-harness-executor/blob/main/docs/confluence/harness-user-manual.md) 를 참고하시기 바랍니다.

---

## 라이선스

MIT
