Stateless의 정의 — v0.1 경계
v0.1은 호출 1회의 순수성을 지킨다. 같은 입력 → 같은 출력. 모든 "기억"은 호출자가 관리. 이 노트의 목적: agent CLI(=v1.0의 최종 형태)의 가장 안쪽 코어를 어디에 그을지 결정한다.
한 줄
v0.1은 agent CLI의 가장 안쪽 LLM 호출 primitive다. agent loop도, memory도, plan도 들어 있지 않다. 그것들이 올라설 토대가 v0.1.
최종 형태와의 관계
- agent CLI는 v0.2부터 형태가 잡힌다 (loop가 생기는 순간).
- v0.1은 그 안쪽의 호출 단위. CLI가 아니라 라이브러리(crate).
- 처음부터 cli/sdk를 분리하는 이유: 위 layer에서 다른 policy 실험이 가능해야 하므로.
왜 "정의"가 먼저인가
설계 실력은 무엇을 안 만들지를 결정하는 데서 시작한다. 빼야 할 게 분명해야 위로 쌓을 때 layer 경계가 깨끗해진다. v0.1을 너무 두껍게 잡으면 v0.2~v0.4에서 다른 패턴(ReAct, Plan-and-Execute, Reflexion, multi-agent)을 실험할 자유가 사라진다.
이건 research/agent-harness/core-engine의 "agent loop 자체는 trivial하다 (~30줄). 99%는 그 주변 인프라" 명제의 자연스러운 귀결.
stateless의 스펙트럼 — 시장 위치
| 도구 | stateless? | 보존하는 상태 | v0.1과의 거리 |
|---|---|---|---|
| async-openai (Rust) | ✅ | HTTP 클라이언트, 인증 | 가장 가까움 |
| OpenAI Python SDK | ✅ | 동일 | 가까움 |
| pi-ai (mariozechner) | ✅ | provider 추상화, 인증 | v0.1 기준점 |
| Claude Agent SDK | partial | session 옵션 | 한 단계 위 |
| pi-coding-agent | ❌ | session, history | v0.2~v0.3 |
| goose | ❌ | session, extension state | v0.5+ |
| Claude Code | ❌ | full state | v1.0 |
v0.1은 pi-ai 수준: HTTP wrapper보다 위(추상화 있음), agent loop보다 한참 아래(상태 보존 없음).
⚠️ 검증 필요: async-openai/pi-ai의 stateless 보장 정도, goose가 SDK 형태로 분리 가능한지 —
references-goose, references-pi-mono 노트에서 1차 자료로 확인.
v0.1이 남기는 5가지 primitive
각 결정마다 왜 남기는가, 어떻게 추상화하는가, 어떤 trade-off가 있는가.
1. Provider 추상화
- 왜 남기는가: vendor lock-in 방지. 캠퍼스 LLM 토큰 endpoint가 어떤 형식이든(OpenAI-compatible일 가능성이 매우 높음) 같은 호출 코드로 다룰 수 있어야 함.
- 추상화 형태:
trait Provider하나. method는complete와stream두 개. - trade-off: 추상화 비용(provider별 quirk를 매번 처리) vs 단일 vendor 단순함. 학내 + OpenAI + Anthropic + 로컬 LLM(llama.cpp/vLLM)을 모두 다뤄야 하므로 추상화 채택.
2. Message 타입
- 왜 남기는가: 모든 LLM 호출의 단위. role/content/tool_call/tool_result 구분이 없으면 위 layer가 매번 변환해야 함.
- 추상화 형태:
enum Role(System/User/Assistant/Tool),enum Content(Text/ToolUse/ToolResult, 추후 Image 등 확장). - trade-off: future-proof를 위한 enum vs 단순 String. 처음엔 변형 적게 시작하고, 멀티모달은 v0.x 단계에서 점진 확장.
3. Tool 정의 schema
- 왜 남기는가: tool은 개념이지 dispatch는 호출자 책임. v0.1은 "어떤 tool이 있는지를 LLM에 알려주기" 까지만.
- 추상화 형태:
struct ToolSpec { name, description, parameters: JsonSchema }. Rust 측은schemars+serde로 자동 추출. - trade-off: 정의만 있고 실행은 어디서? → 호출자가 tool_call을 받고 자기 책임 하에 dispatch. 이게 v0.2 agent loop의 핵심 책임.
4. Streaming primitive
- 왜 남기는가: stateless여도 streaming은 native. 토큰 단위로 받아야 UX(TUI/웹/봇)가 자연스러움.
- 추상화 형태:
Pin<Box<dyn Stream<Item = Result<Chunk>> + Send>>. tokio 생태계 표준. - trade-off: 동기 API도 둘 것인가? →
complete는 동기 결과,stream은 비동기 스트림. 둘 다 제공.
5. Error · Retry · Timeout
- 왜 남기는가: 모든 호출이 fallible. 호출자가 매번 wrap하면 boilerplate가 커지고 일관성 깨짐.
- 추상화 형태:
enum LlmError(Network/Auth/RateLimit/InvalidRequest/Server/Timeout), retry는 명시적 옵션, 기본 off. - trade-off: "기본 retry on" vs "명시적 opt-in". 예측 가능성을 위해 후자. 단, RateLimit는 helper 제공.
v0.2 이후로 미루는 것 6가지
각각 왜 v0.1이 아닌지, 어디로 가는지.
| 빼는 것 | 왜 v0.1이 아닌가 | 어디로 가는가 |
|---|---|---|
| Agent loop | 호출자가 정의하는 게 더 유연. ReAct/Plan-and-Execute/Reflexion 자유 실험 | v0.2 (별도 crate llm-loop) |
| Memory · Compaction | domain-specific. 학생용 일정 agent와 코딩 agent가 다른 전략 필요 | v0.3 (별도 crate) |
| Plan · Reflection | agent loop의 변형. 단순 loop부터 검증 | v0.4 |
| Sub-agent · Delegation | context 격리는 문제가 생긴 뒤 도입 | v0.4 |
| Sandbox · Permission | OS 의존, 보안은 별도 도메인 | v0.8 |
| State 영속화 | backend 다양 (SQLite/file/KV) | v0.3와 함께 |
| Channel · TUI | UI는 SDK 책임 아님 | CLI crate에서 (v0.2+) |
업계 미해결 문제 — agent가 어떻게 action할지
이 SDK가 의도적으로 회피하는 문제. v0.1이 action policy를 들고 있지 않은 이유:
v0.1이 action policy를 들고 있으면
→ 다른 policy 실험이 막힘
→ ReAct, Plan-and-Execute, Reflexion, Tree-of-Thought, Multi-agent 등을
같은 SDK 위에서 시도할 수 없게 됨
대신 v0.1은 모든 policy의 substrate가 된다.
→ policy는 v0.2 이후 별도 crate로 점진 추가
→ 본격적으로 이 문제와 마주치는 시점은 v0.4 (sub-agent / delegation)
이 문제는 OpenAI(Codex), Anthropic(Claude Code), Google(Gemini) 모두 자기 답을 갖고 있지만 수렴된 정답은 없다. 진화 로드맵의 v0.4 단계에서 이 결정에 정면으로 마주친다 — 그게 설계 실력 향상의 가장 큰 성장점이 될 것.
Rust 모듈 초안 (개념 검증)
crate 분할:
llm-core/ ← Provider trait, Message, Tool schema, Error
llm-providers-openai/ ← OpenAI / OpenAI-compatible (학내 endpoint 1순위)
llm-providers-anthropic/
llm-providers-local/ ← llama.cpp / vLLM
핵심 trait 골격 (디테일은 rust-module-structure에서 확정):
#[async_trait]
pub trait Provider: Send + Sync {
async fn complete(&self, req: CompletionRequest)
-> Result<Completion, LlmError>;
fn stream(&self, req: CompletionRequest)
-> Pin<Box<dyn Stream<Item = Result<Chunk, LlmError>> + Send>>;
}
pub struct CompletionRequest {
pub model: String,
pub messages: Vec<Message>,
pub tools: Vec<ToolSpec>,
pub options: SamplingOptions,
}
위 시그니처는 제안이며, references-goose·references-pi-mono 분석 후 최종 확정.
다음 결정 — v0.2 분기점 미리보기
v0.2로 넘어갈 때 첫 결정:
- agent loop을 SDK에 넣을 것인가, 별도 crate인가?
- 채택 후보: 별도 crate(
llm-loop). v0.1 사용자(=loop 없이 LLM만 쓰고 싶은 사람)가 그대로 쓸 수 있게.
진짜 큰 결정은 v0.4의 action policy 선택이다. 그건 evolution-roadmap 노트에서 단계가 가까워지면 깊이 다룬다.
검증 필요 항목 (다음 노트들에서 확인)
- 캠퍼스 LLM 토큰 endpoint가 OpenAI-compatible 인지 — business-segment-entry
- async-openai crate의 추상화 깊이와 v0.1 후보로서 적합성 —
references-goose같은 흐름에서 함께 검토 - pi-ai의 cross-provider context 패턴이 v0.1에 차용 가능한지 — references-pi-mono
- goose의 어떤 부분이 v0.1 수준 SDK로 추출 가능한지 —
references-goose
관련
-
- research/agent-harness/core-engine — agent loop trivial 명제 (이 정의의 정당성 근거)
- research/pi-sdk/overview — pi-ai의 cross-provider context 패턴
- evolution-roadmap — v0.1 → v1.0 단계별 회고 (예정)
references-goose— Rust 1순위 reference 분석 (예정)- rust-module-structure — trait 설계 상세 (예정)