콘텐츠로 이동

ACP 사용 가이드

이 가이드는 Continuum Router를 ACP(Agent Communication Protocol) 에이전트로 사용하는 방법을 설정부터 IDE 클라이언트 연결까지 다룹니다.

사전 요구 사항

  • Continuum Router 바이너리 (설치 참조)
  • 하나 이상의 설정된 LLM 백엔드 (OpenAI, Anthropic, Ollama 등)
  • ACP가 활성화된 설정 파일

설정

config.yamlacp 섹션을 추가합니다:

backends:
  - url: http://localhost:11434
    name: ollama
    models: ["llama3.2"]

acp:
  enabled: true

  transport:
    stdio:
      enabled: true

  capabilities:
    load_session: true
    mcp: true

  default_model: "llama3.2"

  permissions:
    default_policy: ask_always
    auto_allow:
      - read
      - search
      - think
    always_ask:
      - edit
      - delete
      - execute

  sessions:
    max_concurrent: 10
    idle_timeout: "1h"
    storage: memory

전체 설정 레퍼런스는 ACP 아키텍처 페이지를 참조하세요.

ACP 모드로 라우터 시작하기

--mode stdio로 라우터를 실행하면 ACP 전송이 활성화됩니다:

continuum-router --config config.yaml --mode stdio

이 모드에서는 다음과 같이 동작합니다:

  • stdin/stdout은 JSON-RPC 2.0 메시지(NDJSON 형식) 전용으로 예약됩니다
  • stderr로 로그가 출력됩니다 (tracing)
  • HTTP 리스너를 시작하지 않으며, 라우터는 오직 stdio로만 통신합니다

Tip

HTTP API 게이트웨이는 여전히 별도로 실행할 수 있습니다. ACP stdio 모드는 HTTP 모드에 덧붙는 기능이 아니라 별개의 실행 모드입니다.

수동 테스트

stdin으로 JSON-RPC 메시지를 파이프하여 ACP 통신을 테스트할 수 있습니다.

1단계: 프로토콜 초기화

모든 ACP 세션은 initialize 핸드셰이크로 시작해야 합니다:

echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"readTextFile":true,"writeTextFile":true,"terminal":false},"clientInfo":{"name":"manual-test","version":"1.0.0"}},"id":1}' \
  | continuum-router --config config.yaml --mode stdio 2>/dev/null

예상 응답:

{"jsonrpc":"2.0","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"image":false,"audio":false,"embeddedContext":false,"mcp":true,"mcpCapabilities":{"acp":true}},"agentInfo":{"name":"Continuum Router","version":"1.3.0-beta.1"},"authMethods":[]},"id":1}

2단계: 대화형 세션

완전한 대화형 세션에는 socat 같은 도구나 간단한 스크립트를 사용합니다:

# 라우터를 백그라운드로 시작하고 stdin/stdout을 캡처
continuum-router --config config.yaml --mode stdio 2>/dev/null &
ROUTER_PID=$!

# /proc 또는 명명된 파이프로 메시지 전송
# (실용적인 접근 방법은 아래 스크립트 예제 참조)

스크립트로 테스트하기

다음은 셸 스크립트를 사용한 전체 테스트 세션입니다:

#!/bin/bash
# acp-test.sh: ACP 메시지 시퀀스를 라우터로 전송

CONFIG="config.yaml"

{
  # 1. 초기화
  echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"readTextFile":true},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'

  # 응답이 처리될 때까지 대기
  sleep 0.5

  # 2. 새 세션 생성
  echo '{"jsonrpc":"2.0","method":"session/new","params":{"mode":"code"},"id":2}'
  sleep 0.5

  # 3. 프롬프트 전송 (SESSION_ID를 2단계에서 받은 실제 세션 ID로 교체)
  # echo '{"jsonrpc":"2.0","method":"session/prompt","params":{"sessionId":"SESSION_ID","content":[{"type":"text","text":"Hello, what can you do?"}],"stream":false},"id":3}'
  # sleep 2

} | continuum-router --config "$CONFIG" --mode stdio 2>/dev/null

Python으로 테스트하기

양방향 통신을 더 세밀하게 제어하려면 다음과 같이 합니다:

#!/usr/bin/env python3
"""ACP 클라이언트 테스트 스크립트."""

import json
import subprocess
import sys
import threading

def read_responses(proc):
    """라우터의 응답을 읽어 출력합니다."""
    for line in proc.stdout:
        resp = json.loads(line)
        print(json.dumps(resp, indent=2))

def send(proc, method, params=None, msg_id=None):
    """JSON-RPC 메시지를 전송합니다."""
    msg = {"jsonrpc": "2.0", "method": method}
    if params:
        msg["params"] = params
    if msg_id is not None:
        msg["id"] = msg_id
    proc.stdin.write(json.dumps(msg) + "\n")
    proc.stdin.flush()

proc = subprocess.Popen(
    ["continuum-router", "--config", "config.yaml", "--mode", "stdio"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.DEVNULL,
    text=True,
)

# 백그라운드 스레드에서 응답 읽기
reader = threading.Thread(target=read_responses, args=(proc,), daemon=True)
reader.start()

# 1. 초기화
send(proc, "initialize", {
    "protocolVersion": 1,
    "clientCapabilities": {"readTextFile": True, "writeTextFile": True},
    "clientInfo": {"name": "python-test", "version": "1.0.0"},
}, msg_id=1)

import time; time.sleep(1)

# 2. 세션 생성
send(proc, "session/new", {"mode": "code"}, msg_id=2)
time.sleep(1)

# 3. stdin을 닫아 정상 종료 유도
proc.stdin.close()
proc.wait()

IDE 통합

ACP는 IDE 클라이언트가 라우터를 자식 프로세스로 스폰하고 stdio로 통신하도록 설계되었습니다. 일반적인 패턴은 다음과 같습니다:

IDE 프로세스
    ├── 스폰: continuum-router --config config.yaml --mode stdio
    │           stdin  ← IDE가 보내는 JSON-RPC 요청
    │           stdout → IDE로 전달되는 JSON-RPC 응답
    │           stderr → 로그 (디버깅용으로 캡처 가능)
    └── NDJSON으로 통신 (한 줄에 JSON 객체 하나)

통합 단계

  1. 프로세스 스폰: continuum-router --mode stdio --config <path>를 stdin/stdout이 파이프로 연결된 자식 프로세스로 시작합니다.

  2. initialize 전송: 첫 메시지는 반드시 initialize 요청이어야 합니다. 초기화가 완료될 때까지 다른 모든 메서드는 거부됩니다.

  3. 세션 생성: session/new를 모드(code, architect 등)와 함께 호출하여 sessionId를 받습니다.

  4. 프롬프트 전송: 세션 ID와 함께 session/prompt를 호출해 사용자 메시지를 보냅니다. 라우터는 session/update 알림으로 응답을 스트리밍해 돌려줍니다.

  5. 권한 처리: LLM이 도구 작업(파일 편집, 명령 실행)을 요청하면 라우터가 session/request_permission 알림을 보냅니다. IDE는 UI 프롬프트를 표시한 뒤 allow_once, allow_always, reject_once, reject_always 중 하나로 응답해야 합니다.

  6. 라이프사이클 관리: 진행 중인 작업을 중단하려면 session/cancel을, 모드를 바꾸려면 session/set_mode를 사용하고, 정상 종료하려면 stdin을 닫습니다.

예제: Node.js에서 스폰하기

const { spawn } = require("child_process");
const readline = require("readline");

const router = spawn("continuum-router", [
  "--config", "config.yaml",
  "--mode", "stdio",
]);

// NDJSON 응답 파싱
const rl = readline.createInterface({ input: router.stdout });
rl.on("line", (line) => {
  const msg = JSON.parse(line);
  console.log("Received:", JSON.stringify(msg, null, 2));
});

// 디버깅을 위해 stderr 로그 출력
router.stderr.on("data", (data) => {
  console.error("[router log]", data.toString());
});

function send(method, params, id) {
  const msg = JSON.stringify({ jsonrpc: "2.0", method, params, id });
  router.stdin.write(msg + "\n");
}

// 초기화
send("initialize", {
  protocolVersion: 1,
  clientCapabilities: { readTextFile: true, writeTextFile: true },
  clientInfo: { name: "my-ide", version: "1.0.0" },
}, 1);

// initialize 응답을 받은 후 세션 생성...
setTimeout(() => {
  send("session/new", { mode: "code" }, 2);
}, 1000);

MCP 서버 터널링

MCP 서버가 설정되어 있다면, ACP 클라이언트는 별도의 MCP 연결을 관리하지 않고도 라우터를 통해 MCP 서버에 접근할 수 있습니다.

설정

설정에 MCP 서버를 추가합니다:

acp:
  enabled: true
  capabilities:
    mcp: true
  mcp:
    max_connections_per_session: 5
    allowed_servers: []  # 비어 있으면 모두 허용

mcp_servers:
  - id: filesystem
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
  - id: github
    command: npx
    args: ["-y", "@modelcontextprotocol/server-github"]
    env:
      GITHUB_TOKEN: "${GITHUB_TOKEN}"

사용법

초기화한 뒤 MCP 서버에 연결하고 메시지를 라우팅합니다:

// 연결
{"jsonrpc":"2.0","method":"mcp/connect","params":{"id":"filesystem"},"id":10}

// 도구 목록 조회
{"jsonrpc":"2.0","method":"mcp/message","params":{"connectionId":"<returned-uuid>","message":{"jsonrpc":"2.0","method":"tools/list","id":1}},"id":11}

// 연결 해제
{"jsonrpc":"2.0","method":"mcp/disconnect","params":{"connectionId":"<returned-uuid>"},"id":12}

관리자 엔드포인트

ACP 설정과 함께 HTTP 모드로 실행하면 Admin API가 ACP 상태 엔드포인트를 제공합니다:

엔드포인트 설명
GET /admin/acp/status ACP 서브시스템 상태와 기능
GET /admin/acp/sessions 활성 ACP 세션 목록
GET /admin/acp/agent.json 디스커버리용 에이전트 레지스트리 메타데이터
# ACP 상태 확인
curl http://localhost:8080/admin/acp/status

# 활성 세션 보기
curl http://localhost:8080/admin/acp/sessions

# 에이전트 메타데이터 가져오기
curl http://localhost:8080/admin/acp/agent.json

문제 해결

라우터가 즉시 종료되는 경우

설정 파일이 존재하고 YAML 문법이 올바른지 확인하세요. stderr 출력을 확인합니다:

continuum-router --config config.yaml --mode stdio 2>router.log
# 그런 다음 router.log에서 오류 확인

"Server not initialized" 오류 (-32002)

initialize 핸드셰이크가 가장 먼저 전송되는 메시지여야 합니다. 초기화가 완료될 때까지 다른 모든 메서드는 거부됩니다.

프롬프트에 응답이 없는 경우

  • 설정에서 acp.enabledtrue인지 확인합니다
  • 설정한 백엔드에 연결할 수 있는지 확인합니다
  • acp.default_model이 백엔드에서 사용 가능한 모델과 일치하는지 확인합니다
  • stderr 로그에서 백엔드 연결 오류를 살펴봅니다

권한 요청이 동작하지 않는 경우

도구 호출이 아무 안내 없이 거부된다면 권한 설정을 확인하세요:

acp:
  permissions:
    default_policy: ask_always  # 프로덕션에서는 "allow_all"을 사용하지 마세요
    auto_allow:
      - read
      - search
      - think

MCP 서버 연결에 실패하는 경우

  • MCP 서버 명령이 설치되어 있는지 확인합니다 (예: npx 사용 가능 여부)
  • 서버 ID가 설정과 일치하는지 확인합니다
  • server_spawn_timeout 설정을 확인합니다 (기본값 10초)

다음 단계