에이전트 루프 · 도구 시스템 — oxicode ⇄ oxios 해부
LLM은 상태가 없다. 그래서 에이전트의 본질은 "도구 결과를 다시 먹이는 while 루프"다. 이 페이지는 그 루프가 도는 정확한 코드 경로를 — 도구가 정의되고 등록되고 승인되고 실행되고 결과가 돌아오는 순서까지 — 실제 파일:라인 근거와 함께 따라간다. 정적 구조(16 port)는 SDK ↔ oxios 관계, 프레임워크 비교는 에이전트 SDK 비교에 뒀고, 여기는 동적 거동만 다룬다.
결론 먼저 — 세 문장
- 도구 = 트레이트.
AgentTool(이름·설명·JSON Schema·execute)이 실행의 옷을 입고, 매 턴oxicode-ai::ToolJSON으로 갈아입어 LLM 요청에 실린다. - 루프 = oxicode-agent의 소유.
AgentLoop::run_loop(mod.rs:761)의 2중 while이 1턴(응답→도구→결과)을 반복한다. oxios는 자체 루프를 만들지 않고run_streaming()한 번으로 위임한다. - oxios의 차별점 = 도구와 정책. 커널 도구 30+를
KernelToolProvider로 주입하고, 표면·오케스트레이터·수퍼바이저가 그 루프를 감싼다.
0. 전체 지도 — 호출이 지나는 5개 층
같은 루프를 두 소비자가 쓴다. oxicode CLI는 TUI에서 바로 SDK로, oxios는 Web WS·Telegram 표면에서 게이트웨이→오케스트레이터→수퍼바이저를 거쳐 같은 SDK 계약층으로 내려온다.
1. 도구의 정체 — 하나의 트레이트, 두 벌의 옷
oxicode에서 도구는 enum도 구조체 등록도 아닌 async 트레이트다. 실행 로직(Rust 세계)과 모델에게 보이는 정의(JSON 세계)가 서로 다른 타입으로 분리돼 있다 — 이것이 "같은 도구가 공급자를 바꿔도 동작하는" 비결이다.
실행의 옷 — AgentTool (oxicode-agent)
pub trait AgentTool: Send + Sync {
fn name(&self) -> &str; // "bash"
fn label(&self) -> &str; // UI 표시용
fn description(&self) -> &str; // ← 모델이 읽는 설명
fn parameters_schema(&self) -> Value; // ← JSON Schema
fn essential(&self) -> bool { false } // 컨텍스트 축소 시에도 유지
fn tool_tier(&self) -> ToolTier { ToolTier::Exec } // 권한 티어
async fn execute(&self, tool_call_id: &str, params: Value,
signal: Option<oneshot::Receiver<()>>, // 취소 신호
ctx: &ToolContext) -> Result<AgentToolResult, ToolError>;
fn to_definition(&self) -> ToolDefinition; // LLM 전송용 변환
// … 총 15개 메서드 (render_call · intent · execution_mode · tab slot …)
}
// oxicode-agent/src/tools.rs:744-873 — 트레이트다 (enum/구조체 아님) 선택의 옷 — Tool JSON (oxicode-ai)
// oxicode-ai/src/providers/anthropic.rs:205-207, 664-680
body["tools"] = json!([{
"name": "bash",
"description": "Run a shell command …",
"input_schema": { "type": "object", "properties": { … } }
}]);
// 같은 정의가 방언만 바껴 나간다 —
// OpenAI openai.rs:598-620 · Ollama ollama.rs:210-225 · Gemini google_shared.rs:210
모델은 오른쪽만 본다. 매 턴 streaming.rs:57-99에서
ToolRegistry.definitions() → OxTool::new(name, desc, schema) →
context.set_tools()로 새로 조립된다 — 도구 목록은 요청마다 동봉되는 상수가 아니라 매번 계산되는 값이다.
ToolTier { Read, Write, Exec }(tools.rs:732-742)는 도구의 위험도 등급이다.
읽기 도구와 셸 실행이 같은 승인 정책을 통과하면 안 되기 때문 — 아래 §3 관문에서 이 등급이 그대로 승인 판정의 기준이 된다.
구체 예: BashTool(tools/bash.rs:168-171 구조체, :490-540 impl)는 essential = true라
컨텍스트 절약을 위해 도구를 골라 낼 때도 살아남는다.
2. 등록 — ToolRegistry로 모이는 3갈래 공급원
레지스트리는 HashMap<String, Arc<dyn AgentTool>> 하나다(tools.rs:997-1003).
정적 내장·외부 MCP·호스트 커널 도구가 전부 같은 이름 공간에 등록되고, 루프는 그 출처를 구분하지 않는다.
구분하는 책임은 등록하는 쪽에 있다.
oxios가 주입하는 도구 30+ — 실제 목록
| 분류 | 도구 | 근거 · 비고 |
|---|---|---|
| always-on 파일 · 검색 (8) | read · write · edit · grep · find · ls · web_search · get_search_results | registration.rs register_always_on — SDK 파일 도구와 같은 이름 공간 |
| 커널 도메인 (21) | exec · memory_read · memory_write · memory_search · subagent · project · mount · kernel_agent · persona · cron · security · budget · resource · a2a_delegate · a2a_send · a2a_query · knowledge · ask_user · marketplace · skill_forge · calendar · send_email | builtin/mod.rs:70 register_all_kernel_tools — 'canonical list'. memory 3종은 brain-backed 무조건 등록 (': every agent gets memory read + write'), subagent는 oxicode-agent 네이티브 재사용(:88) |
| 브라우저 5종 (feature) | browse · browse_extract · browse_session · browse_script · browse_screenshot | kernel_bridge.rs:81-90 — oxios 자체 브라우저 스위트 (RFC-046, oxibrowser 연계) |
| 외부 MCP (동적) | 서버별 full_name으로 열거 — 정적 목록에 없음 | kernel_bridge.rs:77-78 주석 — 등록 시점에 per-server 결정 + McpToolWrapper (builtin/mod.rs:108) |
확장 계약 — KernelToolProvider (oxicode-sdk)
// oxicode-sdk/src/kernel_bridge.rs:111-124 — 호스트 도구 주입 계약
pub trait KernelToolProvider: Send + Sync {
fn tool_names(&self) -> Vec<&str>;
fn register_tools(&self, registry: &ToolRegistry, context: &KernelToolContext);
}
// KernelToolContext(:24-80) = workspace · agent_id · session_id · permissions
oxios의 구현은 OxiosKernelBridge(oxios-kernel/src/tools/kernel_bridge.rs:41-105) —
tool_names()가 위 표의 목록을 반환하고 register_tools()가
register_always_on + builtin::register_all_kernel_tools(builtin/mod.rs:70)를 호출한다.
SDK 쪽에서 이것이 불리는 지점은 agent_builder.rs:388-393 kernel_tools().
3. 호출 1회의 관문 — ToolCall에서 ToolResult까지 9단계
모델이 tool_use 블록을 내놓는 순간부터 결과가 히스토리에 꽂히기까지, 도구 호출은
9개의 관문을 순서대로 통과한다. 면접에서 "에이전트 안전성"을 묻는다면 이 9단계가 답이다.
승인 판정 — 3분기와 opt-in 설계
| 판정 | 동작 | 근거 |
|---|---|---|
| Allow | 즉시 실행 | config.rs:276, tool_exec.rs:300-311 |
| Deny(reason) | 에러 반환 — 모델에게 거절 사유가 ToolResult로 전달 | config.rs:277 |
| RequireApproval | AgentEvent::ApprovalRequired emit + 에러 — 호스트 UI가 승인 대기 | tool_exec.rs:312-325 |
주의 — 권한 시스템은 opt-in이다. ApprovalConfig.require_approval_for가 비어 있으면
전부 즉시 허용된다(tool_exec.rs:292-294). .oxi 권한 규칙 파일 같은 건 없고 정책은 전부 코드/훅으로 주입한다 —
파일 기반 설정은 MCP에만 있다(.oxicode/mcp.json + 동의 파일 mcp-consent.json, consent.rs:210).
제품 수준의 정책(권한·감사·비용)은 oxios가 SDK port(AccessGate·authorizer)로 끼워 넣는다 —
agent_runtime.rs:1046-1054에서 authorizer · tracer · cost_tracker를 AgentBuilder에 꽂는 게 그 지점.
4. 루프 해부 — 2중 while과 1턴의 14단계
run_loop(mod.rs:761)는 상태 머신이 아니라 플래그 기반 2중 루프다.
안쪽 while이 "1턴"의 단위(모델 응답 → 도구 실행 → 결과 삽입), 바깥 loop는 실행 중 개입(steering·follow-up)을
놓치지 않기 위한 재드레인 층이다. 이중 구조가 "에이전트가 일하는 도중에 말을 걸 수 있는" 이유다.
1턴 전체 시퀀스 — 14단계
- 1 TurnStart { turn_number }
mod.rs:794-804 - 2 steering 메시지 처리 — 실행 중 끼어든 사용자 입력을 히스토리에 삽입
mod.rs:806-814, :675-703 - 3 poll_external_queues — 훅이 새로 넣은 steer/follow-up 드레인
mod.rs:816-818, :465 - 4 maybe_compact — 토큰 임계면 컨텍스트 압축 (아래 §6)
mod.rs:820-821 - 5 append_only.sync_from — prefix-stable 컨텍스트 동기화
mod.rs:823-825 - 6 stream_assistant_response — 시스템 프롬프트+도구 정의 조립 → LLM 스트리밍
mod.rs:827-829, streaming.rs:68-129 - 7 StreamOutcome 분기 — Error→재시도/종료 · Aborted→종료 · TTSR 위반→부분 출력 폐기 후 재시도
mod.rs:831-972, retry.rs - 8 extract_tool_calls — tool_use 없으면 has_more_tool_calls = false
mod.rs:986-990, helpers.rs:7 - 9 execute_tool_calls — 9 관문 통과 (시퀀셜/병렬 분기)
mod.rs:991-1015, tool_exec.rs:340-355 - 10 ToolResult 메시지 삽입 → 루프 가드 → soft-requirement 검사
mod.rs:1020-1169 - 11 TurnEnd 이벤트 → should_stop_after_turn (external_stop이면 즉시 종료)
mod.rs:1176-1190, helpers.rs:50 - 12 안쪽 while 판정 — has_more_tool_calls || pending_messages
mod.rs:794, :1201-1206 - 13 바깥 loop — late steering · follow-up · final steering 재드레인
mod.rs:1212-1234 - 14 todo 리마인더 (유계: signature dedup + 상한) 후 없으면 break
mod.rs:1240-1257
| # | 조건 | 동작 | 근거 |
|---|---|---|---|
| A | 모델이 텍스트만 응답 (tool_use 없음) | 내부 루프 탈출 → 큐 비면 break — 정상 종료 | mod.rs:794, :1258 |
| B | external_stop · Ctrl+C | should_stop_after_turn → TurnEnd 후 종료. 스트리밍 중엔 500ms 주기 취소 감지 | helpers.rs:50, mod.rs:1187-1206, streaming.rs:157-180 |
| C | StopReason::Error (비재시도) | handle_streaming_error → 에러 메시지로 TurnEnd 후 반환 | mod.rs:906-938, :708-759 |
| D | StopReason::Aborted / Cancelled | 즉시 반환 | mod.rs:939-960, :877-884 |
| F | 도구 실행 오류 | TurnEnd 후 반환 (에러는 ToolResult로 모델에게도 전달됨) | mod.rs:995-1014 |
| G | after_tool_call 훅 만장일치 terminate | has_more_tool_calls = false | mod.rs:1015-1018, helpers.rs:39 |
| H | TTSR 룰 위반 (스트리밍 중) | 폐기·재시도 — 종료 아님. 룰 메시지 주입 후 continue | mod.rs:885-938, ttsr.rs |
| I | 같은 도구 호출 5회 반복 | 루프 가드가 steering 주입해 탈출 | mod.rs:1078-1093 |
| J | 최대 턴수 | 없음 — 루프는 가드·soft-requirement·todo 리마인더(모두 유계)로만 보호된다 | mod.rs:777-780 주석 |
TTSR(Time-Traveling Stream Rules) — 스트리밍 중 프로젝트 룰 위반을 TextDelta마다 검사해(ttsr.rs, streaming.rs:316-360) 위반 시 그 턴의 부분 출력을 버리고 룰을 시스템 리마인더로 주입한 뒤 재시도한다(mod.rs:885-938). omp의 TtsrManager 이식이며 Regex + AST(ast-grep) 2종 매칭을 지원한다. (구 보고서의 "Textual Trajectory Stream Rule" 표기는 오기다.)
5. 살아있는 루프 — 컴팩션 · 스트리밍 · 훅 · 세션
컨텍스트 압축 — 2단 컴팩션
compactor는 교체 가능하다 — SDK의 SnapcompactCompactor(vision 모델용 PNG 렌더)를 끼우는 지점이
mod.rs:145-153. 컴팩션 전후로 AppendOnlyContext(mod.rs:782-784)가 prefix 안정성을 지킨다.
스트리밍 · 훅 · 세션
- 실시간 토큰 —
TextDelta → MessageUpdate{StreamDelta}(streaming.rs:303-323)를 emit 클로저가 std mpsc(agent.rs:738-756) 또는 tokio mpsc(:1079-1090)로 소비자에게. oxios는 콜백에서 세션별 StreamingSink로 push(agent_runtime.rs:1102-1117, RFC-015). - 훅은 2층 — 코어 내장
before/after_tool_call클로저(tool_exec.rs:867-902) + SDKHookRunnerport(ports/hooks.rs:90)를HookMiddleware가 PreToolUse/PostToolUse로 라우팅(middleware/hook.rs:24-130). CLI는CommandHookRunner로 셸 훅 실행(bootstrap.rs:103-113). - 세션 지속 — CLI가
persist_session(agent_session.rs:1094-1183)으로 session.json JSONL append, 재개는resume_from_file(:2937-2999)이 브랜치를 역직렬화해 Agent state에 시드. - oxios 장애 복구 — 모델 실패 시 이전 실행의 대화 상태를
import_state로 복원해 fallback 모델이 처음부터가 아니라 체크포인트에서 계속(agent_runtime.rs:1080-1087, RFC-029 P2b).
6. oxios — 루프를 만들지 않고 루프를 산다
oxios의 가장 중요한 설계 결정: 자체 에이전트 루프를 작성하지 않았다. run_agent(agent_runtime.rs:737)는 SDK의 AgentBuilder로 Agent를 조립하고
run_streaming() 한 번을 기다리기만 한다. oxios가 소유한 것은 루프 주변이다 —
표면·오케스트레이션·도구·정책.
oxios의 조립 코드 — agent_runtime.rs (요약)
// oxios/crates/oxios-kernel/src/agent_runtime.rs — run_agent(:737)
let mut builder = engine.oxi() // oxicode-sdk Oxicode 진입점
.agent(agent_config) // AgentBuilder (RFC-014 Phase D)
.workspace(&workspace)
.system_prompt(system_prompt);
// … authorizer · tracer · cost_tracker · rate limit · token budget (:1046-1066)
let built = builder.build()?;
// SDK builder의 .tool()은 concrete 타입만 받는데 oxios 도구는 Arc<dyn AgentTool>.
// SDK가 pre-built ToolRegistry 주입을 지원하지 않으므로
// build 후 agent.tools() 레지스트리에 register_arc로 주입한다 (:1071-1078).
for tool in cspace_tool_arcs {
agent_tools.register_arc(tool);
}
// 루프는 여기서 시작되지 않는다 — 소유권은 SDK에 있다.
let result = agent.run_streaming(prompt, move |event| { … }).await; // :1109-1110 :1030-1036의 주석이 설계 지향을 그대로 보여준다 — SDK builder의 .tool()은 concrete 타입만 받는데
oxios 도구는 Arc<dyn AgentTool>이라 build 후 agent.tools() 레지스트리에 register_arc로 주입한다.
계약을 우회한 게 아니라 SDK가 남겨둔 정식 확장점(:1071-1078 주석 "canonical extension point")을 찾아 쓴 것.
supervisor가 두 개다 — 역할이 다르다
| 이름 | 위치 | 역할 |
|---|---|---|
| BasicSupervisor | oxios/crates/oxios-kernel/src/supervisor.rs | 에이전트 1개 실행 — run_with_directive로 AgentRuntime을 돌린다 |
| TaskSupervisor | oxios/src/supervisor.rs | 프로세스 감시 — 게이트웨이·표면·채널. 웹 표면은 유한 백오프 재시작, 치명 오류 시 OS 재시작 |
흔한 오해 2개 — 짚고 가기
- "oxios는 MCP 서버를 돌린다" → 아니다.
oxios-mcp는 MCP 클라이언트 라이브러리다 (lib.rs:1-4 "Model Context Protocol client library" — Consumer → McpBridge → McpClient → stdio JSON-RPC → 외부 서버). oxios는 외부 MCP 도구를 소비하는 쪽이지, 자신을 MCP로 노출하지 않는다. 반대로 oxibrain-mcp는 서버(5 transport)다 — 방향이 정반대. - "ClawHub 스킬은 도구다" → 아니다. 스킬은
~/.oxios/skills에 설치되는(clawhub/installer.rs — 다운로드→추출→origin.json→lockfile) 문서고,<available_skills>블록이 시스템 프롬프트에 주입된 뒤 모델이read도구로 본문을 로드한다(skill/prompt.rs:26-36). 도구 수 = 스키마 토큰이므로, 지식은 도구가 아니라 문서로 제공하는 게 토큰 경제학상 이득 — Claude Code의 Skill과 같은 progressive disclosure 전략.
7. 면접 답변 스크립트
"에이전트 루프가 정확히 뭔가?" — 30초 답변
LLM은 상태가 없어서 '도구 결과를 다시 먹여야' 다음 판단을 한다. 이 while 루프가 에이전트다. oxicode에서는 oxicode-agent의 AgentLoop::run_loop(mod.rs:761)가 2중 while — 안쪽이 1턴(모델 응답→도구 실행→결과 삽입), 바깥이 실행 중 개입(steering·follow-up) 재드레인. 모델이 tool_use 없이 텍스트만 내보내면 루프가 끝난다.
"도구는 어떻게 모델에게 제공되나?" — 90초 답변
도구 하나 = AgentTool 트레이트(이름·설명·JSON Schema·execute). 매 턴 ToolRegistry에서 definitions()를 모아 oxicode-ai의 Tool로 변환해 요청 body['tools']에 실는다 — 공급자별 방언(Anthropic input_schema / OpenAI function)으로 번역만 바뀐다. 등록은 3갈래: oxicode 내장(with_builtins_cwd) + 외부 MCP(McpManager 스폰→프록시 도구) + 호스트 커널(KernelToolProvider — oxios가 30+ 도구를 여기로 주입). 실행은 9단계 관문: 조회→사전 훅→승인(ToolTier)→실행→사후 훅→ToolResult.
"oxios와 oxicode는 역할이 어떻게 나뉘나?"
루프·도구·와이어(oxicode-ai)는 oxicode가 소유하고, oxios는 그 위에 '에이전트 OS'를 얹는다 — 표면(Web WS·Telegram)·게이트웨이·오케스트레이터(fork)·커널 도구 30+(memory·a2a·cron·browse…)·정책(authorizer·budget). oxios는 자체 while 루프를 만들지 않고 agent.run_streaming() 한 번으로 SDK 루프에 위임한다(agent_runtime.rs:1110). 이벤트만 KernelEvent로 중계해 표면으로 스트리밍.
"무한 루프에 빠지면?"
최대 턴수 제한은 없다. 대신 3중 방어: ToolCallLoopGuard(같은 호출 5회 반복 감지, read/ls/grep 면제), after_tool_call 훅 만장일치 terminate, todo 리마인더의 유계 설계(signature dedup + 상한 — mod.rs:777-780 주석). TTSR은 스트리밍 중 룰 위반을 실시간 감지해 출력을 폐기하고 룰을 주입한 뒤 재시도한다.
근거
이 페이지의 모든 file:line은 2026-08-16 코드베이스 직접 검증(스카웃 3건 교차 + 재검증)이다.
oxicode 경로는 /Volumes/MERCURY/PROJECTS/oxicode/, oxios 경로는 /Volumes/MERCURY/PROJECTS/oxios/ 기준.
- 도구 정의·등록:
oxicode-agent/src/tools.rs(732-742, 744-873, 997-1040, 1128-1175) ·tools/bash.rs(168-540) ·oxicode-ai/src/tools.rs(21-36) - LLM 직렬화:
agent_loop/streaming.rs(57-99) ·oxicode-ai/src/providers/anthropic.rs(205-207, 664-680) · openai.rs(598-620) · ollama.rs(210-225) · google_shared.rs(210) - 루프:
agent_loop/mod.rs(761-1260 run_loop · 1283-1436 maybe_compact) ·helpers.rs·retry.rs·ttsr.rs·oxicode-ai/src/types.rs(100) - 도구 실행·승인:
agent_loop/tool_exec.rs(280-345, 340-518, 867-1003) ·agent_loop/config.rs(276-320) - MCP 클라이언트:
oxicode-agent/src/mcp/(mod.rs 606·882·1163, client.rs 125·225, spawn.rs, consent.rs:210) - SDK 계약:
oxicode-sdk/src/kernel_bridge.rs(24-124) ·agent_builder.rs(388-393) ·ports/hooks.rs(90) ·middleware/hook.rs(24-130) - oxios:
crates/oxios-kernel/src/agent_runtime.rs(737-748, 1016-1117) ·tools/kernel_bridge.rs(41-105) ·tools/builtin/mod.rs(70-123) ·supervisor.rs·orchestrator.rs·agent_lifecycle.rs·skill/prompt.rs(26-36) ·skill/clawhub/installer.rs·src/api/routes/chat.rs(1265-1277) ·src/supervisor.rs·crates/oxios-mcp/src/lib.rs(1-25) - 세션:
oxicode-cli/src/app/agent_session.rs(1094-1183, 2937-2999) ·store/session.rs(893) ·bootstrap.rs(103-113)
관련 페이지 — 정적 구조(16 port·wire 매핑): SDK ↔ oxios 관계 · 프레임워크 비교(제어 흐름 소유권 4티어): 에이전트 SDK 비교 · 전체 생태계 개요: oxi 생태계 해부