gajae-code · 도구 시스템: 정의 형식과 모델 노출

한 줄 요약

AI가 쓸 수 있는 모든 능력(파일 읽기·명령 실행·코드 수정)은 도구(tool) 한 단위로 정의되고, 그중 어떤 것을 모델에게 보여줄지 까다롭게 골라 노출한다. 왜 배우나: “에이전트가 할 수 있는 것”과 “지금 보여주는 것”을 구분하는 규칙이 여기서 정해지기 때문이다.

그림

flowchart TD
  A["세션 시작 · createTools(요청 도구 목록)"] --> B["자동 동반 추가: AST/recipe/goal 짝꿍"]
  B --> C{"이 도구 켜도 되나?<br/>isToolAllowed"}
  C -->|허용| D["공장 호출 → AgentTool 인스턴스"]
  C -->|거부| X["목록에서 제외"]
  D --> E["메타 안내문으로 감싸기"]
  E --> F{"노출 모드 loadMode"}
  F -->|essential 필수| G["초기 도구 배열 → 모델에게 보여줌"]
  F -->|discoverable 검색형| H["숨김 색인에 보관"]
  H -.->|"모델이 search_tool_bm25로 검색"| I["activateDiscoveredTools 활성화"]
  I --> G
  G --> J["모델이 tool_call 호출"]
  J --> K["execute 실행 → 결과(content + details)"]
  K --> L["renderResult 로 화면 표시"]
  M["도구 강제 큐 ToolChoiceQueue"] -->|"다음 턴 이 도구 써라"| G
  M -->|"강제 실패 → 약화(degrade)"| M

쉽게 풀기

도구 시스템을 “공구함을 갖춘 작업자”에 비유하면 쉽다.

flowchart LR
  subgraph 공구함["공구함 = BUILTIN_TOOLS 레지스트리"]
    F1["이름 → 공장(factory)"]
  end
  F1 -->|"BUILTIN_TOOLS[name](session)"| T["도구 1개 = AgentTool<br/>name·label·description·parameters"]
  T --> 서랍{loadMode}
  서랍 -->|essential| 눈앞["작업자(모델) 눈앞"]
  서랍 -->|discoverable| 서랍속["서랍 보관 → 검색 시 꺼냄"]

1. 도구 1개 = 규격에 맞춘 공구. 도구 하나는 AgentTool 인터페이스를 만족하는 객체로, 이름표(name)·UI 라벨(label)·설명서(description)·입력 양식(parameters)을 갖춘다.

2. 공구함 = 레지스트리. 모든 빌트인 도구는 BUILTIN_TOOLS(“이름 → 만드는 법”) 사전에 등록된다. BUILTIN_TOOLS["read"](session)처럼 이름+세션을 넣으면 그 자리에서 찍어낸다. 미리 만들지 않고 필요할 때 생성한다.

3. 한꺼번에 안 보여준다 (점진 공개). 공구가 수십 개면 작업자가 헷갈린다. 처음엔 read·bash·edit 같은 필수(essential) 만 꺼내 두고, 나머지는 서랍에 넣는다. 모델이 search_tool_bm25로 찾으면 그때 꺼내 준다. 이 점진 공개(progressive disclosure) 가 선택 품질 저하와 토큰 낭비를 막는다.

4. “이번엔 꼭 이 공구 써” 강제 (ToolChoice). 모델에게 “다음 턴엔 반드시 이 도구”라고 못 박을 수 있다. 단 프로바이더마다 강제 정도가 달라, 정확히 지명되는 곳도 있고 “아무 도구나 하나는 써”까지만 되는 곳도 있다. 지명이 안 되면 강제를 한 단계 약화(degradation)시키며, 이때도 무한 반복에 빠지지 않게 안전장치를 둔다.

핵심 정리

도구 정의의 3겹 구조

  • 베이스 Tool — 공통 토대. 이름·설명·입력양식 (packages/ai/src/types.ts:667)
  • 확장 AgentTool — UI 라벨·노출 모드·동시성 등 에이전트용 메타 추가 (packages/agent/src/types.ts:411)
  • 결과 AgentToolResult — 모델용 내용과 로그용 데이터를 분리 (packages/agent/src/types.ts:370)

베이스 Tool<TParameters>의 핵심 필드:

이름필수설명
name호출/디스패치용 이름 (예: "read")
description모델에게 보이는 설명(보통 프롬프트 템플릿 렌더 결과)
parameters인자 스키마(zod). 모델엔 JSON Schema로 노출

확장 AgentTool이 더하는 자주 쓰는 메타:

이름필수설명
labelUI 표시용 이름 (예: "Read")
loadMode"essential"(초기 로드) vs "discoverable"(검색으로만 활성화)
execute메인 실행 콜백 → Promise<AgentToolResult>

실행 결과 AgentToolResult<T>:

이름필수설명
content모델에게 돌아가는 콘텐츠 블록 (TextContent | ImageContent)[]
detailsUI/로그용 구조화 데이터(영수증 성격)
isError비throw 실패 표식. agent-loop가 와이어 tool error로 변환

레지스트리/essential 해석 심볼 (packages/coding-agent/src/tools/index.ts):

심볼설명
BUILTIN_TOOLS공개 빌트인 레지스트리. BUILTIN_TOOLS[name](session)
HIDDEN_TOOLSyield/report_finding/resolve 등 숨김 도구
DEFAULT_ESSENTIAL_TOOL_NAMESoverride 비었을 때 기본 ["read","bash","edit"]
computeEssentialBuiltinNames(settings)override 있으면 그것(빌트인 존재 이름만), 없으면 기본값

실제 예시

공구함 — 이름→공장 레지스트리. 등록 패턴은 두 가지다(항상 생성 vs 조건부 생성):

// packages/coding-agent/src/tools/index.ts
export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = ["read", "bash", "edit"] as const;
 
export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
	read: s => new ReadTool(s),
	bash: s => new BashTool(s),
	edit: s => new EditTool(s),
	ast_grep: s => new AstGrepTool(s),
	github: GithubTool.createIf,          // createIf → 조건부 생성(불가시 null)
	search_tool_bm25: SearchToolBm25Tool.createIf,
	skill: SkillTool.createIf,
	goal: s => new GoalTool(s),
	// ...
};

항상 만들어지는 도구는 s => new XTool(s), 설정/환경 따라 빠질 수 있는 도구는 정적 XTool.createIf(null 반환 가능). null이면 그 턴 목록에서 빠진다.

입력 양식(args 스키마)은 zod로 정의하고, 각 필드의 .describe() 텍스트가 그대로 모델에게 인자 설명으로 나간다:

// read.ts — strict 1필드, 인라인 셀렉터
const readSchema = z.object({
	path: z.string().describe('path or url; append :<sel> for line ranges (e.g. "src/foo.ts:50-100")'),
}).strict();
 
// ast-grep.ts — discoverable + summary
const astGrepSchema = z.object({
	pat: z.string().describe("ast pattern"),
	paths: z.array(z.string()).min(1).describe("files, directories, globs, or internal URLs"),
	skip: z.number().default(0).optional(),
});
// readonly loadMode = "discoverable"; readonly summary = "Search code with AST patterns";

새 도구 만들기 체크리스트:

  • name(와이어), label(UI), description, parameters(zod, 각 필드 .describe()).
  • loadMode: essential vs discoverable. discoverable이면 summary 필수.
  • strict/nonAbortable/concurrency(부작용 큰 도구는 exclusive) 결정.
  • executeAgentToolResult 반환 — 모델용 content + 영수증 details 분리.
  • 렌더러는 renderCall/renderResult로 분리, toolRenderers(renderers.ts)에 등록.
  • BUILTIN_TOOLS에 등록(조건부면 createIf).
  • 설정 게이팅 필요 시 createToolsisToolAllowed에 분기 추가.
  • essential 화이트리스트 변경은 tools.essentialOverride 설정 사용.

도구가 모델에게 노출되기까지 (선택 파이프라인)

세션 생성 시 createTools(session, toolNames?)(index.ts:401)가 호출되며 노출 도구가 결정된다. 단계는 아래 흐름과 같다.

flowchart TD
  S1["1 요청 정규화<br/>소문자·중복제거, goal/AST 짝꿍 자동추가"] --> S2{"2 isToolAllowed(name)<br/>설정값 게이팅"}
  S2 -->|통과| S3["3 factory(session) 생성<br/>+ wrapToolWithMetaNotice"]
  S2 -->|"거부/null"| 버림["제외"]
  S3 --> S4["4 resolve 없으면 자동 추가"]
  S4 --> S5{"5 loadMode"}
  S5 -->|essential| 초기["초기 목록 → 모델 노출"]
  S5 -->|discoverable| 숨김["숨김 색인 보관"]
  숨김 -.->|search_tool_bm25 검색| 초기
  1. 요청 정규화: toolNames가 있으면 소문자/중복제거. goal.enabledgoal 자동 추가. text 도구가 있으면 AST 짝꿍 자동 동반(searchast_grep, editast_edit, bashrecipe — 각 설정 켜진 경우).
  2. 허용 게이팅 isToolAllowed(name): 도구별 설정값 확인. 예) evalallowEval, lspenableLsp && lsp.enabled, search_tool_bm25discoveryActive, task는 재귀깊이(task.maxRecursionDepth vs taskDepth). bash는 항상 true.
  3. 공장 호출: 통과 이름들을 factory(session)로 만들고 wrapToolWithMetaNotice로 감싼다. null은 버린다.
  4. resolve 보강: 결과에 resolve가 없으면 항상 추가(보류 액션 해소용 숨김 도구).
  5. essential vs discoverable: essential만 초기 목록에 들어가고, discoverable(예: ast_grep)은 숨겨뒀다가 search_tool_bm25로만 활성화된다.

도구 강제(ToolChoice)는 프로바이더별로 정확도가 달라, 정확 지명이 안 되면 능력 약화로 떨어진다.

flowchart TD
  Q["ToolChoiceQueue 지시<br/>'다음 턴 이 도구 써라'"] --> B["buildNamedToolChoiceResult(name, model)"]
  B -->|"Anthropic/Bedrock"| N1["{"type:tool, name"} 정확 지명"]
  B -->|"OpenAI/Ollama"| N2["{"type:function, name"} 정확 지명"]
  B -->|Google 계열| D1["'required' 지명불가 → degradation"]
  N1 --> R{"resolveToolChoice<br/>exactNamed?"}
  N2 --> R
  D1 --> R
  R -->|정확| OK["resolve/todo_write/yield 등 안전 게이트 통과"]
  R -->|실패| DG["degradeInFlight() → 지시 드롭<br/>무한 재큐 방지"]

요약 & 셀프체크

3줄 요약:

  • 도구는 AgentTool 규격을 만족하는 객체이고, BUILTIN_TOOLS 사전에 “이름→공장”으로 등록돼 필요할 때 찍어낸다.
  • 모델엔 read·bash·edit 같은 필수(essential)만 먼저 보여주고, 나머지(discoverable)는 search_tool_bm25 검색으로만 꺼내는 점진 공개를 쓴다.
  • “다음 턴에 이 도구 써라” 강제(ToolChoice)는 프로바이더별 정확도가 다르고, 정확 지명이 안 되면 능력을 한 단계 약화시키며 무한 재큐를 막는다.

스스로 답해보기:

  1. loadModeessential인 도구와 discoverable인 도구는 모델에게 노출되는 방식이 각각 어떻게 다른가?
  2. BUILTIN_TOOLS에서 s => new XTool(s) 패턴과 XTool.createIf 패턴은 언제 어느 것을 쓰며, 둘의 차이는 무엇인가?
  3. Google 계열 프로바이더에서 “정확히 이 도구를 써라” 강제가 왜 그대로 통하지 않으며, 시스템은 그때 어떻게 대처하는가?

연결

GJ_개요 · _분석축_루브릭 · GJ_10_agent-loop · GJ_50_mcp-integration · GJ_70_guardrails-sandbox-permission-gating

근거 파일

  • /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/tools/index.ts (BUILTIN_TOOLS, HIDDEN_TOOLS, ToolSession, ToolFactory, computeEssentialBuiltinNames, DEFAULT_ESSENTIAL_TOOL_NAMES, createTools/isToolAllowed)
  • /home/seunghyeong/harness-work/gajae-code/packages/agent/src/types.ts (AgentTool, AgentToolResult, AgentToolExecFn, RenderResultOptions)
  • /home/seunghyeong/harness-work/gajae-code/packages/ai/src/types.ts (베이스 Tool, customWireName/customFormat)
  • /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/tools/search-tool-bm25.ts (progressive disclosure, SearchToolBm25Details, createIf)
  • /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/tools/bash.ts (bashSchemaBase/WithAsync, exclusive, 정체성 메타)
  • /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/tools/read.ts (readSchema, ReadToolDetails, nonAbortable)
  • /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/tools/ast-grep.ts (discoverable loadMode + summary, astGrepSchema)
  • /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/edit/index.ts (EditTool, 5모드 parameters union)
  • /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/tools/renderers.ts (toolRenderers 렌더러 분리)
  • /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/utils/tool-choice.ts (buildNamedToolChoiceResult, exactNamed, degradation)
  • /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/session/tool-choice-queue.ts (ToolChoiceDirective, degradeInFlight)
  • /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/capability/tool.ts (CustomTool capability — 사용자정의 도구 등록)

Codex 교차검증 메모

원문에는 별도 Codex 교차검증 섹션이 없었다. 재편집 과정에서 스키마·필드값·파일경로·코드예시·근거는 변경 없이 보존했고, 길이만 압축(중복 문단·반복 설명 제거)하고 상세는 접이식 콜아웃으로, 복잡 구간엔 인라인 mermaid를 추가했다. 사실관계(essential 기본값 ["read","bash","edit"], 프로바이더별 ToolChoice 형태, degradation 처리)는 추측으로 바꾸지 않았다.