OpenClaw — 코어 루프 분석

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는 "개인 어시스턴트"이므로 장시간 작업(대규모 코드 마이그레이션 등)을 허용해야 한다. 반복 횟수가 아닌 시간 기반 제한은 작업 복잡도에 무관하게 동작한다는 장점이 있다.