ACP 스레드 바인딩 에이전트

개요

이 계획은 OpenClaw이 스레드 지원 채널(Discord 우선)에서 프로덕션 수준의 생명주기 및 복구와 함께 ACP 코딩 에이전트를 어떻게 지원해야 하는지를 정의합니다.

관련 문서:

목표 사용자 경험:

  • 사용자가 ACP 세션을 스레드에 생성하거나 포커스
  • 해당 스레드의 사용자 메시지가 바인딩된 ACP 세션으로 라우팅
  • 에이전트 출력이 동일한 스레드 페르소나로 스트리밍
  • 세션은 영구적이거나 명시적 정리 제어를 가진 일회성

결정 요약

장기 권장 사항은 하이브리드 아키텍처:

  • OpenClaw 코어가 ACP 제어 플레인 관심사를 소유
    • 세션 식별 및 메타데이터
    • 스레드 바인딩 및 라우팅 결정
    • 전달 불변성 및 중복 억제
    • 생명주기 정리 및 복구 의미론
  • ACP 런타임 백엔드는 플러그형
    • 첫 백엔드는 acpx 기반 플러그인 서비스
    • 런타임이 ACP 전송, 큐잉, 취소, 재연결을 수행

OpenClaw는 코어에서 ACP 전송 내부를 재구현하지 않아야 합니다. OpenClaw는 라우팅을 위해 순수 플러그인 전용 가로채기 경로에 의존하지 않아야 합니다.

북극성 아키텍처 (궁극적 목표)

ACP를 OpenClaw의 일급 제어 플레인으로 취급하고, 플러그형 런타임 어댑터를 사용합니다.

양보할 수 없는 불변성:

  • 모든 ACP 스레드 바인딩이 유효한 ACP 세션 레코드를 참조
  • 모든 ACP 세션이 명시적 생명주기 상태를 가짐 (creating, idle, running, cancelling, closed, error)
  • 모든 ACP 실행이 명시적 실행 상태를 가짐 (queued, running, completed, failed, cancelled)
  • spawn, bind, 초기 enqueue가 원자적
  • 명령 재시도가 멱등적 (중복 실행이나 중복 Discord 출력 없음)
  • 바인딩된 스레드 채널 출력이 ACP 실행 이벤트의 프로젝션이며, 임시 부작용이 아님

장기 소유권 모델:

  • AcpSessionManager가 유일한 ACP 기록자이자 오케스트레이터
  • 매니저는 우선 게이트웨이 프로세스에 위치; 나중에 동일한 인터페이스 뒤에 전용 사이드카로 이동 가능
  • ACP 세션 키별로 매니저가 하나의 인메모리 액터를 소유 (직렬화된 명령 실행)
  • 어댑터(acpx, 미래 백엔드)는 전송/런타임 구현일 뿐

장기 지속성 모델:

  • ACP 제어 플레인 상태를 전용 SQLite 저장소(WAL 모드)로 이동, OpenClaw 상태 디렉토리 아래
  • 마이그레이션 중 호환성 프로젝션으로 SessionEntry.acp를 유지하되, 진실의 원천으로 사용하지 않음
  • ACP 이벤트를 추가 전용으로 저장하여 재생, 충돌 복구, 결정론적 전달 지원

전달 전략 (궁극적 목표로의 브릿지)

  • 단기 브릿지
    • 현재 스레드 바인딩 메커니즘과 기존 ACP 설정 인터페이스 유지
    • 메타데이터 갭 버그를 수정하고 ACP 턴을 단일 코어 ACP 브랜치를 통해 라우팅
    • 멱등성 키와 폐쇄적 라우팅 검사를 즉시 추가
  • 장기 전환
    • ACP 진실의 원천을 제어 플레인 DB + 액터로 이동
    • 바인딩된 스레드 전달을 순수 이벤트 프로젝션 기반으로
    • 기회주의적 세션 항목 메타데이터에 의존하는 레거시 폴백 동작 제거

순수 플러그인 전용이 아닌 이유

현재 플러그인 훅은 코어 변경 없이 종단 간 ACP 세션 라우팅에 충분하지 않습니다.

  • 스레드 바인딩의 인바운드 라우팅이 코어 디스패치에서 먼저 세션 키로 해석
  • 메시지 훅은 발사 후 잊기(fire-and-forget)이며 메인 응답 경로를 단락시킬 수 없음
  • 플러그인 명령은 제어 작업에 적합하지만 코어 턴별 디스패치 흐름을 대체하기에는 부적합

결과:

  • ACP 런타임은 플러그인화 가능
  • ACP 라우팅 브랜치는 코어에 존재해야 함

재사용할 기존 기반

이미 구현되어 있으며 정규로 유지해야 함:

  • 스레드 바인딩 대상이 subagentacp를 지원
  • 인바운드 스레드 라우팅 오버라이드가 일반 디스패치 전에 바인딩으로 해석
  • 응답 전달에서 웹훅을 통한 아웃바운드 스레드 식별
  • ACP 대상 호환성이 있는 /focus/unfocus 흐름
  • 시작 시 복원되는 영구 바인딩 저장소
  • 아카이브, 삭제, unfocus, reset, delete 시 바인딩 해제 생명주기

이 계획은 해당 기반을 교체하는 것이 아니라 확장합니다.

아키텍처

경계 모델

코어 (OpenClaw 코어에 있어야 함):

  • 응답 파이프라인의 ACP 세션 모드 디스패치 브랜치
  • 부모 + 스레드 중복을 방지하는 전달 중재
  • ACP 제어 플레인 지속성 (마이그레이션 중 SessionEntry.acp 호환성 프로젝션 포함)
  • 세션 reset/delete에 연결된 생명주기 언바인드 및 런타임 분리 의미론

플러그인 백엔드 (acpx 구현):

  • ACP 런타임 워커 감독
  • acpx 프로세스 호출 및 이벤트 파싱
  • ACP 명령 핸들러 (/acp ...) 및 운영자 UX
  • 백엔드별 설정 기본값 및 진단

런타임 소유권 모델

  • 하나의 게이트웨이 프로세스가 ACP 오케스트레이션 상태를 소유
  • ACP 실행은 acpx 백엔드를 통해 감독된 자식 프로세스에서 실행
  • 프로세스 전략은 메시지별이 아닌 활성 ACP 세션 키별 장기 실행

이를 통해 매 프롬프트마다 시작 비용을 피하고 취소 및 재연결 의미론을 신뢰할 수 있게 유지합니다.

코어 런타임 계약

라우팅 코드가 CLI 세부사항에 의존하지 않고 디스패치 로직을 변경하지 않고 백엔드를 전환할 수 있도록 코어 ACP 런타임 계약을 추가합니다:

export type AcpRuntimePromptMode = "prompt" | "steer";

export type AcpRuntimeHandle = {
  sessionKey: string;
  backend: string;
  runtimeSessionName: string;
};

export type AcpRuntimeEvent =
  | { type: "text_delta"; stream: "output" | "thought"; text: string }
  | { type: "tool_call"; name: string; argumentsText: string }
  | { type: "done"; usage?: Record<string, number> }
  | { type: "error"; code: string; message: string; retryable?: boolean };

export interface AcpRuntime {
  ensureSession(input: {
    sessionKey: string;
    agent: string;
    mode: "persistent" | "oneshot";
    cwd?: string;
    env?: Record<string, string>;
    idempotencyKey: string;
  }): Promise<AcpRuntimeHandle>;

  submit(input: {
    handle: AcpRuntimeHandle;
    text: string;
    mode: AcpRuntimePromptMode;
    idempotencyKey: string;
  }): Promise<{ runtimeRunId: string }>;

  stream(input: {
    handle: AcpRuntimeHandle;
    runtimeRunId: string;
    onEvent: (event: AcpRuntimeEvent) => Promise<void> | void;
    signal?: AbortSignal;
  }): Promise<void>;

  cancel(input: {
    handle: AcpRuntimeHandle;
    runtimeRunId?: string;
    reason?: string;
    idempotencyKey: string;
  }): Promise<void>;

  close(input: { handle: AcpRuntimeHandle; reason: string; idempotencyKey: string }): Promise<void>;

  health?(): Promise<{ ok: boolean; details?: string }>;
}

구현 세부사항:

  • 첫 백엔드: 플러그인 서비스로 제공되는 AcpxRuntime
  • 코어는 레지스트리를 통해 런타임을 해석하고 ACP 런타임 백엔드가 없을 때 명시적 운영자 오류로 실패

제어 플레인 데이터 모델 및 지속성

장기 진실의 원천은 전용 ACP SQLite 데이터베이스(WAL 모드)로, 트랜잭션 업데이트와 충돌 안전 복구를 위해 사용:

  • acp_sessions
    • session_key (pk), backend, agent, mode, cwd, state, created_at, updated_at, last_error
  • acp_runs
    • run_id (pk), session_key (fk), state, requester_message_id, idempotency_key, started_at, ended_at, error_code, error_message
  • acp_bindings
    • binding_key (pk), thread_id, channel_id, account_id, session_key (fk), expires_at, bound_at
  • acp_events
    • event_id (pk), run_id (fk), seq, kind, payload_json, created_at
  • acp_delivery_checkpoint
    • run_id (pk/fk), last_event_seq, last_discord_message_id, updated_at
  • acp_idempotency
    • scope, idempotency_key, result_json, created_at, 유니크 (scope, idempotency_key)
export type AcpSessionMeta = {
  backend: string;
  agent: string;
  runtimeSessionName: string;
  mode: "persistent" | "oneshot";
  cwd?: string;
  state: "idle" | "running" | "error";
  lastActivityAt: number;
  lastError?: string;
};

이 문서의 나머지 내용은 원본과 동일한 기술적 세부사항을 포함하며, 라우팅 및 전달, 상태 머신, 액터 모델, 멱등성, 복구, 생명주기, 설정, 구현 명세, 테스트 계획, 위험 및 완화, 수락 체크리스트, 대상 리팩토링 부록까지 상세히 다룹니다. 원본 영문 문서의 코드 블록, 설정 키, 파일 경로, API 인터페이스는 모두 그대로 유지됩니다.