← /geode/portfolioGEODE . 문서
GitHub
가이드
How-to

도구 작성

도구를 정의하고 등록한 뒤 권한 정책으로 게이트를 거는 방법입니다.

도구는 LLM이 부를 수 있는 함수입니다. 새 능력을 루프 안에 직접 넣지 말고 도구로 추가하면, 루프는 얇게 유지되고 권한 게이트와 훅이 그 도구에도 그대로 적용됩니다. 도구 하나를 추가하는 작업은 네 단계로 나뉩니다. 정의, 구현, 등록, 권한 분류입니다.

1. 정의를 등록합니다

LLM이 보는 스키마는 core/tools/definitions.json에 모읍니다. 항목은 리스트의 한 객체이고, name(snake_case), description, input_schema(JSON Schema), 그리고 분류 메타데이터인 category cost_tier를 가집니다.

{
  "name": "weather_lookup",
  "description": "Look up the current weather for a city.",
  "category": "external",
  "cost_tier": "free",
  "input_schema": {
    "type": "object",
    "properties": {
      "city": { "type": "string", "description": "City name" }
    },
    "required": ["city"]
  }
}

categorycost_tier의 허용 값은 core/tools/base.pyVALID_CATEGORIES VALID_COST_TIERS(free / cheap / expensive)에 정의되어 있습니다.

2. 핸들러를 구현합니다

도구는 core/tools/base.pyTool 프로토콜을 따릅니다. name, description, parameters 프로퍼티와 aexecute() 코루틴 네 가지면 유효한 도구입니다. 상속이 아니라 덕 타이핑이므로 클래스를 상속할 필요가 없습니다. 실패는 raise 대신 tool_error()로 구조화된 dict을 돌려줘서 LLM이 분류하고 복구할 수 있게 합니다. 기존 구현은 core/tools/web_tools.pyWebFetchTool을 참고하세요.

# core/tools/weather_tools.py
from typing import Any

class WeatherLookupTool:
    @property
    def name(self) -> str:
        return "weather_lookup"

    @property
    def description(self) -> str:
        return "Look up the current weather for a city."

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name"},
            },
            "required": ["city"],
        }

    async def aexecute(self, **kwargs: Any) -> dict[str, Any]:
        city = kwargs["city"]
        if not city:
            from core.tools.base import tool_error
            return tool_error("city is required", error_type="validation")
        # ... fetch and shape ...
        return {"result": {"city": city, "summary": "..."}}

3. 레지스트리에 등록합니다

핸들러가 존재한다고 자동으로 호출 대상이 되지는 않습니다. core/wiring/container.py build_default_registry()에서 registry.register(WeatherLookupTool())를 추가해야 합니다. 같은 이름이 이미 있으면 ToolRegistry.register ValueError를 던지므로 이름 충돌이 즉시 드러납니다.

# core/wiring/container.py — build_default_registry()
from core.tools.weather_tools import WeatherLookupTool

registry.register(WeatherLookupTool())

4. 권한 등급을 정합니다

권한 분류는 core/agent/safety.py의 frozenset에서 결정됩니다. 도구가 읽기 전용이면 SAFE_TOOLS에 두면 승인 없이 실행됩니다. 영속 상태(메모리, 파일, 자격증명)를 바꾸면 WRITE_TOOLS에 넣어 사용자 확인을 받게 하고, 시스템 접근이면 DANGEROUS_TOOLS에 넣습니다. 비용이 큰 호출이면 EXPENSIVE_TOOLS dict에 예상 비용을 적어 비용 확인 게이트를 켭니다. 이 set들을 ApprovalWorkflow(core/agent/approval.py)가 읽어서 실행 직전에 HITL 프롬프트를 띄웁니다.

# core/agent/safety.py
SAFE_TOOLS = frozenset({
    ...,
    "weather_lookup",  # read-only — no approval prompt
})

모드별·노드별 추가 차단이 필요하면 core/tools/policy.pyPolicyChain으로denied_tools / allowed_tools를 거는 6-layer 정책 체인을 사용합니다.

확인

레지스트리에 실제로 들어갔는지 확인합니다.

uv run python -c "from core.wiring.container import build_default_registry; \
r = build_default_registry(); print('weather_lookup' in r)"

True가 출력되면 도구가 레지스트리에 등록되어 to_anthropic_tools()를 통해 LLM에 노출됩니다. 그 다음 CLI 스모크로 한 번 불러봅니다.

uv run geode "what's the weather in Seoul"

참조: Tool protocol, MCP tools.