@plaited/acp-harness 0.2.5
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/.claude/rules/accuracy.md +43 -0
- package/.claude/rules/bun-apis.md +80 -0
- package/.claude/rules/code-review.md +254 -0
- package/.claude/rules/git-workflow.md +37 -0
- package/.claude/rules/github.md +154 -0
- package/.claude/rules/testing.md +172 -0
- package/.claude/skills/acp-harness/SKILL.md +310 -0
- package/.claude/skills/acp-harness/assets/Dockerfile.acp +25 -0
- package/.claude/skills/acp-harness/assets/docker-compose.acp.yml +19 -0
- package/.claude/skills/acp-harness/references/downstream.md +288 -0
- package/.claude/skills/acp-harness/references/output-formats.md +221 -0
- package/.claude-plugin/marketplace.json +15 -0
- package/.claude-plugin/plugin.json +16 -0
- package/.github/CODEOWNERS +6 -0
- package/.github/workflows/ci.yml +63 -0
- package/.github/workflows/publish.yml +146 -0
- package/.mcp.json +20 -0
- package/CLAUDE.md +92 -0
- package/Dockerfile.test +23 -0
- package/LICENSE +15 -0
- package/README.md +94 -0
- package/bin/cli.ts +670 -0
- package/bin/tests/cli.spec.ts +362 -0
- package/biome.json +96 -0
- package/bun.lock +513 -0
- package/docker-compose.test.yml +21 -0
- package/package.json +57 -0
- package/scripts/bun-test-wrapper.sh +46 -0
- package/src/acp-client.ts +503 -0
- package/src/acp-helpers.ts +121 -0
- package/src/acp-transport.ts +455 -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/tsconfig.json +32 -0
package/src/acp-utils.ts
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal utilities for ACP content manipulation.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Low-level functions for building content blocks and extracting data
|
|
6
|
+
* from session responses. These are used internally by the higher-level
|
|
7
|
+
* helpers in acp-helpers.ts.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
BlobResourceContents,
|
|
14
|
+
ContentBlock,
|
|
15
|
+
PlanEntry,
|
|
16
|
+
SessionNotification,
|
|
17
|
+
SessionUpdate,
|
|
18
|
+
TextContent,
|
|
19
|
+
TextResourceContents,
|
|
20
|
+
ToolCall,
|
|
21
|
+
ToolCallContent,
|
|
22
|
+
} from '@agentclientprotocol/sdk'
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Content Block Builders
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a text content block.
|
|
30
|
+
*
|
|
31
|
+
* @param text - The text content
|
|
32
|
+
* @returns Text content block
|
|
33
|
+
*/
|
|
34
|
+
export const createTextContent = (text: string): ContentBlock => ({
|
|
35
|
+
type: 'text',
|
|
36
|
+
text,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates an image content block from base64 data.
|
|
41
|
+
*
|
|
42
|
+
* @param data - Base64-encoded image data
|
|
43
|
+
* @param mimeType - MIME type (e.g., 'image/png', 'image/jpeg')
|
|
44
|
+
* @returns Image content block
|
|
45
|
+
*/
|
|
46
|
+
export const createImageContent = (data: string, mimeType: string): ContentBlock => ({
|
|
47
|
+
type: 'image',
|
|
48
|
+
data,
|
|
49
|
+
mimeType,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates an audio content block from base64 data.
|
|
54
|
+
*
|
|
55
|
+
* @param data - Base64-encoded audio data
|
|
56
|
+
* @param mimeType - MIME type (e.g., 'audio/wav', 'audio/mp3')
|
|
57
|
+
* @returns Audio content block
|
|
58
|
+
*/
|
|
59
|
+
export const createAudioContent = (data: string, mimeType: string): ContentBlock => ({
|
|
60
|
+
type: 'audio',
|
|
61
|
+
data,
|
|
62
|
+
mimeType,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/** Parameters for creating a resource link */
|
|
66
|
+
export type CreateResourceLinkParams = {
|
|
67
|
+
/** URI to the resource */
|
|
68
|
+
uri: string
|
|
69
|
+
/** Resource name (required by SDK) */
|
|
70
|
+
name: string
|
|
71
|
+
/** Optional MIME type */
|
|
72
|
+
mimeType?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Creates a resource link content block.
|
|
77
|
+
*
|
|
78
|
+
* @param params - Resource link parameters
|
|
79
|
+
* @returns Resource link content block
|
|
80
|
+
*/
|
|
81
|
+
export const createResourceLink = ({ uri, name, mimeType }: CreateResourceLinkParams): ContentBlock => ({
|
|
82
|
+
type: 'resource_link',
|
|
83
|
+
uri,
|
|
84
|
+
name,
|
|
85
|
+
...(mimeType && { mimeType }),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
/** Parameters for creating an embedded text resource */
|
|
89
|
+
export type CreateTextResourceParams = {
|
|
90
|
+
/** URI identifying the resource */
|
|
91
|
+
uri: string
|
|
92
|
+
/** Text content of the resource */
|
|
93
|
+
text: string
|
|
94
|
+
/** Optional MIME type */
|
|
95
|
+
mimeType?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Creates an embedded text resource content block.
|
|
100
|
+
*
|
|
101
|
+
* @param params - Text resource parameters
|
|
102
|
+
* @returns Resource content block
|
|
103
|
+
*/
|
|
104
|
+
export const createTextResource = ({ uri, text, mimeType }: CreateTextResourceParams): ContentBlock => ({
|
|
105
|
+
type: 'resource',
|
|
106
|
+
resource: {
|
|
107
|
+
uri,
|
|
108
|
+
text,
|
|
109
|
+
...(mimeType && { mimeType }),
|
|
110
|
+
} as TextResourceContents,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/** Parameters for creating an embedded blob resource */
|
|
114
|
+
export type CreateBlobResourceParams = {
|
|
115
|
+
/** URI identifying the resource */
|
|
116
|
+
uri: string
|
|
117
|
+
/** Base64-encoded binary data */
|
|
118
|
+
blob: string
|
|
119
|
+
/** Optional MIME type */
|
|
120
|
+
mimeType?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Creates an embedded blob resource content block.
|
|
125
|
+
*
|
|
126
|
+
* @param params - Blob resource parameters
|
|
127
|
+
* @returns Resource content block
|
|
128
|
+
*/
|
|
129
|
+
export const createBlobResource = ({ uri, blob, mimeType }: CreateBlobResourceParams): ContentBlock => ({
|
|
130
|
+
type: 'resource',
|
|
131
|
+
resource: {
|
|
132
|
+
uri,
|
|
133
|
+
blob,
|
|
134
|
+
...(mimeType && { mimeType }),
|
|
135
|
+
} as BlobResourceContents,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// ============================================================================
|
|
139
|
+
// Content Extraction
|
|
140
|
+
// ============================================================================
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Extracts all text from content blocks.
|
|
144
|
+
*
|
|
145
|
+
* @param content - Array of content blocks
|
|
146
|
+
* @returns Concatenated text content
|
|
147
|
+
*/
|
|
148
|
+
export const extractText = (content: ContentBlock[]): string => {
|
|
149
|
+
return content
|
|
150
|
+
.filter((block): block is TextContent & { type: 'text' } => block.type === 'text')
|
|
151
|
+
.map((block) => block.text)
|
|
152
|
+
.join('\n')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Helper to extract content from SessionUpdate (discriminated union)
|
|
157
|
+
*/
|
|
158
|
+
const getUpdateContent = (update: SessionUpdate): ContentBlock | undefined => {
|
|
159
|
+
if (
|
|
160
|
+
update.sessionUpdate === 'user_message_chunk' ||
|
|
161
|
+
update.sessionUpdate === 'agent_message_chunk' ||
|
|
162
|
+
update.sessionUpdate === 'agent_thought_chunk'
|
|
163
|
+
) {
|
|
164
|
+
return update.content
|
|
165
|
+
}
|
|
166
|
+
return undefined
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Helper to extract tool call from SessionUpdate
|
|
171
|
+
*/
|
|
172
|
+
const getUpdateToolCall = (update: SessionUpdate): ToolCall | undefined => {
|
|
173
|
+
if (update.sessionUpdate === 'tool_call') {
|
|
174
|
+
return update
|
|
175
|
+
}
|
|
176
|
+
return undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Helper to extract plan from SessionUpdate
|
|
181
|
+
*/
|
|
182
|
+
const getUpdatePlan = (update: SessionUpdate): PlanEntry[] | undefined => {
|
|
183
|
+
if (update.sessionUpdate === 'plan') {
|
|
184
|
+
return update.entries
|
|
185
|
+
}
|
|
186
|
+
return undefined
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Extracts text from session notifications.
|
|
191
|
+
*
|
|
192
|
+
* @remarks
|
|
193
|
+
* Streaming produces partial tokens that should be concatenated directly.
|
|
194
|
+
* Uses empty string join to preserve the original text structure.
|
|
195
|
+
*
|
|
196
|
+
* @param notifications - Array of session notifications
|
|
197
|
+
* @returns Concatenated text from all updates
|
|
198
|
+
*/
|
|
199
|
+
export const extractTextFromUpdates = (notifications: SessionNotification[]): string => {
|
|
200
|
+
const texts: string[] = []
|
|
201
|
+
for (const notification of notifications) {
|
|
202
|
+
const content = getUpdateContent(notification.update)
|
|
203
|
+
if (content && content.type === 'text') {
|
|
204
|
+
texts.push(content.text)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Join without separator - streaming chunks should be concatenated directly
|
|
208
|
+
return texts.join('')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Extracts all tool calls from session notifications.
|
|
213
|
+
*
|
|
214
|
+
* @param notifications - Array of session notifications
|
|
215
|
+
* @returns Array of all tool calls
|
|
216
|
+
*/
|
|
217
|
+
export const extractToolCalls = (notifications: SessionNotification[]): ToolCall[] => {
|
|
218
|
+
const calls: ToolCall[] = []
|
|
219
|
+
for (const notification of notifications) {
|
|
220
|
+
const toolCall = getUpdateToolCall(notification.update)
|
|
221
|
+
if (toolCall) {
|
|
222
|
+
calls.push(toolCall)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return calls
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Extracts the latest state of each tool call (deduplicated by toolCallId).
|
|
230
|
+
*
|
|
231
|
+
* @param notifications - Array of session notifications
|
|
232
|
+
* @returns Map of tool call ID to latest tool call state
|
|
233
|
+
*/
|
|
234
|
+
export const extractLatestToolCalls = (notifications: SessionNotification[]): Map<string, ToolCall> => {
|
|
235
|
+
const latest = new Map<string, ToolCall>()
|
|
236
|
+
for (const notification of notifications) {
|
|
237
|
+
const toolCall = getUpdateToolCall(notification.update)
|
|
238
|
+
if (toolCall) {
|
|
239
|
+
latest.set(toolCall.toolCallId, toolCall)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return latest
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Extracts the latest plan from session notifications.
|
|
247
|
+
*
|
|
248
|
+
* @param notifications - Array of session notifications
|
|
249
|
+
* @returns Latest plan entries or undefined if no plan
|
|
250
|
+
*/
|
|
251
|
+
export const extractPlan = (notifications: SessionNotification[]): PlanEntry[] | undefined => {
|
|
252
|
+
// Plans are replaced entirely, so find the last one
|
|
253
|
+
for (let i = notifications.length - 1; i >= 0; i--) {
|
|
254
|
+
const notification = notifications[i]
|
|
255
|
+
if (notification) {
|
|
256
|
+
const plan = getUpdatePlan(notification.update)
|
|
257
|
+
if (plan) {
|
|
258
|
+
return plan
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return undefined
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// Tool Call Utilities
|
|
267
|
+
// ============================================================================
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Filters tool calls by status.
|
|
271
|
+
*
|
|
272
|
+
* @param toolCalls - Array of tool calls
|
|
273
|
+
* @param status - Status to filter by
|
|
274
|
+
* @returns Filtered tool calls
|
|
275
|
+
*/
|
|
276
|
+
export const filterToolCallsByStatus = (toolCalls: ToolCall[], status: ToolCall['status']): ToolCall[] => {
|
|
277
|
+
return toolCalls.filter((call) => call.status === status)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Filters tool calls by title.
|
|
282
|
+
*
|
|
283
|
+
* @param toolCalls - Array of tool calls
|
|
284
|
+
* @param title - Tool title to filter by
|
|
285
|
+
* @returns Filtered tool calls
|
|
286
|
+
*/
|
|
287
|
+
export const filterToolCallsByTitle = (toolCalls: ToolCall[], title: string): ToolCall[] => {
|
|
288
|
+
return toolCalls.filter((call) => call.title === title)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Checks if any tool calls have failed.
|
|
293
|
+
*
|
|
294
|
+
* @param toolCalls - Array of tool calls
|
|
295
|
+
* @returns True if any tool call has 'failed' status
|
|
296
|
+
*/
|
|
297
|
+
export const hasToolCallErrors = (toolCalls: ToolCall[]): boolean => {
|
|
298
|
+
return toolCalls.some((call) => call.status === 'failed')
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Gets completed tool calls with their output content.
|
|
303
|
+
*
|
|
304
|
+
* @param toolCalls - Array of tool calls
|
|
305
|
+
* @returns Tool calls that completed with content
|
|
306
|
+
*/
|
|
307
|
+
export const getCompletedToolCallsWithContent = (
|
|
308
|
+
toolCalls: ToolCall[],
|
|
309
|
+
): Array<ToolCall & { content: ToolCallContent[] }> => {
|
|
310
|
+
return toolCalls.filter(
|
|
311
|
+
(call): call is ToolCall & { content: ToolCallContent[] } =>
|
|
312
|
+
call.status === 'completed' && call.content !== undefined && call.content.length > 0,
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ============================================================================
|
|
317
|
+
// Plan Utilities
|
|
318
|
+
// ============================================================================
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Gets plan entries by status.
|
|
322
|
+
*
|
|
323
|
+
* @param plan - Array of plan entries
|
|
324
|
+
* @param status - Status to filter by
|
|
325
|
+
* @returns Filtered plan entries
|
|
326
|
+
*/
|
|
327
|
+
export const filterPlanByStatus = (plan: PlanEntry[], status: PlanEntry['status']): PlanEntry[] => {
|
|
328
|
+
return plan.filter((entry) => entry.status === status)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Calculates plan completion percentage.
|
|
333
|
+
*
|
|
334
|
+
* @param plan - Array of plan entries
|
|
335
|
+
* @returns Percentage of completed entries (0-100)
|
|
336
|
+
*/
|
|
337
|
+
export const getPlanProgress = (plan: PlanEntry[]): number => {
|
|
338
|
+
if (plan.length === 0) return 100
|
|
339
|
+
const completed = plan.filter((entry) => entry.status === 'completed').length
|
|
340
|
+
return Math.round((completed / plan.length) * 100)
|
|
341
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP protocol constants.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Contains all constant values used across the ACP client implementation:
|
|
6
|
+
* - Protocol method names
|
|
7
|
+
* - Protocol version
|
|
8
|
+
* - JSON-RPC error codes
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Protocol Methods
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/** ACP method names */
|
|
16
|
+
export const ACP_METHODS = {
|
|
17
|
+
// Lifecycle
|
|
18
|
+
INITIALIZE: 'initialize',
|
|
19
|
+
SHUTDOWN: 'shutdown',
|
|
20
|
+
|
|
21
|
+
// Sessions
|
|
22
|
+
CREATE_SESSION: 'session/new',
|
|
23
|
+
LOAD_SESSION: 'session/load',
|
|
24
|
+
PROMPT: 'session/prompt',
|
|
25
|
+
CANCEL: 'session/cancel',
|
|
26
|
+
UPDATE: 'session/update',
|
|
27
|
+
REQUEST_PERMISSION: 'session/request_permission',
|
|
28
|
+
SET_MODEL: 'session/set_model',
|
|
29
|
+
|
|
30
|
+
// Protocol-level
|
|
31
|
+
CANCEL_REQUEST: '$/cancel_request',
|
|
32
|
+
} as const
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Protocol Version
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/** Current protocol version - SDK uses number type */
|
|
39
|
+
export const ACP_PROTOCOL_VERSION = 1 as const
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// JSON-RPC Error Codes
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/** Standard JSON-RPC error codes */
|
|
46
|
+
export const JSON_RPC_ERRORS = {
|
|
47
|
+
PARSE_ERROR: -32700,
|
|
48
|
+
INVALID_REQUEST: -32600,
|
|
49
|
+
METHOD_NOT_FOUND: -32601,
|
|
50
|
+
INVALID_PARAMS: -32602,
|
|
51
|
+
INTERNAL_ERROR: -32603,
|
|
52
|
+
REQUEST_CANCELLED: -32800,
|
|
53
|
+
} as const
|
|
54
|
+
|
|
55
|
+
/** Default ACP Client Name */
|
|
56
|
+
export const DEFAULT_ACP_CLIENT_NAME = 'plaited-acp-client'
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 Zod schemas with runtime validation.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* These schemas provide runtime validation for JSON-RPC messages at the
|
|
6
|
+
* transport boundary. While the ACP SDK handles protocol-level types,
|
|
7
|
+
* the JSON-RPC framing layer is our responsibility since we implement
|
|
8
|
+
* a custom stdio transport.
|
|
9
|
+
*
|
|
10
|
+
* The schemas follow JSON-RPC 2.0 specification:
|
|
11
|
+
* - Requests have `id` and `method`
|
|
12
|
+
* - Notifications have `method` but no `id`
|
|
13
|
+
* - Responses have `id` and either `result` or `error`
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { RequestPermissionRequest, SessionNotification } from '@agentclientprotocol/sdk'
|
|
17
|
+
import { z } from 'zod'
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Inlined Type Utilities
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/** Precise type detection beyond typeof operator */
|
|
24
|
+
const trueTypeOf = (obj?: unknown): string => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
|
|
25
|
+
|
|
26
|
+
/** Type guard for precise type checking with TypeScript narrowing */
|
|
27
|
+
const isTypeOf = <T>(obj: unknown, type: string): obj is T => trueTypeOf(obj) === type
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// JSON-RPC Base Schemas
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/** JSON-RPC version literal */
|
|
34
|
+
const JsonRpcVersionSchema = z.literal('2.0')
|
|
35
|
+
|
|
36
|
+
/** Request/response identifier */
|
|
37
|
+
const RequestIdSchema = z.union([z.string(), z.number()])
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* JSON-RPC 2.0 error object schema.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* Standard error codes:
|
|
44
|
+
* - `-32700`: Parse error
|
|
45
|
+
* - `-32600`: Invalid request
|
|
46
|
+
* - `-32601`: Method not found
|
|
47
|
+
* - `-32602`: Invalid params
|
|
48
|
+
* - `-32603`: Internal error
|
|
49
|
+
* - `-32800`: Request cancelled (ACP extension)
|
|
50
|
+
*/
|
|
51
|
+
export const JsonRpcErrorSchema = z.object({
|
|
52
|
+
code: z.number(),
|
|
53
|
+
message: z.string(),
|
|
54
|
+
data: z.unknown().optional(),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
/** JSON-RPC 2.0 request schema */
|
|
58
|
+
export const JsonRpcRequestSchema = z.object({
|
|
59
|
+
jsonrpc: JsonRpcVersionSchema,
|
|
60
|
+
id: RequestIdSchema,
|
|
61
|
+
method: z.string(),
|
|
62
|
+
params: z.unknown().optional(),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/** JSON-RPC 2.0 notification schema (no id, no response expected) */
|
|
66
|
+
export const JsonRpcNotificationSchema = z.object({
|
|
67
|
+
jsonrpc: JsonRpcVersionSchema,
|
|
68
|
+
method: z.string(),
|
|
69
|
+
params: z.unknown().optional(),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/** JSON-RPC 2.0 success response schema */
|
|
73
|
+
export const JsonRpcSuccessResponseSchema = z.object({
|
|
74
|
+
jsonrpc: JsonRpcVersionSchema,
|
|
75
|
+
id: RequestIdSchema,
|
|
76
|
+
result: z.unknown(),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
/** JSON-RPC 2.0 error response schema */
|
|
80
|
+
export const JsonRpcErrorResponseSchema = z.object({
|
|
81
|
+
jsonrpc: JsonRpcVersionSchema,
|
|
82
|
+
id: z.union([RequestIdSchema, z.null()]),
|
|
83
|
+
error: JsonRpcErrorSchema,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/** Union of all JSON-RPC response types */
|
|
87
|
+
export const JsonRpcResponseSchema = z.union([JsonRpcSuccessResponseSchema, JsonRpcErrorResponseSchema])
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Union of all JSON-RPC message types.
|
|
91
|
+
*
|
|
92
|
+
* @remarks
|
|
93
|
+
* Use `safeParse` at transport boundaries for runtime validation.
|
|
94
|
+
* See transport tests for usage patterns.
|
|
95
|
+
*/
|
|
96
|
+
export const JsonRpcMessageSchema = z.union([JsonRpcRequestSchema, JsonRpcNotificationSchema, JsonRpcResponseSchema])
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Inferred Types
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
/** JSON-RPC 2.0 error object */
|
|
103
|
+
export type JsonRpcError = z.infer<typeof JsonRpcErrorSchema>
|
|
104
|
+
|
|
105
|
+
/** JSON-RPC 2.0 request structure */
|
|
106
|
+
export type JsonRpcRequest<T = unknown> = Omit<z.infer<typeof JsonRpcRequestSchema>, 'params'> & {
|
|
107
|
+
params?: T
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** JSON-RPC 2.0 notification structure (no id, no response expected) */
|
|
111
|
+
export type JsonRpcNotification<T = unknown> = Omit<z.infer<typeof JsonRpcNotificationSchema>, 'params'> & {
|
|
112
|
+
params?: T
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** JSON-RPC 2.0 success response */
|
|
116
|
+
export type JsonRpcSuccessResponse<T = unknown> = Omit<z.infer<typeof JsonRpcSuccessResponseSchema>, 'result'> & {
|
|
117
|
+
result: T
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** JSON-RPC 2.0 error response */
|
|
121
|
+
export type JsonRpcErrorResponse = z.infer<typeof JsonRpcErrorResponseSchema>
|
|
122
|
+
|
|
123
|
+
/** Union of all JSON-RPC response types */
|
|
124
|
+
export type JsonRpcResponse<T = unknown> = JsonRpcSuccessResponse<T> | JsonRpcErrorResponse
|
|
125
|
+
|
|
126
|
+
/** Union of all JSON-RPC message types */
|
|
127
|
+
export type JsonRpcMessage<T = unknown> = JsonRpcRequest<T> | JsonRpcNotification<T> | JsonRpcResponse<T>
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// ACP SDK Type Schemas
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* These schemas use z.custom() to validate SDK types at runtime.
|
|
135
|
+
* They validate only the fields we actually use, keeping SDK types
|
|
136
|
+
* as the source of truth while adding runtime safety.
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
/** Type guard for object shape validation */
|
|
140
|
+
const isRecord = (val: unknown): val is Record<string, unknown> => isTypeOf<Record<string, unknown>>(val, 'object')
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Schema for session update notifications.
|
|
144
|
+
*
|
|
145
|
+
* @remarks
|
|
146
|
+
* Validates `sessionId` and `update` fields used in notification handling.
|
|
147
|
+
*/
|
|
148
|
+
export const SessionNotificationSchema = z.custom<SessionNotification>(
|
|
149
|
+
(val): val is SessionNotification =>
|
|
150
|
+
isRecord(val) && 'sessionId' in val && typeof val.sessionId === 'string' && 'update' in val && isRecord(val.update),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Schema for permission requests from agent.
|
|
155
|
+
*
|
|
156
|
+
* @remarks
|
|
157
|
+
* Validates `options` array used in permission handling.
|
|
158
|
+
*/
|
|
159
|
+
export const RequestPermissionRequestSchema = z.custom<RequestPermissionRequest>(
|
|
160
|
+
(val): val is RequestPermissionRequest => isRecord(val) && 'options' in val && Array.isArray(val.options),
|
|
161
|
+
)
|
package/src/acp.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @plaited/acp-harness - ACP client and evaluation harness for TypeScript/Bun projects.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This module provides a headless ACP client for programmatic agent interaction,
|
|
6
|
+
* optimized for testing, evaluation, and training data generation.
|
|
7
|
+
*
|
|
8
|
+
* **Primary exports:**
|
|
9
|
+
* - `createACPClient` - Factory for headless ACP client instances
|
|
10
|
+
* - `createPrompt`, `createPromptWithFiles`, `createPromptWithImage` - Prompt builders
|
|
11
|
+
* - `summarizeResponse` - Response analysis utility
|
|
12
|
+
*
|
|
13
|
+
* **Re-exports from acp-utils (for advanced usage):**
|
|
14
|
+
* - Content builders: `createTextContent`, `createImageContent`, `createAudioContent`,
|
|
15
|
+
* `createResourceLink`, `createTextResource`, `createBlobResource`
|
|
16
|
+
* - Content extractors: `extractText`, `extractTextFromUpdates`, `extractToolCalls`,
|
|
17
|
+
* `extractLatestToolCalls`, `extractPlan`
|
|
18
|
+
* - Tool call utilities: `filterToolCallsByStatus`, `filterToolCallsByTitle`,
|
|
19
|
+
* `hasToolCallErrors`, `getCompletedToolCallsWithContent`
|
|
20
|
+
* - Plan utilities: `filterPlanByStatus`, `getPlanProgress`
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export * from './acp-client.ts'
|
|
26
|
+
export * from './acp-helpers.ts'
|
|
27
|
+
export * from './acp-utils.ts'
|
package/src/acp.types.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP type definitions.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This module contains types specific to the ACP client implementation.
|
|
6
|
+
* For SDK types (ToolCall, ContentBlock, SessionNotification, etc.), import
|
|
7
|
+
* directly from `@agentclientprotocol/sdk`.
|
|
8
|
+
*
|
|
9
|
+
* For runtime validation of JSON-RPC messages, import Zod schemas from
|
|
10
|
+
* `./acp.schemas.ts`.
|
|
11
|
+
*
|
|
12
|
+
* For protocol constants, import from `./acp.constants.ts`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { SessionId } from '@agentclientprotocol/sdk'
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Session Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Session object returned from session creation.
|
|
23
|
+
* Contains the session ID for subsequent operations.
|
|
24
|
+
*/
|
|
25
|
+
export type Session = {
|
|
26
|
+
id: SessionId
|
|
27
|
+
_meta?: { [key: string]: unknown } | null
|
|
28
|
+
}
|