# Claw Code 깊이 읽기 #2 — Rust 워크스페이스 아키텍처 심층 분석

조현상·2026년 4월 1일

ClaudeCode

목록 보기
2/17
post-thumbnail

6개의 크레이트, 20,000줄의 Rust 코드. 그 안에 숨어 있는 설계 결정들을 한 줄씩 읽어본다.


들어가며: 왜 Rust 워크스페이스를 분석하는가

1편에서 Claw Code의 전체 구조와 이중 포팅 전략을 살펴보았다. Python이 "이해를 위한 코드"라면, Rust는 "배포를 위한 코드"였다. 이번 편에서는 그 Rust 코드의 내부를 열어본다.

Claw Code의 Rust 워크스페이스는 단순히 "Python 코드를 Rust로 번역한 것"이 아니다. Rust의 타입 시스템, 트레이트 추상화, 소유권 모델을 적극 활용하여 원본 TypeScript와는 다른 아키텍처적 결정을 내린 부분이 많다. 이 글에서는 각 크레이트를 하나씩 열어보면서, 그 결정들의 의도를 읽어낸다.


1. Workspace 설정: 한 줄에 담긴 철학

모든 분석은 루트 Cargo.toml에서 시작한다. 이 파일은 짧지만, 프로젝트 전체의 설계 철학이 압축되어 있다.

[workspace]
members = ["crates/*"]
resolver = "2"

[workspace.package]
version = "0.1.0"
edition = "2021"
license = "MIT"
publish = false

[workspace.lints.rust]
unsafe_code = "forbid"

[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
module_name_repetitions = "allow"
missing_panics_doc = "allow"
missing_errors_doc = "allow"

unsafe_code = "forbid" — 타협 없는 안전성

이 한 줄이 프로젝트 전체의 보안 기조를 결정한다. deny가 아니라 forbid라는 점이 중요하다. deny는 크레이트 레벨에서 #[allow(unsafe_code)]로 재정의할 수 있지만, forbid는 불가능하다. 즉, 어떤 크레이트에서도 unsafe를 사용할 수 없다.

AI 에이전트 하네스는 사용자의 shell 명령을 실행하고, 파일 시스템을 조작하며, 외부 API와 통신한다. 이런 도구에서 메모리 안전성 결함은 곧 보안 취약점이 된다. forbid 정책은 "우리는 성능을 약간 포기하더라도 안전성을 택한다"는 선언이다.

Clippy pedantic — AI가 작성한 코드도 검증한다

clippy::pedantic은 일반적인 프로젝트에서는 과한 수준의 린팅이다. 하지만 이 프로젝트는 oh-my-codex(OmX) 워크플로우로 AI가 코드를 생성하는 환경이다. AI가 작성한 코드는 "동작하지만 관용적이지 않은" 패턴을 가질 수 있으므로, pedantic 수준의 검증이 합리적이다.

전략적 예외도 주목할 만하다. module_name_repetitions = "allow"api::ApiClient 같은 네이밍을 허용하고, missing_panics_docmissing_errors_doc은 초기 개발 단계에서 문서화 부담을 줄여준다. 이는 "이상은 높게, 현실은 유연하게" 라는 실용적 접근이다.

publish = false — 의도적 비공개

crates.io에 배포하지 않겠다는 선언이다. Claw Code는 프로덕션 바이너리(claw)를 직접 배포하는 방식을 택했으므로, 개별 크레이트를 라이브러리로 공개할 이유가 없다. 이 결정은 API 안정성 부담 없이 내부 인터페이스를 자유롭게 변경할 수 있게 해준다.


2. 크레이트 의존성 그래프: 계층의 해부학

6개 크레이트의 의존성을 그려보면, 명확한 계층 구조가 드러난다:

rusty-claude-cli (최상위 바이너리)
    ├── api ──────── runtime
    ├── tools ───┬── api
    │            └── runtime
    ├── commands ─── runtime
    ├── compat-harness ─┬── commands
    │                   ├── tools
    │                   └── runtime
    └── [crossterm, rustyline, syntect, pulldown-cmark]

이 그래프에서 읽어낼 수 있는 설계 원칙은 세 가지다:

첫째, 순환 의존성이 없다. 의존성 그래프가 완전한 DAG(Directed Acyclic Graph)를 이룬다. Rust의 크레이트 시스템이 순환을 허용하지 않기 때문이기도 하지만, 설계 단계에서 계층을 의식적으로 분리한 결과다.

둘째, runtime이 공유 기반이다. 모든 크레이트가 runtime에 의존한다. runtimeSession, PermissionMode, TokenUsage 같은 핵심 타입과 트레이트를 정의하는 "어휘(vocabulary) 크레이트" 역할을 한다.

셋째, apitools는 서로 독립적이다. API 통신과 도구 실행이 직접 의존하지 않는다는 것은, 이 둘을 독립적으로 테스트하고 교체할 수 있다는 의미다. 이 분리가 있기에 mock API 클라이언트로 도구 실행을 테스트하거나, mock 도구로 API 통합을 테스트하는 것이 가능해진다.


3. api 크레이트 — Anthropic과의 대화 채널

api 크레이트는 Anthropic Claude API와의 HTTP 통신을 담당한다. 겉보기에는 단순한 HTTP 클라이언트지만, 내부에는 SSE 스트리밍, OAuth PKCE, 재시도 로직 등 상당한 복잡성이 숨어 있다.

AnthropicClient: 인증의 계층화

#[derive(Debug, Clone)]
pub struct AnthropicClient {
    http: reqwest::Client,
    auth: AuthSource,
    base_url: String,
    max_retries: u32,
    initial_backoff: Duration,
    max_backoff: Duration,
}

핵심은 AuthSource 열거형이다:

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthSource {
    None,
    ApiKey(String),
    BearerToken(String),
    ApiKeyAndBearer {
        api_key: String,
        bearer_token: String,
    },
}

인증 방식을 4가지 변형으로 분류한 것이 의미심장하다. None은 테스트용, ApiKey는 직접 API 키 사용, BearerToken은 OAuth 토큰, ApiKeyAndBearer는 둘 다 필요한 경우다. 실제 인증 해석(resolution)은 환경에 따라 달라진다:

pub fn resolve_startup_auth_source<F>(
    load_oauth_config: F
) -> Result<AuthSource, ApiError>
where
    F: FnOnce() -> Result<Option<OAuthConfig>, ApiError>,
{
    // 1. ANTHROPIC_API_KEY 환경 변수 확인
    // 2. ANTHROPIC_AUTH_TOKEN 환경 변수 확인
    // 3. 저장된 OAuth 토큰 로드 (만료 시 자동 갱신)
    // 4. 모두 실패하면 에러 반환
}

이 함수의 제네릭 파라미터 F: FnOnce()에 주목하자. OAuth 설정 로딩을 클로저로 주입받음으로써, 인증 해석 로직이 OAuth 구현에 직접 의존하지 않는다. 테스트에서는 || Ok(None)을 넘기면 OAuth 없이 테스트할 수 있다.

재시도 전략: Exponential Backoff

fn backoff_for_attempt(&self, attempt: u32) -> Result<Duration, ApiError> {
    // min=200ms, max=2s 범위의 지수 백오프
}

async fn send_with_retry(&self, request: &MessageRequest)
    -> Result<reqwest::Response, ApiError> {
    // 재시도 가능: 연결 오류, 타임아웃, 408/409/429/5xx
}

재시도 가능 여부를 HTTP 상태 코드별로 분류한 것은 Anthropic API의 실제 동작 패턴을 반영한다. 429 Too Many Requests는 rate limit, 529는 서버 과부하로, 둘 다 시간이 지나면 해결되는 일시적 오류다. 반면 400 Bad Request는 재시도해도 의미가 없다.


4. runtime 크레이트 — 시스템의 심장

runtime은 가장 큰 크레이트이자 전체 시스템의 중심이다. 14개 모듈로 구성되며, 각각이 런타임의 한 측면을 담당한다.

ConversationRuntime<C, T>: 제네릭으로 설계된 대화 엔진

이 구조체가 Claw Code Rust 아키텍처의 핵심이다:

pub struct ConversationRuntime<C, T> {
    session: Session,
    api_client: C,
    tool_executor: T,
    permission_policy: PermissionPolicy,
    system_prompt: Vec<String>,
    max_iterations: usize,
    usage_tracker: UsageTracker,
    hook_runner: HookRunner,
    auto_compaction_input_tokens_threshold: u32,
}

impl<C, T> ConversationRuntime<C, T>
where
    C: ApiClient,
    T: ToolExecutor,
{
    pub fn run_turn(
        &mut self,
        user_input: impl Into<String>,
        mut prompter: Option<&mut dyn PermissionPrompter>,
    ) -> Result<TurnSummary, RuntimeError> { ... }
}

왜 제네릭인가? C: ApiClientT: ToolExecutor라는 타입 파라미터가 핵심이다. 이 설계로 인해 ConversationRuntime구체적인 API 클라이언트나 도구 실행기에 의존하지 않는다. 프로덕션에서는 AnthropicRuntimeClientCliToolExecutor를, 테스트에서는 mock 구현을 넣을 수 있다:

// 프로덕션
ConversationRuntime::new(session, AnthropicRuntimeClient::new(), CliToolExecutor::new(), ...)

// 테스트
ConversationRuntime::new(session, MockApiClient::new(), StaticToolExecutor::new(), ...)

PermissionPrompter만 트레이트 객체(dyn)를 사용하는 것도 의도적이다. 권한 프롬프터는 대화 턴마다 달라질 수 있으므로(예: 첫 번째 턴은 자동 승인, 두 번째부터는 사용자 확인), 런타임에 교체 가능해야 한다.

세 개의 핵심 트레이트

Claw Code의 Rust 아키텍처를 이해하려면, 이 세 트레이트의 계약(contract)을 정확히 이해해야 한다:

// 1. API 통신 추상화
pub trait ApiClient {
    fn stream(&mut self, request: ApiRequest)
        -> Result<Vec<AssistantEvent>, RuntimeError>;
}

// 2. 도구 실행 추상화
pub trait ToolExecutor {
    fn execute(&mut self, tool_name: &str, input: &str)
        -> Result<String, ToolError>;
}

// 3. 권한 확인 추상화
pub trait PermissionPrompter {
    fn decide(&mut self, request: &PermissionRequest)
        -> PermissionPromptDecision;
}

이 세 트레이트가 경계(boundary)를 형성한다. ConversationRuntime은 이 경계 안에서만 동작하며, 경계 너머의 구현 세부사항을 알 필요가 없다. 이것이 이 아키텍처의 테스트 용이성과 확장성의 근본이다.

run_turn() — 대화 한 턴의 전체 흐름

run_turn() 메서드는 약 150줄로, 대화 한 턴의 전체 생명주기를 관리한다. 흐름을 단계별로 풀어보면:

1. 사용자 메시지를 세션에 추가
2. 반복 루프 시작:
   a. 시스템 프롬프트 + 메시지 → ApiRequest 구성
   b. ApiClient::stream() → Vec<AssistantEvent> 수신
   c. 이벤트를 어시스턴트 메시지로 조립
   d. ToolUse 블록 추출
   e. 각 도구 호출에 대해:
      - PermissionPolicy로 권한 확인 (필요 시 PermissionPrompter 호출)
      - PreToolUse 훅 실행
      - ToolExecutor::execute() 호출
      - PostToolUse 훅 실행
      - 도구 결과를 세션에 저장
   f. 미처리 도구가 없으면 루프 종료
3. 자동 압축 임계값 확인
4. TurnSummary 반환 (메시지, 반복 횟수, 사용량, 압축 이벤트)

max_iterations로 무한 루프를 방지하는 것도 중요한 안전장치다. AI 에이전트가 도구를 호출하고, 그 결과를 보고 또 도구를 호출하는 사이클이 끝없이 반복될 수 있기 때문이다.


5. 권한 모델: 3단계 + 2가지 특수 모드

권한 시스템은 runtime/src/permissions.rs에 구현되어 있으며, 단순해 보이지만 정교한 설계를 담고 있다.

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PermissionMode {
    ReadOnly,
    WorkspaceWrite,
    DangerFullAccess,
    Prompt,
    Allow,
}

PartialOrd를 derive한 것에 주목하자. 이 순서(ReadOnly < WorkspaceWrite < DangerFullAccess < Prompt < Allow)가 권한 비교의 기초가 된다.

PermissionPolicy의 인가 로직

pub fn authorize(
    &self,
    tool_name: &str,
    input: &str,
    mut prompter: Option<&mut dyn PermissionPrompter>,
) -> PermissionOutcome {
    let current_mode = self.active_mode();
    let required_mode = self.required_mode_for(tool_name);

    // 현재 권한 >= 필요 권한이면 자동 승인
    if current_mode == PermissionMode::Allow || current_mode >= required_mode {
        return PermissionOutcome::Allow;
    }

    // 에스컬레이션이 필요한 경우 사용자에게 물어봄
    if current_mode == PermissionMode::Prompt
        || (current_mode == PermissionMode::WorkspaceWrite
            && required_mode == PermissionMode::DangerFullAccess)
    {
        return match prompter.as_mut() {
            Some(prompter) => match prompter.decide(&request) {
                PermissionPromptDecision::Allow => PermissionOutcome::Allow,
                PermissionPromptDecision::Deny { reason } =>
                    PermissionOutcome::Deny { reason },
            },
            None => PermissionOutcome::Deny { reason: /* ... */ },
        };
    }

    PermissionOutcome::Deny { reason: /* ... */ }
}

이 로직의 핵심은 "에스컬레이션" 개념이다. WorkspaceWrite 모드에서 bash(DangerFullAccess 필요)를 실행하려 하면, 자동 거부가 아니라 사용자에게 승인을 요청한다. 반면 ReadOnly 모드에서 파일 쓰기를 시도하면 즉시 거부된다. 이 비대칭이 실용성과 안전성의 균형점이다.

도구별 권한 요구사항

// 읽기만 필요한 도구
ToolSpec { name: "read_file", required_permission: PermissionMode::ReadOnly }

// 워크스페이스 수정이 필요한 도구
ToolSpec { name: "write_file", required_permission: PermissionMode::WorkspaceWrite }

// 시스템 전체 접근이 필요한 도구
ToolSpec { name: "bash", required_permission: PermissionMode::DangerFullAccess }

이 매핑이 ToolSpec에 정적으로 선언되어 있다는 점이 중요하다. 권한 요구사항이 도구 정의의 일부이므로, 도구를 추가할 때 반드시 권한을 명시해야 한다. "실수로 권한 없이 위험한 도구를 등록하는" 상황을 구조적으로 방지한다.


6. tools 크레이트 — 18개 도구의 디스패치 테이블

tools 크레이트는 AI 에이전트가 실제로 "행동"하는 부분이다.

ToolSpec: 도구의 자기 서술

pub struct ToolSpec {
    pub name: &'static str,
    pub description: &'static str,
    pub input_schema: Value,         // JSON Schema
    pub required_permission: PermissionMode,
}

namedescription&'static str인 것은 도구 메타데이터가 컴파일 타임에 고정됨을 의미한다. JSON Schema(input_schema)는 Anthropic API에 전달되어 AI가 올바른 형태의 입력을 생성하도록 안내한다.

mvp_tool_specs() 함수가 18개 도구의 전체 목록을 반환한다:

#[must_use]
pub fn mvp_tool_specs() -> Vec<ToolSpec> {
    vec![
        ToolSpec {
            name: "bash",
            description: "Execute a shell command in the current workspace.",
            input_schema: json!({
                "type": "object",
                "properties": {
                    "command": { "type": "string" },
                    "timeout": { "type": "integer", "minimum": 1 },
                    "description": { "type": "string" },
                    "run_in_background": { "type": "boolean" },
                    "dangerouslyDisableSandbox": { "type": "boolean" }
                },
                "required": ["command"],
                "additionalProperties": false
            }),
            required_permission: PermissionMode::DangerFullAccess,
        },
        // ... 17개 더
    ]
}

#[must_use] 어노테이션은 이 함수의 반환값을 무시하면 컴파일 경고가 발생한다는 뜻이다. 도구 목록을 생성해놓고 사용하지 않는 실수를 방지하는 세심한 장치다.

execute_tool(): 패턴 매칭 디스패치

pub fn execute_tool(name: &str, input: &Value) -> Result<String, String> {
    match name {
        "bash"        => from_value::<BashCommandInput>(input).and_then(run_bash),
        "read_file"   => from_value::<ReadFileInput>(input).and_then(run_read_file),
        "write_file"  => from_value::<WriteFileInput>(input).and_then(run_write_file),
        "edit_file"   => from_value::<EditFileInput>(input).and_then(run_edit_file),
        "glob_search" => from_value::<GlobSearchInputValue>(input).and_then(run_glob_search),
        "grep_search" => from_value::<GrepSearchInput>(input).and_then(run_grep_search),
        // ... 더 많은 도구들
        _ => Err(format!("unknown tool: {name}")),
    }
}

이 디스패치 패턴은 의도적으로 단순하다. 트레이트 객체나 레지스트리 패턴 대신 직접적인 match 문을 사용한 이유는 두 가지로 추정된다:

  1. 컴파일 타임 보장: 모든 도구가 패턴에 있는지 컴파일러가 확인
  2. 성능: 가상 함수 테이블(vtable) 없이 직접 디스패치

from_value::<T>(input).and_then(run_fn) 체인도 깔끔하다. JSON 역직렬화(from_value)와 실행(run_fn)을 and_then으로 연결하여, 입력 파싱 실패 시 실행 자체가 건너뛰어진다.

StaticToolExecutor: 테스트를 위한 빌더

pub struct StaticToolExecutor {
    handlers: BTreeMap<String, ToolHandler>,
}

impl StaticToolExecutor {
    pub fn register(
        mut self,
        tool_name: impl Into<String>,
        handler: impl FnMut(&str) -> Result<String, ToolError> + 'static,
    ) -> Self {
        self.handlers.insert(tool_name.into(), Box::new(handler));
        self
    }
}

self를 값으로 받고 Self를 반환하는 소유권 이전 빌더 패턴이다. 이 패턴의 장점은 체이닝이 가능하면서도, 불완전한 빌더를 실수로 사용하는 것을 방지한다는 것이다:

let executor = StaticToolExecutor::new()
    .register("bash", |input| Ok("output".into()))
    .register("read_file", |input| Ok("content".into()));

7. commands 크레이트 — 슬래시 명령어의 세계

SlashCommand 열거형: 정적 타이핑의 힘

pub enum SlashCommand {
    Help,
    Status,
    Compact,
    Model { model: Option<String> },
    Permissions { mode: Option<String> },
    Clear { confirm: bool },
    Cost,
    Resume { session_path: Option<String> },
    Config { section: Option<String> },
    Memory,
    Init,
    Diff,
    Version,
    Export { path: Option<String> },
    Session { action: Option<String>, target: Option<String> },
    // ...
    Unknown(String),
}

TypeScript 원본에서 슬래시 명령어는 문자열 기반이었을 것이다. Rust에서는 이를 열거형의 각 변형(variant)으로 모델링했다. 이로 인해 모든 명령어가 타입 시스템에 등록되고, match 문에서 누락된 명령어가 있으면 컴파일 에러가 발생한다.

Unknown(String) 변형은 확장성을 위한 탈출구다. 알려지지 않은 명령어를 에러가 아닌 데이터로 처리함으로써, 플러그인 시스템이 추가될 때 확장 가능하다.

명령어 파싱의 이중 구조

impl SlashCommand {
    pub fn parse(input: &str) -> Option<Self> {
        let trimmed = input.trim();
        if !trimmed.starts_with('/') {
            return None;     // 슬래시로 시작하지 않으면 명령어가 아님
        }
        // 파싱 로직...
    }
}

pub fn handle_slash_command(
    input: &str,
    session: &Session,
    compaction: CompactionConfig,
) -> Option<SlashCommandResult> {
    // /help, /compact 같은 로컬 명령만 처리
    // /status, /commit 같은 런타임 의존 명령은 None 반환
}

handle_slash_commandOption을 반환하는 것이 핵심이다. 이 함수가 None을 반환하면, 해당 명령어는 런타임 레벨에서 처리된다. 이 이중 구조로 인해, commands 크레이트는 runtime에 최소한으로만 의존하면서도 전체 명령어 세트를 지원한다.


8. rusty-claude-cli — 모든 것이 만나는 곳

CLI 크레이트는 5개의 라이브러리 크레이트를 조합하여 실제 바이너리를 만든다.

LiveCli: 세션 관리자

struct LiveCli {
    model: String,
    allowed_tools: Option<AllowedToolSet>,
    permission_mode: PermissionMode,
    system_prompt: Vec<String>,
    runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
    session: SessionHandle,
}

여기서 ConversationRuntime의 제네릭 파라미터가 구체화된다: <AnthropicRuntimeClient, CliToolExecutor>. 이것이 이 아키텍처의 조립 지점(composition root)이다. 추상화가 구체화로 바뀌는 유일한 장소.

AnthropicRuntimeClient: ApiClient의 실체

struct AnthropicRuntimeClient {
    runtime: tokio::runtime::Runtime,   // Tokio 비동기 런타임
    client: AnthropicClient,
    model: String,
    enable_tools: bool,
    emit_output: bool,
    allowed_tools: Option<AllowedToolSet>,
}

impl ApiClient for AnthropicRuntimeClient {
    fn stream(&mut self, request: ApiRequest)
        -> Result<Vec<AssistantEvent>, RuntimeError> {
        self.runtime.block_on(async {
            let mut stream = self.client
                .stream_message(&message_request)
                .await?;
            // SSE 이벤트 스트리밍 → 터미널 렌더링 → AssistantEvent 수집
        })
    }
}

block_on이 눈에 띈다. ApiClient 트레이트는 동기(sync) 인터페이스지만, 실제 HTTP 호출은 비동기(async)다. block_on으로 비동기를 동기로 변환하는 것은, 대화 루프 자체는 동기적으로 유지하겠다는 설계 결정이다. 이는 ConversationRuntime이 async가 아닌 일반 struct로 남을 수 있게 해주며, 코드 복잡도를 크게 줄여준다.

기본 설정값의 의미

const DEFAULT_MODEL: &str = "claude-opus-4-6";
const DEFAULT_OAUTH_CALLBACK_PORT: u16 = 4545;

기본 모델이 claude-opus-4-6인 것은 Claw Code가 최고 성능의 모델을 기본으로 사용한다는 것이다. 비용보다 기능을 우선시하는, 파워 유저 지향 도구의 특성이 반영되어 있다.


9. compat-harness — TypeScript와의 다리

// compat-harness/src/lib.rs
pub fn extract_manifest() -> Manifest {
    // TypeScript 원본에서 명령어/도구/부트스트랩 정보 추출
}

이 크레이트는 원본 TypeScript 코드의 "표면적(surface area)"을 파악하기 위한 것이다. commands, tools, runtime 모두에 의존하는 유일한 크레이트라는 점에서, 전체 시스템의 "메타 관찰자" 역할을 한다. PARITY.md의 기능 격차 추적이 이 크레이트에 의존한다.


10. 세션 관리와 압축 전략

Session: 대화의 영속화

pub struct Session {
    pub version: u32,
    pub messages: Vec<ConversationMessage>,
}

pub struct ConversationMessage {
    pub role: MessageRole,
    pub blocks: Vec<ContentBlock>,
    pub usage: Option<TokenUsage>,
}

pub enum ContentBlock {
    Text { text: String },
    ToolUse { id: String, name: String, input: String },
    ToolResult {
        tool_use_id: String,
        tool_name: String,
        output: String,
        is_error: bool,
    },
}

ContentBlock 열거형이 Text, ToolUse, ToolResult 세 가지를 담는 것은 Anthropic API의 메시지 구조를 직접 반영한다. 특히 ToolResulttool_nameis_error가 포함된 것은 세션 로그를 나중에 분석할 때 유용하다.

compact_session(): 토큰 예산 관리

pub struct CompactionConfig {
    pub preserve_recent_messages: usize,  // 기본값: 4
    pub max_estimated_tokens: usize,      // 기본값: 10,000
}

pub fn compact_session(
    session: &Session,
    config: CompactionConfig,
) -> CompactionResult {
    // 1. 압축 필요 여부 판단
    // 2. 오래된 메시지를 요약으로 교체
    // 3. 최근 메시지만 보존
    // 4. 시스템 메시지에 컨텍스트 계속 프롬프트 추가
}

이 압축 전략은 Anthropic API의 컨텍스트 윈도우 제한을 관리하는 핵심 메커니즘이다. 오래된 대화를 통째로 버리는 대신 요약으로 교체함으로써, AI가 이전 맥락을 잃지 않으면서도 토큰 예산 내에서 동작할 수 있다.

preserve_recent_messages = 4가 기본값인 이유는 경험적이다. 가장 최근의 사용자 메시지, AI 응답, 도구 호출, 도구 결과 — 이 네 개가 현재 대화의 "작업 기억(working memory)"에 해당하기 때문이다.


11. 사용량 추적과 비용 추정

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TokenUsage {
    pub input_tokens: u32,
    pub output_tokens: u32,
    pub cache_creation_input_tokens: u32,
    pub cache_read_input_tokens: u32,
}

impl TokenUsage {
    pub fn estimate_cost_usd(self) -> UsageCostEstimate {
        self.estimate_cost_usd_with_pricing(
            ModelPricing::default_sonnet_tier()
        )
    }
}

cache_creation_input_tokenscache_read_input_tokens는 Anthropic의 프롬프트 캐싱 기능을 반영한다. 캐시 생성 토큰과 캐시 읽기 토큰의 가격이 다르므로, 정확한 비용 추정을 위해 이들을 별도로 추적해야 한다.

UsageTracker가 세션 전체의 누적 사용량을 추적하는 것도 실용적이다. 개발자가 "이 세션에 얼마나 썼는가"를 실시간으로 확인할 수 있다.


마치며: 설계 패턴의 종합

Claw Code의 Rust 워크스페이스를 관통하는 설계 원칙을 정리하면:

트레이트 기반 추상화: ApiClient, ToolExecutor, PermissionPrompter 세 트레이트가 시스템의 핵심 경계를 형성한다. 이 경계 덕분에 각 구성요소를 독립적으로 테스트하고 교체할 수 있다.

제네릭 + 트레이트 바운드: ConversationRuntime<C: ApiClient, T: ToolExecutor>처럼 제네릭과 트레이트 바운드를 결합하여, 컴파일 타임에 타입 안전성을 보장하면서도 구현을 유연하게 교체할 수 있다.

소유권 이전 빌더: StaticToolExecutor::new().register(...) 패턴처럼, 소유권을 이전하면서 빌드하여 불완전한 객체 사용을 방지한다.

계층적 의존성: 순환 없는 DAG 구조로, 각 크레이트가 자신보다 하위의 크레이트에만 의존한다.

명시적 에러 처리: ApiError, RuntimeError, ToolError, SessionError 등 계층별 에러 타입으로 에러의 출처를 명확히 한다.

다음 편에서는 이 아키텍처의 핵심 통로인 API 통신과 SSE 스트리밍을 더 깊이 파고들어, SseParser가 바이트 스트림을 어떻게 구조화된 이벤트로 변환하는지 살펴본다.


실습 가이드

# 1. Rust 빌드 및 바이너리 크기 확인
cd rust && cargo build --release
ls -lh target/release/claw

# 2. 크레이트별 코드 라인 수 분석
cargo install tokei
tokei crates/

# 3. 의존성 그래프 시각화
cargo install cargo-depgraph
cargo depgraph --workspace-only | dot -Tpng > deps.png

# 4. ToolSpec 구조 확인
grep -n "ToolSpec" crates/tools/src/lib.rs

# 5. ConversationRuntime 제네릭 파라미터 추적
grep -rn "ConversationRuntime" crates/

이 글은 [Claw Code 깊이 읽기] 시리즈의 2편입니다.

시리즈 목차:
1. 프로젝트 배경과 아키텍처 개요
2. Rust 워크스페이스 아키텍처 심층 분석 ← 현재 글
3. API 통신과 SSE 스트리밍 구현
4. 대화 런타임과 세션 관리
5. 도구 시스템과 권한 모델
6. Python 포팅 워크스페이스 분석
7. 테스팅 전략과 패리티 추적
8. TUI 개선 로드맵과 미래 방향

태그: #ClawCode #Rust #아키텍처 #트레이트 #제네릭 #크레이트 #워크스페이스 #AI에이전트

profile
꿈꾸는 개발자

0개의 댓글