@plaited/acp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +89 -0
- package/package.json +65 -0
- package/src/acp-client.ts +503 -0
- package/src/acp-helpers.ts +121 -0
- package/src/acp-transport.ts +448 -0
- package/src/acp-utils.ts +341 -0
- package/src/acp.constants.ts +56 -0
- package/src/acp.schemas.ts +161 -0
- package/src/acp.ts +27 -0
- package/src/acp.types.ts +28 -0
- package/src/tests/acp-client.spec.ts +205 -0
- package/src/tests/acp-helpers.spec.ts +105 -0
- package/src/tests/acp-integration.docker.ts +214 -0
- package/src/tests/acp-transport.spec.ts +153 -0
- package/src/tests/acp-utils.spec.ts +394 -0
- package/src/tests/fixtures/.claude/settings.local.json +8 -0
- package/src/tests/fixtures/.claude/skills/greeting/SKILL.md +17 -0
- package/src/tests/fixtures/calculator-mcp.ts +215 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Plaited
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @plaited/acp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@plaited/acp)
|
|
4
|
+
[](https://github.com/plaited/acp-harness/actions/workflows/ci.yml)
|
|
5
|
+
[](https://opensource.org/licenses/ISC)
|
|
6
|
+
|
|
7
|
+
Unified ACP client and evaluation harness for TypeScript/Bun projects. Connect to ACP-compatible agents programmatically, capture full trajectories, and pipe to downstream analysis tools.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add @plaited/acp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Prerequisite:** Install an ACP adapter:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @zed-industries/claude-code-acp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { createACPClient, createPrompt, summarizeResponse } from '@plaited/acp'
|
|
25
|
+
|
|
26
|
+
const client = createACPClient({
|
|
27
|
+
command: ['claude-code-acp'],
|
|
28
|
+
cwd: '/path/to/project',
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await client.connect()
|
|
32
|
+
const session = await client.createSession()
|
|
33
|
+
|
|
34
|
+
const { updates } = await client.promptSync(
|
|
35
|
+
session.id,
|
|
36
|
+
createPrompt('Create a function that validates email addresses')
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const summary = summarizeResponse(updates)
|
|
40
|
+
console.log(summary.text, summary.completedToolCalls)
|
|
41
|
+
|
|
42
|
+
await client.disconnect()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Recommended: Use the Bundled Skill
|
|
46
|
+
|
|
47
|
+
This package includes a comprehensive **acp-harness skill** designed for AI-assisted evaluation development. The skill provides:
|
|
48
|
+
|
|
49
|
+
- Complete API reference for `createACPClient` and helpers
|
|
50
|
+
- Harness CLI usage with all options and examples
|
|
51
|
+
- Output format schemas (summary and judge formats)
|
|
52
|
+
- LLM-as-judge evaluation templates
|
|
53
|
+
- Downstream integration patterns (Braintrust, jq, custom scorers)
|
|
54
|
+
- Docker execution guidance
|
|
55
|
+
|
|
56
|
+
### Install the Skill
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# For Claude Code, Cursor, OpenCode, Amp, Goose, or Factory
|
|
60
|
+
curl -sSL https://raw.githubusercontent.com/plaited/acp-harness/main/scripts/install-acp.sh | bash
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Once installed, the skill auto-activates when working on evaluation tasks. Ask your AI agent to help you:
|
|
64
|
+
|
|
65
|
+
- Set up evaluation prompts
|
|
66
|
+
- Configure the harness CLI
|
|
67
|
+
- Design scoring pipelines
|
|
68
|
+
- Integrate with Braintrust or custom analysis tools
|
|
69
|
+
|
|
70
|
+
The skill contains everything needed to build agent evaluations - use it as your primary reference.
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
bun install # Install dependencies
|
|
76
|
+
bun run check # Type check + lint + format
|
|
77
|
+
bun test # Run unit tests
|
|
78
|
+
bun run check:write # Auto-fix issues
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Requirements
|
|
82
|
+
|
|
83
|
+
- **Runtime:** Bun >= 1.2.9
|
|
84
|
+
- **ACP Adapter:** `@zed-industries/claude-code-acp` or compatible
|
|
85
|
+
- **API Key:** `ANTHROPIC_API_KEY` environment variable
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
ISC © [Plaited Labs](https://github.com/plaited)
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@plaited/acp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "ACP client and evaluation harness for TypeScript/Bun projects",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"engines": {
|
|
7
|
+
"bun": ">= v1.2.9"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/plaited/acp-harness.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/plaited/acp-harness/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/plaited/acp-harness/tree/main#readme",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./src/acp.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./src/acp.ts",
|
|
21
|
+
"./*.ts": "./src/*.ts"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"./src/**",
|
|
25
|
+
"!./src/**/tests/*",
|
|
26
|
+
"!./src/**/*.spec.ts",
|
|
27
|
+
"!./src/**/*.docker.ts"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"check": "bun run check:biome && bun run check:types && bun run check:package",
|
|
34
|
+
"check:biome": "biome check",
|
|
35
|
+
"check:package": "format-package --check",
|
|
36
|
+
"check:types": "tsc --noEmit",
|
|
37
|
+
"check:write": "biome check --write && format-package --write",
|
|
38
|
+
"prepare": "git rev-parse --git-dir > /dev/null 2>&1 && git config core.hooksPath .hooks || true",
|
|
39
|
+
"test": "bun test",
|
|
40
|
+
"test:docker": "docker compose -f docker-compose.test.yml run --rm acp-test"
|
|
41
|
+
},
|
|
42
|
+
"lint-staged": {
|
|
43
|
+
"*.{js,cjs,jsx,tsx,ts}": [
|
|
44
|
+
"bunx biome check --write --files-ignore-unknown"
|
|
45
|
+
],
|
|
46
|
+
"package.json": [
|
|
47
|
+
"format-package -w"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@agentclientprotocol/sdk": "^0.13.0",
|
|
52
|
+
"zod": "^4.3.5"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"bun": ">=1.2.9"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@biomejs/biome": "2.3.11",
|
|
59
|
+
"@types/bun": "1.3.6",
|
|
60
|
+
"@zed-industries/claude-code-acp": "0.13.0",
|
|
61
|
+
"format-package": "7.0.0",
|
|
62
|
+
"lint-staged": "16.2.7",
|
|
63
|
+
"typescript": "5.9.3"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless ACP client for programmatic agent interaction.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This client enables automated evaluation of ACP-compatible agents like
|
|
6
|
+
* Claude Code, Droid, Gemini CLI, and others. It provides:
|
|
7
|
+
*
|
|
8
|
+
* - **Subprocess management**: Spawn and control agent processes
|
|
9
|
+
* - **Session handling**: Create and manage conversation sessions
|
|
10
|
+
* - **Streaming prompts**: AsyncGenerator for real-time updates
|
|
11
|
+
* - **Sync prompts**: Simple request/response for basic evals
|
|
12
|
+
* - **Auto-permissions**: Automatically approves all permissions for headless use
|
|
13
|
+
*
|
|
14
|
+
* Designed for testing and evaluation, not for user-facing applications.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
AgentCapabilities,
|
|
19
|
+
CancelNotification,
|
|
20
|
+
ClientCapabilities,
|
|
21
|
+
ContentBlock,
|
|
22
|
+
Implementation,
|
|
23
|
+
InitializeRequest,
|
|
24
|
+
InitializeResponse,
|
|
25
|
+
McpServer,
|
|
26
|
+
PromptRequest,
|
|
27
|
+
PromptResponse,
|
|
28
|
+
RequestPermissionRequest,
|
|
29
|
+
RequestPermissionResponse,
|
|
30
|
+
SessionNotification,
|
|
31
|
+
} from '@agentclientprotocol/sdk'
|
|
32
|
+
import { version } from '../package.json' with { type: 'json' }
|
|
33
|
+
import { ACP_METHODS, ACP_PROTOCOL_VERSION, DEFAULT_ACP_CLIENT_NAME } from './acp.constants.ts'
|
|
34
|
+
import { RequestPermissionRequestSchema, SessionNotificationSchema } from './acp.schemas.ts'
|
|
35
|
+
import type { Session } from './acp.types.ts'
|
|
36
|
+
import { createACPTransport } from './acp-transport.ts'
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Types
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
/** Configuration for the ACP client */
|
|
42
|
+
export type ACPClientConfig = {
|
|
43
|
+
/** Command to spawn agent (e.g., ['claude', 'code'] or ['droid']) */
|
|
44
|
+
command: string[]
|
|
45
|
+
/** Working directory for agent process */
|
|
46
|
+
cwd?: string
|
|
47
|
+
/** Environment variables for agent process */
|
|
48
|
+
env?: Record<string, string>
|
|
49
|
+
/** Client info for initialization */
|
|
50
|
+
clientInfo?: Implementation
|
|
51
|
+
/** Client capabilities to advertise */
|
|
52
|
+
capabilities?: ClientCapabilities
|
|
53
|
+
/** Timeout for operations in milliseconds (default: 30000) */
|
|
54
|
+
timeout?: number
|
|
55
|
+
/**
|
|
56
|
+
* Polling interval for streaming updates in milliseconds (default: 50).
|
|
57
|
+
* Lower values provide more responsive updates but increase CPU usage.
|
|
58
|
+
* Consider increasing for testing to reduce timing-related flakiness.
|
|
59
|
+
*/
|
|
60
|
+
pollingInterval?: number
|
|
61
|
+
/**
|
|
62
|
+
* Permission handler for agent requests.
|
|
63
|
+
* Default: auto-approve all permissions (headless mode)
|
|
64
|
+
*/
|
|
65
|
+
onPermissionRequest?: (params: RequestPermissionRequest) => Promise<RequestPermissionResponse>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Session update emitted during prompt streaming */
|
|
69
|
+
export type SessionUpdate = {
|
|
70
|
+
type: 'update'
|
|
71
|
+
params: SessionNotification
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Prompt completion emitted when prompt finishes */
|
|
75
|
+
export type PromptComplete = {
|
|
76
|
+
type: 'complete'
|
|
77
|
+
result: PromptResponse
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Events emitted during prompt streaming */
|
|
81
|
+
export type PromptEvent = SessionUpdate | PromptComplete
|
|
82
|
+
|
|
83
|
+
/** Error thrown by ACP client operations */
|
|
84
|
+
export class ACPClientError extends Error {
|
|
85
|
+
constructor(
|
|
86
|
+
message: string,
|
|
87
|
+
public readonly code?: string,
|
|
88
|
+
) {
|
|
89
|
+
super(message)
|
|
90
|
+
this.name = 'ACPClientError'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Client Implementation
|
|
96
|
+
// ============================================================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Creates a headless ACP client for agent evaluation.
|
|
100
|
+
*
|
|
101
|
+
* @param config - Client configuration including command, cwd, and permission handling
|
|
102
|
+
* @returns Client object with lifecycle, session, and prompt methods
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* The client manages:
|
|
106
|
+
* - Agent subprocess lifecycle (connect/disconnect)
|
|
107
|
+
* - Protocol initialization and capability negotiation
|
|
108
|
+
* - Session creation and management
|
|
109
|
+
* - Prompt streaming with real-time updates
|
|
110
|
+
* - Automatic permission approval for headless evaluation
|
|
111
|
+
*
|
|
112
|
+
* See module-level documentation in `src/acp.ts` for usage guidance.
|
|
113
|
+
* See client tests for usage patterns.
|
|
114
|
+
*/
|
|
115
|
+
export const createACPClient = (config: ACPClientConfig) => {
|
|
116
|
+
const {
|
|
117
|
+
command,
|
|
118
|
+
cwd,
|
|
119
|
+
env,
|
|
120
|
+
clientInfo = { name: DEFAULT_ACP_CLIENT_NAME, version },
|
|
121
|
+
capabilities = {},
|
|
122
|
+
timeout = 30000,
|
|
123
|
+
pollingInterval = 50,
|
|
124
|
+
onPermissionRequest,
|
|
125
|
+
} = config
|
|
126
|
+
|
|
127
|
+
let transport: ReturnType<typeof createACPTransport> | undefined
|
|
128
|
+
let agentCapabilities: AgentCapabilities | undefined
|
|
129
|
+
let initializeResult: InitializeResponse | undefined
|
|
130
|
+
|
|
131
|
+
// Track active prompt sessions for update routing
|
|
132
|
+
const activePrompts = new Map<
|
|
133
|
+
string,
|
|
134
|
+
{
|
|
135
|
+
updates: SessionNotification[]
|
|
136
|
+
resolve: (result: PromptResponse) => void
|
|
137
|
+
reject: (error: Error) => void
|
|
138
|
+
}
|
|
139
|
+
>()
|
|
140
|
+
|
|
141
|
+
// --------------------------------------------------------------------------
|
|
142
|
+
// Permission Handling
|
|
143
|
+
// --------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Default permission handler: auto-approve all requests.
|
|
147
|
+
* For headless evaluation in trusted environments.
|
|
148
|
+
*
|
|
149
|
+
* @remarks
|
|
150
|
+
* Validates params with Zod before processing.
|
|
151
|
+
* Prioritizes `allow_always` for faster headless evaluation with fewer
|
|
152
|
+
* permission round-trips. Cancels if validation fails or no allow option
|
|
153
|
+
* is available.
|
|
154
|
+
*/
|
|
155
|
+
const autoApprovePermission = async (params: RequestPermissionRequest): Promise<RequestPermissionResponse> => {
|
|
156
|
+
const result = RequestPermissionRequestSchema.safeParse(params)
|
|
157
|
+
if (!result.success) {
|
|
158
|
+
return { outcome: { outcome: 'cancelled' } }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { options } = result.data
|
|
162
|
+
|
|
163
|
+
// Priority: allow_always (fewer round-trips) > allow_once
|
|
164
|
+
const allowAlways = options.find((opt) => opt.kind === 'allow_always')
|
|
165
|
+
if (allowAlways) {
|
|
166
|
+
return { outcome: { outcome: 'selected', optionId: allowAlways.optionId } }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const allowOnce = options.find((opt) => opt.kind === 'allow_once')
|
|
170
|
+
if (allowOnce) {
|
|
171
|
+
return { outcome: { outcome: 'selected', optionId: allowOnce.optionId } }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// No allow option available - cancel
|
|
175
|
+
return { outcome: { outcome: 'cancelled' } }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const handlePermissionRequest = onPermissionRequest ?? autoApprovePermission
|
|
179
|
+
|
|
180
|
+
// --------------------------------------------------------------------------
|
|
181
|
+
// Transport Callbacks
|
|
182
|
+
// --------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
const onNotification = (method: string, params: unknown) => {
|
|
185
|
+
if (method === ACP_METHODS.UPDATE) {
|
|
186
|
+
const updateParams = SessionNotificationSchema.parse(params)
|
|
187
|
+
const activePrompt = activePrompts.get(updateParams.sessionId)
|
|
188
|
+
if (activePrompt) {
|
|
189
|
+
activePrompt.updates.push(updateParams)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const onRequest = async (method: string, params: unknown): Promise<unknown> => {
|
|
195
|
+
if (method === ACP_METHODS.REQUEST_PERMISSION) {
|
|
196
|
+
return handlePermissionRequest(RequestPermissionRequestSchema.parse(params))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw new ACPClientError(`Unknown request method: ${method}`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --------------------------------------------------------------------------
|
|
203
|
+
// Lifecycle Methods
|
|
204
|
+
// --------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Connects to the agent by spawning the subprocess and initializing the protocol.
|
|
208
|
+
*
|
|
209
|
+
* @returns Initialize result with agent capabilities
|
|
210
|
+
* @throws {ACPClientError} If already connected or connection fails
|
|
211
|
+
*/
|
|
212
|
+
const connect = async (): Promise<InitializeResponse> => {
|
|
213
|
+
if (transport?.isConnected()) {
|
|
214
|
+
throw new ACPClientError('Already connected')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
transport = createACPTransport({
|
|
218
|
+
command,
|
|
219
|
+
cwd,
|
|
220
|
+
env,
|
|
221
|
+
timeout,
|
|
222
|
+
onNotification,
|
|
223
|
+
onRequest,
|
|
224
|
+
onError: (error) => {
|
|
225
|
+
console.error('[ACP Client Error]:', error.message)
|
|
226
|
+
},
|
|
227
|
+
onClose: (code) => {
|
|
228
|
+
// Reject all active prompts on unexpected close
|
|
229
|
+
for (const [sessionId, prompt] of activePrompts) {
|
|
230
|
+
prompt.reject(new ACPClientError(`Agent process exited with code ${code}`))
|
|
231
|
+
activePrompts.delete(sessionId)
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
await transport.start()
|
|
237
|
+
|
|
238
|
+
// Initialize protocol
|
|
239
|
+
const initParams: InitializeRequest = {
|
|
240
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
241
|
+
clientInfo,
|
|
242
|
+
clientCapabilities: capabilities,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
initializeResult = await transport.request<InitializeResponse>(ACP_METHODS.INITIALIZE, initParams)
|
|
246
|
+
|
|
247
|
+
agentCapabilities = initializeResult?.agentCapabilities
|
|
248
|
+
|
|
249
|
+
return initializeResult
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Disconnects from the agent, closing the subprocess.
|
|
254
|
+
*
|
|
255
|
+
* @param graceful - If true, sends shutdown request first (default: true)
|
|
256
|
+
*/
|
|
257
|
+
const disconnect = async (graceful = true): Promise<void> => {
|
|
258
|
+
if (!transport) return
|
|
259
|
+
|
|
260
|
+
// Cancel all active prompts
|
|
261
|
+
for (const [sessionId, prompt] of activePrompts) {
|
|
262
|
+
prompt.reject(new ACPClientError('Client disconnected'))
|
|
263
|
+
activePrompts.delete(sessionId)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await transport.close(graceful)
|
|
267
|
+
transport = undefined
|
|
268
|
+
agentCapabilities = undefined
|
|
269
|
+
initializeResult = undefined
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// --------------------------------------------------------------------------
|
|
273
|
+
// Session Methods
|
|
274
|
+
// --------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Creates a new conversation session.
|
|
278
|
+
*
|
|
279
|
+
* @param params - Session parameters with working directory and optional MCP servers
|
|
280
|
+
* @returns The created session
|
|
281
|
+
* @throws {ACPClientError} If not connected
|
|
282
|
+
*/
|
|
283
|
+
const createSession = async (params: { cwd: string; mcpServers?: McpServer[] }): Promise<Session> => {
|
|
284
|
+
if (!transport?.isConnected()) {
|
|
285
|
+
throw new ACPClientError('Not connected')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const response = await transport.request<{ sessionId: string }>(ACP_METHODS.CREATE_SESSION, {
|
|
289
|
+
cwd: params.cwd,
|
|
290
|
+
mcpServers: params.mcpServers ?? [],
|
|
291
|
+
})
|
|
292
|
+
return { id: response.sessionId }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Sets the model for a session.
|
|
297
|
+
*
|
|
298
|
+
* @experimental This is an unstable ACP feature and may change.
|
|
299
|
+
* @param sessionId - The session ID to set the model for
|
|
300
|
+
* @param modelId - The model ID (e.g., 'claude-3-5-haiku-20241022', 'claude-sonnet-4-20250514')
|
|
301
|
+
* @throws {ACPClientError} If not connected
|
|
302
|
+
*/
|
|
303
|
+
const setModel = async (sessionId: string, modelId: string): Promise<void> => {
|
|
304
|
+
if (!transport?.isConnected()) {
|
|
305
|
+
throw new ACPClientError('Not connected')
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await transport.request(ACP_METHODS.SET_MODEL, { sessionId, modelId })
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --------------------------------------------------------------------------
|
|
312
|
+
// Prompt Methods
|
|
313
|
+
// --------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Sends a prompt and streams updates as they arrive.
|
|
317
|
+
*
|
|
318
|
+
* @param sessionId - The session ID to send the prompt to
|
|
319
|
+
* @param content - Content blocks for the prompt
|
|
320
|
+
* @yields Session updates and final completion
|
|
321
|
+
* @throws {ACPClientError} If not connected
|
|
322
|
+
*
|
|
323
|
+
* @remarks
|
|
324
|
+
* Use this for evaluation scenarios where you need access to
|
|
325
|
+
* intermediate updates (tool calls, plan changes, etc).
|
|
326
|
+
*/
|
|
327
|
+
async function* prompt(sessionId: string, content: ContentBlock[]): AsyncGenerator<PromptEvent> {
|
|
328
|
+
if (!transport?.isConnected()) {
|
|
329
|
+
throw new ACPClientError('Not connected')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const { promise, resolve, reject } = Promise.withResolvers<PromptResponse>()
|
|
333
|
+
const updates: SessionNotification[] = []
|
|
334
|
+
const promptState = {
|
|
335
|
+
updates,
|
|
336
|
+
resolve,
|
|
337
|
+
reject,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
activePrompts.set(sessionId, promptState)
|
|
341
|
+
|
|
342
|
+
// Send prompt request
|
|
343
|
+
const promptParams: PromptRequest = {
|
|
344
|
+
sessionId,
|
|
345
|
+
prompt: content,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Start the prompt request (don't await - we'll poll for updates)
|
|
349
|
+
const promptPromise = transport
|
|
350
|
+
.request<PromptResponse>(ACP_METHODS.PROMPT, promptParams)
|
|
351
|
+
.then(resolve)
|
|
352
|
+
.catch(reject)
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
// Poll for updates until prompt completes
|
|
356
|
+
let lastYieldedIndex = 0
|
|
357
|
+
|
|
358
|
+
while (true) {
|
|
359
|
+
// Yield any new updates
|
|
360
|
+
while (lastYieldedIndex < promptState.updates.length) {
|
|
361
|
+
const update = promptState.updates[lastYieldedIndex]
|
|
362
|
+
if (update) {
|
|
363
|
+
yield { type: 'update', params: update }
|
|
364
|
+
}
|
|
365
|
+
lastYieldedIndex++
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check if prompt completed
|
|
369
|
+
const raceResult = await Promise.race([
|
|
370
|
+
promise.then((result) => ({ done: true as const, result })),
|
|
371
|
+
new Promise<{ done: false }>((res) => setTimeout(() => res({ done: false }), pollingInterval)),
|
|
372
|
+
])
|
|
373
|
+
|
|
374
|
+
if (raceResult.done) {
|
|
375
|
+
// Yield any remaining updates
|
|
376
|
+
while (lastYieldedIndex < promptState.updates.length) {
|
|
377
|
+
const update = promptState.updates[lastYieldedIndex]
|
|
378
|
+
if (update) {
|
|
379
|
+
yield { type: 'update', params: update }
|
|
380
|
+
}
|
|
381
|
+
lastYieldedIndex++
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Yield completion
|
|
385
|
+
yield {
|
|
386
|
+
type: 'complete',
|
|
387
|
+
result: raceResult.result,
|
|
388
|
+
}
|
|
389
|
+
break
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
await promptPromise
|
|
394
|
+
} finally {
|
|
395
|
+
activePrompts.delete(sessionId)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Sends a prompt and waits for the final result.
|
|
401
|
+
*
|
|
402
|
+
* @param sessionId - The session ID to send the prompt to
|
|
403
|
+
* @param content - Content blocks for the prompt
|
|
404
|
+
* @returns The prompt result with all accumulated updates
|
|
405
|
+
* @throws {ACPClientError} If not connected
|
|
406
|
+
*
|
|
407
|
+
* @remarks
|
|
408
|
+
* Use this for simple evaluation scenarios where you only need
|
|
409
|
+
* the final result. All intermediate updates are collected but
|
|
410
|
+
* returned together at the end.
|
|
411
|
+
*/
|
|
412
|
+
const promptSync = async (
|
|
413
|
+
sessionId: string,
|
|
414
|
+
content: ContentBlock[],
|
|
415
|
+
): Promise<{
|
|
416
|
+
result: PromptResponse
|
|
417
|
+
updates: SessionNotification[]
|
|
418
|
+
}> => {
|
|
419
|
+
const updates: SessionNotification[] = []
|
|
420
|
+
let result: PromptResponse | undefined
|
|
421
|
+
|
|
422
|
+
for await (const event of prompt(sessionId, content)) {
|
|
423
|
+
if (event.type === 'update') {
|
|
424
|
+
updates.push(event.params)
|
|
425
|
+
} else if (event.type === 'complete') {
|
|
426
|
+
result = event.result
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!result) {
|
|
431
|
+
throw new ACPClientError('Prompt completed without result')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return { result, updates }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Cancels an ongoing prompt.
|
|
439
|
+
*
|
|
440
|
+
* @param sessionId - The session ID to cancel
|
|
441
|
+
* @throws {ACPClientError} If not connected
|
|
442
|
+
*/
|
|
443
|
+
const cancelPrompt = async (sessionId: string): Promise<void> => {
|
|
444
|
+
if (!transport?.isConnected()) {
|
|
445
|
+
throw new ACPClientError('Not connected')
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const cancelParams: CancelNotification = { sessionId }
|
|
449
|
+
await transport.notify(ACP_METHODS.CANCEL, cancelParams)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// --------------------------------------------------------------------------
|
|
453
|
+
// State Methods
|
|
454
|
+
// --------------------------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Gets the agent capabilities negotiated during initialization.
|
|
458
|
+
*
|
|
459
|
+
* @returns Agent capabilities or undefined if not connected
|
|
460
|
+
*/
|
|
461
|
+
const getCapabilities = (): AgentCapabilities | undefined => {
|
|
462
|
+
return agentCapabilities
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Gets the full initialization result.
|
|
467
|
+
*
|
|
468
|
+
* @returns Initialize result or undefined if not connected
|
|
469
|
+
*/
|
|
470
|
+
const getInitializeResult = (): InitializeResponse | undefined => {
|
|
471
|
+
return initializeResult
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Checks if the client is connected to an agent.
|
|
476
|
+
*/
|
|
477
|
+
const isConnected = (): boolean => {
|
|
478
|
+
return transport?.isConnected() ?? false
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
// Lifecycle
|
|
483
|
+
connect,
|
|
484
|
+
disconnect,
|
|
485
|
+
|
|
486
|
+
// Sessions
|
|
487
|
+
createSession,
|
|
488
|
+
setModel,
|
|
489
|
+
|
|
490
|
+
// Prompts
|
|
491
|
+
prompt,
|
|
492
|
+
promptSync,
|
|
493
|
+
cancelPrompt,
|
|
494
|
+
|
|
495
|
+
// State
|
|
496
|
+
getCapabilities,
|
|
497
|
+
getInitializeResult,
|
|
498
|
+
isConnected,
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Client instance type */
|
|
503
|
+
export type ACPClient = ReturnType<typeof createACPClient>
|