@plaited/acp-harness 0.2.6 → 0.3.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 +1 -1
- package/README.md +120 -16
- package/bin/cli.ts +105 -636
- package/bin/tests/cli.spec.ts +218 -51
- package/package.json +20 -4
- package/src/acp-client.ts +5 -4
- package/src/acp-transport.ts +14 -7
- package/src/adapter-check.ts +542 -0
- package/src/adapter-scaffold.ts +934 -0
- package/src/balance.ts +232 -0
- package/src/calibrate.ts +300 -0
- package/src/capture.ts +457 -0
- package/src/constants.ts +94 -0
- package/src/grader-loader.ts +174 -0
- package/src/harness.ts +35 -0
- package/src/schemas-cli.ts +239 -0
- package/src/schemas.ts +567 -0
- package/src/summarize.ts +245 -0
- package/src/tests/adapter-check.spec.ts +70 -0
- package/src/tests/adapter-scaffold.spec.ts +112 -0
- package/src/tests/fixtures/grader-bad-module.ts +5 -0
- package/src/tests/fixtures/grader-exec-fail.py +9 -0
- package/src/tests/fixtures/grader-exec-invalid.py +6 -0
- package/src/tests/fixtures/grader-exec.py +29 -0
- package/src/tests/fixtures/grader-module.ts +14 -0
- package/src/tests/grader-loader.spec.ts +153 -0
- package/src/trials.ts +395 -0
- package/src/validate-refs.ts +188 -0
- package/.claude/rules/accuracy.md +0 -43
- package/.claude/rules/bun-apis.md +0 -80
- package/.claude/rules/code-review.md +0 -254
- package/.claude/rules/git-workflow.md +0 -37
- package/.claude/rules/github.md +0 -154
- package/.claude/rules/testing.md +0 -172
- package/.claude/skills/acp-harness/SKILL.md +0 -310
- package/.claude/skills/acp-harness/assets/Dockerfile.acp +0 -25
- package/.claude/skills/acp-harness/assets/docker-compose.acp.yml +0 -19
- package/.claude/skills/acp-harness/references/downstream.md +0 -288
- package/.claude/skills/acp-harness/references/output-formats.md +0 -221
- package/.claude-plugin/marketplace.json +0 -15
- package/.claude-plugin/plugin.json +0 -16
- package/.github/CODEOWNERS +0 -6
- package/.github/workflows/ci.yml +0 -63
- package/.github/workflows/publish.yml +0 -146
- package/.mcp.json +0 -20
- package/CLAUDE.md +0 -92
- package/Dockerfile.test +0 -23
- package/biome.json +0 -96
- package/bun.lock +0 -513
- package/docker-compose.test.yml +0 -21
- package/scripts/bun-test-wrapper.sh +0 -46
- package/src/acp.constants.ts +0 -56
- package/src/acp.schemas.ts +0 -161
- package/src/acp.types.ts +0 -28
- package/src/tests/fixtures/.claude/settings.local.json +0 -8
- package/src/tests/fixtures/.claude/skills/greeting/SKILL.md +0 -17
- package/tsconfig.json +0 -32
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP adapter project scaffolding.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Generates boilerplate for new ACP adapter projects with proper structure,
|
|
6
|
+
* TypeScript configuration, and example handlers.
|
|
7
|
+
*
|
|
8
|
+
* Supports TypeScript and Python adapters.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { stat } from 'node:fs/promises'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { parseArgs } from 'node:util'
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/** Configuration for scaffold generation */
|
|
22
|
+
export type ScaffoldConfig = {
|
|
23
|
+
/** Adapter name (used for package name and directory) */
|
|
24
|
+
name: string
|
|
25
|
+
/** Output directory path */
|
|
26
|
+
outputDir: string
|
|
27
|
+
/** Language: 'ts' or 'python' */
|
|
28
|
+
lang: 'ts' | 'python'
|
|
29
|
+
/** Generate minimal boilerplate only */
|
|
30
|
+
minimal: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Result of scaffold operation */
|
|
34
|
+
export type ScaffoldResult = {
|
|
35
|
+
/** Output directory path */
|
|
36
|
+
outputDir: string
|
|
37
|
+
/** List of created files */
|
|
38
|
+
files: string[]
|
|
39
|
+
/** Language used */
|
|
40
|
+
lang: 'ts' | 'python'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// TypeScript Templates
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
const tsPackageJson = (name: string): string => `{
|
|
48
|
+
"name": "${name}-acp",
|
|
49
|
+
"version": "1.0.0",
|
|
50
|
+
"type": "module",
|
|
51
|
+
"bin": {
|
|
52
|
+
"${name}-acp": "./src/index.ts"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"start": "bun run src/index.ts",
|
|
56
|
+
"check": "bunx @plaited/acp-harness adapter:check bun ./src/index.ts"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@agentclientprotocol/sdk": "^0.0.1"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/bun": "latest",
|
|
63
|
+
"typescript": "^5.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
`
|
|
67
|
+
|
|
68
|
+
const tsTsConfig = (): string => `{
|
|
69
|
+
"compilerOptions": {
|
|
70
|
+
"target": "ES2022",
|
|
71
|
+
"module": "ESNext",
|
|
72
|
+
"moduleResolution": "bundler",
|
|
73
|
+
"strict": true,
|
|
74
|
+
"esModuleInterop": true,
|
|
75
|
+
"skipLibCheck": true,
|
|
76
|
+
"outDir": "dist",
|
|
77
|
+
"declaration": true
|
|
78
|
+
},
|
|
79
|
+
"include": ["src"]
|
|
80
|
+
}
|
|
81
|
+
`
|
|
82
|
+
|
|
83
|
+
const tsIndexFile = (name: string): string => `#!/usr/bin/env bun
|
|
84
|
+
/**
|
|
85
|
+
* ${name} ACP adapter entry point.
|
|
86
|
+
*
|
|
87
|
+
* This adapter translates between the Agent Client Protocol and
|
|
88
|
+
* your agent's native API.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
import { createInterface } from 'node:readline'
|
|
92
|
+
import { handleInitialize } from './handlers/initialize.ts'
|
|
93
|
+
import { handleSessionNew, handleSessionLoad } from './handlers/session-new.ts'
|
|
94
|
+
import { handleSessionPrompt } from './handlers/session-prompt.ts'
|
|
95
|
+
import { handleSessionCancel } from './handlers/session-cancel.ts'
|
|
96
|
+
import type { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification } from './types.ts'
|
|
97
|
+
|
|
98
|
+
// Method handlers
|
|
99
|
+
const methodHandlers: Record<string, (params: unknown) => Promise<unknown>> = {
|
|
100
|
+
initialize: handleInitialize,
|
|
101
|
+
'session/new': handleSessionNew,
|
|
102
|
+
'session/load': handleSessionLoad,
|
|
103
|
+
'session/prompt': handleSessionPrompt,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Notification handlers (no response expected)
|
|
107
|
+
const notificationHandlers: Record<string, (params: unknown) => Promise<void>> = {
|
|
108
|
+
'session/cancel': handleSessionCancel,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Send a JSON-RPC message to stdout.
|
|
113
|
+
*/
|
|
114
|
+
export const sendMessage = (message: JsonRpcResponse | JsonRpcNotification): void => {
|
|
115
|
+
// biome-ignore lint/suspicious/noConsole: Protocol output
|
|
116
|
+
console.log(JSON.stringify(message))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Send a session update notification.
|
|
121
|
+
*/
|
|
122
|
+
export const sendSessionUpdate = (sessionId: string, update: unknown): void => {
|
|
123
|
+
sendMessage({
|
|
124
|
+
jsonrpc: '2.0',
|
|
125
|
+
method: 'session/update',
|
|
126
|
+
params: { sessionId, update },
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Process incoming JSON-RPC message.
|
|
132
|
+
*/
|
|
133
|
+
const processMessage = async (line: string): Promise<void> => {
|
|
134
|
+
let request: JsonRpcRequest | JsonRpcNotification
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
request = JSON.parse(line)
|
|
138
|
+
} catch {
|
|
139
|
+
sendMessage({
|
|
140
|
+
jsonrpc: '2.0',
|
|
141
|
+
id: null,
|
|
142
|
+
error: { code: -32700, message: 'Parse error' },
|
|
143
|
+
})
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check if it's a notification (no id)
|
|
148
|
+
const isNotification = !('id' in request)
|
|
149
|
+
|
|
150
|
+
if (isNotification) {
|
|
151
|
+
const handler = notificationHandlers[request.method]
|
|
152
|
+
if (handler) {
|
|
153
|
+
await handler(request.params)
|
|
154
|
+
}
|
|
155
|
+
// No response for notifications
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// It's a request - send response
|
|
160
|
+
const reqWithId = request as JsonRpcRequest
|
|
161
|
+
const handler = methodHandlers[reqWithId.method]
|
|
162
|
+
|
|
163
|
+
if (!handler) {
|
|
164
|
+
sendMessage({
|
|
165
|
+
jsonrpc: '2.0',
|
|
166
|
+
id: reqWithId.id,
|
|
167
|
+
error: { code: -32601, message: \`Method not found: \${reqWithId.method}\` },
|
|
168
|
+
})
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const result = await handler(reqWithId.params)
|
|
174
|
+
sendMessage({
|
|
175
|
+
jsonrpc: '2.0',
|
|
176
|
+
id: reqWithId.id,
|
|
177
|
+
result,
|
|
178
|
+
})
|
|
179
|
+
} catch (error) {
|
|
180
|
+
sendMessage({
|
|
181
|
+
jsonrpc: '2.0',
|
|
182
|
+
id: reqWithId.id,
|
|
183
|
+
error: {
|
|
184
|
+
code: -32603,
|
|
185
|
+
message: error instanceof Error ? error.message : 'Internal error',
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Main loop: read lines from stdin
|
|
192
|
+
const rl = createInterface({
|
|
193
|
+
input: process.stdin,
|
|
194
|
+
output: process.stdout,
|
|
195
|
+
terminal: false,
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
rl.on('line', processMessage)
|
|
199
|
+
|
|
200
|
+
// Handle clean shutdown
|
|
201
|
+
process.on('SIGTERM', () => {
|
|
202
|
+
rl.close()
|
|
203
|
+
process.exit(0)
|
|
204
|
+
})
|
|
205
|
+
`
|
|
206
|
+
|
|
207
|
+
const tsTypesFile = (): string => `/**
|
|
208
|
+
* TypeScript types for JSON-RPC 2.0 protocol.
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
export type JsonRpcRequest = {
|
|
212
|
+
jsonrpc: '2.0'
|
|
213
|
+
id: string | number
|
|
214
|
+
method: string
|
|
215
|
+
params?: unknown
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export type JsonRpcNotification = {
|
|
219
|
+
jsonrpc: '2.0'
|
|
220
|
+
method: string
|
|
221
|
+
params?: unknown
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export type JsonRpcSuccessResponse = {
|
|
225
|
+
jsonrpc: '2.0'
|
|
226
|
+
id: string | number
|
|
227
|
+
result: unknown
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export type JsonRpcErrorResponse = {
|
|
231
|
+
jsonrpc: '2.0'
|
|
232
|
+
id: string | number | null
|
|
233
|
+
error: {
|
|
234
|
+
code: number
|
|
235
|
+
message: string
|
|
236
|
+
data?: unknown
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse
|
|
241
|
+
|
|
242
|
+
export type ContentBlock =
|
|
243
|
+
| { type: 'text'; text: string }
|
|
244
|
+
| { type: 'image'; source: { type: 'base64'; mediaType: string; data: string } }
|
|
245
|
+
`
|
|
246
|
+
|
|
247
|
+
const tsInitializeHandler = (name: string): string => `/**
|
|
248
|
+
* Initialize handler - protocol handshake.
|
|
249
|
+
*/
|
|
250
|
+
|
|
251
|
+
type InitializeParams = {
|
|
252
|
+
protocolVersion: number
|
|
253
|
+
clientInfo: { name: string; version: string }
|
|
254
|
+
clientCapabilities: Record<string, unknown>
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
type InitializeResult = {
|
|
258
|
+
protocolVersion: number
|
|
259
|
+
agentInfo: { name: string; version: string }
|
|
260
|
+
agentCapabilities: {
|
|
261
|
+
loadSession?: boolean
|
|
262
|
+
promptCapabilities?: {
|
|
263
|
+
image?: boolean
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export const handleInitialize = async (params: unknown): Promise<InitializeResult> => {
|
|
269
|
+
const { protocolVersion } = params as InitializeParams
|
|
270
|
+
|
|
271
|
+
if (protocolVersion !== 1) {
|
|
272
|
+
throw new Error(\`Unsupported protocol version: \${protocolVersion}\`)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
protocolVersion: 1,
|
|
277
|
+
agentInfo: {
|
|
278
|
+
name: '${name}',
|
|
279
|
+
version: '1.0.0',
|
|
280
|
+
},
|
|
281
|
+
agentCapabilities: {
|
|
282
|
+
loadSession: false,
|
|
283
|
+
promptCapabilities: {
|
|
284
|
+
image: false,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
`
|
|
290
|
+
|
|
291
|
+
const tsSessionNewHandler = (): string => `/**
|
|
292
|
+
* Session handlers - create and load sessions.
|
|
293
|
+
*/
|
|
294
|
+
|
|
295
|
+
import { sessionManager } from '../session-manager.ts'
|
|
296
|
+
|
|
297
|
+
type SessionNewParams = {
|
|
298
|
+
cwd: string
|
|
299
|
+
mcpServers?: unknown[]
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
type SessionNewResult = {
|
|
303
|
+
sessionId: string
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export const handleSessionNew = async (params: unknown): Promise<SessionNewResult> => {
|
|
307
|
+
const { cwd, mcpServers = [] } = params as SessionNewParams
|
|
308
|
+
|
|
309
|
+
const sessionId = sessionManager.createSession({
|
|
310
|
+
cwd,
|
|
311
|
+
mcpServers,
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
return { sessionId }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
type SessionLoadParams = {
|
|
318
|
+
sessionId: string
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export const handleSessionLoad = async (params: unknown): Promise<SessionNewResult> => {
|
|
322
|
+
const { sessionId } = params as SessionLoadParams
|
|
323
|
+
|
|
324
|
+
const session = sessionManager.getSession(sessionId)
|
|
325
|
+
if (!session) {
|
|
326
|
+
throw new Error(\`Session not found: \${sessionId}\`)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { sessionId }
|
|
330
|
+
}
|
|
331
|
+
`
|
|
332
|
+
|
|
333
|
+
const tsSessionPromptHandler = (): string => `/**
|
|
334
|
+
* Session prompt handler - process prompts and emit updates.
|
|
335
|
+
*/
|
|
336
|
+
|
|
337
|
+
import { sessionManager } from '../session-manager.ts'
|
|
338
|
+
import { sendSessionUpdate } from '../index.ts'
|
|
339
|
+
import type { ContentBlock } from '../types.ts'
|
|
340
|
+
|
|
341
|
+
type PromptParams = {
|
|
342
|
+
sessionId: string
|
|
343
|
+
prompt: ContentBlock[]
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
type PromptResult = {
|
|
347
|
+
content: ContentBlock[]
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const handleSessionPrompt = async (params: unknown): Promise<PromptResult> => {
|
|
351
|
+
const { sessionId, prompt } = params as PromptParams
|
|
352
|
+
|
|
353
|
+
const session = sessionManager.getSession(sessionId)
|
|
354
|
+
if (!session) {
|
|
355
|
+
throw new Error(\`Session not found: \${sessionId}\`)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Extract text from content blocks
|
|
359
|
+
const promptText = prompt
|
|
360
|
+
.filter((block): block is ContentBlock & { type: 'text' } => block.type === 'text')
|
|
361
|
+
.map((block) => block.text)
|
|
362
|
+
.join('\\n')
|
|
363
|
+
|
|
364
|
+
// Emit thinking update
|
|
365
|
+
sendSessionUpdate(sessionId, {
|
|
366
|
+
sessionUpdate: 'agent_thought_chunk',
|
|
367
|
+
content: { type: 'text', text: 'Processing your request...' },
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
// TODO: Replace with your agent's actual API call
|
|
371
|
+
const response = await processWithYourAgent(promptText, session.cwd)
|
|
372
|
+
|
|
373
|
+
// Emit message update
|
|
374
|
+
sendSessionUpdate(sessionId, {
|
|
375
|
+
sessionUpdate: 'agent_message_chunk',
|
|
376
|
+
content: { type: 'text', text: response },
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
content: [{ type: 'text', text: response }],
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Replace this with your actual agent API call.
|
|
386
|
+
*/
|
|
387
|
+
const processWithYourAgent = async (prompt: string, _cwd: string): Promise<string> => {
|
|
388
|
+
// Example echo implementation - replace with real agent call
|
|
389
|
+
return \`Echo: \${prompt}\`
|
|
390
|
+
}
|
|
391
|
+
`
|
|
392
|
+
|
|
393
|
+
const tsSessionCancelHandler = (): string => `/**
|
|
394
|
+
* Session cancel handler - cancel ongoing prompts.
|
|
395
|
+
*/
|
|
396
|
+
|
|
397
|
+
type CancelParams = {
|
|
398
|
+
sessionId: string
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Track active requests for cancellation
|
|
402
|
+
const activeRequests = new Map<string, AbortController>()
|
|
403
|
+
|
|
404
|
+
export const handleSessionCancel = async (params: unknown): Promise<void> => {
|
|
405
|
+
const { sessionId } = params as CancelParams
|
|
406
|
+
|
|
407
|
+
const controller = activeRequests.get(sessionId)
|
|
408
|
+
if (controller) {
|
|
409
|
+
controller.abort()
|
|
410
|
+
activeRequests.delete(sessionId)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Register an active request for cancellation support.
|
|
416
|
+
*/
|
|
417
|
+
export const registerActiveRequest = (
|
|
418
|
+
sessionId: string,
|
|
419
|
+
controller: AbortController
|
|
420
|
+
): void => {
|
|
421
|
+
activeRequests.set(sessionId, controller)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Unregister an active request after completion.
|
|
426
|
+
*/
|
|
427
|
+
export const unregisterActiveRequest = (sessionId: string): void => {
|
|
428
|
+
activeRequests.delete(sessionId)
|
|
429
|
+
}
|
|
430
|
+
`
|
|
431
|
+
|
|
432
|
+
const tsSessionManager = (): string => `/**
|
|
433
|
+
* Session manager - tracks active conversation sessions.
|
|
434
|
+
*/
|
|
435
|
+
|
|
436
|
+
import { randomUUID } from 'node:crypto'
|
|
437
|
+
|
|
438
|
+
type Session = {
|
|
439
|
+
id: string
|
|
440
|
+
cwd: string
|
|
441
|
+
mcpServers: unknown[]
|
|
442
|
+
createdAt: Date
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
class SessionManager {
|
|
446
|
+
#sessions = new Map<string, Session>()
|
|
447
|
+
|
|
448
|
+
createSession(params: { cwd: string; mcpServers: unknown[] }): string {
|
|
449
|
+
const id = \`sess_\${randomUUID().slice(0, 8)}\`
|
|
450
|
+
this.#sessions.set(id, {
|
|
451
|
+
id,
|
|
452
|
+
cwd: params.cwd,
|
|
453
|
+
mcpServers: params.mcpServers,
|
|
454
|
+
createdAt: new Date(),
|
|
455
|
+
})
|
|
456
|
+
return id
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
getSession(id: string): Session | undefined {
|
|
460
|
+
return this.#sessions.get(id)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
deleteSession(id: string): boolean {
|
|
464
|
+
return this.#sessions.delete(id)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
listSessions(): Session[] {
|
|
468
|
+
return Array.from(this.#sessions.values())
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export const sessionManager = new SessionManager()
|
|
473
|
+
`
|
|
474
|
+
|
|
475
|
+
const tsReadme = (name: string): string => `# ${name} ACP Adapter
|
|
476
|
+
|
|
477
|
+
ACP (Agent Client Protocol) adapter for ${name}.
|
|
478
|
+
|
|
479
|
+
## Quick Start
|
|
480
|
+
|
|
481
|
+
\`\`\`bash
|
|
482
|
+
# Install dependencies
|
|
483
|
+
bun install
|
|
484
|
+
|
|
485
|
+
# Run the adapter
|
|
486
|
+
bun run start
|
|
487
|
+
|
|
488
|
+
# Or run directly
|
|
489
|
+
bun run src/index.ts
|
|
490
|
+
\`\`\`
|
|
491
|
+
|
|
492
|
+
## Verify Compliance
|
|
493
|
+
|
|
494
|
+
\`\`\`bash
|
|
495
|
+
# Run compliance checker
|
|
496
|
+
bun run check
|
|
497
|
+
|
|
498
|
+
# Or manually
|
|
499
|
+
bunx @plaited/acp-harness adapter:check bun ./src/index.ts
|
|
500
|
+
\`\`\`
|
|
501
|
+
|
|
502
|
+
## Test with Harness
|
|
503
|
+
|
|
504
|
+
\`\`\`bash
|
|
505
|
+
# Create test prompts
|
|
506
|
+
echo '{"id":"test-1","input":"Hello"}' > prompts.jsonl
|
|
507
|
+
|
|
508
|
+
# Run capture
|
|
509
|
+
bunx @plaited/acp-harness capture prompts.jsonl bun ./src/index.ts -o results.jsonl
|
|
510
|
+
|
|
511
|
+
# View results
|
|
512
|
+
cat results.jsonl | jq .
|
|
513
|
+
\`\`\`
|
|
514
|
+
|
|
515
|
+
## Implementation
|
|
516
|
+
|
|
517
|
+
Replace the placeholder in \`src/handlers/session-prompt.ts\`:
|
|
518
|
+
|
|
519
|
+
\`\`\`typescript
|
|
520
|
+
const processWithYourAgent = async (prompt: string, cwd: string): Promise<string> => {
|
|
521
|
+
// Call your agent's API here
|
|
522
|
+
const response = await yourAgentClient.chat(prompt)
|
|
523
|
+
return response.text
|
|
524
|
+
}
|
|
525
|
+
\`\`\`
|
|
526
|
+
|
|
527
|
+
## Protocol Reference
|
|
528
|
+
|
|
529
|
+
See the [ACP Specification](https://agentclientprotocol.org) for protocol details.
|
|
530
|
+
`
|
|
531
|
+
|
|
532
|
+
// ============================================================================
|
|
533
|
+
// Python Templates
|
|
534
|
+
// ============================================================================
|
|
535
|
+
|
|
536
|
+
const pythonAdapter = (name: string): string => `#!/usr/bin/env python3
|
|
537
|
+
"""
|
|
538
|
+
${name} ACP adapter.
|
|
539
|
+
|
|
540
|
+
ACP (Agent Client Protocol) adapter for ${name}.
|
|
541
|
+
Translates between JSON-RPC 2.0 and your agent's native API.
|
|
542
|
+
"""
|
|
543
|
+
|
|
544
|
+
import json
|
|
545
|
+
import sys
|
|
546
|
+
import uuid
|
|
547
|
+
from typing import Any, Dict, Optional
|
|
548
|
+
|
|
549
|
+
# Session storage
|
|
550
|
+
sessions: Dict[str, Dict[str, Any]] = {}
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def create_session(cwd: str, mcp_servers: list) -> str:
|
|
554
|
+
"""Create a new session."""
|
|
555
|
+
session_id = f"sess_{uuid.uuid4().hex[:8]}"
|
|
556
|
+
sessions[session_id] = {
|
|
557
|
+
"id": session_id,
|
|
558
|
+
"cwd": cwd,
|
|
559
|
+
"mcp_servers": mcp_servers,
|
|
560
|
+
}
|
|
561
|
+
return session_id
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def get_session(session_id: str) -> Optional[Dict[str, Any]]:
|
|
565
|
+
"""Get session by ID."""
|
|
566
|
+
return sessions.get(session_id)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def send_message(message: Dict[str, Any]) -> None:
|
|
570
|
+
"""Send JSON-RPC message to stdout."""
|
|
571
|
+
print(json.dumps(message), flush=True)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def send_session_update(session_id: str, update: Dict[str, Any]) -> None:
|
|
575
|
+
"""Send session update notification."""
|
|
576
|
+
send_message({
|
|
577
|
+
"jsonrpc": "2.0",
|
|
578
|
+
"method": "session/update",
|
|
579
|
+
"params": {"sessionId": session_id, "update": update},
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def handle_initialize(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
584
|
+
"""Handle initialize request."""
|
|
585
|
+
protocol_version = params.get("protocolVersion", 0)
|
|
586
|
+
if protocol_version != 1:
|
|
587
|
+
raise ValueError(f"Unsupported protocol version: {protocol_version}")
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
"protocolVersion": 1,
|
|
591
|
+
"agentInfo": {"name": "${name}", "version": "1.0.0"},
|
|
592
|
+
"agentCapabilities": {
|
|
593
|
+
"loadSession": False,
|
|
594
|
+
"promptCapabilities": {"image": False},
|
|
595
|
+
},
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def handle_session_new(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
600
|
+
"""Handle session/new request."""
|
|
601
|
+
cwd = params.get("cwd", ".")
|
|
602
|
+
mcp_servers = params.get("mcpServers", [])
|
|
603
|
+
session_id = create_session(cwd, mcp_servers)
|
|
604
|
+
return {"sessionId": session_id}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def handle_session_prompt(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
608
|
+
"""Handle session/prompt request."""
|
|
609
|
+
session_id = params["sessionId"]
|
|
610
|
+
session = get_session(session_id)
|
|
611
|
+
if not session:
|
|
612
|
+
raise ValueError(f"Session not found: {session_id}")
|
|
613
|
+
|
|
614
|
+
# Extract text from prompt blocks
|
|
615
|
+
prompt_text = " ".join(
|
|
616
|
+
block["text"]
|
|
617
|
+
for block in params.get("prompt", [])
|
|
618
|
+
if block.get("type") == "text"
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# Send thinking update
|
|
622
|
+
send_session_update(session_id, {
|
|
623
|
+
"sessionUpdate": "agent_thought_chunk",
|
|
624
|
+
"content": {"type": "text", "text": "Processing your request..."},
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
# TODO: Replace with your agent's actual API call
|
|
628
|
+
response = process_with_your_agent(prompt_text, session["cwd"])
|
|
629
|
+
|
|
630
|
+
# Send message update
|
|
631
|
+
send_session_update(session_id, {
|
|
632
|
+
"sessionUpdate": "agent_message_chunk",
|
|
633
|
+
"content": {"type": "text", "text": response},
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
return {"content": [{"type": "text", "text": response}]}
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def process_with_your_agent(prompt: str, cwd: str) -> str:
|
|
640
|
+
"""Replace with your actual agent API call."""
|
|
641
|
+
return f"Echo: {prompt}"
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
# Method handlers
|
|
645
|
+
METHOD_HANDLERS = {
|
|
646
|
+
"initialize": handle_initialize,
|
|
647
|
+
"session/new": handle_session_new,
|
|
648
|
+
"session/prompt": handle_session_prompt,
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def process_message(line: str) -> None:
|
|
653
|
+
"""Process incoming JSON-RPC message."""
|
|
654
|
+
try:
|
|
655
|
+
request = json.loads(line)
|
|
656
|
+
except json.JSONDecodeError:
|
|
657
|
+
send_message({
|
|
658
|
+
"jsonrpc": "2.0",
|
|
659
|
+
"id": None,
|
|
660
|
+
"error": {"code": -32700, "message": "Parse error"},
|
|
661
|
+
})
|
|
662
|
+
return
|
|
663
|
+
|
|
664
|
+
# Check if notification (no id)
|
|
665
|
+
if "id" not in request:
|
|
666
|
+
# Handle notification silently
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
method = request.get("method", "")
|
|
670
|
+
handler = METHOD_HANDLERS.get(method)
|
|
671
|
+
|
|
672
|
+
if not handler:
|
|
673
|
+
send_message({
|
|
674
|
+
"jsonrpc": "2.0",
|
|
675
|
+
"id": request["id"],
|
|
676
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
677
|
+
})
|
|
678
|
+
return
|
|
679
|
+
|
|
680
|
+
try:
|
|
681
|
+
result = handler(request.get("params", {}))
|
|
682
|
+
send_message({
|
|
683
|
+
"jsonrpc": "2.0",
|
|
684
|
+
"id": request["id"],
|
|
685
|
+
"result": result,
|
|
686
|
+
})
|
|
687
|
+
except Exception as e:
|
|
688
|
+
send_message({
|
|
689
|
+
"jsonrpc": "2.0",
|
|
690
|
+
"id": request["id"],
|
|
691
|
+
"error": {"code": -32603, "message": str(e)},
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def main() -> None:
|
|
696
|
+
"""Main loop: read lines from stdin."""
|
|
697
|
+
for line in sys.stdin:
|
|
698
|
+
line = line.strip()
|
|
699
|
+
if line:
|
|
700
|
+
process_message(line)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
if __name__ == "__main__":
|
|
704
|
+
main()
|
|
705
|
+
`
|
|
706
|
+
|
|
707
|
+
const pythonReadme = (name: string): string => `# ${name} ACP Adapter
|
|
708
|
+
|
|
709
|
+
ACP (Agent Client Protocol) adapter for ${name} (Python).
|
|
710
|
+
|
|
711
|
+
## Quick Start
|
|
712
|
+
|
|
713
|
+
\`\`\`bash
|
|
714
|
+
# Make executable
|
|
715
|
+
chmod +x adapter.py
|
|
716
|
+
|
|
717
|
+
# Run the adapter
|
|
718
|
+
python adapter.py
|
|
719
|
+
\`\`\`
|
|
720
|
+
|
|
721
|
+
## Verify Compliance
|
|
722
|
+
|
|
723
|
+
\`\`\`bash
|
|
724
|
+
bunx @plaited/acp-harness adapter:check python ./adapter.py
|
|
725
|
+
\`\`\`
|
|
726
|
+
|
|
727
|
+
## Test with Harness
|
|
728
|
+
|
|
729
|
+
\`\`\`bash
|
|
730
|
+
# Create test prompts
|
|
731
|
+
echo '{"id":"test-1","input":"Hello"}' > prompts.jsonl
|
|
732
|
+
|
|
733
|
+
# Run capture
|
|
734
|
+
bunx @plaited/acp-harness capture prompts.jsonl python ./adapter.py -o results.jsonl
|
|
735
|
+
|
|
736
|
+
# View results
|
|
737
|
+
cat results.jsonl | jq .
|
|
738
|
+
\`\`\`
|
|
739
|
+
|
|
740
|
+
## Implementation
|
|
741
|
+
|
|
742
|
+
Replace the placeholder in \`adapter.py\`:
|
|
743
|
+
|
|
744
|
+
\`\`\`python
|
|
745
|
+
def process_with_your_agent(prompt: str, cwd: str) -> str:
|
|
746
|
+
# Call your agent's API here
|
|
747
|
+
response = your_agent_client.chat(prompt)
|
|
748
|
+
return response.text
|
|
749
|
+
\`\`\`
|
|
750
|
+
|
|
751
|
+
## Protocol Reference
|
|
752
|
+
|
|
753
|
+
See the [ACP Specification](https://agentclientprotocol.org) for protocol details.
|
|
754
|
+
`
|
|
755
|
+
|
|
756
|
+
// ============================================================================
|
|
757
|
+
// Scaffold Implementation
|
|
758
|
+
// ============================================================================
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Generate TypeScript adapter project.
|
|
762
|
+
*/
|
|
763
|
+
const scaffoldTypeScript = async (config: ScaffoldConfig): Promise<string[]> => {
|
|
764
|
+
const { name, outputDir, minimal } = config
|
|
765
|
+
const files: string[] = []
|
|
766
|
+
|
|
767
|
+
// Create directories
|
|
768
|
+
await Bun.write(join(outputDir, 'src', 'handlers', '.gitkeep'), '')
|
|
769
|
+
|
|
770
|
+
// Core files
|
|
771
|
+
await Bun.write(join(outputDir, 'package.json'), tsPackageJson(name))
|
|
772
|
+
files.push('package.json')
|
|
773
|
+
|
|
774
|
+
await Bun.write(join(outputDir, 'tsconfig.json'), tsTsConfig())
|
|
775
|
+
files.push('tsconfig.json')
|
|
776
|
+
|
|
777
|
+
await Bun.write(join(outputDir, 'src', 'index.ts'), tsIndexFile(name))
|
|
778
|
+
files.push('src/index.ts')
|
|
779
|
+
|
|
780
|
+
await Bun.write(join(outputDir, 'src', 'types.ts'), tsTypesFile())
|
|
781
|
+
files.push('src/types.ts')
|
|
782
|
+
|
|
783
|
+
await Bun.write(join(outputDir, 'src', 'session-manager.ts'), tsSessionManager())
|
|
784
|
+
files.push('src/session-manager.ts')
|
|
785
|
+
|
|
786
|
+
// Handler files
|
|
787
|
+
await Bun.write(join(outputDir, 'src', 'handlers', 'initialize.ts'), tsInitializeHandler(name))
|
|
788
|
+
files.push('src/handlers/initialize.ts')
|
|
789
|
+
|
|
790
|
+
await Bun.write(join(outputDir, 'src', 'handlers', 'session-new.ts'), tsSessionNewHandler())
|
|
791
|
+
files.push('src/handlers/session-new.ts')
|
|
792
|
+
|
|
793
|
+
await Bun.write(join(outputDir, 'src', 'handlers', 'session-prompt.ts'), tsSessionPromptHandler())
|
|
794
|
+
files.push('src/handlers/session-prompt.ts')
|
|
795
|
+
|
|
796
|
+
await Bun.write(join(outputDir, 'src', 'handlers', 'session-cancel.ts'), tsSessionCancelHandler())
|
|
797
|
+
files.push('src/handlers/session-cancel.ts')
|
|
798
|
+
|
|
799
|
+
// README (unless minimal)
|
|
800
|
+
if (!minimal) {
|
|
801
|
+
await Bun.write(join(outputDir, 'README.md'), tsReadme(name))
|
|
802
|
+
files.push('README.md')
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return files
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Generate Python adapter project.
|
|
810
|
+
*/
|
|
811
|
+
const scaffoldPython = async (config: ScaffoldConfig): Promise<string[]> => {
|
|
812
|
+
const { name, outputDir, minimal } = config
|
|
813
|
+
const files: string[] = []
|
|
814
|
+
|
|
815
|
+
await Bun.write(join(outputDir, 'adapter.py'), pythonAdapter(name))
|
|
816
|
+
files.push('adapter.py')
|
|
817
|
+
|
|
818
|
+
if (!minimal) {
|
|
819
|
+
await Bun.write(join(outputDir, 'README.md'), pythonReadme(name))
|
|
820
|
+
files.push('README.md')
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return files
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Run adapter scaffolding with configuration object.
|
|
828
|
+
*
|
|
829
|
+
* @param config - Scaffold configuration
|
|
830
|
+
* @returns Scaffold result with created files
|
|
831
|
+
*/
|
|
832
|
+
export const runScaffold = async (config: ScaffoldConfig): Promise<ScaffoldResult> => {
|
|
833
|
+
const { outputDir, lang } = config
|
|
834
|
+
|
|
835
|
+
// Create output directory
|
|
836
|
+
await Bun.write(join(outputDir, '.gitkeep'), '')
|
|
837
|
+
|
|
838
|
+
const files = lang === 'python' ? await scaffoldPython(config) : await scaffoldTypeScript(config)
|
|
839
|
+
|
|
840
|
+
return {
|
|
841
|
+
outputDir,
|
|
842
|
+
files,
|
|
843
|
+
lang,
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// ============================================================================
|
|
848
|
+
// CLI Entry Point
|
|
849
|
+
// ============================================================================
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Adapter scaffold command CLI handler.
|
|
853
|
+
*
|
|
854
|
+
* @param args - Command line arguments (after 'adapter:scaffold')
|
|
855
|
+
*/
|
|
856
|
+
export const adapterScaffold = async (args: string[]): Promise<void> => {
|
|
857
|
+
const { values, positionals } = parseArgs({
|
|
858
|
+
args,
|
|
859
|
+
options: {
|
|
860
|
+
output: { type: 'string', short: 'o' },
|
|
861
|
+
lang: { type: 'string', default: 'ts' },
|
|
862
|
+
minimal: { type: 'boolean', default: false },
|
|
863
|
+
help: { type: 'boolean', short: 'h' },
|
|
864
|
+
},
|
|
865
|
+
allowPositionals: true,
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
if (values.help) {
|
|
869
|
+
// biome-ignore lint/suspicious/noConsole: CLI help output
|
|
870
|
+
console.log(`
|
|
871
|
+
Usage: acp-harness adapter:scaffold [name] [options]
|
|
872
|
+
|
|
873
|
+
Arguments:
|
|
874
|
+
name Adapter name (used for package name)
|
|
875
|
+
|
|
876
|
+
Options:
|
|
877
|
+
-o, --output Output directory (default: ./<name>-acp)
|
|
878
|
+
--lang Language: ts or python (default: ts)
|
|
879
|
+
--minimal Generate minimal boilerplate only
|
|
880
|
+
-h, --help Show this help message
|
|
881
|
+
|
|
882
|
+
Examples:
|
|
883
|
+
# Scaffold TypeScript adapter
|
|
884
|
+
acp-harness adapter:scaffold my-agent
|
|
885
|
+
|
|
886
|
+
# Scaffold Python adapter
|
|
887
|
+
acp-harness adapter:scaffold my-agent --lang python
|
|
888
|
+
|
|
889
|
+
# Scaffold to specific directory
|
|
890
|
+
acp-harness adapter:scaffold my-agent -o ./adapters/my-agent
|
|
891
|
+
`)
|
|
892
|
+
return
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const name = positionals[0]
|
|
896
|
+
if (!name) {
|
|
897
|
+
console.error('Error: adapter name is required')
|
|
898
|
+
console.error('Example: acp-harness adapter:scaffold my-agent')
|
|
899
|
+
process.exit(1)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const lang = values.lang === 'python' ? 'python' : 'ts'
|
|
903
|
+
const outputDir = values.output ?? `./${name}-acp`
|
|
904
|
+
|
|
905
|
+
// Check if directory already exists
|
|
906
|
+
const dirExists = await stat(outputDir).catch(() => null)
|
|
907
|
+
if (dirExists) {
|
|
908
|
+
console.error(`Error: directory already exists: ${outputDir}`)
|
|
909
|
+
process.exit(1)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const result = await runScaffold({
|
|
913
|
+
name,
|
|
914
|
+
outputDir,
|
|
915
|
+
lang,
|
|
916
|
+
minimal: values.minimal ?? false,
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
// biome-ignore lint/suspicious/noConsole: CLI output
|
|
920
|
+
console.log(`
|
|
921
|
+
Scaffolded ${result.lang === 'ts' ? 'TypeScript' : 'Python'} adapter: ${name}
|
|
922
|
+
|
|
923
|
+
Created files:
|
|
924
|
+
${result.files.map((f) => ` ${result.outputDir}/${f}`).join('\n')}
|
|
925
|
+
|
|
926
|
+
Next steps:
|
|
927
|
+
cd ${result.outputDir}
|
|
928
|
+
${result.lang === 'ts' ? ' bun install' : ' chmod +x adapter.py'}
|
|
929
|
+
${result.lang === 'ts' ? ' bun run start' : ' python adapter.py'}
|
|
930
|
+
|
|
931
|
+
Verify compliance:
|
|
932
|
+
acp-harness adapter:check ${result.lang === 'ts' ? 'bun ./src/index.ts' : 'python ./adapter.py'}
|
|
933
|
+
`)
|
|
934
|
+
}
|