@tanstack/ai-acp 0.1.0
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/README.md +428 -0
- package/dist/esm/adapters/compatible.d.ts +206 -0
- package/dist/esm/adapters/compatible.js +313 -0
- package/dist/esm/adapters/compatible.js.map +1 -0
- package/dist/esm/adapters/projection.d.ts +29 -0
- package/dist/esm/adapters/projection.js +83 -0
- package/dist/esm/adapters/projection.js.map +1 -0
- package/dist/esm/index.d.ts +21 -0
- package/dist/esm/index.js +35 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/messages/prompt.d.ts +19 -0
- package/dist/esm/messages/prompt.js +37 -0
- package/dist/esm/messages/prompt.js.map +1 -0
- package/dist/esm/permissions.d.ts +8 -0
- package/dist/esm/permissions.js +44 -0
- package/dist/esm/permissions.js.map +1 -0
- package/dist/esm/session/acp-client.d.ts +37 -0
- package/dist/esm/session/acp-client.js +140 -0
- package/dist/esm/session/acp-client.js.map +1 -0
- package/dist/esm/session/sandbox-server.d.ts +46 -0
- package/dist/esm/session/sandbox-server.js +114 -0
- package/dist/esm/session/sandbox-server.js.map +1 -0
- package/dist/esm/stream/queue.d.ts +15 -0
- package/dist/esm/stream/queue.js +54 -0
- package/dist/esm/stream/queue.js.map +1 -0
- package/dist/esm/stream/translate.d.ts +38 -0
- package/dist/esm/stream/translate.js +320 -0
- package/dist/esm/stream/translate.js.map +1 -0
- package/dist/esm/transport/resolve.d.ts +7 -0
- package/dist/esm/transport/resolve.js +34 -0
- package/dist/esm/transport/resolve.js.map +1 -0
- package/dist/esm/transport/stdio.d.ts +3 -0
- package/dist/esm/transport/stdio.js +51 -0
- package/dist/esm/transport/stdio.js.map +1 -0
- package/dist/esm/transport/types.d.ts +29 -0
- package/dist/esm/transport/websocket.d.ts +17 -0
- package/dist/esm/transport/websocket.js +135 -0
- package/dist/esm/transport/websocket.js.map +1 -0
- package/dist/esm/types/acp-types.d.ts +75 -0
- package/package.json +61 -0
- package/src/adapters/compatible.ts +675 -0
- package/src/adapters/projection.ts +164 -0
- package/src/index.ts +85 -0
- package/src/messages/prompt.ts +71 -0
- package/src/permissions.ts +71 -0
- package/src/session/acp-client.ts +238 -0
- package/src/session/sandbox-server.ts +195 -0
- package/src/stream/queue.ts +61 -0
- package/src/stream/translate.ts +412 -0
- package/src/transport/resolve.ts +46 -0
- package/src/transport/stdio.ts +63 -0
- package/src/transport/types.ts +33 -0
- package/src/transport/websocket.ts +192 -0
- package/src/types/acp-types.ts +79 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapt a sandbox {@link SpawnHandle} into byte streams for ACP stdio
|
|
3
|
+
* (newline-delimited JSON-RPC).
|
|
4
|
+
*/
|
|
5
|
+
import type { SpawnHandle } from '@tanstack/ai-sandbox'
|
|
6
|
+
import type { AcpByteTransport } from './types'
|
|
7
|
+
|
|
8
|
+
export function spawnHandleToAcpTransport(
|
|
9
|
+
handle: SpawnHandle,
|
|
10
|
+
): AcpByteTransport {
|
|
11
|
+
const encoder = new TextEncoder()
|
|
12
|
+
const decoder = new TextDecoder()
|
|
13
|
+
|
|
14
|
+
const readable = new ReadableStream<Uint8Array>({
|
|
15
|
+
async start(controller) {
|
|
16
|
+
try {
|
|
17
|
+
for await (const chunk of handle.stdout) {
|
|
18
|
+
controller.enqueue(encoder.encode(chunk))
|
|
19
|
+
}
|
|
20
|
+
controller.close()
|
|
21
|
+
} catch (error) {
|
|
22
|
+
controller.error(error)
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const writable = new WritableStream<Uint8Array>({
|
|
28
|
+
write(chunk) {
|
|
29
|
+
return handle.stdin.write(decoder.decode(chunk))
|
|
30
|
+
},
|
|
31
|
+
close() {
|
|
32
|
+
return handle.stdin.end()
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
let tail = ''
|
|
37
|
+
void (async () => {
|
|
38
|
+
try {
|
|
39
|
+
for await (const chunk of handle.stderr) {
|
|
40
|
+
tail = (tail + chunk).slice(-4096)
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// stderr closed
|
|
44
|
+
}
|
|
45
|
+
})()
|
|
46
|
+
|
|
47
|
+
const exited: Promise<never> = handle.wait().then((code) => {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`ACP harness process exited unexpectedly (code ${code}).${
|
|
50
|
+
tail.trim() !== '' ? `\nstderr: ${tail.trim()}` : ''
|
|
51
|
+
}`,
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
void exited.catch(() => undefined)
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
writable,
|
|
58
|
+
readable,
|
|
59
|
+
exited,
|
|
60
|
+
stderrTail: () => tail,
|
|
61
|
+
kill: () => handle.kill(),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ndJsonStream } from '@agentclientprotocol/sdk'
|
|
2
|
+
import type { SpawnHandle } from '@tanstack/ai-sandbox'
|
|
3
|
+
|
|
4
|
+
/** How JSON-RPC messages are framed on the wire. */
|
|
5
|
+
export type AcpMessageFraming = 'ndjson' | 'frame'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Byte-level duplex transport consumed by {@link ndJsonStream} for stdio
|
|
9
|
+
* harnesses.
|
|
10
|
+
*/
|
|
11
|
+
export interface AcpByteTransport {
|
|
12
|
+
writable: WritableStream<Uint8Array>
|
|
13
|
+
readable: ReadableStream<Uint8Array>
|
|
14
|
+
/** Resolves (throws) when the underlying connection exits unexpectedly. */
|
|
15
|
+
exited: Promise<never>
|
|
16
|
+
/** Last bytes of stderr, for error messages. */
|
|
17
|
+
stderrTail: () => string
|
|
18
|
+
kill: () => Promise<void>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Parsed JSON-RPC object streams passed to {@link ClientSideConnection}. */
|
|
22
|
+
export type AcpJsonRpcStream = ReturnType<typeof ndJsonStream>
|
|
23
|
+
|
|
24
|
+
export type AcpSessionTransport =
|
|
25
|
+
| { kind: 'stdio'; process: SpawnHandle }
|
|
26
|
+
| {
|
|
27
|
+
kind: 'stream'
|
|
28
|
+
stream: AcpJsonRpcStream
|
|
29
|
+
dispose: () => Promise<void>
|
|
30
|
+
stderrTail?: () => string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type AcpTransportPreference = 'auto' | 'stdio' | 'websocket'
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { ndJsonStream } from '@agentclientprotocol/sdk'
|
|
2
|
+
import type { AcpJsonRpcStream, AcpMessageFraming } from './types'
|
|
3
|
+
|
|
4
|
+
function waitForWebSocketOpen(
|
|
5
|
+
ws: WebSocket,
|
|
6
|
+
signal?: AbortSignal,
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
if (ws.readyState === WebSocket.OPEN) return Promise.resolve()
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const onOpen = (): void => {
|
|
11
|
+
cleanup()
|
|
12
|
+
resolve()
|
|
13
|
+
}
|
|
14
|
+
const onError = (): void => {
|
|
15
|
+
cleanup()
|
|
16
|
+
reject(new Error('WebSocket connection failed'))
|
|
17
|
+
}
|
|
18
|
+
const onAbort = (): void => {
|
|
19
|
+
cleanup()
|
|
20
|
+
ws.close()
|
|
21
|
+
reject(signal?.reason ?? new Error('WebSocket connection aborted'))
|
|
22
|
+
}
|
|
23
|
+
const cleanup = (): void => {
|
|
24
|
+
ws.removeEventListener('open', onOpen)
|
|
25
|
+
ws.removeEventListener('error', onError)
|
|
26
|
+
signal?.removeEventListener('abort', onAbort)
|
|
27
|
+
}
|
|
28
|
+
ws.addEventListener('open', onOpen)
|
|
29
|
+
ws.addEventListener('error', onError)
|
|
30
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Wrap a stream controller so its terminal calls are idempotent. A WebSocket can
|
|
36
|
+
* fire `close` after `error` (or after the reader cancels), and closing/erroring
|
|
37
|
+
* an already-settled controller throws `ERR_INVALID_STATE` out of the listener.
|
|
38
|
+
*/
|
|
39
|
+
function idempotentController<T>(
|
|
40
|
+
controller: ReadableStreamDefaultController<T>,
|
|
41
|
+
): {
|
|
42
|
+
enqueue: (chunk: T) => void
|
|
43
|
+
close: () => void
|
|
44
|
+
error: (reason: unknown) => void
|
|
45
|
+
} {
|
|
46
|
+
let settled = false
|
|
47
|
+
return {
|
|
48
|
+
enqueue: (chunk) => {
|
|
49
|
+
if (!settled) controller.enqueue(chunk)
|
|
50
|
+
},
|
|
51
|
+
close: () => {
|
|
52
|
+
if (settled) return
|
|
53
|
+
settled = true
|
|
54
|
+
controller.close()
|
|
55
|
+
},
|
|
56
|
+
error: (reason) => {
|
|
57
|
+
if (settled) return
|
|
58
|
+
settled = true
|
|
59
|
+
controller.error(reason)
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One JSON-RPC object per WebSocket text frame (e.g. `grok agent serve`).
|
|
66
|
+
*/
|
|
67
|
+
export function webSocketFrameToAcpStream(ws: WebSocket): AcpJsonRpcStream {
|
|
68
|
+
const decoder = new TextDecoder()
|
|
69
|
+
|
|
70
|
+
const readable = new ReadableStream<unknown>({
|
|
71
|
+
start(rawController) {
|
|
72
|
+
const controller = idempotentController(rawController)
|
|
73
|
+
ws.addEventListener('message', (event) => {
|
|
74
|
+
const text =
|
|
75
|
+
typeof event.data === 'string'
|
|
76
|
+
? event.data
|
|
77
|
+
: decoder.decode(event.data as ArrayBuffer)
|
|
78
|
+
const trimmed = text.trim()
|
|
79
|
+
if (trimmed === '') return
|
|
80
|
+
try {
|
|
81
|
+
controller.enqueue(JSON.parse(trimmed))
|
|
82
|
+
} catch (error) {
|
|
83
|
+
controller.error(
|
|
84
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
ws.addEventListener('close', () => controller.close())
|
|
89
|
+
ws.addEventListener('error', () =>
|
|
90
|
+
controller.error(new Error('WebSocket connection error')),
|
|
91
|
+
)
|
|
92
|
+
},
|
|
93
|
+
cancel() {
|
|
94
|
+
ws.close()
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const writable = new WritableStream({
|
|
99
|
+
write(message) {
|
|
100
|
+
ws.send(JSON.stringify(message))
|
|
101
|
+
},
|
|
102
|
+
close() {
|
|
103
|
+
ws.close()
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return { readable, writable } as AcpJsonRpcStream
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function webSocketNdjsonToAcpStream(ws: WebSocket): AcpJsonRpcStream {
|
|
111
|
+
const encoder = new TextEncoder()
|
|
112
|
+
const decoder = new TextDecoder()
|
|
113
|
+
|
|
114
|
+
const readable = new ReadableStream<Uint8Array>({
|
|
115
|
+
start(rawController) {
|
|
116
|
+
const controller = idempotentController(rawController)
|
|
117
|
+
ws.addEventListener('message', (event) => {
|
|
118
|
+
const text =
|
|
119
|
+
typeof event.data === 'string'
|
|
120
|
+
? event.data
|
|
121
|
+
: decoder.decode(event.data as ArrayBuffer)
|
|
122
|
+
controller.enqueue(encoder.encode(text))
|
|
123
|
+
})
|
|
124
|
+
ws.addEventListener('close', () => controller.close())
|
|
125
|
+
ws.addEventListener('error', () =>
|
|
126
|
+
controller.error(new Error('WebSocket connection error')),
|
|
127
|
+
)
|
|
128
|
+
},
|
|
129
|
+
cancel() {
|
|
130
|
+
ws.close()
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const writable = new WritableStream<Uint8Array>({
|
|
135
|
+
write(chunk) {
|
|
136
|
+
ws.send(chunk)
|
|
137
|
+
},
|
|
138
|
+
close() {
|
|
139
|
+
ws.close()
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
return ndJsonStream(writable, readable)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface ConnectAcpWebSocketOptions {
|
|
147
|
+
headers?: Record<string, string>
|
|
148
|
+
signal?: AbortSignal
|
|
149
|
+
framing?: AcpMessageFraming
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface AcpWebSocketConnection {
|
|
153
|
+
stream: AcpJsonRpcStream
|
|
154
|
+
close: () => void
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Open a WebSocket to an in-sandbox ACP server and adapt it for
|
|
159
|
+
* {@link ClientSideConnection}.
|
|
160
|
+
*/
|
|
161
|
+
function openWebSocket(
|
|
162
|
+
url: string,
|
|
163
|
+
headers?: Record<string, string>,
|
|
164
|
+
): WebSocket {
|
|
165
|
+
if (headers === undefined) return new WebSocket(url)
|
|
166
|
+
// Node/ws accepts `{ headers }`; DOM lib constructor types omit this overload.
|
|
167
|
+
return Reflect.construct(WebSocket, [url, { headers }]) as WebSocket
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function connectAcpWebSocket(
|
|
171
|
+
url: string,
|
|
172
|
+
options: ConnectAcpWebSocketOptions = {},
|
|
173
|
+
): Promise<AcpWebSocketConnection> {
|
|
174
|
+
const ws = openWebSocket(url, options.headers)
|
|
175
|
+
await waitForWebSocketOpen(ws, options.signal)
|
|
176
|
+
|
|
177
|
+
const framing = options.framing ?? 'frame'
|
|
178
|
+
const stream =
|
|
179
|
+
framing === 'ndjson'
|
|
180
|
+
? webSocketNdjsonToAcpStream(ws)
|
|
181
|
+
: webSocketFrameToAcpStream(ws)
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
stream,
|
|
185
|
+
close: () => ws.close(),
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Convert an HTTP sandbox channel URL to a WebSocket base URL. */
|
|
190
|
+
export function httpChannelUrlToWsBase(channelUrl: string): string {
|
|
191
|
+
return channelUrl.replace(/^http/i, 'ws').replace(/\/$/, '')
|
|
192
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural subset of the Agent Client Protocol (ACP) types that harness
|
|
3
|
+
* adapters consume.
|
|
4
|
+
*
|
|
5
|
+
* Defined structurally (rather than imported from `@agentclientprotocol/sdk`)
|
|
6
|
+
* so the stream translator stays a pure, fixture-testable state machine.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type AcpContentBlock =
|
|
10
|
+
| { type: 'text'; text: string }
|
|
11
|
+
| { type: string; [key: string]: unknown }
|
|
12
|
+
|
|
13
|
+
export type AcpToolCallStatus =
|
|
14
|
+
| 'pending'
|
|
15
|
+
| 'in_progress'
|
|
16
|
+
| 'completed'
|
|
17
|
+
| 'failed'
|
|
18
|
+
|
|
19
|
+
export interface AcpToolCallUpdate {
|
|
20
|
+
toolCallId: string
|
|
21
|
+
title?: string | null
|
|
22
|
+
kind?: string | null
|
|
23
|
+
status?: AcpToolCallStatus | null
|
|
24
|
+
rawInput?: unknown
|
|
25
|
+
rawOutput?: unknown
|
|
26
|
+
content?: Array<{
|
|
27
|
+
type: string
|
|
28
|
+
content?: AcpContentBlock
|
|
29
|
+
[key: string]: unknown
|
|
30
|
+
}> | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type AcpSessionUpdate =
|
|
34
|
+
| { sessionUpdate: 'agent_message_chunk'; content: AcpContentBlock }
|
|
35
|
+
| { sessionUpdate: 'agent_thought_chunk'; content: AcpContentBlock }
|
|
36
|
+
| ({ sessionUpdate: 'tool_call' } & AcpToolCallUpdate)
|
|
37
|
+
| ({ sessionUpdate: 'tool_call_update' } & AcpToolCallUpdate)
|
|
38
|
+
| { sessionUpdate: 'plan'; entries: Array<unknown> }
|
|
39
|
+
| { sessionUpdate: 'available_commands_update' }
|
|
40
|
+
| { sessionUpdate: 'current_mode_update' }
|
|
41
|
+
| { sessionUpdate: 'user_message_chunk'; content: AcpContentBlock }
|
|
42
|
+
|
|
43
|
+
export type AcpStopReason =
|
|
44
|
+
| 'end_turn'
|
|
45
|
+
| 'max_tokens'
|
|
46
|
+
| 'max_turn_requests'
|
|
47
|
+
| 'refusal'
|
|
48
|
+
| 'cancelled'
|
|
49
|
+
| (string & {})
|
|
50
|
+
|
|
51
|
+
export interface AcpUsage {
|
|
52
|
+
inputTokens?: number | null
|
|
53
|
+
outputTokens?: number | null
|
|
54
|
+
totalTokens?: number | null
|
|
55
|
+
cachedReadTokens?: number | null
|
|
56
|
+
thoughtTokens?: number | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface AcpPermissionOption {
|
|
60
|
+
optionId: string
|
|
61
|
+
name: string
|
|
62
|
+
kind: 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AcpPermissionRequest {
|
|
66
|
+
sessionId: string
|
|
67
|
+
toolCall: AcpToolCallUpdate
|
|
68
|
+
options: Array<AcpPermissionOption>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type AcpPermissionOutcome =
|
|
72
|
+
| { outcome: 'cancelled' }
|
|
73
|
+
| { outcome: 'selected'; optionId: string }
|
|
74
|
+
|
|
75
|
+
export type AcpPermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions'
|
|
76
|
+
|
|
77
|
+
export type PermissionHandler = (
|
|
78
|
+
request: AcpPermissionRequest,
|
|
79
|
+
) => Promise<AcpPermissionOutcome> | AcpPermissionOutcome
|