research

ZeroClaw — Tool 시스템

ZeroClaw — Tool 시스템

Layer 2 학습. 실제 소스 코드 기반. 파일: src/tools/traits.rs, src/tools/mod.rs, src/agent/prompt.rs


전체 구조

Tool trait (계약 정의)
    ↓
개별 Tool 구현체 (ShellTool, FileReadTool, MemoryRecallTool, ...)
    ↓
all_tools_with_runtime() (레지스트리 조립)
    ↓
Agent::build() (agent에 주입)
    ↓
SystemPromptBuilder::ToolsSection (LLM에 노출)
    ↓
ToolDispatcher (tool call 파싱 & 실행)

Tool trait — 계약

// src/tools/traits.rs
#[async_trait]
pub trait Tool: Send + Sync {
    fn name(&self) -> &str;
    fn description(&self) -> &str;
    fn parameters_schema(&self) -> serde_json::Value;
    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult>;

    // default method: 위 3개로 자동 구성
    fn spec(&self) -> ToolSpec {
        ToolSpec {
            name: self.name().to_string(),
            description: self.description().to_string(),
            parameters: self.parameters_schema(),
        }
    }
}

pub struct ToolResult {
    pub success: bool,
    pub output: String,       // LLM에 돌려줄 텍스트
    pub error: Option<String>,
}

pub struct ToolSpec {
    pub name: String,
    pub description: String,
    pub parameters: serde_json::Value,  // JSON Schema
}

커스텀 Tool 구현 예시

// 학교 LMS 크롤러 tool
struct FetchAssignmentTool {
    db: Arc<Db>,
    security: Arc<SecurityPolicy>,
}

#[async_trait]
impl Tool for FetchAssignmentTool {
    fn name(&self) -> &str { "fetch_assignments" }

    fn description(&self) -> &str {
        "학교 LMS에서 미제출 과제 목록을 조회한다."
    }

    fn parameters_schema(&self) -> serde_json::Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "course": {
                    "type": "string",
                    "description": "과목명 (생략하면 전체 조회)"
                }
            }
        })
    }

    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
        let course = args.get("course").and_then(|v| v.as_str());
        let assignments = crawl_lms(course).await?;

        Ok(ToolResult {
            success: true,
            output: serde_json::to_string(&assignments)?,
            error: None,
        })
    }
}

이것만 구현하면 ZeroClaw agent가 자동으로 LLM에 노출하고 호출한다.


레지스트리 조립 — all_tools_with_runtime()

config에 따라 활성화된 tool들을 Vec<Box<dyn Tool>>로 조립한다.

기본 내장 도구 (항상 등록):

ShellTool          shell 명령 실행 (allowlist, timeout 포함)
FileReadTool       파일 읽기 (경로 검증)
FileWriteTool      파일 쓰기
FileEditTool       파일 라인 편집
GlobSearchTool     파일 glob 검색
ContentSearchTool  파일 내용 검색
CronAddTool        cron 작업 추가
MemoryStoreTool    기억 저장
MemoryRecallTool   기억 조회
MemoryForgetTool   기억 삭제
ScheduleTool       일정 관리
GitOperationsTool  git 작업
CalculatorTool     계산기
WeatherTool        날씨 조회

조건부 등록 (config에 따라):

BrowserTool         browser.enabled = true
HttpRequestTool     http_request.enabled = true
WebSearchTool       web_search.enabled = true
JiraTool            jira.enabled = true
NotionTool          notion.enabled = true
ComposioTool        composio_key 있을 때
DelegateTool        agents 설정이 있을 때

SecurityPolicy가 생성 시 주입된다

Arc::new(ShellTool::new_with_sandbox(
    security.clone(),  // ← 보안 정책 주입
    runtime,
    sandbox,
)),

ShellTool은 실행 전 security.is_allowed_command(cmd)로 명령어 검증, security.is_rate_limited()로 비율 제한을 체크한다.


System Prompt 주입 — ToolsSection

agent가 초기화되면 build_system_prompt()가 호출되고, ToolsSection이 모든 tool을 LLM에게 설명하는 텍스트를 생성한다.

// src/agent/prompt.rs
impl PromptSection for ToolsSection {
    fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
        let mut out = String::from("## Tools\n\n");
        for tool in ctx.tools {
            writeln!(
                out,
                "- **{}**: {}\n  Parameters: `{}`",
                tool.name(),
                tool.description(),      // ← description()이 LLM에게 노출
                tool.parameters_schema() // ← schema가 파라미터 설명으로
            );
        }
        // XmlToolDispatcher면 여기에 <tool_call> 사용법도 추가
        out.push_str(ctx.dispatcher_instructions);
        Ok(out)
    }
}

결과적으로 LLM이 받는 system prompt에는:

## Tools

- **shell**: 쉘 명령을 실행한다.
  Parameters: `{"type":"object","properties":{"command":{"type":"string"}}}`
- **fetch_assignments**: 학교 LMS에서 미제출 과제 목록을 조회한다.
  Parameters: `{"type":"object","properties":{"course":{...}}}`
...

전체 데이터 흐름

① Config → all_tools_with_runtime()
     → Vec<Box<dyn Tool>> 조립 (Security 주입)

② Agent::build()
     → tool_specs = tools.iter().map(|t| t.spec()).collect()
     → tools와 tool_specs 모두 보관

③ turn() 첫 호출
     → build_system_prompt()
     → ToolsSection.build() → tool 목록 텍스트 생성
     → history에 system message로 push

④ turn() 루프
     → ChatRequest { messages, tools: Some(&tool_specs) }
     → provider.chat(...) 호출
        ├─ NativeDispatcher: tool_specs를 API tools 배열로 전달
        └─ XmlDispatcher: tool 목록이 이미 system prompt 텍스트에 있음

⑤ LLM 응답 → tool call 파싱
     → executor.execute_tool_call(call)
     → tools.iter().find(|t| t.name() == call.name)
     → tool.execute(args).await
     → ToolResult { success, output, error }

ArcToolRef 패턴

MCP tool 등 나중에 동적으로 추가되는 tool은 Arc<dyn Tool>로 관리된다. Box<dyn Tool>과 함께 쓰기 위해 ArcToolRef 래퍼를 사용한다.

pub struct ArcToolRef(pub Arc<dyn Tool>);

#[async_trait]
impl Tool for ArcToolRef {
    fn name(&self) -> &str { self.0.name() }
    fn description(&self) -> &str { self.0.description() }
    fn parameters_schema(&self) -> serde_json::Value { self.0.parameters_schema() }
    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
        self.0.execute(args).await
    }
}

커스텀 tool 추가 방법

1. tool 구현 (새 파일)

// src/tools/my_tool.rs
pub struct MyTool { ... }
#[async_trait]
impl Tool for MyTool { ... }

2. all_tools_with_runtime()에 등록

// src/tools/mod.rs - all_tools_with_runtime() 함수 내
tool_arcs.push(Arc::new(MyTool::new(security.clone())));

끝이야. system prompt 주입, LLM 노출, 파싱, 실행까지 전부 자동으로 처리된다.


관련