OpenClaw — 코어 루프 분석
Table of Contents
1. 아키텍처 — Gateway + Pi-Core 이중 구조
2. 호출 체인 (agentCommand → attempt)
3. 메인 재시도 루프 (run.ts)
4. 시도 실행 (attempt.ts)
5. 스트리밍 파이프라인 — 9중 래핑
6. 플러그인 컨텍스트 엔진
7. 도구 설계
8. 종합 인사이트
1. 아키텍처 — Gateway + Pi-Core 이중 구조
OpenClaw의 핵심 차별점: 에이전트 루프를 직접 구현하지 않는다. Pi-agent-core 런타임에 위임하고, 그 위에 Gateway(멀티플랫폼), 재시도, 컨텍스트 엔진을 계층화한다.
사용자 입력 (CLI, Telegram, Discord, WhatsApp, Android, iOS, macOS)
│
▼
Gateway Layer src/agents/agent-command.ts
│ 메시지 수신 → 세션 resolve → 인증 → 모델 선택
│ 스킬 스냅샷 로딩 → thinking level resolve
▼
Retry/Failover Layer src/agents/pi-embedded-runner/run.ts
│ while(true) {
│ runEmbeddedAttemptWithBackend()
│ - context overflow → compaction → retry
│ - auth failure → profile rotation
│ - rate limit → backoff → retry
│ - empty response → retry
│ - planning-only → "act now" injection → retry
│ - model failure → failover
│ }
▼
Attempt Layer src/agents/pi-embedded-runner/run/attempt.ts
│ 도구 생성 → 시스템 프롬프트 빌드
│ 워크스페이스 부트스트랩 → 컨텍스트 엔진 어셈블
│ 9중 스트리밍 래퍼 → pi-agent-core에 제출
▼
Pi-Agent-Core Runtime (packages/agent/ 라이브러리)
│ runLoop() ← Pi와 동일한 에이전트 루프
│ streamAssistantResponse() → executeToolCalls()
│ steering/followUp 메시지 큐
▼
결과 스트림 → Gateway → 사용자
Insight: OpenClaw = Pi Core + Gateway + 재시도 + 컨텍스트 엔진
에이전트 루프의 "심장"은 Pi와 완전히 동일하다. OpenClaw가 추가하는 것은 (1) 멀티플랫폼 Gateway, (2) 정교한 재시도/실패복구, (3) 플러그인 컨텍스트 엔진, (4) 세션 직렬화. 이는 "루프 자체는 단순하고, 복잡성은 루프 주변에 있다"는 원칙의 또 다른 증거.
2. 호출 체인 GATEWAY
src/agents/agent-command.ts (~1123줄)// 진입점 1: CLI/로컬 (신뢰됨)
export async function agentCommand(params) {
return agentCommandInternal({
...params,
senderIsOwner: true // 로컬 = 항상 소유자
});
}
// 진입점 2: HTTP/WS (외부)
export async function agentCommandFromIngress(params) {
return agentCommandInternal({
...params,
// senderIsOwner는 명시적으로 전달해야 함
});
}
// 내부 구현
async function agentCommandInternal(params) {
// (1) 메시지 검증 + 세션 resolve
const session = await resolveSession(params);
// (2) 에이전트 런타임 설정 resolve
const runtimeConfig = await resolveAgentRuntimeConfig(session);
// (3) 모델 선택 (저장된 설정 → 명시적 지정 → allowlist 검사)
const model = resolveModelOverride(runtimeConfig);
// (4) 스킬 스냅샷 로딩
const skills = await buildSkillsSnapshot(session.workspace);
// (5) Thinking level + Verbose level resolve
const thinkingLevel = resolveThinkingLevel(params, runtimeConfig);
// (6) 실행 — 모델 폴백 포함
const result = await runWithModelFallback(async () => {
return attemptExecutionRuntime.runAgentAttempt({
session, model, skills, thinkingLevel, ...
});
});
// (7) 세션 저장 (토큰 사용량, 모델 정보)
await session.store.update({ tokens: result.usage, model: result.model });
// (8) 결과 전달
return deliverAgentCommandResult(result);
}
3. 메인 재시도 루프 PI-RUNNER
src/agents/pi-embedded-runner/run.ts (~2088줄)export async function runEmbeddedPiAgent(params) {
// 세션 레인 큐잉: 같은 세션의 요청을 직렬화
return enqueueCommandInLane(sessionKey, async () => {
// 모델 + 인증 프로필 resolve
const profileCandidates = resolveProfileCandidates(params);
// 컨텍스트 엔진 초기화 (1회만)
await ensureContextEnginesInitialized();
const contextEngine = resolveContextEngine(params);
let compactionAttempts = 0;
let profileIndex = 0;
// ═══════════════════════════════════════════════════════
// 메인 재시도 루프 — while(true)
// 이것은 에이전트 루프가 아님! pi-core가 에이전트 루프.
// 이것은 "에이전트 시도의 재시도/폴백/복구 루프"
// ═══════════════════════════════════════════════════════
while (true) {
try {
const result = await runEmbeddedAttemptWithBackend({
...params,
profile: profileCandidates[profileIndex],
contextEngine,
});
return result; // 성공 → 탈출
} catch (error) {
// ── Context Overflow → 컴팩션 후 재시도 ──
if (isContextOverflow(error) && compactionAttempts < 3) {
await compactSession(params.session);
compactionAttempts++;
continue;
}
// ── Timeout → 컴팩션 후 재시도 ──
if (isTimeoutTriggered(error) && compactionAttempts < 2) {
await compactSession(params.session);
compactionAttempts++;
continue;
}
// ── Auth 실패 → 프로필 로테이션 ──
if (isAuthFailure(error) && profileIndex < profileCandidates.length - 1) {
profileIndex++;
continue;
}
// ── Rate Limit → 백오프 + 프로필 로테이션 ──
if (isRateLimited(error)) {
if (profileIndex < profileCandidates.length - 1) {
profileIndex++;
} else {
await backoff(error);
profileIndex = 0; // 처음부터 다시
}
continue;
}
// ── Planning-only 감지 → "act now" 주입 ──
// 모델이 계획만 세우고 행동하지 않을 때
if (isPlanningOnly(result)) {
params.steeringMessage = "Stop planning and take action now.";
continue;
}
// ── Empty Response → 재시도 ──
if (isEmptyResponse(result)) {
continue;
}
// ── 모델 실패 → FailoverError → 상위로 전파 ──
if (shouldFailover(error)) {
throw new FailoverError(error);
}
throw error; // 복구 불가능
}
}
});
}
Insight: "에이전트 루프"와 "재시도 루프"의 분리
OpenClaw의 while(true)는 에이전트 루프가 아니다. pi-core의 runLoop()가 에이전트 루프이고, 이 while은 "에이전트 시도가 실패했을 때 복구하는 상위 루프"다. 이 분리 덕분에 에이전트 로직(도구 실행)과 인프라 로직(재시도, 프로필 로테이션, 컴팩션)이 깔끔하게 격리된다. Pi/Hermes/ironclaw는 이 두 관심사를 하나의 루프에 섞는다.
4. 시도 실행 PI-RUNNER
src/agents/pi-embedded-runner/run/attempt.ts (~2000줄)export async function runEmbeddedAttempt(params) {
// (1) 샌드박스 + 워크스페이스 resolve
const sandbox = resolveSandboxContext(params);
// (2) 도구 생성 — 채널/샌드박스/기능 컨텍스트 반영
const tools = createOpenClawCodingTools({
workspace: params.workspace,
channel: params.channel, // CLI vs Telegram vs Discord → 다른 도구 세트
sandbox: sandbox,
capabilities: params.capabilities,
});
// (3) 시스템 프롬프트 빌드
const systemPrompt = buildEmbeddedSystemPrompt({
skills: params.skills, // 스킬 스냅샷
docs: params.docs, // 워크스페이스 문서
ttsHints: params.ttsHints, // 음성 힌트
sandboxInfo: sandbox, // 샌드박스 정보
memorySection: memoryPrompt, // 메모리 프롬프트
channelCapabilities: params.channel.capabilities,
});
// (4) 세션 매니저 열기 + 히스토리 검증
const sessionManager = new SessionManager(sessionFile);
await sessionManager.sanitizeReplayHistory();
// (5) 컨텍스트 엔진 부트스트랩 + 어셈블
await contextEngine.bootstrap(sessionManager.messages);
const assembled = await contextEngine.assemble({
tokenBudget: modelContextWindow,
messages: sessionManager.messages,
});
// (6) 스트리밍 파이프라인 구성 — 9중 래핑
let streamFn = baseStreamFn;
streamFn = wrapThinkingBlockDrop(streamFn); // thinking 블록 제거
streamFn = wrapToolCallIdSanitize(streamFn); // tool call ID 정리
streamFn = wrapOpenAIReasoningDowngrade(streamFn); // OpenAI reasoning 다운그레이드
streamFn = wrapYieldAbort(streamFn); // yield 중단 처리
streamFn = wrapMalformedToolCallRepair(streamFn); // 깨진 도구 호출 수리
streamFn = wrapIdleTimeout(streamFn); // idle 타임아웃
streamFn = wrapCacheTrace(streamFn); // 캐시 추적
streamFn = wrapPayloadLogging(streamFn); // 페이로드 로깅
streamFn = wrapSensitiveStopReasonRecovery(streamFn); // 민감 종료 이유 복구
// (7) 이벤트 구독 — pi-core 이벤트를 OpenClaw 스트림으로 브릿지
subscribeEmbeddedPiSession(piSession, {
onToolEvent: (event) => emit({ stream: "tool", event }),
onAssistantDelta: (delta) => emit({ stream: "assistant", delta }),
onLifecycle: (phase) => emit({ stream: "lifecycle", phase }),
});
// (8) Pi-core에 프롬프트 제출
await piSession.prompt(messages, { signal, timeout });
// (9) 후처리: 컨텍스트 엔진 afterTurn
await contextEngine.afterTurn();
}
5. 스트리밍 파이프라인 — 9중 래핑
baseStreamFn (LLM API 호출)
│
├── wrapThinkingBlockDrop thinking 블록 제거 (프로바이더별)
├── wrapToolCallIdSanitize tool call ID 형식 정규화
├── wrapOpenAIReasoningDowngrade OpenAI reasoning 포맷 다운그레이드
├── wrapYieldAbort 세션 양도(yield) 시 중단
├── wrapMalformedToolCallRepair 깨진 JSON 도구 호출 자동 수리
├── wrapIdleTimeout LLM idle 타임아웃 (기본 120초)
├── wrapCacheTrace 캐시 히트/미스 추적
├── wrapPayloadLogging 디버깅용 페이로드 로깅
└── wrapSensitiveStopReasonRecovery 민감 종료 이유 복구
Insight: 스트리밍 래퍼 = 미들웨어 패턴
이 9중 래핑은 Express.js의 미들웨어 스택과 같은 패턴이다. 각 래퍼가 독립적으로 하나의 관심사를 처리하고, 합성(composition)으로 전체 파이프라인을 구성한다. 이 패턴 덕분에 새 프로바이더 지원이나 새 기능 추가가 래퍼 하나 추가로 끝난다. Pi/Hermes/ironclaw는 이런 수준의 스트리밍 처리가 없다 — 멀티 프로바이더 지원의 깊이 차이.
6. 플러그인 컨텍스트 엔진 CONTEXT
src/context-engine/6.1 ContextEngine 인터페이스
interface ContextEngine {
// 초기화: 세션 시작 시 1회
bootstrap(messages: Message[]): Promise<void>;
// 유지보수: 매 턴 사이
maintain(): Promise<void>;
// 새 메시지 수집: 메시지가 추가될 때
ingest(message: Message): Promise<IngestResult>;
ingestBatch(messages: Message[]): Promise<IngestResult[]>;
// 턴 후 처리: 영속화, 백그라운드 압축
afterTurn(): Promise<void>;
// 컨텍스트 조립: 모델 호출 전에 토큰 버짓 내로 메시지 조립
assemble(params: {
tokenBudget: number;
messages: Message[];
}): Promise<AssembleResult>;
// 압축: 컨텍스트 윈도우 꽉 찰 때 or /compact
compact(params: {
messages: Message[];
currentTokens: number;
}): Promise<CompactResult>;
// 서브에이전트 관련
prepareSubagentSpawn(task: string): Promise<SubagentContext>;
onSubagentEnded(result: SubagentResult): Promise<void>;
// 정리
dispose(): Promise<void>;
}
6.2 레지스트리 — 슬롯 기반 교체
// src/context-engine/registry.ts
// 플러그인이 자신의 컨텍스트 엔진을 등록
registerContextEngine({
name: "my-vector-engine",
slot: "default", // "default" 슬롯을 대체
owner: "my-plugin",
factory: () => new MyVectorContextEngine(),
});
// resolve 시: 설정 슬롯 → default 슬롯 → LegacyContextEngine
resolveContextEngine(params) {
// 1. 설정에서 지정한 엔진 슬롯 확인
const configSlot = params.config?.contextEngine;
if (configSlot && registry.has(configSlot)) {
return registry.get(configSlot).factory();
}
// 2. default 슬롯 확인
if (registry.has("default")) {
return registry.get("default").factory();
}
// 3. 폴백: LegacyContextEngine (no-op ingest, pass-through assemble)
return new LegacyContextEngine();
}
6.3 LegacyContextEngine — 기본값
class LegacyContextEngine implements ContextEngine {
// ingest: no-op (SessionManager가 영속화 담당)
async ingest(message) { return {}; }
// assemble: pass-through (메시지를 그대로 전달)
async assemble({ messages }) { return { messages }; }
// compact: pi-core의 컴팩션 런타임에 위임
async compact(params) {
return delegateCompactionToRuntime(params);
}
}
Insight: 컨텍스트 엔진의 4가지 라이프사이클 포인트
Ingest(새 메시지 수집) → Assemble(토큰 버짓 내 조립) → Compact(압축) → AfterTurn(영속화).이 추상화 덕분에 기본(pass-through), 벡터 검색 기반, DAG 기반, 요약 기반 등 다양한 컨텍스트 관리 전략을 플러그인으로 교체할 수 있다. Hermes도
ContextEngine ABC를 가지고 있지만, OpenClaw의 구현이 더 완성도가 높다.
7. 도구 설계 TOOLS
src/agents/openclaw-tools.ts (~270줄)export function createOpenClawTools(params) {
const tools = [];
// ─── 코어 도구 (Pi-core 내장) ───
// exec (bash), read, edit, write → 별도 생성 불필요
// ─── 커뮤니케이션 ───
tools.push(createMessageTool(params.channel)); // 멀티플랫폼 메시지 전송
tools.push(createTtsTool(params.ttsConfig)); // 음성 합성
// ─── 생성 ───
tools.push(createImageGenerateTool()); // 이미지 생성
tools.push(createVideoGenerateTool()); // 비디오 생성
tools.push(createMusicGenerateTool()); // 음악 생성
// ─── 세션 관리 ───
tools.push(createSessionsListTool()); // 세션 목록
tools.push(createSessionsSendTool()); // 다른 세션에 메시지 전송
tools.push(createSessionsSpawnTool()); // 서브세션 생성
tools.push(createSessionsYieldTool()); // 세션 양도
// ─── 웹 ───
tools.push(createWebSearchTool()); // 웹 검색
tools.push(createWebFetchTool()); // 웹 페이지 가져오기
// ─── 자동화 ───
tools.push(createCronTool()); // 크론 작업
tools.push(createGatewayTool()); // 게이트웨이 관리
// ─── 멀티에이전트 ───
tools.push(createSubagentsTool()); // 서브에이전트 관리
tools.push(createAgentsListTool()); // 에이전트 목록
// ─── 플러그인 도구 ───
const pluginTools = await resolveOpenClawPluginToolsForOptions(params);
tools.push(...pluginTools);
return tools;
}
Pi-core 도구 vs OpenClaw 도구
Pi-core 내장 (코딩): exec, read, edit, write — 이들은 별도 생성 없이 자동으로 포함OpenClaw 추가 (플랫폼): 메시지, TTS, 이미지/비디오/음악 생성, 세션, 크론, 웹 — 멀티플랫폼 AI 어시스턴트에 필요한 도구들
이 분리가 OpenClaw의 정체성: "코딩 에이전트"가 아니라 "개인 AI 어시스턴트"
8. 종합 인사이트
1. 유일하게 "에이전트 루프를 빌리는" 시스템
Pi, ironclaw, Hermes는 에이전트 루프를 직접 구현한다. OpenClaw만 Pi-core를 라이브러리로 사용한다. 이는 "루프 자체에 혁신이 있는 게 아니라, 루프 주변의 인프라에 가치가 있다"는 판단. Gateway, 재시도, 컨텍스트 엔진, 스트리밍 파이프라인이 OpenClaw의 실제 차별점.
2. "재시도 루프"와 "에이전트 루프"의 명확한 분리
run.ts(재시도/폴백)와 attempt.ts(시도 실행)의 분리는 아키텍처적으로 가장 깔끔하다. 인프라 관심사(인증, 프로필 로테이션, 컴팩션)와 에이전트 관심사(도구 실행, 판단)가 다른 파일에 산다. 자기 시스템을 만든다면 이 분리를 참고할 것.
3. 멀티플랫폼 = 하나의 에이전트, 여러 인터페이스
CLI, Telegram, Discord, WhatsApp, Android, iOS, macOS — 모두 같은 agentCommand()로 들어와서 같은 pi-core 루프를 실행한다. 차이점은 (1) senderIsOwner 신뢰 수준, (2) 채널별 도구 가용성 (capabilities), (3) 출력 포맷. 이 Gateway 패턴이 OpenClaw의 진짜 가치.
4. 48시간 기본 타임아웃
Pi: maxSteps 없음. ironclaw: 50회. Hermes: 90회. OpenClaw: 48시간. 이 차이는 사용 시나리오의 차이를 반영한다. OpenClaw는 "개인 어시스턴트"이므로 장시간 작업(대규모 코드 마이그레이션 등)을 허용해야 한다. 반복 횟수가 아닌 시간 기반 제한은 작업 복잡도에 무관하게 동작한다는 장점이 있다.