TanStack
Harness

Use a harness from an editor (ACP)

Your users want your agent in their editor. Editors such as Zed start an agent as a process and talk the Agent Client Protocol (ACP) over stdin and stdout. serveAcp makes your harness an ACP v2 agent.

1. Install the package

shell
npm i @tanstack/ai-acp

2. Serve the harness

Make a script that the editor starts:

ts
import { serveAcp } from '@tanstack/ai-acp/agent'
import { createHarnessHost, defineHarness } from '@tanstack/ai-harness'
import { openaiText } from '@tanstack/ai-openai'
import { memoryPersistence } from '@tanstack/ai-persistence'

export const assistant = defineHarness({
  name: 'acme/assistant',
  adapter: openaiText('gpt-6.1-sol'),
})

const host = createHarnessHost({ persistence: memoryPersistence() })

serveAcp({ host, harness: assistant, version: '1.0.0' })
  • Each ACP session is one harness session. The ACP session id is the thread id.
  • user is the user of the editor, for example serveAcp({ host, harness: assistant, user: { id: 'ada' } }). Every session opens as that user, so /connect saves keys for them. On a host with access, user is required.
  • The prompts of a session run one after another, in the order that the editor sent them. A prompt with a large file keeps its place while the file uploads. The harness busy setting does not apply.
  • The agent holds a session only while a prompt runs or waits. An idle session closes after the host lifecycle.closeIdleSessionsAfterMs, and the next prompt opens it again.
  • version is the version that initialize reports. Default: '0.0.0'.
  • cwd is the working directory that the session list shows for each session. Default: process.cwd().
  • Use a persistent store instead of memoryPersistence() if a session must stay after the process stops.

3. Add the agent to the editor

Give the editor the command that runs the script, for example node agent.js. The editor then sends prompts, and the harness streams the answer back.

What the editor gets

  • The text and the reasoning of the main agent. Text from child agents stays out of the main message.
  • Each tool call, of the main agent and of child agents, as a tool call update.
  • Each wait of a turn, by its kind (see Waits in the editor).
  • Cancel stops the running turn, also while the editor asks for an answer. A turn that fails ends with the error stop reason and the message.
  • Image blocks, audio blocks, and embedded files of a prompt go to the media store of the session. A file that the harness refuses fails the prompt with an invalid params error. See Send and show media.

Waits in the editor

The agent sends each wait to the editor by its kind:

KindWhat the editor shows
approvalA permission request with the id of the real tool call. Allow always shows only for an approval of permissions(), because its answer schema takes always.
question, humanA form elicitation of the answer schema.
sign-inA URL elicitation with the sign-in page.
client-toolNothing. The editor cannot run it, so the agent cancels it.
  • The editor must declare form and URL elicitation in initialize. If it does not, the agent cancels that wait, and the model continues without the answer.
  • A question that a tool asks while it runs (step.ask, an MCP server) is a form elicitation too. An answer that does not fit the schema asks again.
  • If the editor cancels the prompt while it asks, the turn stops. Its waits stay open until the next prompt.

Open an old session

The editor lists the sessions of the harness, newest first. The user picks one, and the next prompt continues that thread.

  • The list shows the top-level sessions of the harness that user can list. Sessions of child agents stay out.
  • The list comes from the session index of the host. Use a persistent store, so that the list keeps the sessions after the process stops.
  • A session id that the session index does not know, or that user cannot read, fails with a not found error.

Known limits

  • ACP v2 is still a draft, so this API is experimental.
  • A client tool call, and a wait that the editor did not declare it can show, are cancelled. The model then continues without the answer.
  • Each session in the list shows the same working directory: the cwd of the agent. If the editor asks for the sessions of another directory, the list is empty.
  • An old session opens without its earlier messages. The editor shows only the new turns. The model still gets the full thread.
  • A resource link in a prompt is only named. The harness does not read it. The model can open it with its own tools.

Use your own stream

serveAcp uses stdin and stdout when you pass no stream. To use another transport, pass an ACP Stream, or call createAcpAgent({ host, harness }) and connect the app yourself:

ts
import { createAcpAgent } from '@tanstack/ai-acp/agent'

export const app = createAcpAgent({ host, harness: assistant })

One agent app serves one connection. It reads the editor capabilities from that connection, and a second connection to it fails with an error. To serve more editors, for example over HTTP, make one app for each connection. With the AcpServer of @agentclientprotocol/sdk/experimental/server, pass createAgent: () => createAcpAgent({ host, harness: assistant }).

What you have now

  • Your harness as an agent in any ACP v2 editor.
  • Approvals, questions, and sign-ins in the editor, and cancel, media, and old sessions.