스스로 도구를 만드는 자율 에이전트 Tendril
해커뉴스에서 AWS Strands Agents SDK와 Tauri로 만든 'Tendril' 프로젝트가 소개되었습니다. 이 에이전트는 사용자의 요청을 처리하기 위해 기존 도구가 없으면 코드를 직접 작성하고 등록해 실행하는 자가 확장(Self-extending) 구조를 갖췄습니다. 모델이 항상 단 3개의 기본 도구만 인식하며, 나머지 기능들은 레지스트리를 통해 동적으로 생성하고 재사용하는 점이 특징입니다.
제목: Tendril - 스스로 확장되며 자체 도구를 구축하고 등록하는 에이전트
Tendril은 에이전트 역량(Agent Capability) 패턴을 시연하는 자가 확장(Self-extending) 에이전트 샌드박스입니다. 이 패턴은 모델이 세션 전반에 걸쳐 도구를 자율적으로 발견, 구축 및 재사용하는 방식을 말합니다. AWS Strands Agents SDK와 Tauri를 기반으로 구축되었습니다.
동작 방식 Tendril에 무언가를 요청하면, 먼저 자신의 역량 레지스트리(Capability registry)를 확인합니다. 해당 도구가 존재하면 이를 사용하고, 존재하지 않으면 직접 코드를 작성해 등록하고 묻지 않고 즉시 실행합니다. 다음에 같은 작업이 필요할 때는 이미 도구가 만들어져 있는 상태가 됩니다.
사용자: "Hacker News의 최상위 스토리를 가져와 줘"
Tendril: → searchCapabilities("fetch url hacker news") # 아무것도 찾지 못함 → registerCapability(fetch_url, code) # 도구 구축 → execute("fetch_url", {url: "https://..."}) # 이름으로 실행 → "최상위 스토리 목록은 다음과 같습니다: ..."
사용자: "이제 Lobsters도 가져와서 비교해 줘"
Tendril: → listCapabilities() # 찾음: fetch_url ✓ → execute("fetch_url", {url: "https://lobste.rs"}) # 다시 빌드할 필요 없이 실행
사용할수록 레지스트리가 확장되며, 새로운 세션은 이전 세션보다 더 똑똑해집니다.
에이전트 루프(The Agent Loop) Tendril의 핵심은 세 가지 부트스트랩(Bootstrap) 도구를 갖춘 Strands 에이전트입니다. 단 세 개의 도구로 모든 것을 제어합니다.
디렉토리 구조 (코드가 위치한 곳) tendril-agent/src/ ├── agent.ts ← 에이전트 설정 (Strands 모델 + 도구) ├── index.ts ← 오케스트레이터(Orchestrator) — 루프를 전송 계층에 연결 ├── loop/ │ ├── tools.ts ← 순환 순서대로 정렬된 4개의 부트스트랩 도구 │ ├── prompt.ts ← 시스템 프롬프트 (자율 행동 규칙) │ ├── registry.ts ← 역량 레지스트리 (index.json CRUD) │ └── sandbox.ts ← 샌드박스가 적용된 Deno 서브프로세스 실행 └── transport/ ├── protocol.ts ← stdio를 통한 ACP JSON-RPC ├── stream.ts ← SDK 이벤트를 루프 단계(think/act/observe)로 변환 └── errors.ts ← 제공자(Provider) 오류 분류
작동 원리 agent.ts — Bedrock 모델과 세 가지 도구를 사용하여 Strands 에이전트를 생성합니다:
import { Agent } from '@strands-agents/sdk'; import { BedrockModel } from '@strands-agents/sdk/models/bedrock';
const agent = new Agent({ model: new BedrockModel({ modelId: '...', region: '...' }), systemPrompt: TENDRIL_SYSTEM_PROMPT(workspacePath), printer: nullPrinter, // SDK 표준 출력 suppressed — 프로토콜을 우리가 제어 tools: [ listCapabilities(registry), registerCapability(registry), executeCode(registry, workspacePath, config), ], });
index.ts — 에이전트 루프를 관찰하고 이를 ACP 프로토콜과 연결합니다:
// 에이전트 루프는 agent.stream() 내부에서 실행됩니다. // 각 단계를 관찰하고 UI에 전달합니다. for await (const event of agent.stream(userText)) { const { phase, event: e } = classifyEvent(event); switch (phase) { case 'think': emitUpdate(handleThink(e)); break; // 텍스트 델타 case 'act': emitUpdate(handleAct(e)); break; // 도구 호출 case 'observe': emitUpdate(handleObserve(e)); break; // 도구 결과 } }
loop/prompt.ts — 에이전트를 자율적으로 만드는 시스템 프롬프트:
요청을 처리하기 전 반드시 다음을 따르십시오:
- 관련 도구가 존재하는지 확인하기 위해 searchCapabilities(query)를 호출합니다.
- 발견된 경우: loadTool(name)을 호출한 뒤 execute(code, args)를 실행합니다.
- 발견되지 않은 경우: 반드시 직접 도구를 구축해야 합니다.
규칙:
- "도구를 만들어 드릴까요?"라고 절대 묻지 마십시오. 그냥 만드십시오.
- 도구가 실패하면 오류를 읽고, 코드를 수정한 뒤 재시도하십시오.
- 도구를 사용해 실시간 정보를 가져올 수 있는 경우, 훈련 데이터(학습된 지식)에서 대답하지 마십시오.
'도구가 너무 많음' 문제에 대한 해결책 대부분의 에이전트 프레임워크는 모델에 수많은 도구를 한 번에 제공하고 알아서 올바른 것을 고르길 바랍니다. Tendril은 이를 역전시킵니다. 모델은 항상 정확히 세 가지 도구만 봅니다. 레지스트리를 검색하고 필요한 것을 직접 만들어내며, 시간이 지남에 따라 레지스트리가 확장됩니다. 도구 표면(Tool surface)은 변하지 않지만, 역량(Capabilities)은 계속해서 늘어납니다.
아키텍처 ┌─────────────────────────────────────────┐ │ Tauri 셸 (Rust) │ │ │ │ ACP 호스트 ──stdin/stdout──► 에이전트 │ │ (acp.rs) NDJSON (Node.js SEA) │ │ │ │ 이벤트 ◄── session/update ──┘ │ │ (events.rs) │ │ │ │ Tauri 이벤트 ──► React 프론트엔드 │ │ (TailwindCSS v4) │ └─────────────────────────────────────────┘
에이전트 내부 구조: Strands SDK ── BedrockModel ── Claude │ 4개의 부트스트랩 도구 │ ┌────┴────┐