oxicode-sdk ↔ oxios, 누가 누구를 감싸는가
oxios는 겉보기엔 거대한 Rust 데몬이지만, 정작 "LLM한테 말 걸고 도구를 실행하는" 핵심 두뇌는 자기가 짠 코드가 아닙니다. crates.io에서 받아온 oxicode-sdk 라이브러리를 통째로 빌려 쓰죠. 이 페이지는 그 관계를 코드 레벨에서 뜯어봅니다 — 어떤 타입을 어디서 가져다 쓰는지 실제 파일:라인까지 인용해서, oxios가 SDK 위에 정확히 무엇을 얹었고 무엇은 절대 손대지 않는지 보여줍니다.
한 줄로 정리하면
먼저 결론부터 박고 시작합니다. 나머지 섹션은 전부 이 한 문장을 코드로 증명하는 과정이에요.
oxicode-sdk가 밖으로 내놓는 것들
oxicode-sdk의 lib.rs는 885줄짜리 파일 하나인데, 여기 있는 모든 안정 API는
#[oxicode_stable(since = "0.63.0")] 매크로로 표시돼 있습니다.
oxicode-ai·oxicode-agent를 한 줄로 재노출(re-export)하는
"단일 의존성 패턴"을 지향하고 있어서, oxios 입장에서는 이 크레이트 하나만 물면
두 계층 아래 있는 기능까지 전부 손에 들어옵니다.
| 심볼 | 정의 위치 | oxios에서의 쓰임 |
|---|---|---|
Oxicode / OxicodeBuilder | lib.rs:76 | 엔진 팩토리 — OxiosEngine이 그대로 감싼다. |
Agent / AgentConfig / AgentEvent | lib.rs:265–271 | 에이전트 실행 루프 — agent_runtime.rs가 매 디렉티브마다 fresh Agent를 빌드. |
EventBus | lib.rs:132 | 커널 이벤트 채널 — EventBus<KernelEvent> 타입 그대로 재사용. |
MessageBus / InterAgentMessage | lib.rs:97 | A2A 브로드캐스트 — a2a_api.rs의 MessageBus::new(256). |
TokenBundle / AuthStore / OAuthError | lib.rs:224–227 | 자격증명 저장소 + 마이그레이션 — credential.rs/onboarding.rs/registry.rs. |
Authorizer / Tracer / CostTracker | lib.rs:151–166 | RFC-014 Phase D 관측 핸들 — OxiosEngine::with_* 메서드로 옵션 부착. |
CircuitBreaker / DefaultCircuitBreaker | lib.rs:388–390 circuit-breaker | 글로벌 LLM 회로차단기 — agent_runtime.rs:52가 그대로 사용. |
실제로 이 표면이 어떻게 조립되는지는 모듈 목록만 봐도 감이 옵니다 (lib.rs 38–68줄):
// oxicode-sdk/src/lib.rs — 15개 포트 모듈, prelude가 이걸 한 줄로 묶는다 pub mod agent_builder; pub mod agent_group; pub mod bridge; pub mod builder; pub mod coordination; pub mod delegation; pub mod event_bus; pub mod kernel_bridge; pub mod message_bus; pub mod middleware; pub mod observability; pub mod ports; pub mod prelude; // oxicode_sdk::prelude::* 한 줄로 전부 가져오는 글루 pub mod routing; pub mod security; pub mod workflow_dsl; pub mod workflow_engine;
그리고 README가 보여주는 표준 사용 패턴은 이렇습니다 — 이 몇 줄이 사실상 SDK 전체의 축소판입니다:
use oxicode_sdk::prelude::*; let oxicode = OxicodeBuilder::new().with_builtins().api_key("anthropic", "sk-ant-...").build(); let agent = oxicode.agent(AgentConfig { model_id: "anthropic/claude-sonnet-4-20250514".into(), max_iterations: 20, ..Default::default() }) .workspace("/my/project") .coding_tools() .system_prompt("You are a senior Rust developer.") .build()?; let (response, events) = agent.run("Refactor main.rs".into()).await?;
SDK 스스로 인정한 약점, oxios가 메운 자리
DESIGN_IMPROVEMENTS_V2.md 자기 반성 노트재밌는 건 SDK의 v2 설계 문서가 스스로 "근본 문제 4건"을 인정하고 있다는 점입니다. AgentEvent::Usage는 정의만 됐지 실제로 emit되는 곳이 없고, AgentState::record_usage()는 테스트 코드 말고는 호출된 적이 0건이고, ProviderEvent에는 EndTurn variant 없이 Done만 있고, Agent::run()/run_streaming()은 둘 다 !Send future라서 spawn_blocking이 강제로 필요합니다.
정리하면 스트리밍-퍼스트, Send 제약, 컴팩션 이벤트 누락이 SDK가 문서로 인정한 갭이고, oxios는 이걸 그냥 참고 감수하는 게 아니라 ProviderEvent::Done에서 usage를 직접 추출하고 tokio 태스크 경계를 세심하게 나눠 !Send 제약을 우회하는 식으로 실제 코드에서 메워가며 씁니다. SDK가 다 못 준 부분을 소비자가 감수하고 메운다는 게, 바로 이 페이지 전체가 말하려는 "행동 vs 정책" 경계의 실제 사례입니다.
oxios가 실제로 선언한 의존
말로만 "의존한다"가 아니라, Cargo.toml과 Cargo.lock에 정확히 뭐라고 적혀있는지 인용으로 확인합니다.
// /Volumes/MERCURY/PROJECTS/oxios/Cargo.toml:122–127 (workspace.dependencies) // oxicode engine — single dependency for everything // (oxicode-ai is re-exported via oxicode-sdk). // 0.73.0: SDK 0.72가 browsing re-exports를 삭제 — oxibrowser-core는 // 이미 직접 의존이었기 때문에 oxios는 영향 없음 (RFC-046). oxicode-sdk = { version = "0.73.0", features = ["delegation", "circuit-breaker", "router"] } // Cargo.toml:200–206 (본체 [dependencies]) [dependencies] oxios-kernel = { version = "1.39.0", path = "crates/oxios-kernel", default-features = false } // ... oxicode-sdk = { workspace = true }
그런데 SDK를 실제로 컴파일하는 크레이트는 oxios-kernel 하나뿐이 아닙니다. kernel의 Cargo.toml을 보면 재밌는 게 하나 더 있습니다:
// crates/oxios-kernel/Cargo.toml:35–43 oxios-mcp = { version = "1.39.0", path = "../oxios-mcp" } oxicode-sdk = { workspace = true } // oxibrain daemon client (RFC-047) — path dep until oxibrain-client 0.1 // is published to crates.io. oxibrain-client = { version = "0.1", path = "../../../oxibrain/crates/oxibrain-client" } // 0.73.0: BrowseProgress::PdfExported, etc. oxicode-agent = "0.73.0"
즉 oxios-kernel은 oxicode-sdk뿐 아니라 oxicode-agent도 직접 끌어옵니다 — SDK가 다 커버 못 하는 부분(주로
oxicode-agent의 원본 타입, MCP 내부 타입)을 직접 가져다 쓰는 거죠. 반대로 oxios-markdown·oxios-calendar·oxios-gateway·oxios-mcp
같은 나머지 크레이트는 oxicode-sdk를 직접 끌어오지 않고 oxios-kernel을 거쳐서만 간접적으로 씁니다.
예외가 딱 하나 더 있는데, oxios-ouroboros는 model_resolver.rs와 engine.rs에서 SDK 타입을
직접 임포트합니다 — SDK 직접 의존을 갖는 두 번째이자 마지막 크레이트입니다 (§04에서 다시 봅니다).
// Cargo.lock:6504–6508 [[package]] name = "oxicode-sdk" version = "0.73.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16298d2d35c7c6593f8c1e877f1ac9f9866439cc93e15426cede191f2a2f8cb"
"oxicode-sdk is crates.io only. Never add as path dep." — 규칙은 문서에만 있는 게 아니라
Cargo.lock의 registry+ 소스가 그대로 증거입니다. [patch.crates-io]에
path 오버라이드 코드가 남아있긴 하지만 Cargo.toml:286–288에서 통째로 주석 처리돼 있고요.
활성화된 feature는 딱 셋 — delegation, circuit-breaker, router. 그리고 oxios 안의 어떤 크레이트도
SDK 타입을 pub use로 재노출하지 않습니다 — oxios 내부 타입과 SDK 타입을 1:1로 매핑하는
어댑터만 존재할 뿐입니다.
실제 호출 지점 — file:line으로 증명
여기가 이 페이지의 핵심입니다. "의존한다"는 선언 말고, 실제 코드에서 SDK 타입이 어디에 어떻게 박혀 있는지 — 88개가 넘는 파일 중 의미가 가장 큰 6곳만 추려서 인용합니다.
engine.rs — 엔진 래퍼
crates/oxios-kernel/src/engine.rsOxiosEngine은 SDK의 Oxicode 인스턴스를 감싸는 구조체입니다. 라우팅·자격증명·관측 핸들을 전부 옵션 필드로 들고 있다가, Provider 생성이나 카탈로그 주입 같은 실무는 그대로 SDK에 위임합니다.
// engine.rs:19 use oxicode_sdk::{CatalogConfig, FileModelCatalog, ModelCatalog, Oxicode, OxicodeBuilder}; // engine.rs:48–50 — OxiosEngine이 옵션 필드로 들고 있는 SDK 핸들 authorizer: Option<Arc<oxicode_sdk::Authorizer>>, tracer: Option<Arc<oxicode_sdk::Tracer>>, cost_tracker: Option<Arc<oxicode_sdk::CostTracker>>, // engine.rs:314–316 — Provider 생성을 SDK에 위임 pub fn create_provider(&self, name: &str) -> Result<Arc<dyn oxicode_sdk::Provider>> { Ok(self.oxi.create_provider(name)?) }
agent_runtime.rs — 가장 두꺼운 결합
crates/oxios-kernel/src/agent_runtime.rs디렉티브 하나를 실행할 때마다 이 파일이 SDK의 Agent를 새로 빌드합니다. 문서 주석이 그 의도를 못박아 두고 있습니다 — "engine.oxi().agent()(AgentBuilder)로 미들웨어·관측·보안 통합을 오xicode-sdk 0.23.0부터 그대로 가져온다"고요.
// agent_runtime.rs:31–33 use oxicode_sdk::observability::AuditTrail; use oxicode_sdk::{Agent, AgentConfig, AgentEvent, CompactionEvent, CompactionStrategy}; use oxicode_sdk::{SearchCache, ToolExecutionMode, ToolRegistry}; // agent_runtime.rs:52 — 글로벌 회로차단기도 SDK 타입 그대로 use oxicode_sdk::{BreakerState, CircuitBreaker, DefaultCircuitBreaker}; // agent_runtime.rs:732–735 doc-comment, 원문 그대로 // "Uses engine.oxi().agent() (AgentBuilder) for full middleware, // observability, and security integration from oxicode-sdk 0.23.0."
event_bus.rs — 커널 이벤트버스 재사용
crates/oxios-kernel/src/event_bus.rsoxios는 tokio::sync::broadcast를 다시 구현하지 않습니다. SDK가 이미 만들어 둔 제네릭 래퍼를 타입 별칭 하나로 그대로 빌려 씁니다.
// event_bus.rs:1–27 //! Event bus: inter-agent communication via oxicode_sdk::EventBus<KernelEvent>. //! it reuses oxicode_sdk::EventBus<E>, a generic wrapper over tokio::sync::broadcast. use oxicode_sdk::EventBus as SdkEventBus; pub type EventBus = SdkEventBus<KernelEvent>;
a2a_api.rs — A2A + MessageBus
crates/oxios-kernel/src/kernel_handle/a2a_api.rsA2A(Agent-to-Agent) 프로토콜 자체는 oxios가 직접 구현하지만, 브로드캐스트 채널만큼은 SDK의 MessageBus를 그대로 빌립니다.
// a2a_api.rs:10–38 message_bus: oxicode_sdk::MessageBus, message_bus: oxicode_sdk::MessageBus::new(256), pub fn message_bus(&self) -> &oxicode_sdk::MessageBus { &self.message_bus } pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<oxicode_sdk::InterAgentMessage> { self.message_bus.subscribe() }
credential.rs — 자격증명 저장/마이그레이션
crates/oxios-kernel/src/credential.rsSDK의 TokenBundle을 그대로 직렬화해서 ~/.oxios/auth.json(OXICODE_HOME)과 ~/.oxicode/auth.json(oxicode-cli 호환) 두 저장소를 동기화하는 브로커 역할을 합니다.
// credential.rs — load/save는 SDK 함수 호출, 언제·어디서 부를지는 oxios 정책 if let Ok(Some(token)) = oxicode_sdk::load_token(provider) { ... } let token = oxicode_sdk::TokenBundle { ... }; if let Err(e) = oxicode_sdk::save_token(provider, &token) { ... } // 255–256 — OAuthError::Json이면 레거시 마이그레이션 트리거 (RFC-014 Phase F) fn is_legacy_auth_error(err: &oxicode_sdk::OAuthError) -> bool { matches!(err, oxicode_sdk::OAuthError::Json(_)) } // 305–306 — 두 홈 디렉토리에 동기화 let store = oxicode_sdk::AuthStore { tokens: migrated }; oxicode_sdk::save_auth_store(&store)?;
oxios-ouroboros — SDK 타입 직접 의존 (예외 크레이트)
crates/oxios-ouroboros/src/{model_resolver,engine}.rs§03에서 예고했던 그 예외입니다. oxios-kernel을 거치지 않고 직접 SDK 타입을 끌어다 쓰는 유일한 다른 크레이트가 oxios-ouroboros입니다.
// model_resolver.rs:23 use oxicode_sdk::{Model, Provider}; // engine.rs:11, 451–453 use oxicode_sdk::{Context, Message, ProviderEvent, UserMessage}; let model = oxicode_sdk::Model::new(id, id, oxicode_sdk::Api::OpenAiCompletions, "test", ""); let provider: Arc<dyn oxicode_sdk::Provider> = Arc::new(oxicode_sdk::OpenAiProvider::with_base_url_and_key(...));
engine_api.rs:1460–1481의 try_validate()는 API 키 하나 검증하자고
별도의 임시 Oxicode 인스턴스를 통째로 새로 만듭니다. Context에 "Hi" 한 마디만 넣고
스트림을 열어서 응답이 오는지만 확인하고 바로 버리는 거죠. 이게 가능한 이유는 SDK의 격리 속성 덕분입니다 —
"두 Oxicode 인스턴스는 상태를 공유하지 않는다"는 설계를 그대로 활용한 사례입니다.
전체 워크스페이스를 grep하면 oxicode_sdk::*를 쓰는 파일이 88개 이상 나오는데, 위 6곳이
그중에서 가장 결합의 형태가 뚜렷하게 보이는 지점들입니다.
한 장으로 보는 호출 체인
위에서 본 개별 호출 지점들을 하나의 그림으로 이어 붙이면, 요청 하나가 실제로 어떤 경로를 타고 LLM까지 갔다가 돌아오는지 보입니다.
OxiosEngine = 얇은 어댑터
SDK의 Oxicode 인스턴스를 감싸는 얇은 어댑터일 뿐입니다. 라우팅·자격증명·관측 핸들을 옵션 필드로 들고 있다가, 디렉티브가 들어올 때마다 agent_runtime.rs::run_agent가 새 Agent를 빌드합니다.
A2A는 독립, 채널만 빌림
A2A 프로토콜 자체는 oxios가 직접 구현하지만, 그 브로드캐스트 채널은 SDK의 MessageBus를 그대로 빌려 씁니다 (a2a_api.rs:21).
자격증명은 SDK 타입 그대로 직렬화
TokenBundle을 SDK에서 그대로 가져와 두 홈 디렉토리에 동기화합니다 (credential.rs:305). 저장 로직은 SDK 함수, 언제·어디에 저장할지는 oxios 정책입니다.
가벼운 작업은 스트리밍 직접 사용
컴팩션·지식렌즈·키검증은 Context+Provider 스트리밍을 직접 씁니다 — Agent 전체를 새로 만들 필요가 없는 가벼운 작업이라서 토큰과 오버헤드를 아낍니다.
소유권 vs 경계 — oxios가 안 하는 것
oxios가 SDK 위에 뭘 얹었는지는 이미 봤으니, 이제 반대로 — oxios가 절대 손대지 않는 게 뭔지가 오히려 더 중요한 신호입니다.
| 책임 영역 | 담당 | 인용 |
|---|---|---|
| LLM 스트리밍, 도구 루프, 컴팩션 이벤트 | SDK | agent_runtime.rs:732 "wraps oxicode-sdk's Agent" |
| Provider 풀, 라우팅 | SDK (router feature) | engine.rs:446 RoutingControl |
| 회로차단기, 비용 추적, 트레이싱 | SDK (circuit-breaker, observability) | agent_runtime.rs:52 DefaultCircuitBreaker |
| 이벤트버스, 메시지버스 | SDK 재사용 | event_bus.rs:14, a2a_api.rs:13 |
| OAuth 토큰 영속화 | SDK 함수 + oxios 정책 | credential.rs:255 OAuthError::Json 분기 |
| RBAC(AccessManager) + 경로 샌드박스 + Merkle 감사 | oxios 자체 | AGENTS.md:99 |
| OxiosEngine 어댑터, hot-swap, 카탈로그 디스크 캐시 | oxios 자체 | engine.rs:506 |
| 채널 (Web/CLI/Telegram/Remote) | oxios 자체 | Cargo.toml:177–198 features |
| A2A 프로토콜, Ouroboros 오케스트레이션 | oxios 자체 | a2a_api.rs:10 |
| HookRunner ↔ 외부 스크립트 브리지 | oxios 자체 (SDK는 trait만) | hook_runner.rs:7 |
| Memory Daemon (oxibrain) | oxios 외부 (path dep) | Cargo.toml:257 |
| Browser 도구 | oxios 자체 (oxibrowser-core 직접 의존) | Cargo.toml:254 |
oxicode-ai를 직접 의존하지 않습니다 — 유일한 예외가 §04에서 본 oxios-ouroboros입니다.
SDK의 ProviderPool/RateLimitPolicy도 안 씁니다 — 0.61에서 SDK 쪽이 아예 제거했거든요
(agent_runtime.rs:1018–1022 주석). SDK의 브라우징 re-export에도 의존하지 않습니다 — 0.72에서
삭제됐을 때도 oxios는 이미 oxibrowser-core를 직접 잡고 있어서 영향이 없었습니다 (RFC-046).
그리고 SDK의 전역 상태를 아예 쓰지 않습니다 — 디렉티브마다 OxicodeBuilder → Oxicode → Agent를
매번 새로 만드는 것도 결국 "격리"와 "hot-swap 가능성"을 얻기 위한 선택입니다.
0.23 → 0.73, 그 사이 무슨 일이 있었나
AgentBuilder 채택 → 0.61ProviderPool 제거
단일 AgentBuilder로 수렴 → 0.66oxi-* → oxicode-*
전면 rename → 0.73현재
delegation/circuit-breaker/router
버전별 변경 로그는 넘어가고 흐름만 짚자면 — oxios는 SDK의 메이저 변경을 계속 따라가되, 호환성이 깨지면
자체 우회를 구축하는 패턴을 반복합니다. 0.72에서 SDK가 브라우징 re-export를 삭제했을 때도 oxios는 이미
갖고 있던 oxibrowser-core 직접 의존 덕분에 아무 영향 없이 넘어갔죠.
면접 한 줄 요약
실제로 면접에서 이 관계를 설명해야 한다면, 이 세 문장이면 충분합니다.
한 문장으로
"oxios는 oxicode-sdk를 부팅 로더 한 번 호출하고 그 위에서 자체 엔진(OxiosEngine)을 돌린다 — SDK의 Oxicode 인스턴스를 감싸서 디렉티브마다 새 Agent를 빌드하고, AccessManager·A2A·OAuth 브로커·채널·메모리 데몬은 전부 oxios가 자체 구현한다."
코드로 증명하라면
"engine.rs:19가 OxicodeBuilder/Oxicode/ModelCatalog을 가져오고, agent_runtime.rs:732가 매 디렉티브마다 engine.oxi().agent(cfg).build()로 새 Agent를 만드는 게 핵심 — 동시에 credential.rs:305에서 AuthStore를 두 홈 디렉토리에 동기화한다."
경계는 명확하다
"SDK = 행동(인터페이스 + 참조구현), oxios = 정책(RBAC + OAuth + 채널 + 카탈로그 캐시). oxios는 oxicode-sdk를 crates.io로만 받고 path dep로 끌어오지 않으며, 어떤 타입도 SDK로부터 pub use 재노출하지 않는다 (AGENTS.md:158)."
oxios는 oxicode-sdk를 "두뇌"로 빌려 쓰는 Agent Operating System 껍데기입니다. SDK가 LLM 호출·도구 등록·이벤트버스·토큰 저장소 같은 행동을 제공하면, oxios는 AccessManager·A2A·OAuth 브로커·채널·메모리 데몬 같은 정책을 그 위에 입힙니다.