메뉴
HN
Hacker News 33일 전

스스로 도구를 만드는 자율 에이전트 Tendril

IMP
8/10
핵심 요약

해커뉴스에서 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 — 에이전트를 자율적으로 만드는 시스템 프롬프트:

요청을 처리하기 전 반드시 다음을 따르십시오:

  1. 관련 도구가 존재하는지 확인하기 위해 searchCapabilities(query)를 호출합니다.
  2. 발견된 경우: loadTool(name)을 호출한 뒤 execute(code, args)를 실행합니다.
  3. 발견되지 않은 경우: 반드시 직접 도구를 구축해야 합니다.

규칙:

  • "도구를 만들어 드릴까요?"라고 절대 묻지 마십시오. 그냥 만드십시오.
  • 도구가 실패하면 오류를 읽고, 코드를 수정한 뒤 재시도하십시오.
  • 도구를 사용해 실시간 정보를 가져올 수 있는 경우, 훈련 데이터(학습된 지식)에서 대답하지 마십시오.

'도구가 너무 많음' 문제에 대한 해결책 대부분의 에이전트 프레임워크는 모델에 수많은 도구를 한 번에 제공하고 알아서 올바른 것을 고르길 바랍니다. 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개의 부트스트랩 도구 │ ┌────┴────┐

원문 보기
원문 보기 (영어)
Tendril A self-extending agentic sandbox that demonstrates the Agent Capability pattern — where the model discovers, builds, and reuses tools autonomously across sessions. Built with AWS Strands Agents SDK and Tauri . What it does You ask Tendril to do something. It checks its capability registry. If a tool exists, it uses it. If not, it writes one , registers it, and executes it — all without asking. Next time you need the same thing, the tool is already there. You: "fetch the top stories from Hacker News" Tendril: → searchCapabilities("fetch url hacker news") # nothing found → registerCapability(fetch_url, code) # builds a tool → execute("fetch_url", {url: "https://..."}) # runs it by name → "Here are the top stories: ..." You: "now fetch Lobsters and compare" Tendril: → listCapabilities() # found: fetch_url ✓ → execute("fetch_url", {url: "https://lobste.rs"})# runs it — no rebuild The registry grows with use. Every session is smarter than the last. The Agent Loop The core of Tendril is a Strands agent with three bootstrap tools . That's it — three tools to rule them all. Where it lives tendril-agent/src/ ├── agent.ts ← Agent configuration (Strands model + tools) ├── index.ts ← Orchestrator — wires loop to transport ├── loop/ ← The agentic loop │ ├── tools.ts ← 4 bootstrap tools in cycle order │ ├── prompt.ts ← System prompt (autonomous behaviour rules) │ ├── registry.ts ← Capability registry (index.json CRUD) │ └── sandbox.ts ← Deno subprocess execution with sandboxing └── transport/ ← Conversation framing + stream observation ├── protocol.ts ← ACP JSON-RPC over stdio ├── stream.ts ← SDK events → loop phases (think/act/observe) └── errors.ts ← Provider error classification How it works agent.ts — Creates the Strands agent with a Bedrock model and three tools: 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 , // suppress SDK stdout — we own the protocol tools : [ listCapabilities ( registry ) , registerCapability ( registry ) , executeCode ( registry , workspacePath , config ) , ] , } ) ; index.ts — Observes the agentic loop and bridges it to the ACP protocol: // The agentic loop runs inside agent.stream(). // We observe each phase and forward to the UI. for await ( const event of agent . stream ( userText ) ) { const { phase , event : e } = classifyEvent ( event ) ; switch ( phase ) { case 'think' : emitUpdate ( handleThink ( e ) ) ; break ; // text delta case 'act' : emitUpdate ( handleAct ( e ) ) ; break ; // tool call case 'observe' : emitUpdate ( handleObserve ( e ) ) ; break ; // tool result } } loop/prompt.ts — The system prompt that makes the agent autonomous: BEFORE acting on any request: 1. Call searchCapabilities(query) to check if a relevant tool exists 2. If found: call loadTool(name) then execute(code, args) 3. If NOT found: you MUST build the tool yourself. RULES: - NEVER ask "would you like me to create a tool?" — just build it. - If a tool fails, read the error, fix the code, and retry. - NEVER answer from training data when a tool could get live information. The "too many tools" solution Most agent frameworks give the model a big bag of tools and hope it picks the right one. Tendril inverts this — the model always sees exactly three tools . It searches a registry, builds what it needs, and the registry grows over time. The tool surface never changes; the capabilities do. Architecture ┌─────────────────────────────────────────┐ │ Tauri Shell (Rust) │ │ │ │ ACP Host ──stdin/stdout──► Agent │ │ (acp.rs) NDJSON (Node.js SEA)│ │ │ │ │ │ Events ◄── session/update ──┘ │ │ (events.rs) │ │ │ │ │ Tauri Events ──► React Frontend │ │ (TailwindCSS v4) │ └─────────────────────────────────────────┘ Agent internals: Strands SDK ── BedrockModel ── Claude │ 4 bootstrap tools │ ┌────┴────┐ │ Registry │ ←→ index.json + tools/*.ts └─────────┘ │ ┌────┴────┐ │ Sandbox │ ←→ Deno subprocess (scoped permissions) └─────────┘ Communication : JSON-RPC 2.0 over NDJSON (newline-delimited JSON) on stdin/stdout. The agent is a standalone process — the Tauri host spawns it as a sidecar. Protocol : Implements the Agent Integrator Specification (ACP) — the same protocol used by Claude Code and similar agent hosts. Tech Stack Component Technology Desktop shell Tauri 2.x (Rust) Frontend React 18 + TailwindCSS v4 Agent TypeScript (Node.js SEA binary) Agent framework @strands-agents/sdk Inference AWS Bedrock (Claude via Strands BedrockModel) Code sandbox Deno (bundled, subprocess with permission flags) Protocol JSON-RPC 2.0 / NDJSON over stdio Prerequisites Node.js 22+ (for building the agent) Rust toolchain (for Tauri) AWS credentials configured for Bedrock access ( ~/.aws/credentials ) Quick Start git clone https://github.com/serverless-dna/tendril.git cd tendril make dev This will: Install dependencies ( npm install for agent and UI) Build the agent ( esbuild bundle) Download Deno (bundled as sidecar) Create sidecar shims with platform triple Launch Tauri dev mode On first launch, pick a workspace folder. Configure your AWS profile and model in Settings. Configuration All settings live at ~/.tendril/config.json : { "workspace" : " /Users/you/tendril-workspace " , "model" : { "provider" : " bedrock " , "modelId" : " us.anthropic.claude-sonnet-4-5-20250514 " , "region" : " us-east-1 " , "profile" : " your-aws-profile " }, "sandbox" : { "denoPath" : " deno " , "timeoutMs" : 45000 , "allowedDomains" : [] }, "agent" : { "maxTurns" : 100 } } allowedDomains : empty = unrestricted network. Set ["api.example.com"] to restrict. Capability Registry Capabilities are stored in the workspace as plain files: ~/tendril-workspace/ index.json ← registry (name, triggers, suppression rules) tools/ fetch_url.ts ← tool implementation (TypeScript, runs in Deno) summarize_text.ts parse_json.ts Each capability has: name : snake_case identifier capability : one-sentence description triggers : conversational signals that should invoke it suppression : conditions that prevent invocation The model writes these definitions. You can inspect, edit, or delete them — they're just files. Makefile Targets make dev Build agent + sidecars, launch Tauri dev make build Build agent (esbuild bundle) make test Run agent tests (vitest) make lint tsc --noEmit + cargo clippy make fmt cargo fmt --check make check Full quality gate (fmt + lint + test) make release Quality gate + cargo tauri build make clean Remove all build artifacts Project Structure tendril/ tendril-agent/ TypeScript Strands sidecar src/ Agent source (see "The Agent Loop" above) tests/ vitest tests package.json sea-config.json Node.js SEA build config tendril-ui/ Tauri + React desktop app src/ React components + hooks src-tauri/ Rust backend (ACP host, event forwarding) package.json docs/ Specs and reference implementations specs/ Feature specifications and plans Makefile License MIT