본문으로 건너뛰기

OpenCode

OpenCode 어댑터는 로컬 HTTP 서버(@opencode-ai/sdk)를 통해 OpenCode를 구동하여 채팅 백엔드로 실행합니다. HTTP provider 어댑터와 달리 이는 하네스 어댑터입니다. OpenCode가 자체 에이전트 루프를 실행하고 셸 명령, 파일 읽기 및 편집, 검색 같은 자체 도구를 서버에서 로컬로 실행합니다. 각 chat() 호출은 하나의 전체 하네스 턴을 실행합니다. 어시스턴트 텍스트와 추론은 실제 토큰 수준 델타로 스트리밍되며, 하네스의 도구 활동은 UI에서 렌더링할 수 있도록 이미 해결된 도구 호출 이벤트로 다시 스트리밍됩니다.

서버 전용입니다. 어댑터는 opencode serve 프로세스를 생성하거나 여기에 연결하므로 Node.js 서버 환경에서만 작동하며 브라우저에서는 작동하지 않습니다. OpenCode가 실행되는 머신의 셸을 OpenCode에 제공하는 것처럼 취급하고 그에 맞게 권한을 구성해야 합니다.

설치

npm install @tanstack/ai-opencode

호스트에 opencode CLI를 설치하고 provider 인증을 완료해야 합니다.

npm install -g opencode-ai
opencode auth login

실행 가능한 데모는 examples/sandbox-cloudflare에 있습니다. UI에서 Claude Code, Codex 또는 Grok Build를 선택할 수 있으며, 세션 재개, 하네스 도구 타임라인, 도구 브리징이 Workers의 TanStack Start 앱에 연결되어 있습니다. 영속적이고 새로고침 후에도 유지되는 실행을 일반 Node에서 동일하게 연결하려면(Docker의 Claude Code) examples/sandbox-web을 참조하세요. 이 어댑터로 교체하는 것은 한 줄만 변경하면 됩니다(src/sandbox-agent.ts).

모델

OpenCode는 provider에 종속되지 않습니다. 구성된 provider가 지원하는 모든 provider/model id를 확인합니다. 모델은 provider/model 형식으로 지정합니다(어댑터는 첫 번째 /에서 분할합니다).

import { chat } from "@tanstack/ai";
import { opencodeText } from "@tanstack/ai-opencode";

const stream = chat({
adapter: opencodeText("anthropic/claude-sonnet-4-5", {
directory: "/path/to/project",
permissionMode: "acceptEdits",
}),
messages: [{ role: "user", content: "Fix the failing test in utils.test.ts" }],
});

구성

옵션설명
directory하네스 세션의 작업 디렉터리입니다. 기본값은 process.cwd()입니다.
baseUrl턴마다 새 서버를 생성하는 대신 이미 실행 중인 opencode serve(예: http://127.0.0.1:4096)에 연결합니다.
hostname생성된 서버의 호스트 이름입니다. 기본값은 SDK 기본값(127.0.0.1)입니다.
port생성된 서버의 포트입니다. 기본값은 SDK 기본값(4096)입니다.
permissionMode'default'(브리징된 도구는 실행하고 프롬프트가 필요한 나머지 작업은 거부), 'acceptEdits'(파일 편집도 자동 승인), 또는 'bypassPermissions'(모두 허용)입니다.
onPermissionRequest사용자 지정 권한 핸들러입니다. 기본 정책 전체를 대체합니다.
config어댑터의 MCP 및 권한 구성과 병합되는 추가 OpenCode 구성입니다.

호출별 재정의(sessionId, permissionMode, directory)는 modelOptions를 통해 전달합니다.

권한

OpenCode는 파일을 변경하거나 명령을 실행하기 전에 권한을 요청합니다. 헤드리스 서버에는 해당 프롬프트에 응답할 사람이 없으므로 어댑터가 정책을 자동으로 적용하며, 턴이 멈추지 않습니다.

  • 'default' — 브리징된 TanStack 도구는 실행하고, 그 외 프롬프트가 필요한 작업(편집, 셸, 웹 가져오기)은 거부합니다.
  • 'acceptEdits' — 파일 변경 요청(edit / write / patch)도 추가로 자동 승인합니다.
  • 'bypassPermissions' — 모든 작업을 승인합니다. 샌드박스 또는 임시 디렉터리에서만 사용해야 합니다.

onPermissionRequest를 제공하여 자체 정책(예: 특정 명령의 허용 목록)을 구현합니다.

상태 저장 세션

OpenCode 세션은 상태를 저장합니다. 하네스는 턴 사이에 전체 작업 컨텍스트(읽은 파일, 실행한 명령, 도출한 결론)를 유지합니다. 어댑터는 새 실행마다 세션 id를 opencode.session-id라는 사용자 지정 스트림 이벤트로 노출합니다. 재개하려면 이를 modelOptions.sessionId로 다시 전달합니다. 재개할 때는 최신 사용자 메시지만 전송합니다. 이전 컨텍스트는 하네스가 이미 보유하고 있습니다.

서버 엔드포인트:

import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { opencodeText } from "@tanstack/ai-opencode";

export async function POST(request: Request) {
const params = await chatParamsFromRequest(request);

// Extra fields the client puts in the connection `body` arrive here.
const sessionId =
typeof params.forwardedProps.sessionId === "string"
? params.forwardedProps.sessionId
: undefined;

const stream = chat({
adapter: opencodeText("anthropic/claude-sonnet-4-5", {
directory: "/path/to/project",
permissionMode: "acceptEdits",
}),
messages: params.messages,
modelOptions: { sessionId },
});

return toServerSentEventsResponse(stream);
}

클라이언트(React) — 사용자 지정 이벤트에서 세션 id를 캡처하고 이후 요청에서 다시 전송합니다.

import { useState } from "react";
import { useChat } from "@tanstack/ai-react";
import { fetchServerSentEvents } from "@tanstack/ai-client";

function CodingAssistant() {
const [sessionId, setSessionId] = useState<string | undefined>(undefined);

const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat", () => ({
body: { sessionId },
})),
onCustomEvent: (name, value) => {
if (
name === "opencode.session-id" &&
typeof value === "object" &&
value !== null &&
"sessionId" in value &&
typeof value.sessionId === "string"
) {
setSessionId(value.sessionId);
}
},
});

// ... render messages; harness tool activity (bash, edit, read, ...)
// arrives as regular tool-call parts with results.
}

세션은 세션을 실행한 서버에 존재하므로 동일한 서버 인스턴스(또는 공유 baseUrl)를 대상으로 할 때만 재개할 수 있습니다.

도구

이 어댑터를 통해 흐르는 도구는 두 종류입니다.

  1. 내장 하네스 도구는 OpenCode 자체가 실행하며 결과가 이미 첨부된 도구 호출 이벤트로 스트리밍됩니다: bash, edit, write, read, grep, 그리고 에이전트의 진행 중인 todo 계획(opencode.todo 사용자 지정 이벤트로 노출됨)입니다. 코드에서 이를 실행하지 않습니다.

  2. 사용자의 TanStack 도구는 하네스 안으로 브리징됩니다. 어댑터는 턴이 진행되는 동안 127.0.0.1에서 수명이 짧은 Streamable-HTTP MCP 서버를 시작하고 OpenCode에 등록합니다. 평소처럼 toolDefinition().server()로 도구를 정의합니다. 도구 호출 이벤트는 등록한 이름으로 돌아옵니다(OpenCode가 내부적으로 MCP 도구에 tanstack_… 접두사를 붙이며, 어댑터가 이를 제거합니다).

import { z } from "zod";
import { chat, toolDefinition } from "@tanstack/ai";
import { opencodeText } from "@tanstack/ai-opencode";

const lookupTicket = toolDefinition({
name: "lookup_ticket",
description: "Look up an issue ticket by id",
inputSchema: z.object({ ticketId: z.string() }),
}).server(async ({ ticketId }) => {
return { ticketId, status: "open", title: "Crash on startup" };
});

const stream = chat({
adapter: opencodeText("anthropic/claude-sonnet-4-5"),
messages: [{ role: "user", content: "What's the status of ticket T-123?" }],
tools: [lookupTicket],
});

클라이언트 측 도구와 승인 게이트 도구는 지원되지 않습니다. 하네스는 실행 중인 프로세스 내부에서 도구를 실행하므로 브라우저 왕복이나 사람의 승인을 기다리기 위해 HTTP 요청 사이에서 일시 중지할 수 없습니다. 서버 execute() 구현이 없는 도구 또는 needsApproval로 표시된 도구를 전달하면 설명적인 오류와 함께 즉시 실패합니다. 이러한 도구는 일반 provider 어댑터를 사용하여 하네스 외부에서 실행해야 합니다.

구조화된 출력

chat()outputSchema를 전달합니다. OpenCode에는 기본 스키마 플래그가 없습니다. 어댑터는 프롬프트에 JSON Schema를 추가하고 마지막 어시스턴트 텍스트를 파싱합니다(마크다운 펜스는 제거됩니다). 도구 활동은 계속 스트리밍됩니다. 객체는 structured-output.complete으로 도착합니다.

import { chat } from "@tanstack/ai"
import { opencodeText } from "@tanstack/ai-opencode"
import { defineSandbox, withSandbox } from "@tanstack/ai-sandbox"
import { dockerSandbox } from "@tanstack/ai-sandbox-docker"
import { z } from "zod"

const Report = z.object({
summary: z.string(),
filesChanged: z.array(z.string()),
})

const sandbox = defineSandbox({
id: "repo-report",
provider: dockerSandbox({ image: "node:22" }),
})

const report = await chat({
adapter: opencodeText("anthropic/claude-opus-4-5"),
messages: [{ role: "user", content: "Review this repo." }],
outputSchema: Report,
middleware: [withSandbox(sandbox)],
})

report.summary

이 경로는 마지막 어시스턴트 메시지에서 JSON을 파싱합니다. 추출만 수행하는 작업이라면 @tanstack/ai-openai 같은 모델 어댑터를 사용합니다.

클라이언트에서는 동일한 스키마를 useChat에 전달하고 final을 읽습니다. partial은 마지막까지 비어 있습니다.

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

const Report = z.object({
summary: z.string(),
filesChanged: z.array(z.string()),
})

function ReportView() {
const { final, isLoading } = useChat({
connection: fetchServerSentEvents("/api/repo-report"),
outputSchema: Report,
})

if (isLoading) return <p>The agent is inspecting the repo.</p>
if (!final) return null
return <p>{final.summary}</p>
}

클라이언트를 포함한 전체 안내는 하네스 에이전트를 참조하세요.

제한 사항

  • 서버 전용(Node)입니다. 어댑터는 opencode serve 프로세스를 생성하거나 여기에 연결합니다.
  • 하네스가 에이전트 루프를 소유합니다. TanStack의 에이전트 루프 전략과 반복별 미들웨어는 하네스 턴 내부에 적용되지 않습니다.
  • 샘플링 제어가 없습니다. 여기에는 temperature 방식의 옵션이 없습니다.
  • 세션은 서버 로컬입니다. 재개하려면 동일한 서버 인스턴스(또는 공유 baseUrl)에 요청해야 합니다.
  • 콜드 스타트가 있습니다. 턴마다 서버를 생성하면 첫 토큰 지연이 추가됩니다. 이를 피하려면 어댑터를 장시간 실행되는 baseUrl에 연결합니다.