BoxAgnts 툴 시스템 (3) — 툴 등록 및 핫 리로드 전체 흐름

발행: (2026년 6월 10일 PM 01:49 GMT+9)
10 분 소요
원문: Dev.to

Source: Dev.to

툴 등록은 가벼운 모듈처럼 보일 수 있습니다 — 디렉터리를 스캔하고, 파일을 읽고, 해시 테이블을 채우는 정도죠. 하지만 이를 올바르게, 그리고 신뢰성 있게 구현하려면 인코딩 감지, 텍스트 파싱, 레이스 컨디션, 시작 성능 등 처음엔 눈에 띄지 않는 문제들을 다뤄야 합니다. 이 글에서는 .wasm 파일이 AI가 호출할 수 있는 툴이 되기까지의 전체 흐름을 추적하며 각 단계를 상세히 살펴봅니다.

등록이 해결해야 할 문제들

이 모듈이 해야 할 일을 명확히 합시다. .wasm 파일이 extensions 디렉터리에 놓이면 시스템은 다음을 알아야 합니다:

  • 툴 이름이 무엇인지
  • 어떤 파라미터를 가지고 있는지, 타입은 무엇이며 각각이 필수인지 여부
  • 어떤 권한 레벨에 속하는지
  • 기능 설명은 무엇인지 (AI 모델이 호출 결정을 할 때 사용)
  • 키워드는 무엇인지 (AI 모델 검색에 사용)

전통적인 접근 방식은 개발자가 .wasm 옆에 JSON Schema 파일을 제공하도록 하는 것입니다. 이 방식에는 동기화 문제가 있습니다: 스키마에서는 파라미터가 string이라고 명시했지만 실제 코드는 number로 처리한다든가, 스키마는 업데이트되지 않았지만 툴에 새로운 파라미터가 추가된 경우, 스키마에 오류가 있어도 툴이 정상적으로 등록되고 실행 시 영원히 실패한다든가. 게다가 스키마를 만들기 위해서는 개발자가 BoxAgnts의 스키마 포맷을 별도로 이해해야 합니다.

BoxAgnts는 스키마 소스를 “수동으로 작성”에서 “툴 자체 기술”로 바꿉니다 — WASM 툴을 직접 실행하고 --help를 전달해 출력되는 도움말 텍스트를 파싱합니다. 즉, 툴 개발자는 표준 CLI 프로그램 관례만 따르면 됩니다. Rust의 clap, Go의 cobra, Python의 argparse 등 어떤 언어의 CLI 인자 파싱 라이브러리를 사용해 파라미터를 정의하면, BoxAgnts가 모든 정보를 자동으로 추출합니다.

인코딩 감지

첫 번째 기술적 디테일은 stdout을 읽은 뒤 등장합니다. WASM 툴의 --help 출력은 문자열이 아니라 바이트 스트림이므로, 디코딩하기 전에 인코딩을 감지해야 합니다. UTF‑8이라고 무조건 가정하면 GBK나 Shift‑JIS로 인코딩된 툴은 파싱에 실패합니다.

BoxAgnts는 chardetng를 인코딩 감지에 사용합니다:

// wasm-tools/src/decode.rs
pub fn decode_bytes(bytes: Bytes) -> (String, &'static str, bool) {
    let mut detector = chardetng::EncodingDetector::new(
        chardetng::Iso2022JpDetection::Allow
    );
    detector.feed(&bytes, true);
    let encoding = detector.guess(None, chardetng::Utf8Detection::Allow);
    let (cow, _, had_errors) = encoding.decode(&bytes);
    (cow.into_owned(), encoding.name(), had_errors)
}

chardetng는 Mozilla가 개발한 인코딩 감지 라이브러리로, Firefox에서 웹 페이지 인코딩을 자동으로 판단할 때 사용됩니다. 짧은 텍스트(--help 출력은 보통 몇 KB 이하)에서도 매우 높은 정확도를 보입니다. Iso2022JpDetection::Allow는 일본 환경에서 온 WASM 툴을 위해 ISO‑2022‑JP 감지를 활성화하고, Utf8Detection::Allow는 무작위 바이너리 데이터를 UTF‑8 텍스트로 오인식하는 것을 방지하기 위해 UTF‑8 무결성을 검증합니다.

디코딩이 끝나면 문자열, 인코딩 이름, 디코딩 오류 여부 세 가지가 반환됩니다. 이후 파서는 깨끗한 UTF‑8 텍스트를 받게 됩니다.

도움말 텍스트 파서

--help 출력을 파싱하는 일은 결코 간단하지 않습니다. CLI 라이브러리마다 출력 형식이 다르기 때문입니다. 예를 들어 clap의 --help-h는 상세 수준이 다르고(전자는 long_about을 포함, 후자는 about만 포함), 일부 라이브러리는 Options:Arguments: 블록 사이에 들여쓰기가 일관되지 않으며, 서브커맨드는 Commands: 혹은 Subcommands: 헤더 아래에 나타날 수 있습니다.

BoxAgnts 파서는 wasm-tools/src/registry/parser.rs에 위치하며 다음 흐름을 따릅니다:

1. 두 개의 도움말 텍스트 가져오기

pub async fn fetch_help_texts(program: &str) -> Result {
    let short_candidates = vec![vec!["-h"], vec!["--help"]];
    let long_candidates = vec![vec!["--help"], vec!["-h"]];
    let short_help = run_first_help_candidate(program, &short_candidates).await?;
    let long_help = run_first_help_candidate(program, &long_candidates).await?;
    Ok(HelpTextPair { short_help, long_help })
}

왜 두 개를 가져오나요? 많은 CLI 프로그램이 -h(짧은 도움말)와 --help(긴 도움말)에서 서로 다른 출력을 제공하기 때문입니다. -h는 파라미터 이름과 한 줄 설명만 나열하고, --help는 더 상세한 long_about을 포함합니다. BoxAgnts는 두 출력을 병합합니다:

  • 툴 이름과 버전은 짧은 도움말에서 추출(가장 간결하고 신뢰할 수 있음)
  • 긴 설명(long_about), 키워드(Keywords:), 권한 레벨(PermissionLevel:)은 긴 도움말을 우선 사용
  • 파라미터 목록(properties)과 필수 항목(required)은 두 도움말을 모두 합쳐서 구성—긴 도움말을 기본, 짧은 도움말을 보조로 활용

2. 출력 정당성 검증

모든 WASM 프로그램이 툴이 되는 것은 아닙니다. run_first_help_candidate는 출력이 도착한 뒤 정당성을 검사합니다:

pub fn looks_like_help_output(text: &str) -> bool {
    let has_usage = text.lines().any(|l| l.trim_start().starts_with("Usage:"));
    let has_options = text.lines().any(|l| l.trim() == "Options:");
    let has_arguments = text.lines().any(|l| l.trim() == "Arguments:");
    let has_commands = text.lines().any(|l| {
        let t = l.trim();
        t == "Commands:" || t == "Subcommands:"
    });
    has_usage || has_options || has_arguments || has_commands
}

출력에는 최소 하나 이상의 Usage:, Options:, Arguments:, Commands: 블록 헤더가 포함돼야 합니다. 예를 들어 HTTP 서버와 같이 CLI 툴이 아닌 WASM 프로그램은 이 조건을 만족하지 못하므로 파서는 등록을 거부하고 오류를 로그합니다.

3. 필드별 추출

fn parse_help_text(help: &str) -> Result {
    let lines: Vec = help.lines().collect();

    let (name, version) = parse_name_version(lines[0])?;
    // First line format: "base64 1.0.0" → name="base64", version="1.0.0"

    let about = lines.iter().skip(1)
        .find(|l| !l.trim().is_empty())
        .ok_or("missing about line")?
        .trim().to_string();

    let keywords = extract_single_line_field(help, "Keywords:");
    let permission_level = extract_single_line_field(help, "PermissionLevel:");

    let properties = parse_options_section(&lines)?;    // Options: block
    let (arg_props, arg_required) = parse_arguments_section(&lines)?;  // Arguments: block
    let commands = parse_commands_section(&lines)?;     // Commands: block
    // ...
}

파라미터 파싱의 핵심은 두 함수에 있습니다:

  • parse_options_section
    Options: 라인을 찾은 뒤, 그 다음 라인들을 옵션 정의(--mode 혹은 -m, --mode 형식)로 해석합니다. 여기서 파라미터 이름, 타입(“ 안에서 추출), 그리고 설명(라인 끝의 자유 텍스트)을 추출합니다.

  • parse_arguments_section
    Arguments: 라인을 찾고, 위치 인자를 “ 형식으로 파싱합니다. 대괄호([])가 있으면 선택적임을 의미합니다.

두 함수 모두 정규식 매칭을 사용합니다. 옵션 파싱 패턴은 --([a-zA-Z][a-zA-Z0-9_-]*)이며, 필요에 따라 형태의 각괄호를 포함할 수 있습니다. 인자 파싱은 형태를 매치하고, 대괄호 존재 여부로 옵션 여부를 판단합니다.

4. 병합 및 중복 제거

merge_required-h--help에서 추출한 필수 파라미터 리스트를 합칩니다:

fn merge_required(short: &[String], long: &[String]) -> Vec
0 조회
Back to Blog

관련 글

더 보기 »

Eidentic 소개

Today we're releasing Eidentic, an open-source TypeScript SDK for building AI agents with self-improving memory and the production fundamentals built in — not b...

Typescript의 타입

Introdução Tipos são uma forma de definir a “forma” ou o contrato dos dados que estamos usando no código. Pensando em Javascript puro, ele é dinâmico: você pode...