research

Stateless의 정의 — v0.1 경계

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 SDKpartialsession 옵션한 단계 위
pi-coding-agentsession, historyv0.2~v0.3
goosesession, extension statev0.5+
Claude Codefull statev1.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는 completestream 두 개.
  • 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 · Compactiondomain-specific. 학생용 일정 agent와 코딩 agent가 다른 전략 필요v0.3 (별도 crate)
Plan · Reflectionagent loop의 변형. 단순 loop부터 검증v0.4
Sub-agent · Delegationcontext 격리는 문제가 생긴 뒤 도입v0.4
Sandbox · PermissionOS 의존, 보안은 별도 도메인v0.8
State 영속화backend 다양 (SQLite/file/KV)v0.3와 함께
Channel · TUIUI는 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

관련