Tool 등록·발견·실행 아키텍처
도구 시스템의 세 가지 관심사
섹션 제목: “도구 시스템의 세 가지 관심사”에이전트가 도구를 사용하려면 세 단계가 필요합니다. 어떤 도구가 있는지 알고, 어떻게 호출하는지 알고, 실제로 실행해야 합니다. 이 세 관심사를 하나의 클래스에 넣으면 테스트가 어렵고, 도구 추가 시 많은 코드가 변경됩니다.
레지스트리 패턴은 이를 명확히 분리합니다.
┌────────────────────────────────────────────────┐│ Tool Registry ││ ││ ① Definition Layer → 도구 메타데이터, 설명 ││ ② Schema Layer → JSON Schema (LLM 호출용) ││ ③ Dispatch Layer → 실제 실행 + 권한 검사 │└────────────────────────────────────────────────┘도구 레지스트리 구현
섹션 제목: “도구 레지스트리 구현”interface ToolDefinition { name: string; description: string; parameters: JsonSchema; requiresApproval: boolean; category: 'read' | 'write' | 'execute' | 'network';}
type ToolHandler = (params: Record<string, unknown>) => Promise<unknown>;
class ToolRegistry { private definitions = new Map<string, ToolDefinition>(); private handlers = new Map<string, ToolHandler>();
// ① 도구 등록 (정의 + 핸들러 분리) register(def: ToolDefinition, handler: ToolHandler): void { this.definitions.set(def.name, def); this.handlers.set(def.name, handler); }
// ② LLM에게 제공할 스키마 생성 toOpenAISchema(): OpenAITool[] { return Array.from(this.definitions.values()).map((def) => ({ type: 'function', function: { name: def.name, description: def.description, parameters: def.parameters, }, })); }
// ③ 실행 + 권한 검사 async dispatch( name: string, params: Record<string, unknown>, approvalCallback?: (def: ToolDefinition) => Promise<boolean> ): Promise<unknown> { const def = this.definitions.get(name); const handler = this.handlers.get(name);
if (!def || !handler) { throw new Error(`Unknown tool: ${name}`); }
if (def.requiresApproval && approvalCallback) { const approved = await approvalCallback(def); if (!approved) throw new Error(`Tool ${name} not approved`); }
return handler(params); }}9-pass 퍼지 매칭
섹션 제목: “9-pass 퍼지 매칭”LLM이 파일 편집 도구를 호출할 때, 정확한 라인 번호나 문자열을 항상 올바르게 지정하지는 않습니다. OpenDev에서 개발한 9-pass 퍼지 매칭은 근사적으로 일치하는 파일 영역을 찾아 편집을 허용합니다.
9단계는 정확도가 높은 것부터 낮은 순서로 시도됩니다.
| Pass | 매칭 전략 | 설명 |
|---|---|---|
| 1 | Simple | 정확한 문자열 일치 |
| 2 | Line-trimmed | 줄 끝 공백을 제거해 비교 |
| 3 | Block-anchor | 첫 줄·마지막 줄로 후보 영역 탐색 |
| 4 | Whitespace-normalized | 연속 공백을 정규화해 비교 |
| 5 | Indentation-flexible | 들여쓰기·빈 줄 차이 허용 |
| 6 | Escape-normalized | 이스케이프 시퀀스 차이 정규화 |
| 7 | Trimmed-boundary | 부분 매칭을 원본 줄 경계로 확장 |
| 8 | Context-aware | 비어 있지 않은 양 끝 줄과 문맥 비교 |
| 9 | Multi-occurrence | 여러 후보의 줄별 매칭 처리 |
셸 실행 6단계 파이프라인
섹션 제목: “셸 실행 6단계 파이프라인”에이전트가 셸 명령을 실행할 때는 단순히 exec(command)를 호출하지 않습니다. 보안과 안정성을 위한 6단계 파이프라인을 거칩니다.
┌──────────────────────────────────────────────────┐│ 1단계: 환경 검증 → 허용된 디렉토리, 권한 확인 ││ 2단계: 명령 인터셉트 → 위험 패턴 차단 (rm -rf 등) ││ 3단계: 실행 → 프로세스 생성, 타임아웃 설정 ││ 4단계: 출력 수집 → stdout/stderr 버퍼링 ││ 5단계: 의존성 자동설치 → import 오류 감지 → pip/npm ││ 6단계: 프로세스 추적 → PID 등록, 종료 모니터링 │└──────────────────────────────────────────────────┘async function executeShell(command: string): Promise<ShellResult> { // 1단계: 환경 검증 validateWorkingDirectory(process.cwd());
// 2단계: 명령 인터셉트 const interceptResult = interceptDangerousCommands(command); if (interceptResult.blocked) { throw new Error(`Command blocked: ${interceptResult.reason}`); }
// 3단계: 실행 const proc = spawn('bash', ['-c', command], { timeout: 30_000, cwd: process.cwd(), });
// 4단계: 출력 수집 const { stdout, stderr, exitCode } = await collectOutput(proc);
// 5단계: 의존성 자동 설치 if (isMissingDependencyError(stderr)) { const pkg = extractMissingPackage(stderr); await autoInstall(pkg); return executeShell(command); // 재시도 }
// 6단계: 프로세스 추적 종료 processTracker.deregister(proc.pid);
return { stdout, stderr, exitCode };}도구 아키텍처는 정의·스키마·디스패치를 분리한 레지스트리 패턴으로 구현합니다. LLM의 근사적인 파일 위치 지정을 처리하는 9-pass 퍼지 매칭과, 보안과 안정성을 보장하는 6단계 셸 실행 파이프라인이 실용적인 도구 시스템의 핵심입니다.