본문으로 건너뛰기

ACP 호환 하네스

Agent Client Protocol(ACP)을 사용하는 코딩 에이전트 CLI인 grok, gemini --acp 등은 샌드박스에서 구동할 수 있는 장기 실행 JSON-RPC 세션을 제공합니다. 에이전트마다 전용 패키지를 사용하는 대신 acpCompatible모든 ACP 호환 CLI에 대한 chat() 어댑터를 생성합니다. 실행 방법을 한 번 구성하고 호출마다 모델을 선택한 뒤 샌드박스에 전달하면 됩니다.

OpenAI-Compatible 어댑터에 대응하는 하네스입니다. 에이전트가 ACP를 사용하지만 @tanstack/ai-* 패키지가 없을 때 사용합니다. 전용 하네스 어댑터(Grok Build 등)가 있다면 이를 우선 사용합니다. 전용 어댑터에는 모델별로 선별된 메타데이터와 공급업체별 동작이 포함되어 있습니다.

인증

기본 authMode'api-key'입니다. 에이전트가 머신의 CLI 로그인을 사용해야 하면 'host'로 설정합니다. 샌드박스 타입이 이 값을 선택하지는 않습니다. 하네스 인증을 참조하세요.

import { acpCompatibleText } from "@tanstack/ai-acp"

acpCompatibleText("composer-2.5", {
name: "acp",
command: ({ model }) => `grok agent -m '${model}' --always-approve stdio`,
authMethodId: "xai.api_key",
})

acpCompatibleText("composer-2.5", {
name: "acp",
command: ({ model }) => `grok agent -m '${model}' --always-approve stdio`,
authMode: "host",
})
  • 'api-key'(기본값): authMethodId와 함께 authenticate를 호출합니다.
  • 'host': ACP authenticate를 건너뜁니다. 머신의 CLI 로그인을 사용합니다.

같은 chat() 호출에 outputSchema를 전달합니다. acpCompatible은 프롬프트에 스키마를 추가하고 마지막 어시스턴트 텍스트를 파싱합니다. 해당 턴에도 네이티브 하네스 도구는 계속 실행됩니다. 타입이 지정된 객체는 await chat(), useChat().final 또는 어시스턴트의 structured-output 파트에서 읽습니다. 하네스 에이전트를 참조하세요.

설치

acpCompatible@tanstack/ai-acp에 포함되어 있습니다. 샌드박스 내부에서 구동하므로 샌드박스 패키지와 프로바이더도 설치합니다.

npm install @tanstack/ai-acp @tanstack/ai @tanstack/ai-sandbox @tanstack/ai-sandbox-docker

기본 사용법

acpCompatible({ name, command })으로 하네스를 한 번 구성한 다음 호출마다 모델을 선택합니다. command는 샌드박스 내부에서 에이전트의 ACP 서버를 stdio로 시작하는 셸 명령을 생성합니다.

import { chat } from '@tanstack/ai'
import { acpCompatible } from '@tanstack/ai-acp'
import {
createSecrets,
defineSandbox,
defineWorkspace,
githubRepo,
withSandbox,
} from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
import { messages } from './chat-context'

// Configure the "pi" agent harness once:
const pi = acpCompatible({
name: 'pi',
command: ({ model, harnessCwd }) => `pi --acp -m ${model} --cwd ${harnessCwd}`,
authMethodId: 'pi-api-key', // when the harness advertises an ACP auth method
refusalMessage: 'Pi refused the request.',
})

const sandbox = defineSandbox({
id: 'pi-agent',
provider: dockerSandbox({ image: 'node:22' }),
workspace: defineWorkspace({
source: githubRepo({ repo: 'owner/app' }),
setup: ['npm install -g pi-cli'], // install the agent CLI into the image
secrets: createSecrets({ PI_API_KEY: process.env.PI_API_KEY ?? '' }),
}),
})

const stream = chat({
adapter: pi('pi-fast'),
messages,
middleware: [withSandbox(sandbox)],
})

샌드박스 확인, chat() 도구 → MCP 브리징, 세션 재개, 권한 처리, 중단, AG-UI 이벤트 변환을 포함한 전체 ACP 흐름을 바로 사용할 수 있습니다.

일회성 사용

단일 모델만 사용하는 경우 하네스 팩토리를 생략하고 acpCompatibleText로 어댑터를 인라인 생성합니다.

import { chat } from '@tanstack/ai'
import { acpCompatibleText } from '@tanstack/ai-acp'
import { withSandbox } from '@tanstack/ai-sandbox'
import { sandbox } from './sandbox'
import { messages } from './chat-context'

const stream = chat({
adapter: acpCompatibleText('pi-fast', {
name: 'pi',
command: ({ model }) => `pi --acp -m ${model}`,
}),
messages,
middleware: [withSandbox(sandbox)],
})

타입이 지정된 출력

같은 chat() 호출에 outputSchema를 전달합니다. 에이전트가 네이티브 도구를 실행한 후 어댑터가 마지막 어시스턴트 텍스트를 JSON으로 파싱합니다.

import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { acpCompatibleText } from '@tanstack/ai-acp'
import { withSandbox } from '@tanstack/ai-sandbox'
import { z } from 'zod'
import { sandbox } from './sandbox'
import { messages } from './chat-context'

const ReportSchema = z.object({
name: z.string(),
oneLiner: z.string(),
})

export async function POST() {
const stream = chat({
adapter: acpCompatibleText('pi-fast', {
name: 'pi',
command: ({ model }) => `pi --acp -m ${model}`,
}),
messages,
outputSchema: ReportSchema,
stream: true,
middleware: [withSandbox(sandbox)],
})
return toServerSentEventsResponse(stream)
}

클라이언트에서는 도구 호출과 추론을 확인하기 위해 messages[].parts를 순회합니다. 타입이 지정된 객체는 structured-output 파트 또는 useChat().final에서 읽습니다.

import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
import { z } from 'zod'

const ReportSchema = z.object({
name: z.string(),
oneLiner: z.string(),
})

function Report() {
const { messages, final } = useChat({
connection: fetchServerSentEvents('/api/report'),
outputSchema: ReportSchema,
})

return (
<>
{messages.map((message) =>
message.parts.map((part, index) => {
if (part.type === 'tool-call') {
return <p key={part.id}>{part.name}</p>
}
if (part.type === 'structured-output') {
const report = part.data ?? part.partial
return report?.name ? <h2 key={index}>{report.name}</h2> : null
}
return null
}),
)}
{final ? <p>{final.oneLiner}</p> : null}
</>
)
}

전체 파트 목록과 repo-report 예시는 하네스 에이전트를 참조하세요.

타입이 지정된 모델 및 옵션

openaiCompatible과 마찬가지로 하네스의 모델과 호출별 옵션을 선언하여 전체를 타입 검사할 수 있습니다. models는 팩토리 인자를 제한합니다. modelOptionschat({ modelOptions })가 허용하는 항목을 설명하는 타입 전용 브랜드({} as { … }, 런타임에는 사용하지 않음)입니다. 선언한 옵션은 기본 ACP 옵션과 병합되어 command / openTransportctx.modelOptions로 전달되므로 CLI 플래그로 변환할 수 있습니다.

import { acpCompatible } from '@tanstack/ai-acp'

const pi = acpCompatible({
name: 'pi',
models: ['pi-fast', 'pi-pro'],
modelOptions: {} as { reasoningEffort?: 'low' | 'high' },
command: ({ model, harnessCwd, modelOptions }) =>
`pi --acp -m ${model} --cwd ${harnessCwd}` +
(modelOptions?.reasoningEffort ? ` --effort ${modelOptions.reasoningEffort}` : ''),
})

pi('pi-pro') // ok
// pi('pi-ultra') // type error — not in `models`
import { chat } from '@tanstack/ai'
import { withSandbox } from '@tanstack/ai-sandbox'
import { pi } from './pi-harness'
import { sandbox } from './sandbox'
import { messages } from './chat-context'

const stream = chat({
adapter: pi('pi-pro'),
modelOptions: { reasoningEffort: 'high' }, // typed against the declared options
messages,
middleware: [withSandbox(sandbox)],
})

선언 내용과 관계없이 기본 옵션은 항상 modelOptions에서 사용할 수 있습니다. sessionId(재개), cwd, authMode, authMethodId, permissionMode가 해당합니다.

구성

필드용도
name (필수)하네스 레이블, 로그 접두사 및 <name>.session-id CUSTOM 이벤트 이름입니다.
models이 하네스가 허용하는 모델 ID입니다. 선언하면 harness('id')가 타입 안전해지고 알 수 없는 ID가 거부됩니다. 생략하면 모든 문자열을 허용합니다.
modelOptionschat({ modelOptions })을 통해 허용되는 호출별 옵션의 타입 전용 브랜드입니다. {} as { … }로 선언하며, 기본 옵션과 병합되어 command / openTransportctx.modelOptions에 노출됩니다.
command{ model, cwd, harnessCwd, sandbox, env, modelOptions, signal }에서 stdio 실행 명령을 생성합니다. openTransport를 지정하지 않으면 필수입니다.
skillsDir하네스의 스킬 디렉터리입니다(워크스페이스 루트 기준 상대 경로, 예: '.pi/skills'). Claude Code의 .claude/skills처럼 하네스의 네이티브 규칙을 따릅니다. withSandbox 워크스페이스의 gitSkill이 여기에 연결됩니다. 생략하면 gitSkills가 연결되지 않으며 경고가 표시됩니다.
openTransportAcpSessionTransport를 직접 엽니다(예: serve 프로세스를 시작하고 WebSocket으로 연결). command보다 우선합니다.
cwd샌드박스 내부의 작업 디렉터리입니다(기본값 /workspace).
env하네스 프로세스에 추가할 환경 변수입니다.
authMode'api-key'(기본값)는 authMethodId를 사용합니다. 'host'는 ACP authenticate를 건너뜁니다. 하네스 인증을 참조하세요.
authMethodId세션 시작 전에 선택할 ACP 인증 방법입니다. authMode'host'이면 무시됩니다.
permissionMode'default' | 'acceptEdits' | 'bypassPermissions'(기본값)입니다.
permissions'headless'(자동 해결, 기본값) 또는 'interactive'(ask 프롬프트에 approval-requested 이벤트를 내보냄)입니다.
onPermissionRequest사용자 지정 권한 핸들러이며 permissions/permissionMode보다 우선합니다.
refusalMessage하네스가 요청을 거부할 때의 RUN_ERROR 메시지입니다.
planEventNameACP plan 업데이트를 이 이름의 CUSTOM 이벤트로 내보냅니다.
emitDiff실행 후 cwdgit difffile.changed CUSTOM 이벤트로 내보냅니다(기본적으로 비활성화).
onExtNotification공급업체의 _x/… JSON-RPC 알림을 처리합니다.
buildPrompt채팅 기록을 하네스 프롬프트에 매핑하는 방식을 재정의합니다.

WebSocket 및 사용자 지정 전송

일부 하네스는 stdio 대신 WebSocket으로 접근하는 ACP 서버를 실행합니다(grok agent serve 패턴). openTransport로 전송을 직접 엽니다. 동일한 컨텍스트를 받고 AcpSessionTransport를 반환합니다. 모든 정리 작업은 반환된 전송의 dispose에 넣습니다.

import { acpCompatible, startAcpServerInSandbox } from '@tanstack/ai-acp'

const myAgent = acpCompatible({
name: 'my-agent',
openTransport: async ({ sandbox, model, harnessCwd, signal }) => {
const server = await startAcpServerInSandbox(sandbox, {
port: 9100,
cwd: harnessCwd,
command: `my-agent serve --bind 0.0.0.0:9100 -m ${model}`,
readyMarker: 'listening',
buildWsUrl: ({ channel, port }) =>
`${channel.url.replace(/^http/i, 'ws')}:${port}`,
...(signal ? { signal } : {}),
})
const ws = await server.connect(signal)
return {
kind: 'stream',
stream: ws.stream,
dispose: async () => {
ws.close()
await server.dispose()
},
}
},
})

권한

샌드박스 내부에서는 샌드박스 자체가 보안 경계이므로 permissionMode: 'bypassPermissions'와 기본 'headless' 전략을 사용하면 에이전트가 확인 없이 파일을 편집하고 명령을 실행할 수 있습니다. 대신 클라이언트에 도구 승인을 표시하려면 'interactive'로 전환합니다.

import { acpCompatible } from '@tanstack/ai-acp'

const pi = acpCompatible({
name: 'pi',
command: ({ model }) => `pi --acp -m ${model}`,
permissions: 'interactive', // emit approval-requested events for `ask` prompts
permissionMode: 'acceptEdits', // still auto-approve file edits
})

에이전트로 브리징된 chat() 제공 도구는 모드와 관계없이 항상 자동 승인됩니다.

세션 재개

어댑터는 실행할 때마다 <name>.session-id(예: pi.session-id)라는 CUSTOM 이벤트로 하네스 세션 ID를 내보냅니다. 다음 호출에서 해당 ID를 modelOptions.sessionId로 다시 전달하면 하네스가 세션을 재개합니다. 에이전트가 이전 컨텍스트를 이미 보유하므로 마지막 사용자 메시지만 전송됩니다.

import { chat, chatParamsFromRequest, toServerSentEventsResponse } from '@tanstack/ai'
import { withSandbox } from '@tanstack/ai-sandbox'
import { pi } from './pi-harness' // the configured `acpCompatible(...)` factory
import { sandbox } from './sandbox'

export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const sessionId =
typeof params.forwardedProps.sessionId === 'string'
? params.forwardedProps.sessionId
: undefined

const stream = chat({
adapter: pi('pi-fast'),
messages: params.messages,
middleware: [withSandbox(sandbox)],
modelOptions: { sessionId },
})

return toServerSentEventsResponse(stream)
}

워크스페이스 스킬

withSandbox를 통해 워크스페이스를 프로비저닝하면 acpCompatible이 스킬을 하네스에 배치합니다. 각 스킬 유형은 해당 하네스가 요구하는 위치에 놓입니다.

워크스페이스 입력acpCompatible의 배치 방식
mcpSkill(name, config)newSessionmcpServers를 통해 네이티브 ACP로 에이전트에 전달됩니다(시크릿/bearer 헤더가 해결됨). 구성 파일이 필요하지 않다는 점이 파일 기반 하네스에 대한 ACP의 장점입니다.
gitSkill({ repo })부트스트랩 중 복제한 후 선언된 skillsDir(예: .pi/skills)에 연결됩니다. skillsDir를 생략하면 연결되지 않으며 경고가 표시됩니다.
fileSkill({ path, content })부트스트랩 중 워크스페이스 루트에 기록됩니다(프로바이더와 무관).
instructions부트스트랩 중 AGENTS.md(및 심볼릭 링크)에 기록됩니다.
agentSkill(name), plugins범용 ACP 프리미티브가 없으므로 경고 후 건너뜁니다. 대신 gitSkill 또는 MCP 서버를 제공합니다.

워크스페이스에 선언된 secrets는 생성/재개 시 에이전트 환경에 주입됩니다(스냅샷에는 절대 영속화되지 않음). 따라서 하네스 CLI는 이를 다른 환경 변수와 동일하게 사용합니다.

프로토콜 지원 범위

acpCompatible은 ACP의 클라이언트 / 오케스트레이션 측을 구현하며, 전체 프로토콜 표면이 아니라 한 번의 전체 프롬프트 턴을 에이전트로 구동하는 데 충분한 범위를 제공합니다. 규격을 준수하는 최소 클라이언트입니다. 구현하지 않는 항목은 기능으로 제한되거나(미지원임을 알리는 것이 규격에 정의된 동작임) 렌더링 선택일 뿐이며 위반이 아닙니다.

지원됨:

  • initialize 핸드셰이크 — clientInfo와 프로토콜 버전을 보내고 버전을 협상하며 기능을 알립니다.
  • 에이전트가 인증 방법을 알리는 경우의 authenticate, session/new, session/load(재개), session/prompt, session/cancel입니다.
  • 네 가지 옵션 종류를 모두 지원하는 session/request_permission이며 권한 모드에 따라 매핑됩니다.
  • 턴 출력을 전달하는 모든 스트리밍 session/update입니다: agent_message_chunk, agent_thought_chunk(→ 추론), tool_call / tool_call_update, plan.
  • 다섯 가지 중지 이유(end_turn, max_tokens, max_turn_requests, refusal, cancelled)입니다.

CUSTOM 스트림 이벤트로 노출됨(AG-UI 채팅 이벤트 프로토콜에는 텍스트가 아닌 어시스턴트 출력을 위한 일급 이벤트가 없으므로 CUSTOM을 사용합니다.)

  • <name>.session-id — 하네스 세션 ID, resume 를 위해.
  • <name>.message-content — 텍스트가 아닌 에이전트 콘텐츠 (image / audio / resource / resource_link 블록). 그 value{ content: <ACP content block> } 입니다. 텍스트가 아닌 도구 콘텐츠 (차이, 터미널, 이미지) 는 TOOL_CALL_RESULT 페이로드 내부에 보존됩니다.
  • 계획 이벤트, planEventName 를 설정할 때.

구현되지 않음(의도된 동작):

  • fs/read_text_file, fs/write_text_file, terminal/* — 미지원으로 알립니다. 에이전트는 파일 시스템과 셸에 직접 접근할 수 있는 샌드박스 내부에서 실행되므로 이를 클라이언트에 위임하지 않습니다.
  • 멀티모달 프롬프트 전송 — 프롬프트는 텍스트로 전송됩니다. (에이전트의 멀티모달 출력은 위의 message-content를 통해 노출됩니다.)
  • 증분 usage_update(대신 최종 턴 사용량을 보고함), available_commands_update, current_mode_update 및 실험적 기능(elicitation, NES, providers, session modes/config)입니다.

다음 단계