지식위키

Codex · 도구 시스템 (ToolSpec 정의 형식과 모델 노출 / ToolRouter 디스패치)

Codex · 도구 시스템 (ToolSpec 정의 형식과 모델 노출 / ToolRouter 디스패치)

한 줄 요약

Codex는 AI에게 “쓸 수 있는 도구 목록”을 JSON으로 만들어 건네고, AI가 “이 도구를 쓰겠다”고 답하면 그 호출을 실제 코드로 연결해 실행하는 중개소(ToolRouter)를 둔다. 왜 배우나: AI가 쉘 실행·파일 수정·웹검색 같은 “실제 행동”을 할 수 있는 이유가 전부 이 도구 시스템에서 시작되기 때문이다.

그림

flowchart TD
  A[턴 시작] --> B["도구 목록 새로 계산<br/>build_tool_router: 기능/모델/설정 평가"]
  B --> C["핸들러마다 명세·노출방식 수집"]
  C --> D{"노출 방식?"}
  D -- 바로 노출 Direct --> E[모델에 보이는 도구 목록]
  D -- 검색용 Deferred --> F[tool_search 색인에만 등록]
  E --> G[도구 목록을 JSON으로 변환해 모델에 전달]
  G --> H["모델: 이 도구를 이 인자로 쓰겠다 응답"]
  H --> I[응답을 ToolCall로 해석]
  I --> J[이름으로 핸들러 찾아 실행]
  J --> K[실행 결과를 다시 모델에 돌려줌]
  H -. tool_search 호출 .-> F
  F -. 검색 매칭된 도구 .-> G

쉽게 풀기

도구 시스템을 음식점의 메뉴판과 주문 시스템에 비유하면 쉽다.

  1. 도구 정의 = 메뉴 항목 하나 만들기. 메뉴 항목은 결국 세 가지로 끝난다 — 음식 이름, 음식 설명, 그리고 “주문서에 뭘 적어야 하는지”(예: 매운맛 단계, 양 선택). 도구도 똑같이 이름(name) + 설명(description) + 입력 형식(parameters, JSON 스키마) 세 가지로 정의된다.

  2. 모델 노출 = 손님에게 메뉴판 건네기. 주방은 만들 수 있는 모든 음식 중에서 “오늘 낼 수 있는 것”만 골라 메뉴판으로 인쇄한다. Codex도 매 턴마다 지금 모델·설정·환경에서 켤 수 있는 도구만 골라 JSON 목록으로 만들어 모델에게 건넨다.

  3. 실행 = 주문 받아 주방에 넘기기. 손님(모델)이 “1번 메뉴를 이렇게 주문할게요”라고 하면, 주문 시스템(ToolRouter)이 그 이름으로 담당 요리사(핸들러)를 찾아 실제 조리를 시키고, 완성된 음식(결과)을 손님에게 돌려준다.

여기에 영리한 장치가 하나 더 있다. 메뉴가 수백 개라면 메뉴판이 너무 두꺼워진다(=토큰 폭증). 그래서 Codex는 자주 안 쓰는 메뉴는 메뉴판에서 빼두고, 대신 “메뉴 검색대”(tool_search) 하나만 올려둔다. 손님이 “한식 매운 거 뭐 있어요?”라고 검색하면 그때서야 해당 메뉴들이 메뉴판에 추가되는 식이다. 이걸 deferred(지연) 도구라고 부른다.

핵심 정리

도구 하나의 최상위 형식은 ToolSpec이라는 enum이고, 어떤 종류인지에 따라 JSON의 "type" 값이 달라진다.

종류(variant)JSON "type"쓰임새
Function"function"가장 흔한 일반 도구 (shell, apply_patch 등)
Namespace"namespace"여러 function 도구를 한 묶음으로 (MCP 서버 등)
ToolSearch"tool_search"지연 도구를 검색해 꺼내오는 메타 도구
ImageGeneration"image_generation"이미지 생성
WebSearch"web_search"웹검색
Freeform"custom"문법 기반 자유형식 도구

[!note] "function" 한 개의 실제 필드 (ResponsesApiTool)

  • name (필수): 모델이 호출할 때 쓰는 이름
  • description (필수): 모델이 읽는 자연어 설명
  • strict (필수): Structured Outputs strict 모드 여부 — 현재 대부분 false
  • parameters (필수): 입력 인자 JSON 스키마(JsonSchema)
  • defer_loading (선택): true면 초기 목록에서 빼고 tool_search로만 노출 / None이면 생략
  • output_schema (내부용): #[serde(skip)]이라 모델에는 가지 않음

[!note] 입력 형식을 적는 JsonSchema — OpenAI Structured Outputs의 서브셋 핵심 키만: "type"(string/number/object/array…), description, "enum"(허용 값), items(배열 요소), properties(객체 속성), required(필수 속성), "additionalProperties". 합성·참조용으로 any_of/one_of/all_of, "$ref", "$defs"도 지원. 생성 헬퍼: JsonSchema::string/number/boolean/integer/array/object/string_enum(...). 외부(MCP)에서 들어온 스키마는 parse_tool_input_schema()가 정리→가지치기→압축(약 1k 토큰/4000바이트 예산 초과 시 손실 압축)한다.

노출 방식(exposure) 4가지 — 핸들러가 자기 도구를 모델에 어떻게 보일지 정한다.

  • Direct — 처음부터 도구 목록에 보임 (기본값)
  • DirectModelOnly — 모델에만 직접 노출
  • Deferred — 목록에서 빼고 tool_search로만 검색 노출 (이때 search_info 필요)
  • Hidden — 목록에 안 보이고 디스패치(내부 호출)만 가능

[!note] 모델 변환 전 중립 메타데이터 (ToolDefinition) 다운스트림(특히 MCP 도구)은 ToolSpec으로 바뀌기 전 ToolDefinition(name·description·input_schema·output_schema·defer_loading)을 거친다. tool_definition_to_responses_api_tool()이 이걸 ResponsesApiTool로 변환한다(strict: false). .into_deferred()를 붙이면 output_schema를 비우고 지연 로딩으로 바꾼다.

실제 예시

먼저 실제로 모델에게 나가는 JSON 모양이다(테스트가 보장).

// /home/seunghyeong/harness-work/codex/codex-rs/tools/src/tool_spec_tests.rs
// ToolSpec::Function → create_tools_json_for_responses_api 결과
{
  "type": "function",
  "name": "demo",
  "description": "A demo tool",
  "strict": false,
  "parameters": {
    "type": "object",
    "properties": { "foo": { "type": "string" } }
  }
}
// ToolSpec::Namespace
{
  "type": "namespace",
  "name": "mcp__demo__",
  "description": "Demo tools",
  "tools": [ { "type": "function", "name": "lookup_order", "description": "Look up an order",
               "strict": false, "parameters": { "type": "object",
               "properties": { "order_id": { "type": "string" } } } } ]
}
// ToolSpec::ToolSearch
{
  "type": "tool_search",
  "execution": "sync",
  "description": "Search app tools",
  "parameters": { "type": "object", "properties": {
      "query": { "type": "string", "description": "Tool search query" } },
      "required": ["query"], "additionalProperties": false }
}

실제 빌트인 도구 정의 예 — shell_command (복붙용 패턴).

// /home/seunghyeong/harness-work/codex/codex-rs/core/src/tools/handlers/shell_spec.rs
ToolSpec::Function(ResponsesApiTool {
    name: "shell_command".to_string(),
    description: "Runs a shell command and returns its output.\n- Always set the `workdir` ...".to_string(),
    strict: false,
    defer_loading: None,
    parameters: JsonSchema::object(
        BTreeMap::from([
            ("command".to_string(), JsonSchema::string(Some("Shell script to run ...".to_string()))),
            ("workdir".to_string(), JsonSchema::string(Some("Working directory ...".to_string()))),
            ("timeout_ms".to_string(), JsonSchema::number(Some("Maximum command runtime ...".to_string()))),
        ]),
        Some(vec!["command".to_string()]),   // required
        Some(false.into()),                  // additionalProperties: false
    ),
    output_schema: None,
})

새 빌트인 도구 echo_tool을 직접 만든다면 (스펙 + 핸들러 + 등록).

// 1) 스펙: ToolSpec::Function 으로 name + JSON 스키마 정의
use codex_tools::{JsonSchema, ResponsesApiTool, ToolSpec};
use std::collections::BTreeMap;

pub fn create_echo_tool() -> ToolSpec {
    ToolSpec::Function(ResponsesApiTool {
        name: "echo_tool".to_string(),
        description: "Echoes the given text back.".to_string(),
        strict: false,
        defer_loading: None,
        parameters: JsonSchema::object(
            BTreeMap::from([(
                "text".to_string(),
                JsonSchema::string(Some("Text to echo.".to_string())),
            )]),
            Some(vec!["text".to_string()]),  // required
            Some(false.into()),              // additionalProperties: false
        ),
        output_schema: None,
    })
}

// 2) 핸들러: ToolExecutor + CoreToolRuntime 구현 (plan.rs 패턴 참고)
pub struct EchoHandler;
impl ToolExecutor<ToolInvocation> for EchoHandler {
    fn tool_name(&self) -> ToolName { ToolName::plain("echo_tool") }
    fn spec(&self) -> ToolSpec { create_echo_tool() }
    // exposure() 기본값 = Direct → 초기 tool 목록에 노출됨
    fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> {
        Box::pin(async move {
            let ToolPayload::Function { arguments } = invocation.payload else {
                return Err(FunctionCallError::RespondToModel("bad payload".into()));
            };
            // arguments(JSON 문자열) 파싱 후 처리 → Box<dyn ToolOutput> 반환
            todo!()
        })
    }
}
impl CoreToolRuntime for EchoHandler {}

// 3) 등록: spec_plan.rs 의 add_core_utility_tools 등에서
//    planned_tools.add(EchoHandler);   // (조건부면 feature 체크 후)

[!note] 만들 때 체크리스트

  • ToolSpec::Functionname/description/parameters(JsonSchema) 세 가지를 채웠다.
  • requiredadditionalProperties:false를 명시했다(strict 미사용이어도 권장).
  • 핸들러가 tool_name()(=spec의 name과 동일!)·spec()·handle()을 구현했고 CoreToolRuntime을 단다.
  • exposure(): 항상 노출=Direct, 검색으로만=Deferred(이때 search_info 필요), 디스패치만=Hidden.
  • 동시 실행 허용이면 supports_parallel_tool_calls()를 true로(부작용 없는 read-only 도구만).
  • spec_plan.rsadd_* 함수에서 planned_tools.add(...)로 등록(feature/모델 조건 포함).
  • 이름 충돌 주의: registry는 중복 이름 등록 시 panic/에러.

요약 & 셀프체크

3줄 요약:

  1. 도구 하나는 이름 + 설명 + 입력 스키마로 정의되고, ToolSpec enum의 종류에 따라 JSON "type"이 달라진다.
  2. 매 턴마다 build_tool_router()가 켤 도구를 새로 골라 JSON으로 모델에 건네고, 모델이 호출하면 ToolRouter가 이름으로 핸들러를 찾아 실행 후 결과를 되돌린다.
  3. 도구가 너무 많으면 Deferred로 빼두고 tool_search(BM25 검색) 한 개만 노출해 토큰 폭증 없이 온디맨드로 꺼낸다.

스스로 답해보기:

  • 도구를 정의하는 세 가지 핵심 요소는 무엇인가? (이름이 핸들러와 일치해야 하는 이유는?)
  • DirectDeferred 노출의 차이는 무엇이며, 수백 개 MCP 도구에 Deferred를 쓰는 이유는?
  • 모델이 응답한 function-call이 실제 코드로 연결되기까지 거치는 단계(파싱→디스패치→실행→반환)를 말로 설명할 수 있는가?

[!note] 더 깊이 — 언제 무엇이 켜지고, 어떻게 흘러가나 언제: 매 턴마다. ToolRouter::from_turn_context()build_tool_router()가 모델 정보(ModelInfo)·기능 플래그(Features)·설정(Config)·MCP/확장/동적 도구를 보고 목록을 새로 계산한다. 어떤 빌트인이 켜지나: shell 계열은 shell_type_for_model_and_features()UnifiedExec(=exec_command+write_stdin) / shell_command / Disabled 중 결정. apply_patch는 환경+model_info.apply_patch_tool_type.is_some()일 때. update_plan/view_image/get_context_remaining/request_permissions 등은 환경·feature 플래그로 on/off. web_search/image_generation은 hosted_model_tool_specs()에서 provider capability·auth·feature로 결정(use_responses_lite 모델은 호스티드 도구 미전송). MCP/확장/동적 도구는 런타임 목록을 그대로 등록. 노출 단계: 각 핸들러의 spec()·exposure()를 모아 build_model_visible_specs_and_registry()가 Direct인 것만 Vec<ToolSpec>으로 만들고, 같은 네임스페이스 function을 merge_into_namespaces()로 묶는다. 상위에서 create_tools_json_for_responses_api()로 직렬화되어 요청의 tools 필드가 된다. 실행 단계: 모델 응답의 ResponseItemToolRouter::build_tool_call()ToolCall로 파싱(FunctionCall→Function, ToolSearchCall→ToolSearch, CustomToolCall→Custom). ToolRegistry::dispatch_any_with_terminal_outcome()가 이름으로 핸들러를 찾아 PreToolUse 훅 → handle() → PostToolUse 훅 → 텔레메트리 처리 후 FunctionCallOutput으로 반환. 미등록 이름은 “unsupported call: …” 에러 응답. 병렬 실행: ToolRouterRuntime(parallel.rs)이 각 호출을 tokio::spawn하되 supports_parallel_tool_calls()가 true일 때만 동시 실행. false면 직렬화. tool_search 동작: Deferred 도구의 search_info()(BM25 메타데이터)를 모아 tool_search 한 개를 노출. 모델이 tool_search(query, limit)를 부르면 BM25(bm25 크레이트) 검색→coalesce_loadable_tool_specs로 묶어 반환→다음 모델 호출의 tool 목록에 동적 추가.

[!tip] 기존 Codex 교차검증 메모(보존) 근거 파일:

  • /home/seunghyeong/harness-work/codex/codex-rs/tools/src/tool_spec.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/tools/src/tool_definition.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/tools/src/responses_api.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/tools/src/json_schema.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/tools/src/tool_config.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/tools/src/tool_executor.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/tools/src/tool_spec_tests.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/tools/registry.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/tools/router.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/tools/spec_plan.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/tools/parallel.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/tools/handlers/shell_spec.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/tools/handlers/plan.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/tools/handlers/plan_spec.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/tools/handlers/tool_search.rs
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/tools/handlers/tool_search_spec.rs

연결

CX_개요 · _분석축_루브릭