openclaw · hermes · Vercel PAT 같은 완제품 agent와 달리, coreouto는 자기 영역에 맞는 맞춤형 agent를 찍어낼 수 있도록 "loop 코어"만 제공하는 플랫폼형 라이브러리다. 아래에서 "도대체 무엇을 확장할 수 있는가"를 도식과 함께 분석한다.
기존 agent들은 이미 특정 용도로 조립된 완제품이다. coreouto는 스스로를 framework가 아니라 library로 규정한다. 제공하는 것은 딱 하나의 loop뿐이다.
"coreouto는 framework가 되려 하지 않는다. CLI도, server mode도, deployment pipeline도,
plugin system도 없다. 그것은 library다: import하고, call하면, return한다." — docs/philosophy.md
종료 규칙은 단 하나다:
모델이 텍스트는 있고 tool call은 없는 응답을 내면 → 그 텍스트가 최종 답이고 루프 종료.
그 외 provider가 unrecoverable하게 종료(token 초과 · refusal · content filter · 서버 실패)해도 종료.
while True:
response = await provider.create(...)
if not tool_calls:
if response.content:
return Response(...) # ← 유일한 정상 종료
continue # 텍스트·tool call 둘 다 없으면 재프롬프트
# tool call이 있으면 실행하고 루프 계속
| 철학 | 의미 | 코드에서의 발현 |
|---|---|---|
| Minimalism 최소주의 | loop 하나만 잘하면 된다 | agent.py를 10분이면 다 읽는다. 숨은 상태머신 없음 |
| Extensibility 확장성 | 모든 주요 컴포넌트가 교체 가능 | provider/tool/preset/hook 전부 단순 인터페이스 |
| Explicitness 명시성 | 내가 시키지 않으면 아무 일도 안 일어남 | auto-discovery 없음. tool·hook 직접 등록 |
| Fragmentation 파편화 | 각 조각이 독립적으로 동작 | 모듈마다 독립 registry(dict). 서로 모름 |
| Conciseness 간결성 | 사용 코드가 짧고 읽기 쉽다 | tool 5줄, preset 3줄, 호출 1줄 |
public API와 각 registry를 종합하면 확장 축은 정확히 5개다. 각각 독립된 module-level dict registry를 가지며 서로를 모른다.
무엇이든 LLM 백엔드로. 상속 불필요한 duck-typed Protocol.
타입힌트→JSON Schema, docstring→설명 자동 추출.
직렬화·DB저장 가능한 Pydantic 데이터. to_config()로 찍어냄.
7개 이벤트에 sync/async 콜백. 관측·개입 지점.
agent를 tool로 감쌈. 정적 위임 / 동적 dispatch.
@runtime_checkable
class Provider(Protocol):
async def create(self, messages, *, model, tools=None, ...) -> LLMResponse: ...
def format_assistant_message(self, response) -> Message: ...
def format_tool_result(self, tool_call, result) -> Message: ...
→ OpenAI·Anthropic·Google뿐 아니라 로컬 모델·프록시·human-in-the-loop·mock까지 provider로 감쌀 수 있다. 기본 제공: openai, openai-response, anthropic, google.
| 이벤트 | 발화 시점 | 대표 용도 |
|---|---|---|
BEFORE_LLM_CALL | LLM 호출 직전 | 메시지 검사/수정, 프롬프트 로깅 |
AFTER_LLM_CALL | LLM 응답 직후 | 토큰 집계, 응답 로깅 |
BEFORE_TOOL_CALL | tool 실행 직전 | 인자 검증, 승인 게이트 |
AFTER_TOOL_CALL | tool 실행 직후 | 결과 로깅, 사용량 수집 |
ON_ITERATION | 매 루프 반복마다 | 진행 알림, 자동 요약 트리거 |
ON_FINISH | 루프 종료 시 | 최종 후처리, 재프롬프트 결정 |
ON_USER_INJECTION | 런타임 메시지 주입 시 | human-in-the-loop 처리 |
contrib/hooks.py 레시피: token_collection_hook, auto_summarize_hook, token_limit_warning_hook, iteration_notification_hook, tool_usage_collection_hook.
examples는 단순 데모가 아니라 "각 확장 축을 어떻게 꽂는가"를 난이도 순으로 배치한 커리큘럼이다. 번호에서 08·11이 빠져 있는 것도 특징적(의도적 파편화 — 각 예제가 독립적).
| # | 파일 | 확장하는 것 | 핵심 학습 포인트 |
|---|---|---|---|
| 01 | 01_simple.py | (없음) | 최소 실행. AgentConfig 직접 생성 → agent.call() |
| 02 | 02_tools.py | Tool | @register_tool로 함수 노출. 스키마=타입힌트, 설명=docstring |
| 03 | 03_presets.py | Preset | 이름 붙은 config 번들. OpenAI·Anthropic·Google 3개 동시 등록 → 축의 독립성 |
| 04 | 04_hooks.py | Hook | 3가지 부착법: inline hook / token_collection_hook(sink 공유) / iteration_notification_hook |
| 05 | 05_multi_agent.py | Multi(정적) | agent_as_tool("researcher")로 coordinator 위임. MiniMax를 OpenAI 호환 base_url로 |
| 06 | 06_custom_provider.py | Provider(자작) | API키·네트워크·SDK 없이 메서드 3개 MyEchoProvider. Protocol이 duck-typed임을 증명 |
| 07 | 07_provider_settings.py | 설정 정규화 | canonical 8키(temperature/max_tokens…)를 provider 네이티브 kwarg로 번역. 비표준은 provider_passthrough |
| 09 | 09_agent_delegation.py | Multi(동적) | make_delegate_tool() → 런타임에 researcher/writer/critic 선택 dispatch. reasoning_effort |
| 10 | 10_custom_endpoints.py | Provider 라우팅 | 같은 OpenAIProvider로 local(ollama)/프록시/MiniMax/Zhipu/Moonshot 등 7개 엔드포인트 |
| 12 | 12_history.py | 대화 상태(앱 책임) | 상태 자동관리 안 함. call(history=...)로 ①누적 ②가짜 주입 ③빈 리스트 3패턴 |
| 13 | 13_inject.py | 런타임 개입 | inject_user_message()로 루프 도중 메시지 큐잉. hook/동시task 주입. HITL·websocket |
| 14 | 14_anthropic_reasoning.py | 고급 설정 | reasoning_effort: none~max를 Anthropic adaptive thinking으로 매핑. deep vs fast 대비 |
| 15 | 15_multimodal_tool_results.py | Tool 반환 확장 | tool이 ImageBlock/DocumentBlock 반환 → 모델이 이미지·PDF를 실제로 "본다" |
1. 06·12·13이 API 키 없이 Mock/Echo provider로 동작 → provider 추상화가 진짜로 loose하다는 코드 증명. 이게 "core"라는 증거.
2. 05·09·10이 전부 OpenAI 호환 base_url 트릭 → MiniMax·Moonshot·Zhipu·Ollama 등 어떤 벤더든 OpenAIProvider(base_url=...) 한 줄로 편입.
3. 12·13이 "coreouto는 대화 상태·요약·개입을 자동으로 안 한다"를 반복 강조 → 바로 이 부분이 사용자가 자기 영역에 맞게 채워 넣어야 하는 빈칸(RAG·memory·요약·HITL).
4. 번호 순서 = 확장 난이도 곡선: 최소실행 → 단일 축 → 자작 provider → 오케스트레이션 → 운영 디테일.
15_multimodal_tool_results.py)| provider | images | documents | video | audio |
|---|---|---|---|---|
| anthropic | ✔ | ✔ | ✔ | ✔ |
| openai-response | ✔ | ✔ | ✘ | ✘ |
| google (new SDK) | ✔ | ✔ | ✔ | ✔ |
| openai (chat) | ✘ | ✘ | ✘ | ✘ → ValueError |
docs/philosophy.md의 "우리가 일부러 넣지 않은 것" 목록이 곧 사용자 영역의 확장 슬롯 목록이다:
| 일부러 뺀 기능 | coreouto가 대신 주는 것 | 사용자가 채우는 방법 |
|---|---|---|
| Auto-summarization | timing만 (ON_ITERATION) | auto_summarize_hook에 요약 함수 주입 |
| Agent-to-agent 통신 | agent_as_tool 위임 primitive | 원하는 토폴로지 직접 조립 |
| 자동 retry | tool 에러를 LLM에 결과로 전달 | hook 또는 provider 래핑 |
| RAG / memory / vector store | tool 시스템 | 검색·기억 tool 직접 작성 |
| Streaming | 없음(전체 응답 수집) | provider 레벨 또는 hook |
| Config 파일(YAML) | Python 코드 | 코드로 register |
| 관측성(로깅/트레이싱) | hook 시스템 | OpenTelemetry 등 hook 연결 |