@wrongstack/acp
v0.318.0
Published
ACP (Agent Client Protocol) integration for WrongStack — client + agent support
Maintainers
Readme
@wrongstack/acp — ACP v1 SDK
Agent Client Protocol (ACP) v1 implementation for WrongStack. The live client and
server use WrongStack's JSON-RPC implementation; the /sdk export provides the official
@agentclientprotocol/sdk APIs.
WrongStack implements the ACP v1 client and server session flows, with stdio and WebSocket support plus an HTTP server transport. Some optional protocol surfaces are intentionally unavailable; see COMPLIANCE.md for the tested surface and current limitations.
Public entry points
| Import | Authority |
|---|---|
| @wrongstack/acp | WrongStack's v1 client/server implementation, integrations, and stable v1 contracts |
| @wrongstack/acp/client | Client-only implementation surface |
| @wrongstack/acp/agent | Server/agent-only implementation surface |
| @wrongstack/acp/v1 | WrongStack-owned stable v1 protocol contracts only |
| @wrongstack/acp/sdk | Official @agentclientprotocol/sdk APIs and experimental transport helpers |
| @wrongstack/acp/legacy | Deprecated pre-v1 draft envelope types; compatibility only |
New code must not import legacy draft contracts from the package root. Use /v1
for protocol types and /sdk only when intentionally adopting the upstream SDK.
Compliance
The package test suite includes client/server protocol tests and an in-process JSON round-trip loopback test. See COMPLIANCE.md for the method matrix and the distinction between wire handling and functional behavior.
Installation
pnpm add @wrongstack/acpQuick Start
ACP Client (connect to an external agent)
import { ACPSession, textContent } from '@wrongstack/acp';
// Spawn and initialize an ACP agent
const session = await ACPSession.start({
command: 'claude',
projectRoot: process.cwd(),
});
// Run a prompt turn
const result = await session.prompt(
[textContent('Generate a unit test for this function.')],
new AbortController().signal,
);
console.log(result.text); // Agent's response
await session.close();ACP Server (expose WrongStack as an ACP agent)
import { WrongStackACPServer, makeACPServerAgentTurn } from '@wrongstack/acp/agent';
const agentFor = async (sessionId: string) => {
// Create a WrongStack Agent instance for this session
return createAgent({ provider: 'anthropic', model: 'claude-3-opus' });
};
const server = new WrongStackACPServer({
runTurn: makeACPServerAgentTurn({ agentFor }),
transport: 7788, // HTTP mode on port 7788
});
await server.start();Using the Official SDK (for advanced use cases)
import {
AcpServer,
AgentApp,
createHttpStream,
createWebSocketStream,
} from '@wrongstack/acp/sdk';The /sdk entry point re-exports the official SDK's public high-level APIs, method
constants, and experimental HTTP/Node/WebSocket helpers. WrongStack's ACPSession
is intentionally available from the package root or /client, not from /sdk.
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ @wrongstack/acp │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Client SDK │ │ Server SDK │ │
│ │ (acp-session) │ │ (protocol-handler) │
│ │ │ │ │ │
│ │ › ACPSession │ │ › ACPProtocolHandler │
│ │ › ACPSession- │ │ › WrongStackACPServer │
│ │ Error │ │ › ACPServerAgentTurn │
│ │ › FileServer │ │ › ACPSessionStore │
│ │ › Terminal- │ │ › RunTurn │
│ │ Server │ │ │
│ └────────┬────────┘ └──────────┬──────────────────────────┘ │
│ │ │ │
│ ┌────────┴────────────────────────┴────┐ │
│ │ Transports │ │
│ │ stdio │ HTTP │ WebSocket │ SSE │ │
│ └───────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Official SDK Bridge (@agentclientprotocol/ │ │
│ │ sdk) │ │
│ │ │ │
│ │ › AcpServer, AgentApp, ClientApp │ │
│ │ › ActiveSession, SessionBuilder │ │
│ │ › createWebSocketStream │ │
│ │ › Schema types (200+ ACP types) │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘Client SDK
ACPSession
An ACPSession connects to an external ACP-supporting agent (Claude Code,
Gemini CLI, Codex CLI, etc.) as a subprocess or over a network transport.
Connection
const session = await ACPSession.start({
command: 'claude',
args: ['--model', 'anthropic-test-model'],
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
cwd: '/path/to/project',
projectRoot: '/path/to/project',
mcpServers: [
{ name: 'filesystem', command: '/usr/bin/mcp-filesystem' },
],
});Authentication
if (session.requiresAuth()) {
const methods = session.getAuthMethods();
// methods: [{ id: 'agent-login', name: 'Agent login', ... }]
await session.authenticate('agent-login');
}Sessions
// Prompt (creates session automatically if needed)
const result = await session.prompt(
[textContent('Hello!'), imageContent('image/png', base64Data)],
signal,
);
// Load existing session
await session.loadSession('sess_abc123');
// Resume without replay
await session.resumeSession('sess_abc123');
// List sessions
const { sessions } = await session.listSessions();
// Delete a session
await session.deleteSession('sess_abc123');
// Graceful close
await session.close();Accessors
session.getCapabilities(); // AgentCapabilities
session.getAuthMethods(); // AuthMethod[]
session.getAgentInfo(); // { name, title?, version }
session.requiresAuth(); // boolean
session.getSessionId(); // SessionId | nullContent Helpers
import { textContent, imageContent, audioContent } from '@wrongstack/acp';
// Text
textContent('Hello world');
// Image (only if agent's promptCapabilities.image === true)
imageContent('image/png', base64String);
// Audio (only if agent's promptCapabilities.audio === true)
audioContent('audio/wav', base64String);
// Check agent capabilities first
const caps = session.getCapabilities();
if (caps.promptCapabilities?.image) {
blocks.push(imageContent('image/png', screenshot));
}File & Terminal Servers
Clients implement fs/* and terminal/* methods that the agent calls:
import { FileServer, TerminalServer } from '@wrongstack/acp/client';
const fileServer = new FileServer({ projectRoot: '/path' });
const { content } = await fileServer.readTextFile({ path: '/path/file.ts' });
const terminalServer = new TerminalServer({ projectRoot: '/path' });
const { terminalId } = terminalServer.create({
command: 'node',
args: ['-e', 'console.log("hi")'],
});Server SDK
WrongStackACPServer
Exposes WrongStack as an ACP-compatible agent:
import {
WrongStackACPServer,
makeACPServerAgentTurn,
} from '@wrongstack/acp/agent';
const server = new WrongStackACPServer({
runTurn: makeACPServerAgentTurn({
agentFor: async (sessionId, cwd) => {
return myAgentFactory(sessionId, cwd);
},
}),
agentName: 'my-agent',
defaultCwd: process.cwd(),
transport: 7788, // HTTP mode (omit for stdio)
host: '127.0.0.1',
});
await server.start();HTTP Transport
When transport is a number, the server listens as HTTP:
# Client connects via:
curl -X POST http://127.0.0.1:7788 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}'Session Persistence
import { ACPSessionStore } from '@wrongstack/acp/agent';
const store = new ACPSessionStore({ dir: './.acp-sessions' });
await store.init();
await store.save(sessionState);
const loaded = await store.load('sess_abc');
const all = await store.list();
await store.delete('sess_abc');Plan & Usage Updates
The server emits plan and usage_update notifications when the Agent
provides them:
// In your agent factory:
const result = await agent.run(prompt, { signal });
// If result.plan is an array, session/update plan notifications fire
// If result.usage is provided, session/update usage_update firesSupported Methods
Client → Agent (ACPSession methods)
| Method | ACPSession API | Status |
|--------|---------------|--------|
| initialize | ACPSession.start() | ✅ |
| authenticate | session.authenticate(methodId) | ✅ |
| logout | session.logout() | ✅ |
| session/new | Auto-created on first prompt() | ✅ |
| session/load | session.loadSession(id) | ✅ |
| session/resume | session.resumeSession(id) | ✅ |
| session/close | session.close() | ✅ |
| session/delete | session.deleteSession(id) | ✅ |
| session/list | session.listSessions() | ✅ |
| session/fork | session.forkSession(id) | ✅ |
| session/prompt | session.prompt(blocks, signal) | ✅ |
| session/cancel | Via AbortSignal | ✅ |
| session/set_mode | session.setMode(sessionId, modeId) | ✅ |
| session/set_config_option | session.setConfigOption(sessionId, optionId, value) | ✅ |
| providers/list | session.listProviders() | ✅ |
| providers/set | session.setProvider(providerId, config?) | ✅ |
| providers/disable | session.disableProvider() | ✅ |
| mcp/message | session.mcpMessage(connectionId, message) | ✅ |
Agent → Client (handled by ACPSession)
| Method | Handler | Status |
|--------|---------|--------|
| session/update | Stream pump (11 stable discriminators; unknown/unstable updates are tolerated) | ✅ |
| session/request_permission | Permission policy callback | ✅ |
| fs/read_text_file | FileServer (sandboxed) | ✅ |
| fs/write_text_file | FileServer (sandboxed) | ✅ |
| terminal/create | TerminalServer | ✅ |
| terminal/output | TerminalServer | ✅ |
| terminal/wait_for_exit | TerminalServer | ✅ |
| terminal/kill | TerminalServer | ✅ |
| terminal/release | TerminalServer | ✅ |
Server (Agent) — handled by ACPProtocolHandler
| Method | Handler | Status |
|--------|---------|--------|
| initialize | handleInitialize | ✅ |
| authenticate | handleAuthenticate | ✅ |
| logout | handleLogout | ✅ |
| session/new | handleSessionNew | ✅ |
| session/load | handleSessionLoad | ✅ |
| session/resume | handleSessionResume | ✅ |
| session/close | handleSessionClose | ✅ |
| session/delete | handleSessionDelete | ✅ |
| session/fork | handleSessionFork | ✅ |
| session/list | handleSessionList | ✅ |
| session/prompt | handleSessionPrompt | ✅ |
| session/cancel | Notification handler | ✅ |
| session/set_mode | handleSetMode | ✅ |
| session/set_config_option | handleSetConfigOption | ✅ |
| providers/list | Returns an empty provider list; provider configuration stays in wstack auth | Limited |
| providers/set | Returns an error directing callers to wstack auth | Unsupported |
| providers/disable | Acknowledges without changing CLI provider configuration | Limited |
| mcp/message | Returns an unavailable error | Unsupported |
| document/* | Standard method-not-found response | Unsupported |
| nes/* | Standard method-not-found response | Unsupported |
| elicitation/* | Standard method-not-found response | Unsupported |
| $/cancel_request | Notification handler | ✅ |
Transport
| Transport | Client | Server | Library |
|-----------|--------|--------|---------|
| stdio | ✅ ACPSession.start() | ✅ WrongStackACPServer | Built-in |
| HTTP | ❌ No ACPSession HTTP client | ✅ transport: <port> | Built-in server |
| WebSocket | ✅ ACPSession.connectWebSocket() | ✅ via the CLI --ws bridge | Built-in |
| SSE | SDK exports only; no WrongStack command wires it | SDK exports only; no WrongStack command wires it | Official SDK re-export |
Error Handling
import { ACPSession, ACPSessionError } from '@wrongstack/acp';
try {
const result = await session.prompt(blocks, signal);
} catch (err) {
if (err instanceof ACPSessionError) {
switch (err.kind) {
case 'spawn_failed': // Child process couldn't start
case 'init_failed': // Initialize handshake failed
case 'auth_failed': // Authentication rejected
case 'prompt_failed': // Prompt turn returned error
case 'aborted': // User aborted via signal
case 'closed': // Session was closed
case 'unsupported_capability': // Agent can't do what we need
case 'protocol_error': // Unexpected wire message
}
}
}