<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Streaming on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/ko/tags/streaming/</link><description>Recent content in Streaming on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Mon, 06 Apr 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/ko/tags/streaming/index.xml" rel="self" type="application/rss+xml"/><item><title>Claude Code 하네스 해부학 #1 — 진입점에서 응답까지, 요청 하나의 여정</title><link>https://ice-ice-bear.github.io/ko/posts/2026-04-06-harness-anatomy-1/</link><pubDate>Mon, 06 Apr 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/ko/posts/2026-04-06-harness-anatomy-1/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post Claude Code 하네스 해부학 #1 — 진입점에서 응답까지, 요청 하나의 여정" /&gt;&lt;h2 id="개요"&gt;개요
&lt;/h2&gt;&lt;p&gt;Claude Code의 소스 구조를 27세션에 걸쳐 체계적으로 해부하는 시리즈의 첫 번째 글이다. 이 포스트에서는 사용자가 터미널에 &amp;ldquo;hello&amp;quot;를 입력했을 때, 응답이 화면에 출력되기까지 거치는 &lt;strong&gt;11개 TypeScript 파일의 전체 콜스택&lt;/strong&gt;을 추적한다.&lt;/p&gt;
&lt;h2 id="분석-대상-11개-핵심-파일"&gt;분석 대상: 11개 핵심 파일
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;#&lt;/th&gt;
 &lt;th&gt;경로&lt;/th&gt;
 &lt;th&gt;줄 수&lt;/th&gt;
 &lt;th&gt;역할&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;1&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;entrypoints/cli.tsx&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;302&lt;/td&gt;
 &lt;td&gt;CLI 부트스트랩, 인자 파싱, 모드 라우팅&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;main.tsx&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;4,683&lt;/td&gt;
 &lt;td&gt;메인 REPL 컴포넌트, Commander 설정&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;3&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;commands.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;754&lt;/td&gt;
 &lt;td&gt;커맨드 레지스트리&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;4&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;context.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;189&lt;/td&gt;
 &lt;td&gt;시스템 프롬프트 조립, CLAUDE.md 주입&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;5&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;QueryEngine.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1,295&lt;/td&gt;
 &lt;td&gt;세션 관리, SDK 인터페이스&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;6&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;query.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1,729&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;핵심 턴 루프&lt;/strong&gt; — API + 도구 실행&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;7&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;services/api/client.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;389&lt;/td&gt;
 &lt;td&gt;HTTP 클라이언트, 4개 프로바이더 라우팅&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;8&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;services/api/claude.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;3,419&lt;/td&gt;
 &lt;td&gt;Messages API 래퍼, SSE 스트리밍, 재시도&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;9&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;services/tools/toolOrchestration.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;188&lt;/td&gt;
 &lt;td&gt;동시성 파티셔닝&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;10&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;services/tools/StreamingToolExecutor.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;530&lt;/td&gt;
 &lt;td&gt;스트리밍 중 도구 실행&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;11&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;services/tools/toolExecution.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1,745&lt;/td&gt;
 &lt;td&gt;도구 디스패치, 권한 검사&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;총 &lt;strong&gt;15,223줄&lt;/strong&gt;을 추적한다.&lt;/p&gt;
&lt;h2 id="1-진입과-부트스트랩-clitsx---maintsx"&gt;1. 진입과 부트스트랩: cli.tsx -&amp;gt; main.tsx
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;cli.tsx&lt;/code&gt;는 302줄에 불과하지만, 놀라울 정도로 많은 &lt;strong&gt;패스트-패스(fast-path)&lt;/strong&gt; 분기가 존재한다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cli.tsx:37 --version -&amp;gt; 즉시 출력, import 0개
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cli.tsx:53 --dump-system -&amp;gt; 최소 import
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cli.tsx:100 --daemon-worker -&amp;gt; 워커 전용 경로
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cli.tsx:112 remote-control -&amp;gt; 브릿지 모드
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cli.tsx:185 ps/logs/attach -&amp;gt; 백그라운드 세션
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cli.tsx:293 기본 경로 -&amp;gt; main.tsx 동적 import
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;설계 의도&lt;/strong&gt;: &lt;code&gt;--version&lt;/code&gt; 하나를 위해 &lt;code&gt;main.tsx&lt;/code&gt;의 4,683줄을 로딩하지 않겠다는 것이다. CLI 도구의 체감 응답성에 직접 영향을 미치는 최적화다.&lt;/p&gt;
&lt;p&gt;기본 경로에서는 동적 import로 &lt;code&gt;main.tsx&lt;/code&gt;를 로드한다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// cli.tsx:293-297
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt;: &lt;span class="kt"&gt;cliMain&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kr"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;../main.js&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cliMain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;main.tsx&lt;/code&gt;가 4,683줄인 이유는 다음을 모두 포함하기 때문이다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;사이드 이펙트 import&lt;/strong&gt; (1-209행): &lt;code&gt;profileCheckpoint&lt;/code&gt;, &lt;code&gt;startMdmRawRead&lt;/code&gt;, &lt;code&gt;startKeychainPrefetch&lt;/code&gt; — 모듈 평가 시점에 병렬 서브프로세스를 시작하여 약 65ms의 macOS keychain 읽기를 숨긴다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commander 설정&lt;/strong&gt; (585행~): CLI 인자 파싱, 10+개 모드별 분기&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React/Ink REPL 렌더링&lt;/strong&gt;: 터미널 UI 마운트&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;헤드리스 경로&lt;/strong&gt; (&lt;code&gt;-p&lt;/code&gt;/&lt;code&gt;--print&lt;/code&gt;): UI 없이 &lt;code&gt;QueryEngine&lt;/code&gt; 직접 사용&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="2-프롬프트-조립-contextts의-dual-memoize"&gt;2. 프롬프트 조립: context.ts의 dual-memoize
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;context.ts&lt;/code&gt;는 189줄의 작은 파일이지만 시스템 프롬프트의 동적 부분을 전담한다. 두 개의 메모이즈된 함수가 핵심이다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;getSystemContext()&lt;/code&gt;&lt;/strong&gt; (context.ts:116): git 상태(branch, status, 최근 커밋)를 수집&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;getUserContext()&lt;/code&gt;&lt;/strong&gt; (context.ts:155): CLAUDE.md 파일들을 탐색/파싱&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;왜 분리했는가?&lt;/strong&gt; Anthropic Messages API의 프롬프트 캐싱 전략과 직결된다. 시스템 프롬프트와 사용자 컨텍스트의 캐시 수명이 다르므로 &lt;code&gt;cache_control&lt;/code&gt;을 서로 다르게 적용해야 한다. &lt;code&gt;memoize&lt;/code&gt;로 감싸서 세션 내 한 번만 계산한다.&lt;/p&gt;
&lt;p&gt;context.ts:170-176에서 &lt;code&gt;setCachedClaudeMdContent()&lt;/code&gt;를 호출하는 것은 &lt;strong&gt;순환 의존성을 끊기 위한 장치&lt;/strong&gt;다 — yoloClassifier가 CLAUDE.md 내용을 필요로 하지만, 직접 import하면 permissions -&amp;gt; yoloClassifier -&amp;gt; claudemd -&amp;gt; permissions 순환이 발생한다.&lt;/p&gt;
&lt;h2 id="3-asyncgenerator-체인-아키텍처의-척추"&gt;3. AsyncGenerator 체인: 아키텍처의 척추
&lt;/h2&gt;&lt;p&gt;Claude Code의 전체 데이터 플로우는 &lt;code&gt;AsyncGenerator&lt;/code&gt; 체인으로 구성된다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;QueryEngine.submitMessage()* -&amp;gt; query()* -&amp;gt; queryLoop()* -&amp;gt; queryModelWithStreaming()*
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;모든 핵심 함수가 &lt;code&gt;async function*&lt;/code&gt;이다. 이는 단순한 구현 선택이 아니라 &lt;strong&gt;아키텍처적 결정&lt;/strong&gt;이다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Backpressure&lt;/strong&gt;: 소비자가 느리면 생산자도 대기&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;취소&lt;/strong&gt;: AbortController와 결합하여 즉각적인 취소 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;합성&lt;/strong&gt;: &lt;code&gt;yield*&lt;/code&gt;로 제너레이터 체인을 자연스럽게 연결&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;상태 관리&lt;/strong&gt;: 루프 내 로컬 변수가 턴 간 상태를 자연스럽게 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;QueryEngine.submitMessage()&lt;/code&gt; (QueryEngine.ts:209)의 시그니처를 보면:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;submitMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;ContentBlockParam&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;uuid?&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;isMeta?&lt;/span&gt;: &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AsyncGenerator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;SDKMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;SDK 모드에서 각 메시지는 &lt;strong&gt;yield로 스트리밍&lt;/strong&gt;되며, Node.js의 backpressure가 자연스럽게 구현된다.&lt;/p&gt;
&lt;h2 id="4-핵심-턴-루프-queryts의-whiletrue"&gt;4. 핵심 턴 루프: query.ts의 while(true)
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;query.ts&lt;/code&gt;(1,729줄)의 &lt;code&gt;queryLoop()&lt;/code&gt;가 실제 API+도구 루프다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// query.ts:307
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 1. queryModelWithStreaming() 호출 -&amp;gt; SSE 스트림
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 2. 스트리밍 이벤트를 yield
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 3. 도구 호출 감지 -&amp;gt; runTools()/StreamingToolExecutor
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 4. 도구 결과를 메시지에 추가
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 5. stop_reason == &amp;#34;end_turn&amp;#34; -&amp;gt; break
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// stop_reason == &amp;#34;tool_use&amp;#34; -&amp;gt; continue
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;State&lt;/code&gt; 타입(query.ts:204)이 중요하다. &lt;code&gt;messages&lt;/code&gt;, &lt;code&gt;toolUseContext&lt;/code&gt;, &lt;code&gt;autoCompactTracking&lt;/code&gt;, &lt;code&gt;maxOutputTokensRecoveryCount&lt;/code&gt; 등 루프 상태를 명시적 레코드로 관리하여, continue 사이트에서 한 번에 갱신한다.&lt;/p&gt;
&lt;h2 id="5-api-통신-4개-프로바이더와-캐싱"&gt;5. API 통신: 4개 프로바이더와 캐싱
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;client.ts:88&lt;/code&gt;의 &lt;code&gt;getAnthropicClient()&lt;/code&gt;는 4가지 프로바이더를 지원한다:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;프로바이더&lt;/th&gt;
 &lt;th&gt;SDK&lt;/th&gt;
 &lt;th&gt;동적 import 이유&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Anthropic Direct&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;Anthropic&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;기본, 즉시 로딩&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;AWS Bedrock&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;AnthropicBedrock&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;AWS SDK 수 MB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Azure Foundry&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;AnthropicFoundry&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Azure Identity 수 MB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;GCP Vertex&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;AnthropicVertex&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Google Auth 수 MB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;claude.ts&lt;/code&gt;(3,419줄)의 핵심 함수 체인:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;queryModelWithStreaming() (claude.ts:752)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; queryModel()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; withRetry()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; anthropic.beta.messages.stream() (SDK 호출)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;캐싱 전략은 &lt;code&gt;getCacheControl()&lt;/code&gt; (claude.ts:358)이 1시간 TTL 여부를 사용자 유형, 피처 플래그, 쿼리 소스에 따라 결정한다.&lt;/p&gt;
&lt;h2 id="6-도구-오케스트레이션-3-tier-동시성"&gt;6. 도구 오케스트레이션: 3-tier 동시성
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
 TC["도구 호출 배열&amp;lt;br/&amp;gt;[ReadFile, ReadFile, Bash, ReadFile]"]
 P["partitionToolCalls()&amp;lt;br/&amp;gt;toolOrchestration.ts:91"]
 B1["Batch 1&amp;lt;br/&amp;gt;ReadFile + ReadFile&amp;lt;br/&amp;gt;isConcurrencySafe=true"]
 B2["Batch 2&amp;lt;br/&amp;gt;Bash&amp;lt;br/&amp;gt;isConcurrencySafe=false"]
 B3["Batch 3&amp;lt;br/&amp;gt;ReadFile&amp;lt;br/&amp;gt;isConcurrencySafe=true"]
 PAR["Promise.all()&amp;lt;br/&amp;gt;최대 10개 동시"]
 SEQ["순차 실행"]
 PAR2["Promise.all()"]

 TC --&gt; P
 P --&gt; B1
 P --&gt; B2
 P --&gt; B3
 B1 --&gt; PAR
 B2 --&gt; SEQ
 B3 --&gt; PAR2

 style B1 fill:#e8f5e9
 style B2 fill:#ffebee
 style B3 fill:#e8f5e9&lt;/pre&gt;&lt;p&gt;&lt;code&gt;StreamingToolExecutor&lt;/code&gt;(530줄)는 이 배치 파티셔닝을 &lt;strong&gt;스트리밍 컨텍스트&lt;/strong&gt;로 확장한다. API 응답이 스트리밍되는 도중에 도구 호출을 감지하면 즉시 실행을 시작한다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;addTool()&lt;/code&gt; (StreamingToolExecutor.ts:76) — 큐에 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;processQueue()&lt;/code&gt; (StreamingToolExecutor.ts:140) — 동시성 확인 후 즉시 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getRemainingResults()&lt;/code&gt; (StreamingToolExecutor.ts:453) — 모든 도구 완료 대기&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;에러 전파 규칙&lt;/strong&gt;: Bash 에러만 형제 도구를 취소한다 (&lt;code&gt;siblingAbortController&lt;/code&gt;). Read/WebFetch 에러는 다른 도구에 영향을 주지 않는다. 이는 Bash 명령 간의 암묵적 의존성(mkdir 실패 -&amp;gt; 후속 명령 무의미)을 반영한 설계다.&lt;/p&gt;
&lt;h2 id="전체-데이터-플로우"&gt;전체 데이터 플로우
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;sequenceDiagram
 participant User as 사용자
 participant CLI as cli.tsx
 participant Main as main.tsx
 participant QE as QueryEngine
 participant Query as query.ts
 participant Claude as claude.ts
 participant API as Anthropic API
 participant Tools as toolOrchestration
 participant Exec as toolExecution

 User-&gt;&gt;CLI: "hello" 입력
 CLI-&gt;&gt;Main: dynamic import
 Main-&gt;&gt;QE: new QueryEngine()
 QE-&gt;&gt;Query: query()
 Query-&gt;&gt;Claude: queryModelWithStreaming()
 Claude-&gt;&gt;API: anthropic.beta.messages.stream()
 API--&gt;&gt;Claude: SSE 스트림

 alt stop_reason == end_turn
 Claude--&gt;&gt;User: 응답 출력
 else stop_reason == tool_use
 Claude--&gt;&gt;Query: tool_use blocks
 Query-&gt;&gt;Tools: partitionToolCalls()
 Tools-&gt;&gt;Exec: runToolUse()
 Exec-&gt;&gt;Exec: canUseTool() + tool.call()
 Exec--&gt;&gt;Query: 도구 결과
 Note over Query: while(true) 다음 반복
 end&lt;/pre&gt;&lt;h2 id="rust-갭-지도-미리보기"&gt;Rust 갭 지도 미리보기
&lt;/h2&gt;&lt;p&gt;동일한 요청을 Rust 포트에서 추적한 결과, &lt;strong&gt;31개 갭&lt;/strong&gt;을 식별했다:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;우선순위&lt;/th&gt;
 &lt;th&gt;갭 수&lt;/th&gt;
 &lt;th&gt;핵심 예시&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;P0 (치명적)&lt;/td&gt;
 &lt;td&gt;2&lt;/td&gt;
 &lt;td&gt;동기 ApiClient, StreamingToolExecutor 부재&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;P1 (높음)&lt;/td&gt;
 &lt;td&gt;6&lt;/td&gt;
 &lt;td&gt;3-tier 동시성, 프롬프트 캐싱, Agent 도구&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;P2 (보통)&lt;/td&gt;
 &lt;td&gt;7&lt;/td&gt;
 &lt;td&gt;멀티 프로바이더, 노력 제어, 샌드박스&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;구현 완료&lt;/td&gt;
 &lt;td&gt;11&lt;/td&gt;
 &lt;td&gt;자동 압축, SSE 파서, OAuth, 설정 로딩&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;구현 완료율: 36% (11/31)&lt;/strong&gt;. 다음 포스트에서 이 갭들의 핵심인 대화 루프를 깊이 파고든다.&lt;/p&gt;
&lt;h2 id="인사이트"&gt;인사이트
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AsyncGenerator가 아키텍처의 척추다&lt;/strong&gt; — 단순한 구현 기법이 아니라 backpressure, 취소, 합성을 한 번에 해결하는 설계 결정이다. Rust에서는 &lt;code&gt;Stream&lt;/code&gt; trait이 대응하지만 &lt;code&gt;yield*&lt;/code&gt; 합성의 인체공학이 크게 다르다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;main.tsx 4,683줄은 기술 부채다&lt;/strong&gt; — Commander 설정, React 컴포넌트, 상태 관리가 한 파일에 혼재. 역사적 성장의 결과로, 모듈 분리의 기회가 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;도구 동시성이 단순하지 않다&lt;/strong&gt; — &amp;ldquo;전부 병렬&amp;rdquo; 또는 &amp;ldquo;전부 직렬&amp;quot;이 아닌 3계층 모델(읽기 배치, 쓰기 순차, Bash 형제 취소)은 실전 에이전트 하네스의 핵심 설계 요소다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;다음 포스트: &lt;a class="link" href="https://ice-ice-bear.github.io/posts/2026-04-06-harness-anatomy-2/" &gt;#2 — 대화 루프의 심장, StreamingToolExecutor와 7개의 continue&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</description></item><item><title>Claude Code 하네스 해부학 #2 — 대화 루프의 심장, StreamingToolExecutor와 7개의 continue</title><link>https://ice-ice-bear.github.io/ko/posts/2026-04-06-harness-anatomy-2/</link><pubDate>Mon, 06 Apr 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/ko/posts/2026-04-06-harness-anatomy-2/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post Claude Code 하네스 해부학 #2 — 대화 루프의 심장, StreamingToolExecutor와 7개의 continue" /&gt;&lt;h2 id="개요"&gt;개요
&lt;/h2&gt;&lt;p&gt;시리즈 첫 번째 글에서 &amp;ldquo;hello&amp;rdquo; 한마디가 11개 파일을 관통하는 여정을 추적했다. 이번 포스트에서는 그 여정의 심장부인 &lt;code&gt;query.ts&lt;/code&gt; 1,729줄의 &lt;code&gt;while(true)&lt;/code&gt; 루프를 완전 해부한다. 7가지 &lt;code&gt;continue&lt;/code&gt; 경로가 만드는 탄력적 실행 모델, &lt;code&gt;StreamingToolExecutor&lt;/code&gt;의 4단계 상태 머신, 그리고 &lt;code&gt;partitionToolCalls()&lt;/code&gt;의 3계층 동시성 모델을 분석한 뒤, Rust 프로토타입에서 이를 어떻게 재현했는지 대조한다.&lt;/p&gt;
&lt;h2 id="분석-대상-10개-핵심-파일"&gt;분석 대상: 10개 핵심 파일
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;#&lt;/th&gt;
 &lt;th&gt;경로&lt;/th&gt;
 &lt;th&gt;줄 수&lt;/th&gt;
 &lt;th&gt;역할&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;1&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;query/config.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;46&lt;/td&gt;
 &lt;td&gt;이뮤터블 런타임 게이트 스냅샷&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;query/deps.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;40&lt;/td&gt;
 &lt;td&gt;테스트 가능한 I/O 경계 (DI)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;3&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;query/tokenBudget.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;93&lt;/td&gt;
 &lt;td&gt;토큰 예산 관리, 자동 연속/중단 결정&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;4&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;query/stopHooks.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;473&lt;/td&gt;
 &lt;td&gt;Stop/TaskCompleted/TeammateIdle 훅&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;5&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;query.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1,729&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;핵심&lt;/strong&gt; &amp;ndash; while(true) 턴 루프&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;6&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;QueryEngine.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1,295&lt;/td&gt;
 &lt;td&gt;세션 래퍼, SDK 인터페이스&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;7&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;toolOrchestration.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;188&lt;/td&gt;
 &lt;td&gt;도구 파티셔닝 + 동시성 제어&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;8&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;StreamingToolExecutor.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;530&lt;/td&gt;
 &lt;td&gt;SSE 중 도구 파이프라이닝&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;9&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;toolExecution.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1,745&lt;/td&gt;
 &lt;td&gt;도구 디스패치, 권한 검사&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;10&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;toolHooks.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;650&lt;/td&gt;
 &lt;td&gt;Pre/PostToolUse 훅 파이프라인&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;총 &lt;strong&gt;6,789줄&lt;/strong&gt;의 핵심 오케스트레이션 코드를 해부한다.&lt;/p&gt;
&lt;h2 id="1-queryloop의-7가지-continue-경로"&gt;1. queryLoop()의 7가지 continue 경로
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;query.ts&lt;/code&gt;의 &lt;code&gt;queryLoop()&lt;/code&gt; 함수(query.ts:241)는 단순한 API 호출 루프가 아니다. 7가지 &lt;code&gt;continue&lt;/code&gt; 사유를 가진 &lt;strong&gt;탄력적 실행기&lt;/strong&gt;다. 각 경로는 고유한 장애 시나리오를 처리한다:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;사유&lt;/th&gt;
 &lt;th&gt;행&lt;/th&gt;
 &lt;th&gt;설명&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;collapse_drain_retry&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1114&lt;/td&gt;
 &lt;td&gt;컨텍스트 축소 드레인 후 재시도&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;reactive_compact_retry&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1162&lt;/td&gt;
 &lt;td&gt;반응형 압축 후 재시도 (413 복구)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;max_output_tokens_escalate&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1219&lt;/td&gt;
 &lt;td&gt;8k -&amp;gt; 64k 토큰 에스컬레이션&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;max_output_tokens_recovery&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1248&lt;/td&gt;
 &lt;td&gt;&amp;ldquo;이어서 작성하라&amp;rdquo; 넛지 메시지 주입&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;stop_hook_blocking&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1303&lt;/td&gt;
 &lt;td&gt;Stop 훅이 블로킹 에러 반환&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;token_budget_continuation&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1337&lt;/td&gt;
 &lt;td&gt;토큰 예산 미달로 계속&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;next_turn&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1725&lt;/td&gt;
 &lt;td&gt;도구 실행 완료 후 다음 턴&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;State 타입이 핵심이다&lt;/strong&gt; (query.ts:204-217). 루프 상태를 10개 필드의 레코드로 관리한다. 왜 개별 변수가 아닌 레코드인가? &lt;code&gt;continue&lt;/code&gt; 사이트가 7곳이며, 각각 &lt;code&gt;state = { ... }&lt;/code&gt;으로 한 번에 갱신한다. 9개 변수를 개별 할당하면 하나를 빠뜨리는 실수가 발생하기 쉽다. &lt;strong&gt;레코드 갱신은 타입 시스템이 누락을 잡아준다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="루프-한-반복의-전체-흐름"&gt;루프 한 반복의 전체 흐름
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. 전처리 (365-447): snip 압축, 마이크로 컴팩트, 컨텍스트 축소
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. 자동 압축 (454-543): 성공 시 메시지 교체 후 continue
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 블로킹 한도 검사 (628-648): 토큰 임계값 초과 시 즉시 종료
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. API 스트리밍 (654-863): SSE 이벤트를 for await로 소비
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5. 도구 없는 종료 경로 (1062-1357): 413 복구, max_output 복구, stop 훅
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;6. 도구 있는 계속 경로 (1360-1728): 나머지 도구 실행 -&amp;gt; next_turn
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="2-streamingtoolexecutor의-4단계-상태-머신"&gt;2. StreamingToolExecutor의 4단계 상태 머신
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;StreamingToolExecutor.ts&lt;/code&gt;(530줄)는 Claude Code에서 가장 정교한 동시성 패턴이다. 핵심 아이디어: &lt;strong&gt;API 응답이 아직 스트리밍되는 동안 이미 완성된 도구 호출의 실행을 시작&lt;/strong&gt;한다.&lt;/p&gt;
&lt;p&gt;모델이 &lt;code&gt;[ReadFile(&amp;quot;a.ts&amp;quot;), ReadFile(&amp;quot;b.ts&amp;quot;), Bash(&amp;quot;make test&amp;quot;)]&lt;/code&gt;를 한 번에 호출하는 경우, 파이프라이닝 없이는 세 도구 블록이 모두 도착한 후에야 실행이 시작된다. 파이프라이닝에서는 &lt;code&gt;ReadFile(&amp;quot;a.ts&amp;quot;)&lt;/code&gt; 블록이 완성되는 즉시 파일 읽기가 시작된다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;stateDiagram-v2
 [*] --&gt; queued: addTool()
 queued --&gt; executing: processQueue()&amp;lt;br/&amp;gt;canExecuteTool() == true
 queued --&gt; completed: 사전 취소&amp;lt;br/&amp;gt;getAbortReason() != null

 executing --&gt; completed: 도구 실행 완료&amp;lt;br/&amp;gt;또는 sibling abort

 completed --&gt; yielded: getCompletedResults()&amp;lt;br/&amp;gt;순서대로 yield

 yielded --&gt; [*]

 note right of queued
 processQueue()는 addTool()
 + 이전 도구 완료 시 자동 트리거
 end note

 note right of completed
 Bash 에러 시
 siblingAbortController.abort()
 형제 도구만 취소
 end note&lt;/pre&gt;&lt;h3 id="동시성-결정-로직-canexecutetool-line-129"&gt;동시성 결정 로직 (canExecuteTool, line 129)
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;실행 가능 조건:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 현재 실행 중인 도구가 없음 (executingTools.length === 0)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 또는: 이 도구가 concurrencySafe이고 실행 중인 모든 도구도 concurrencySafe
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;read-only 도구끼리는 병렬 실행이 가능하지만, write 도구가 하나라도 있으면 그 도구가 끝날 때까지 다음 도구는 대기한다.&lt;/p&gt;
&lt;h3 id="siblingabortcontroller--계층적-취소"&gt;siblingAbortController &amp;ndash; 계층적 취소
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;siblingAbortController&lt;/code&gt;(line 46-61)는 &lt;code&gt;toolUseContext.abortController&lt;/code&gt;의 자식이다. Bash 도구가 에러를 발생시키면 &lt;code&gt;siblingAbortController.abort('sibling_error')&lt;/code&gt;를 호출하여 &lt;strong&gt;형제 도구만 취소&lt;/strong&gt;한다. 부모 컨트롤러는 영향을 받지 않으므로 전체 쿼리는 계속 실행된다.&lt;/p&gt;
&lt;p&gt;왜 Bash 에러만 형제를 취소하는가? &lt;code&gt;mkdir -p dir &amp;amp;&amp;amp; cd dir &amp;amp;&amp;amp; make&lt;/code&gt;에서 mkdir이 실패하면 후속 명령은 무의미하다. ReadFile이나 WebFetch 실패는 독립적이므로 다른 도구에 영향을 주지 않아야 한다.&lt;/p&gt;
&lt;h2 id="3-partitiontoolcalls--3계층-동시성-모델"&gt;3. partitionToolCalls &amp;ndash; 3계층 동시성 모델
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;toolOrchestration.ts&lt;/code&gt;(188줄)는 도구 실행의 동시성 모델 전체를 정의한다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
 TC["도구 호출 배열&amp;lt;br/&amp;gt;[ReadFile, ReadFile, Bash, ReadFile]"]
 P["partitionToolCalls()&amp;lt;br/&amp;gt;toolOrchestration.ts:91"]
 B1["Batch 1&amp;lt;br/&amp;gt;ReadFile + ReadFile&amp;lt;br/&amp;gt;isConcurrencySafe=true"]
 B2["Batch 2&amp;lt;br/&amp;gt;Bash&amp;lt;br/&amp;gt;isConcurrencySafe=false"]
 B3["Batch 3&amp;lt;br/&amp;gt;ReadFile&amp;lt;br/&amp;gt;isConcurrencySafe=true"]
 PAR["Promise.all()&amp;lt;br/&amp;gt;최대 10개 동시"]
 SEQ["순차 실행"]
 PAR2["Promise.all()"]

 TC --&gt; P
 P --&gt; B1
 P --&gt; B2
 P --&gt; B3
 B1 --&gt; PAR
 B2 --&gt; SEQ
 B3 --&gt; PAR2

 style B1 fill:#e8f5e9
 style B2 fill:#ffebee
 style B3 fill:#e8f5e9&lt;/pre&gt;&lt;p&gt;규칙은 단순하다: 연속된 &lt;code&gt;isConcurrencySafe&lt;/code&gt; 도구는 하나의 배치로 묶고, 그렇지 않은 도구는 각각 독립 배치가 된다. 이 결정은 &lt;strong&gt;도구 정의 자체에서&lt;/strong&gt; 나온다 &amp;ndash; &lt;code&gt;tool.isConcurrencySafe(parsedInput)&lt;/code&gt; 호출로 결정된다. 같은 도구라도 입력에 따라 동시성 안전성이 달라질 수 있다.&lt;/p&gt;
&lt;h3 id="컨텍스트-수정자와-경쟁-조건"&gt;컨텍스트 수정자와 경쟁 조건
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;왜 배치 완료 후 순서대로 적용하는가?&lt;/strong&gt; 병렬 실행 중 컨텍스트 수정자를 즉시 적용하면 경쟁 조건이 발생한다. A가 먼저 완료되어 컨텍스트를 수정하면, 아직 실행 중인 B는 수정 전 컨텍스트로 시작했지만 수정 후 컨텍스트를 보게 된다. 배치 완료 후 원래 도구 순서대로 적용하면 결정론적 결과를 보장한다 (toolOrchestration.ts:54-62).&lt;/p&gt;
&lt;h2 id="4-도구-실행-파이프라인과-훅"&gt;4. 도구 실행 파이프라인과 훅
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;toolExecution.ts&lt;/code&gt;(1,745줄)의 &lt;code&gt;runToolUse()&lt;/code&gt;(line 337)가 개별 도구 호출의 전체 생명주기를 관리한다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;runToolUse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="err"&gt;진입점&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="mf"&gt;1.&lt;/span&gt; &lt;span class="n"&gt;findToolByName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="n"&gt;deprecated&lt;/span&gt; &lt;span class="err"&gt;별칭으로&lt;/span&gt; &lt;span class="err"&gt;재시도&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;345&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;356&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="mf"&gt;2.&lt;/span&gt; &lt;span class="n"&gt;abort&lt;/span&gt; &lt;span class="err"&gt;체크&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="err"&gt;이미&lt;/span&gt; &lt;span class="err"&gt;취소되었으면&lt;/span&gt; &lt;span class="n"&gt;CANCEL_MESSAGE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;415&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="mf"&gt;3.&lt;/span&gt; &lt;span class="n"&gt;streamedCheckPermissionsAndCallTool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="err"&gt;권한&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="err"&gt;실행&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="err"&gt;훅&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;455&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;checkPermissionsAndCallTool&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Zod&lt;/span&gt; &lt;span class="err"&gt;스키마로&lt;/span&gt; &lt;span class="err"&gt;입력&lt;/span&gt; &lt;span class="err"&gt;유효성&lt;/span&gt; &lt;span class="err"&gt;검사&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;615&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="k"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;validateInput&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="err"&gt;커스텀&lt;/span&gt; &lt;span class="err"&gt;검증&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;683&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="err"&gt;투기적&lt;/span&gt; &lt;span class="err"&gt;분류기&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bash&lt;/span&gt; &lt;span class="err"&gt;전용&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;740&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;runPreToolUseHooks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;resolveHookPermissionDecision&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;921&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="k"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="err"&gt;실제&lt;/span&gt; &lt;span class="err"&gt;실행&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1207&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;runPostToolUseHooks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="err"&gt;결과&lt;/span&gt; &lt;span class="err"&gt;변환&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="resolvehookpermissiondecision의-핵심-불변식"&gt;resolveHookPermissionDecision의 핵심 불변식
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;resolveHookPermissionDecision()&lt;/code&gt;(toolHooks.ts:332)에서 &lt;strong&gt;훅의 &lt;code&gt;allow&lt;/code&gt;가 settings.json의 deny/ask 규칙을 바이패스하지 않는다&lt;/strong&gt; (toolHooks.ts:373). 훅이 allow해도 &lt;code&gt;checkRuleBasedPermissions()&lt;/code&gt;를 통과해야 한다. 이것은 &amp;ldquo;훅은 자동화 도우미이지 보안 우회가 아니다&amp;quot;라는 설계 원칙을 반영한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;훅 결과가 allow일 때:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; checkRuleBasedPermissions() 호출
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; null이면 통과 (규칙 없음)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; deny이면 규칙이 훅을 오버라이드
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; ask이면 사용자 프롬프트 필요
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="5-rust-대조--152줄-vs-1729줄"&gt;5. Rust 대조 &amp;ndash; 152줄 vs 1,729줄
&lt;/h2&gt;&lt;p&gt;Rust의 &lt;code&gt;ConversationRuntime::run_turn()&lt;/code&gt;은 &lt;strong&gt;152줄의 단일 &lt;code&gt;loop {}&lt;/code&gt;&lt;/strong&gt; (conversation.rs:183-272)로 구성된다. TS의 7가지 continue 경로 중 Rust에 존재하는 것은 &lt;code&gt;next_turn&lt;/code&gt;(도구 실행 완료 후 다음 턴) 딱 하나뿐이다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;TS continue 사유&lt;/th&gt;
 &lt;th&gt;Rust 상태&lt;/th&gt;
 &lt;th&gt;이유&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;collapse_drain_retry&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;미구현&lt;/td&gt;
 &lt;td&gt;컨텍스트 축소 없음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;reactive_compact_retry&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;미구현&lt;/td&gt;
 &lt;td&gt;413 복구 없음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;max_output_tokens_escalate&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;미구현&lt;/td&gt;
 &lt;td&gt;8k-&amp;gt;64k 에스컬레이션 없음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;max_output_tokens_recovery&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;미구현&lt;/td&gt;
 &lt;td&gt;멀티턴 넛지 없음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;stop_hook_blocking&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;미구현&lt;/td&gt;
 &lt;td&gt;Stop 훅 없음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;token_budget_continuation&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;미구현&lt;/td&gt;
 &lt;td&gt;토큰 예산 시스템 없음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;next_turn&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;구현됨&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;도구 결과 후 API 재호출&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="가장-치명적인-갭-동기적-api-소비"&gt;가장 치명적인 갭: 동기적 API 소비
&lt;/h3&gt;&lt;p&gt;Rust의 &lt;code&gt;ApiClient&lt;/code&gt; 트레이트 시그니처가 모든 것을 말해준다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;: &lt;span class="nc"&gt;ApiRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AssistantEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RuntimeError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;반환 타입이 &lt;code&gt;Vec&amp;lt;AssistantEvent&amp;gt;&lt;/code&gt;다. &lt;strong&gt;스트리밍이 아니다.&lt;/strong&gt; SSE 이벤트를 모두 수집한 후 벡터로 반환한다. 이로 인해 모델이 5개 ReadFile을 호출할 때, TS는 첫 번째 ReadFile이 스트리밍 중에 실행 완료될 수 있지만, Rust는 5개 모두 스트리밍이 끝난 후에야 순차 실행을 시작한다. &lt;strong&gt;레이턴시 차이가 도구 수에 비례하여 증가&lt;/strong&gt;한다.&lt;/p&gt;
&lt;h2 id="6-rust-프로토타입--갭-브릿지"&gt;6. Rust 프로토타입 &amp;ndash; 갭 브릿지
&lt;/h2&gt;&lt;p&gt;S04 프로토타입에서 P0 갭 3개를 브릿지하는 오케스트레이션 레이어를 구현했다:&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 subgraph TS["TS 스트리밍 파이프라인"]
 direction TB
 ts1["SSE 이벤트 스트림"]
 ts2["StreamingToolExecutor&amp;lt;br/&amp;gt;4-state machine"]
 ts3["getCompletedResults()&amp;lt;br/&amp;gt;yield 순서 보장"]
 ts1 --&gt; ts2 --&gt; ts3
 end

 subgraph Rust["Rust 프로토타입"]
 direction TB
 rs1["EventStream&amp;lt;br/&amp;gt;tokio async"]
 rs2["StreamingPipeline&amp;lt;br/&amp;gt;tokio::spawn + mpsc"]
 rs3["MessageEnd 후&amp;lt;br/&amp;gt;채널 수집 + 정렬"]
 rs1 --&gt; rs2 --&gt; rs3
 end

 subgraph Bridge["핵심 매핑"]
 direction TB
 b1["yield -&gt; tx.send()"]
 b2["yield* -&gt; 채널 전달"]
 b3["for await -&gt; while let recv()"]
 end

 TS ~~~ Bridge ~~~ Rust

 style TS fill:#e1f5fe
 style Rust fill:#fff3e0
 style Bridge fill:#f3e5f5&lt;/pre&gt;&lt;h3 id="프로토타입의-3가지-구현"&gt;프로토타입의 3가지 구현
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;1. 비동기 스트리밍&lt;/strong&gt;: &lt;code&gt;ApiClient&lt;/code&gt; 트레이트를 비동기 스트림으로 확장. &lt;code&gt;MessageStream::next_event()&lt;/code&gt;가 이미 비동기이므로 소비 측만 변경하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 도구 파이프라이닝&lt;/strong&gt;: &lt;code&gt;ToolUseEnd&lt;/code&gt; 이벤트 수신 시 누적된 입력으로 &lt;code&gt;ToolCall&lt;/code&gt;을 조립하고 &lt;code&gt;tokio::spawn&lt;/code&gt;으로 즉시 백그라운드 실행을 시작한다. &lt;code&gt;mpsc::unbounded_channel&lt;/code&gt;로 완료 순서로 수집하고 나중에 원래 순서로 정렬한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 3-tier 동시성&lt;/strong&gt;: &lt;code&gt;ToolCategory&lt;/code&gt; enum(ReadOnly/Write/BashLike)에 따라 파티셔닝. ReadOnly 배치는 &lt;code&gt;Semaphore(10)&lt;/code&gt; + &lt;code&gt;tokio::spawn&lt;/code&gt;으로 최대 10개 병렬. BashLike는 순차 실행 + 에러 시 나머지 중단.&lt;/p&gt;
&lt;h3 id="프로토타입-커버리지"&gt;프로토타입 커버리지
&lt;/h3&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;TS 기능&lt;/th&gt;
 &lt;th&gt;프로토타입&lt;/th&gt;
 &lt;th&gt;상태&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;partitionToolCalls()&lt;/code&gt; 3-tier&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;partition_into_runs()&lt;/code&gt; + &lt;code&gt;ToolCategory&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;구현&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;runToolsConcurrently()&lt;/code&gt; max 10&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;Semaphore(10)&lt;/code&gt; + &lt;code&gt;tokio::spawn&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;구현&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;siblingAbortController&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;BashLike에서 &lt;code&gt;break&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;단순화&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;StreamingToolExecutor.addTool()&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;ToolUseEnd&lt;/code&gt; 시 &lt;code&gt;tokio::spawn&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;구현&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;PreToolUse hook deny/allow&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;HookDecision::Allow/Deny&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;구현&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;PostToolUse output transform&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;HookResult::transformed_output&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;구현&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;4-state machine (queued-&amp;gt;yielded)&lt;/td&gt;
 &lt;td&gt;spawned/completed 2-state&lt;/td&gt;
 &lt;td&gt;미완성&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;413 복구 / max_output 에스컬레이션&lt;/td&gt;
 &lt;td&gt;&amp;ndash;&lt;/td&gt;
 &lt;td&gt;미구현&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;preventContinuation&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&amp;ndash;&lt;/td&gt;
 &lt;td&gt;미구현&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="정지-조건-비교"&gt;정지 조건 비교
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;조건&lt;/th&gt;
 &lt;th&gt;TS&lt;/th&gt;
 &lt;th&gt;Rust&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;도구 없음 (end_turn)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;handleStopHooks()&lt;/code&gt; 실행 후 종료&lt;/td&gt;
 &lt;td&gt;즉시 &lt;code&gt;break&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;토큰 예산 초과&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;checkTokenBudget()&lt;/code&gt; 3가지 결정&lt;/td&gt;
 &lt;td&gt;없음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;max_output_tokens&lt;/td&gt;
 &lt;td&gt;에스컬레이션 + 멀티턴 복구&lt;/td&gt;
 &lt;td&gt;없음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;413 prompt-too-long&lt;/td&gt;
 &lt;td&gt;컨텍스트 축소 + 반응형 압축&lt;/td&gt;
 &lt;td&gt;에러 전파&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;maxTurns&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;maxTurns&lt;/code&gt; 파라미터 (query.ts:1696)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;max_iterations&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;수확 체감&lt;/td&gt;
 &lt;td&gt;3회 이상 + 500토큰 미만 증가&lt;/td&gt;
 &lt;td&gt;없음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;tokenBudget.ts&lt;/code&gt;(93줄)의 &lt;code&gt;checkTokenBudget()&lt;/code&gt;은 &lt;strong&gt;프롬프트 크기가 아닌 응답 연속 여부&lt;/strong&gt;를 제어한다. &lt;code&gt;COMPLETION_THRESHOLD = 0.9&lt;/code&gt;(전체 버짓의 90% 미만이면 계속), &lt;code&gt;DIMINISHING_THRESHOLD = 500&lt;/code&gt;(연속 3회 이상 매번 500토큰 미만 생성 시 수확 체감으로 중단). &lt;code&gt;nudgeMessage&lt;/code&gt;가 명시적으로 &amp;ldquo;do not summarize&amp;quot;를 지시한다.&lt;/p&gt;
&lt;h2 id="설계-결정의-핵심--왜-asyncgenerator인가"&gt;설계 결정의 핵심 &amp;ndash; 왜 AsyncGenerator인가
&lt;/h2&gt;&lt;p&gt;전체 파이프라인이 &lt;code&gt;async function*&lt;/code&gt; 체인이다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;QueryEngine.submitMessage()* -&amp;gt; query()* -&amp;gt; queryLoop()* -&amp;gt; deps.callModel()*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;runTools()* -&amp;gt; runToolUse()* -&amp;gt; handleStopHooks()* -&amp;gt; executeStopHooks()*
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이 선택의 핵심 이점: &lt;strong&gt;제어 흐름의 반전 없이 복잡한 상태 기계를 구현&lt;/strong&gt;할 수 있다. 7가지 &lt;code&gt;continue&lt;/code&gt; 경로에서 &lt;code&gt;state = { ... }&lt;/code&gt;로 상태를 명시적으로 구성하고 &lt;code&gt;continue&lt;/code&gt;하면 된다. 콜백 기반이었다면 상태 관리가 분산되어 7가지 복구 경로의 정합성을 보장하기 어려웠을 것이다.&lt;/p&gt;
&lt;p&gt;Rust에서는 &lt;code&gt;yield&lt;/code&gt; 키워드가 안정화되지 않았으므로, &lt;code&gt;tokio::sync::mpsc&lt;/code&gt; 채널로 대체한다. &lt;code&gt;yield&lt;/code&gt; -&amp;gt; &lt;code&gt;tx.send()&lt;/code&gt;, &lt;code&gt;yield*&lt;/code&gt; -&amp;gt; 채널 전달, &lt;code&gt;for await...of&lt;/code&gt; -&amp;gt; &lt;code&gt;while let Some(v) = rx.recv()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="인사이트"&gt;인사이트
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;query.ts의 7가지 continue는 &amp;ldquo;에러 처리&amp;quot;가 아니라 &amp;ldquo;탄력성 엔진&amp;quot;이다&lt;/strong&gt; &amp;ndash; 413 에러에서 컨텍스트를 축소하고, max_output에서 토큰을 에스컬레이션하며, stop 훅 블로킹에서 에러를 모델에 피드백한다. 이 복구 파이프라인이 장시간 자율 작업의 안정성을 보장한다. Rust에서 이를 재현하려면 단순한 &lt;code&gt;loop {}&lt;/code&gt; 이상의 상태 관리가 필요하다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;StreamingToolExecutor는 성능 최적화가 아니라 UX 결정이다&lt;/strong&gt; &amp;ndash; 도구 5개를 직렬로 실행하면 사용자가 체감하는 대기 시간이 직렬 합산이 된다. 파이프라이닝은 벤치마크 수치가 아니라 &amp;ldquo;응답을 기다리는&amp;rdquo; 시간을 줄이는 사용자 경험 요소다. Rust 프로토타입에서 &lt;code&gt;tokio::spawn&lt;/code&gt; + &lt;code&gt;mpsc&lt;/code&gt; 채널로 이를 20줄 이내에 구현할 수 있었다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;정적 파티셔닝 + 런타임 동시성의 이중 구조가 안전성과 성능을 양립시킨다&lt;/strong&gt; &amp;ndash; &lt;code&gt;partitionToolCalls()&lt;/code&gt;가 컴파일 타임에 배치를 나누고, &lt;code&gt;canExecuteTool()&lt;/code&gt;이 런타임에 실행 가능 여부를 판단한다. 이 이중 구조 덕분에 비스트리밍 경로(&lt;code&gt;runTools&lt;/code&gt;)와 스트리밍 경로(&lt;code&gt;StreamingToolExecutor&lt;/code&gt;)가 동일한 동시성 의미론을 공유한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;다음 포스트: &lt;a class="link" href="https://ice-ice-bear.github.io/posts/2026-04-06-harness-anatomy-3/" &gt;#3 &amp;ndash; 42개 도구의 설계 철학, BashTool부터 AgentTool까지&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</description></item></channel></rss>