@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,320 @@
|
|
|
1
|
+
import { EventType, buildBaseUsage } from "@tanstack/ai";
|
|
2
|
+
const BRIDGED_MCP_SERVER_NAME = "tanstack";
|
|
3
|
+
function matchBridgedToolName(title, bridgedToolNames) {
|
|
4
|
+
if (!title || !bridgedToolNames) return void 0;
|
|
5
|
+
if (bridgedToolNames.has(title)) return title;
|
|
6
|
+
for (const name of bridgedToolNames) {
|
|
7
|
+
if (title.startsWith(`${name} (`)) return name;
|
|
8
|
+
}
|
|
9
|
+
return void 0;
|
|
10
|
+
}
|
|
11
|
+
function resolveToolName(update, bridgedToolNames) {
|
|
12
|
+
return matchBridgedToolName(update.title, bridgedToolNames) ?? update.kind ?? "tool";
|
|
13
|
+
}
|
|
14
|
+
function stringifyToolOutput(update) {
|
|
15
|
+
if (update.rawOutput !== void 0) {
|
|
16
|
+
return typeof update.rawOutput === "string" ? update.rawOutput : JSON.stringify(update.rawOutput);
|
|
17
|
+
}
|
|
18
|
+
const blocks = update.content ?? [];
|
|
19
|
+
const text = blocks.map(
|
|
20
|
+
(block) => block.content && typeof block.content.text === "string" ? block.content.text : ""
|
|
21
|
+
).join("");
|
|
22
|
+
const hasNonText = blocks.some(
|
|
23
|
+
(block) => block.type !== "content" || block.content !== void 0 && block.content.type !== "text"
|
|
24
|
+
);
|
|
25
|
+
if (hasNonText) return JSON.stringify(blocks);
|
|
26
|
+
if (text !== "") return text;
|
|
27
|
+
return JSON.stringify({ status: update.status ?? "completed" });
|
|
28
|
+
}
|
|
29
|
+
function buildUsage(usage) {
|
|
30
|
+
if (!usage) return void 0;
|
|
31
|
+
const promptTokens = usage.inputTokens ?? 0;
|
|
32
|
+
const completionTokens = usage.outputTokens ?? 0;
|
|
33
|
+
const result = buildBaseUsage({
|
|
34
|
+
promptTokens,
|
|
35
|
+
completionTokens,
|
|
36
|
+
totalTokens: usage.totalTokens ?? promptTokens + completionTokens
|
|
37
|
+
});
|
|
38
|
+
if (usage.cachedReadTokens) {
|
|
39
|
+
result.promptTokensDetails = { cachedTokens: usage.cachedReadTokens };
|
|
40
|
+
}
|
|
41
|
+
if (usage.thoughtTokens) {
|
|
42
|
+
result.completionTokensDetails = { reasoningTokens: usage.thoughtTokens };
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
async function* translateAcpStream(events, ctx) {
|
|
47
|
+
const { model, runId, threadId, genId, labels } = ctx;
|
|
48
|
+
const now = () => Date.now();
|
|
49
|
+
let runStarted = false;
|
|
50
|
+
const unresolvedToolCalls = /* @__PURE__ */ new Set();
|
|
51
|
+
const knownToolCalls = /* @__PURE__ */ new Set();
|
|
52
|
+
let textMessageId = null;
|
|
53
|
+
let textContent = "";
|
|
54
|
+
let reasoningId = null;
|
|
55
|
+
function* startRun() {
|
|
56
|
+
if (runStarted) return;
|
|
57
|
+
runStarted = true;
|
|
58
|
+
yield {
|
|
59
|
+
type: EventType.RUN_STARTED,
|
|
60
|
+
runId,
|
|
61
|
+
threadId,
|
|
62
|
+
model,
|
|
63
|
+
timestamp: now(),
|
|
64
|
+
...ctx.parentRunId !== void 0 && { parentRunId: ctx.parentRunId }
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function* closeText() {
|
|
68
|
+
if (textMessageId !== null) {
|
|
69
|
+
yield {
|
|
70
|
+
type: EventType.TEXT_MESSAGE_END,
|
|
71
|
+
messageId: textMessageId,
|
|
72
|
+
model,
|
|
73
|
+
timestamp: now()
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
textMessageId = null;
|
|
77
|
+
textContent = "";
|
|
78
|
+
}
|
|
79
|
+
function* closeReasoning() {
|
|
80
|
+
if (reasoningId !== null) {
|
|
81
|
+
yield {
|
|
82
|
+
type: EventType.REASONING_MESSAGE_END,
|
|
83
|
+
messageId: reasoningId,
|
|
84
|
+
model,
|
|
85
|
+
timestamp: now()
|
|
86
|
+
};
|
|
87
|
+
yield {
|
|
88
|
+
type: EventType.REASONING_END,
|
|
89
|
+
messageId: reasoningId,
|
|
90
|
+
model,
|
|
91
|
+
timestamp: now()
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
reasoningId = null;
|
|
95
|
+
}
|
|
96
|
+
function* synthesizeUnresolvedResults() {
|
|
97
|
+
for (const toolCallId of unresolvedToolCalls) {
|
|
98
|
+
yield {
|
|
99
|
+
type: EventType.TOOL_CALL_RESULT,
|
|
100
|
+
toolCallId,
|
|
101
|
+
messageId: genId(),
|
|
102
|
+
model,
|
|
103
|
+
timestamp: now(),
|
|
104
|
+
content: JSON.stringify({ status: "interrupted" })
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
unresolvedToolCalls.clear();
|
|
108
|
+
}
|
|
109
|
+
function* openToolCall(update) {
|
|
110
|
+
if (knownToolCalls.has(update.toolCallId)) return;
|
|
111
|
+
knownToolCalls.add(update.toolCallId);
|
|
112
|
+
const toolCallName = resolveToolName(update, ctx.bridgedToolNames);
|
|
113
|
+
const input = {
|
|
114
|
+
...update.title != null && { title: update.title },
|
|
115
|
+
...update.rawInput !== void 0 && update.rawInput !== null ? typeof update.rawInput === "object" ? update.rawInput : { input: update.rawInput } : {}
|
|
116
|
+
};
|
|
117
|
+
const args = JSON.stringify(input);
|
|
118
|
+
yield {
|
|
119
|
+
type: EventType.TOOL_CALL_START,
|
|
120
|
+
toolCallId: update.toolCallId,
|
|
121
|
+
toolCallName,
|
|
122
|
+
toolName: toolCallName,
|
|
123
|
+
model,
|
|
124
|
+
timestamp: now()
|
|
125
|
+
};
|
|
126
|
+
yield {
|
|
127
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
128
|
+
toolCallId: update.toolCallId,
|
|
129
|
+
model,
|
|
130
|
+
timestamp: now(),
|
|
131
|
+
delta: args,
|
|
132
|
+
args
|
|
133
|
+
};
|
|
134
|
+
yield {
|
|
135
|
+
type: EventType.TOOL_CALL_END,
|
|
136
|
+
toolCallId: update.toolCallId,
|
|
137
|
+
toolCallName,
|
|
138
|
+
toolName: toolCallName,
|
|
139
|
+
model,
|
|
140
|
+
timestamp: now(),
|
|
141
|
+
input
|
|
142
|
+
};
|
|
143
|
+
unresolvedToolCalls.add(update.toolCallId);
|
|
144
|
+
}
|
|
145
|
+
function* resolveToolCall(update) {
|
|
146
|
+
yield* openToolCall(update);
|
|
147
|
+
unresolvedToolCalls.delete(update.toolCallId);
|
|
148
|
+
yield {
|
|
149
|
+
type: EventType.TOOL_CALL_RESULT,
|
|
150
|
+
toolCallId: update.toolCallId,
|
|
151
|
+
messageId: genId(),
|
|
152
|
+
model,
|
|
153
|
+
timestamp: now(),
|
|
154
|
+
content: stringifyToolOutput(update),
|
|
155
|
+
...update.status === "failed" && { state: "output-error" }
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function* handleUpdate(update) {
|
|
159
|
+
if (update.sessionUpdate === "agent_message_chunk") {
|
|
160
|
+
yield* closeReasoning();
|
|
161
|
+
if (update.content.type !== "text") {
|
|
162
|
+
if (labels.contentEvent !== void 0) {
|
|
163
|
+
yield* closeText();
|
|
164
|
+
yield {
|
|
165
|
+
type: EventType.CUSTOM,
|
|
166
|
+
model,
|
|
167
|
+
timestamp: now(),
|
|
168
|
+
name: labels.contentEvent,
|
|
169
|
+
value: { content: update.content }
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const text = typeof update.content.text === "string" ? update.content.text : "";
|
|
175
|
+
if (text === "") return;
|
|
176
|
+
if (textMessageId === null) {
|
|
177
|
+
textMessageId = genId();
|
|
178
|
+
yield {
|
|
179
|
+
type: EventType.TEXT_MESSAGE_START,
|
|
180
|
+
messageId: textMessageId,
|
|
181
|
+
model,
|
|
182
|
+
timestamp: now(),
|
|
183
|
+
role: "assistant"
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
textContent += text;
|
|
187
|
+
yield {
|
|
188
|
+
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
189
|
+
messageId: textMessageId,
|
|
190
|
+
model,
|
|
191
|
+
timestamp: now(),
|
|
192
|
+
delta: text,
|
|
193
|
+
content: textContent
|
|
194
|
+
};
|
|
195
|
+
} else if (update.sessionUpdate === "agent_thought_chunk") {
|
|
196
|
+
yield* closeText();
|
|
197
|
+
const thought = typeof update.content.text === "string" ? update.content.text : "";
|
|
198
|
+
if (thought === "") return;
|
|
199
|
+
if (reasoningId === null) {
|
|
200
|
+
reasoningId = genId();
|
|
201
|
+
yield {
|
|
202
|
+
type: EventType.REASONING_START,
|
|
203
|
+
messageId: reasoningId,
|
|
204
|
+
model,
|
|
205
|
+
timestamp: now()
|
|
206
|
+
};
|
|
207
|
+
yield {
|
|
208
|
+
type: EventType.REASONING_MESSAGE_START,
|
|
209
|
+
messageId: reasoningId,
|
|
210
|
+
role: "reasoning",
|
|
211
|
+
model,
|
|
212
|
+
timestamp: now()
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
yield {
|
|
216
|
+
type: EventType.REASONING_MESSAGE_CONTENT,
|
|
217
|
+
messageId: reasoningId,
|
|
218
|
+
delta: thought,
|
|
219
|
+
model,
|
|
220
|
+
timestamp: now()
|
|
221
|
+
};
|
|
222
|
+
} else if (update.sessionUpdate === "tool_call") {
|
|
223
|
+
yield* closeText();
|
|
224
|
+
yield* closeReasoning();
|
|
225
|
+
yield* openToolCall(update);
|
|
226
|
+
if (update.status === "completed" || update.status === "failed") {
|
|
227
|
+
yield* resolveToolCall(update);
|
|
228
|
+
}
|
|
229
|
+
} else if (update.sessionUpdate === "tool_call_update") {
|
|
230
|
+
if (update.status === "completed" || update.status === "failed") {
|
|
231
|
+
yield* resolveToolCall(update);
|
|
232
|
+
} else if (update.status === "in_progress" && update.rawInput !== void 0) {
|
|
233
|
+
yield* closeText();
|
|
234
|
+
yield* closeReasoning();
|
|
235
|
+
if (!knownToolCalls.has(update.toolCallId)) {
|
|
236
|
+
yield* openToolCall(update);
|
|
237
|
+
} else {
|
|
238
|
+
const input = {
|
|
239
|
+
...update.title != null && { title: update.title },
|
|
240
|
+
...typeof update.rawInput === "object" && update.rawInput !== null ? update.rawInput : { input: update.rawInput }
|
|
241
|
+
};
|
|
242
|
+
const args = JSON.stringify(input);
|
|
243
|
+
yield {
|
|
244
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
245
|
+
toolCallId: update.toolCallId,
|
|
246
|
+
model,
|
|
247
|
+
timestamp: now(),
|
|
248
|
+
delta: args,
|
|
249
|
+
args
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} else if (update.sessionUpdate === "plan" && labels.planEvent !== void 0) {
|
|
254
|
+
yield {
|
|
255
|
+
type: EventType.CUSTOM,
|
|
256
|
+
model,
|
|
257
|
+
timestamp: now(),
|
|
258
|
+
name: labels.planEvent,
|
|
259
|
+
value: { entries: update.entries }
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
for await (const event of events) {
|
|
265
|
+
ctx.onAcpEvent?.(event);
|
|
266
|
+
if (event.kind === "session") {
|
|
267
|
+
yield* startRun();
|
|
268
|
+
yield {
|
|
269
|
+
type: EventType.CUSTOM,
|
|
270
|
+
model,
|
|
271
|
+
timestamp: now(),
|
|
272
|
+
name: labels.sessionIdEvent,
|
|
273
|
+
value: { sessionId: event.sessionId }
|
|
274
|
+
};
|
|
275
|
+
} else if (event.kind === "update") {
|
|
276
|
+
yield* startRun();
|
|
277
|
+
yield* handleUpdate(event.update);
|
|
278
|
+
} else {
|
|
279
|
+
yield* startRun();
|
|
280
|
+
yield* closeText();
|
|
281
|
+
yield* closeReasoning();
|
|
282
|
+
yield* synthesizeUnresolvedResults();
|
|
283
|
+
if (event.stopReason === "refusal") {
|
|
284
|
+
const message = labels.refusalMessage ?? "The harness refused the request.";
|
|
285
|
+
yield {
|
|
286
|
+
type: EventType.RUN_ERROR,
|
|
287
|
+
model,
|
|
288
|
+
timestamp: now(),
|
|
289
|
+
message,
|
|
290
|
+
code: "refusal",
|
|
291
|
+
error: { message, code: "refusal" }
|
|
292
|
+
};
|
|
293
|
+
} else {
|
|
294
|
+
const usage = buildUsage(event.usage);
|
|
295
|
+
const finishReason = event.stopReason === "max_tokens" || event.stopReason === "max_turn_requests" ? "length" : "stop";
|
|
296
|
+
yield {
|
|
297
|
+
type: EventType.RUN_FINISHED,
|
|
298
|
+
runId,
|
|
299
|
+
threadId,
|
|
300
|
+
model,
|
|
301
|
+
timestamp: now(),
|
|
302
|
+
finishReason,
|
|
303
|
+
...usage !== void 0 && { usage }
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch (error) {
|
|
309
|
+
yield* closeText();
|
|
310
|
+
yield* closeReasoning();
|
|
311
|
+
yield* synthesizeUnresolvedResults();
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export {
|
|
316
|
+
BRIDGED_MCP_SERVER_NAME,
|
|
317
|
+
matchBridgedToolName,
|
|
318
|
+
translateAcpStream
|
|
319
|
+
};
|
|
320
|
+
//# sourceMappingURL=translate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate.js","sources":["../../../src/stream/translate.ts"],"sourcesContent":["import { EventType, buildBaseUsage } from '@tanstack/ai'\nimport type { StreamChunk, TokenUsage } from '@tanstack/ai'\nimport type {\n AcpSessionUpdate,\n AcpStopReason,\n AcpToolCallUpdate,\n AcpUsage,\n} from '../types/acp-types'\n\nexport const BRIDGED_MCP_SERVER_NAME = 'tanstack'\n\nexport type AcpStreamEvent =\n | { kind: 'session'; sessionId: string }\n | { kind: 'update'; update: AcpSessionUpdate }\n | { kind: 'done'; stopReason: AcpStopReason; usage?: AcpUsage }\n\nexport interface AcpTranslateLabels {\n sessionIdEvent: string\n planEvent?: string\n refusalMessage?: string\n /**\n * When set, non-text agent message content (image / audio / resource /\n * resource_link blocks) is surfaced as a CUSTOM event under this name instead\n * of being dropped. Each event's `value` is `{ content: <ACP content block> }`.\n * Omit to keep the text-only behavior.\n */\n contentEvent?: string\n}\n\nexport interface TranslateContext {\n model: string\n runId: string\n threadId: string\n parentRunId?: string\n genId: () => string\n labels: AcpTranslateLabels\n bridgedToolNames?: ReadonlySet<string>\n onAcpEvent?: (event: AcpStreamEvent) => void\n}\n\nexport function matchBridgedToolName(\n title: string | null | undefined,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): string | undefined {\n if (!title || !bridgedToolNames) return undefined\n if (bridgedToolNames.has(title)) return title\n for (const name of bridgedToolNames) {\n if (title.startsWith(`${name} (`)) return name\n }\n return undefined\n}\n\nfunction resolveToolName(\n update: AcpToolCallUpdate,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): string {\n return (\n matchBridgedToolName(update.title, bridgedToolNames) ??\n update.kind ??\n 'tool'\n )\n}\n\nfunction stringifyToolOutput(update: AcpToolCallUpdate): string {\n if (update.rawOutput !== undefined) {\n return typeof update.rawOutput === 'string'\n ? update.rawOutput\n : JSON.stringify(update.rawOutput)\n }\n const blocks = update.content ?? []\n const text = blocks\n .map((block) =>\n block.content && typeof block.content.text === 'string'\n ? block.content.text\n : '',\n )\n .join('')\n // Preserve non-text tool content (diff / terminal / image / resource blocks)\n // by serializing the structured array, rather than collapsing to a stub.\n const hasNonText = blocks.some(\n (block) =>\n block.type !== 'content' ||\n (block.content !== undefined && block.content.type !== 'text'),\n )\n if (hasNonText) return JSON.stringify(blocks)\n if (text !== '') return text\n return JSON.stringify({ status: update.status ?? 'completed' })\n}\n\nfunction buildUsage(usage: AcpUsage | undefined): TokenUsage | undefined {\n if (!usage) return undefined\n const promptTokens = usage.inputTokens ?? 0\n const completionTokens = usage.outputTokens ?? 0\n const result = buildBaseUsage({\n promptTokens,\n completionTokens,\n totalTokens: usage.totalTokens ?? promptTokens + completionTokens,\n })\n if (usage.cachedReadTokens) {\n result.promptTokensDetails = { cachedTokens: usage.cachedReadTokens }\n }\n if (usage.thoughtTokens) {\n result.completionTokensDetails = { reasoningTokens: usage.thoughtTokens }\n }\n return result\n}\n\nexport async function* translateAcpStream(\n events: AsyncIterable<AcpStreamEvent>,\n ctx: TranslateContext,\n): AsyncIterable<StreamChunk> {\n const { model, runId, threadId, genId, labels } = ctx\n const now = () => Date.now()\n\n let runStarted = false\n const unresolvedToolCalls = new Set<string>()\n const knownToolCalls = new Set<string>()\n\n let textMessageId: string | null = null\n let textContent = ''\n let reasoningId: string | null = null\n\n function* startRun(): Generator<StreamChunk> {\n if (runStarted) return\n runStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: now(),\n ...(ctx.parentRunId !== undefined && { parentRunId: ctx.parentRunId }),\n }\n }\n\n function* closeText(): Generator<StreamChunk> {\n if (textMessageId !== null) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: textMessageId,\n model,\n timestamp: now(),\n }\n }\n textMessageId = null\n textContent = ''\n }\n\n function* closeReasoning(): Generator<StreamChunk> {\n if (reasoningId !== null) {\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n }\n reasoningId = null\n }\n\n function* synthesizeUnresolvedResults(): Generator<StreamChunk> {\n for (const toolCallId of unresolvedToolCalls) {\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId,\n messageId: genId(),\n model,\n timestamp: now(),\n content: JSON.stringify({ status: 'interrupted' }),\n }\n }\n unresolvedToolCalls.clear()\n }\n\n function* openToolCall(update: AcpToolCallUpdate): Generator<StreamChunk> {\n if (knownToolCalls.has(update.toolCallId)) return\n knownToolCalls.add(update.toolCallId)\n const toolCallName = resolveToolName(update, ctx.bridgedToolNames)\n const input = {\n ...(update.title != null && { title: update.title }),\n ...(update.rawInput !== undefined && update.rawInput !== null\n ? typeof update.rawInput === 'object'\n ? (update.rawInput as Record<string, unknown>)\n : { input: update.rawInput }\n : {}),\n }\n const args = JSON.stringify(input)\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId: update.toolCallId,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: update.toolCallId,\n model,\n timestamp: now(),\n delta: args,\n args,\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: update.toolCallId,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n input,\n }\n unresolvedToolCalls.add(update.toolCallId)\n }\n\n function* resolveToolCall(update: AcpToolCallUpdate): Generator<StreamChunk> {\n yield* openToolCall(update)\n unresolvedToolCalls.delete(update.toolCallId)\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId: update.toolCallId,\n messageId: genId(),\n model,\n timestamp: now(),\n content: stringifyToolOutput(update),\n ...(update.status === 'failed' && { state: 'output-error' as const }),\n }\n }\n\n function* handleUpdate(update: AcpSessionUpdate): Generator<StreamChunk> {\n if (update.sessionUpdate === 'agent_message_chunk') {\n yield* closeReasoning()\n // Non-text content (image / audio / resource / resource_link): surface it\n // as a CUSTOM event when the harness opted in, instead of dropping it.\n if (update.content.type !== 'text') {\n if (labels.contentEvent !== undefined) {\n yield* closeText()\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: labels.contentEvent,\n value: { content: update.content },\n }\n }\n return\n }\n const text =\n typeof update.content.text === 'string' ? update.content.text : ''\n if (text === '') return\n if (textMessageId === null) {\n textMessageId = genId()\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: textMessageId,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n }\n textContent += text\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: textMessageId,\n model,\n timestamp: now(),\n delta: text,\n content: textContent,\n }\n } else if (update.sessionUpdate === 'agent_thought_chunk') {\n yield* closeText()\n const thought =\n typeof update.content.text === 'string' ? update.content.text : ''\n if (thought === '') return\n if (reasoningId === null) {\n reasoningId = genId()\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningId,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n }\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningId,\n delta: thought,\n model,\n timestamp: now(),\n }\n } else if (update.sessionUpdate === 'tool_call') {\n yield* closeText()\n yield* closeReasoning()\n yield* openToolCall(update)\n if (update.status === 'completed' || update.status === 'failed') {\n yield* resolveToolCall(update)\n }\n } else if (update.sessionUpdate === 'tool_call_update') {\n if (update.status === 'completed' || update.status === 'failed') {\n yield* resolveToolCall(update)\n } else if (\n update.status === 'in_progress' &&\n update.rawInput !== undefined\n ) {\n yield* closeText()\n yield* closeReasoning()\n if (!knownToolCalls.has(update.toolCallId)) {\n yield* openToolCall(update)\n } else {\n const input = {\n ...(update.title != null && { title: update.title }),\n ...(typeof update.rawInput === 'object' && update.rawInput !== null\n ? (update.rawInput as Record<string, unknown>)\n : { input: update.rawInput }),\n }\n const args = JSON.stringify(input)\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: update.toolCallId,\n model,\n timestamp: now(),\n delta: args,\n args,\n }\n }\n }\n } else if (\n update.sessionUpdate === 'plan' &&\n labels.planEvent !== undefined\n ) {\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: labels.planEvent,\n value: { entries: update.entries },\n }\n }\n }\n\n try {\n for await (const event of events) {\n ctx.onAcpEvent?.(event)\n\n if (event.kind === 'session') {\n yield* startRun()\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: labels.sessionIdEvent,\n value: { sessionId: event.sessionId },\n }\n } else if (event.kind === 'update') {\n yield* startRun()\n yield* handleUpdate(event.update)\n } else {\n yield* startRun()\n yield* closeText()\n yield* closeReasoning()\n yield* synthesizeUnresolvedResults()\n\n if (event.stopReason === 'refusal') {\n const message =\n labels.refusalMessage ?? 'The harness refused the request.'\n yield {\n type: EventType.RUN_ERROR,\n model,\n timestamp: now(),\n message,\n code: 'refusal',\n error: { message, code: 'refusal' },\n }\n } else {\n const usage = buildUsage(event.usage)\n const finishReason =\n event.stopReason === 'max_tokens' ||\n event.stopReason === 'max_turn_requests'\n ? ('length' as const)\n : ('stop' as const)\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason,\n ...(usage !== undefined && { usage }),\n }\n }\n }\n }\n } catch (error) {\n yield* closeText()\n yield* closeReasoning()\n yield* synthesizeUnresolvedResults()\n throw error\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SandboxHandle } from '@tanstack/ai-sandbox';
|
|
2
|
+
import { AcpTransportPreference } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Pick stdio vs WebSocket for ACP based on sandbox capabilities and caller
|
|
5
|
+
* preference.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveAcpTransportMode(sandbox: SandboxHandle, preference?: AcpTransportPreference): 'stdio' | 'websocket';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function resolveAcpTransportMode(sandbox, preference = "auto") {
|
|
2
|
+
if (preference === "stdio") {
|
|
3
|
+
if (sandbox.capabilities.writableStdin !== true) {
|
|
4
|
+
throw new Error(
|
|
5
|
+
`${sandbox.provider}: ACP stdio transport requires capabilities.writableStdin, but this sandbox cannot write to process stdin. Use transport: 'websocket' or 'auto'.`
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
return "stdio";
|
|
9
|
+
}
|
|
10
|
+
if (preference === "websocket") {
|
|
11
|
+
if (sandbox.capabilities.ports !== true) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`${sandbox.provider}: ACP WebSocket transport requires capabilities.ports so the harness server can be reached from the orchestrator.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
if (sandbox.capabilities.backgroundProcesses !== true) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`${sandbox.provider}: ACP WebSocket transport requires capabilities.backgroundProcesses to run the in-sandbox harness server.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return "websocket";
|
|
22
|
+
}
|
|
23
|
+
if (sandbox.capabilities.writableStdin === true) return "stdio";
|
|
24
|
+
if (sandbox.capabilities.ports === true && sandbox.capabilities.backgroundProcesses === true) {
|
|
25
|
+
return "websocket";
|
|
26
|
+
}
|
|
27
|
+
throw new Error(
|
|
28
|
+
`${sandbox.provider}: cannot run ACP — no writable stdin and no port-exposed background-process support. Use a Docker/local-process sandbox or enable ports on the provider.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
resolveAcpTransportMode
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sources":["../../../src/transport/resolve.ts"],"sourcesContent":["import type { SandboxHandle } from '@tanstack/ai-sandbox'\nimport type { AcpTransportPreference } from './types'\n\n/**\n * Pick stdio vs WebSocket for ACP based on sandbox capabilities and caller\n * preference.\n */\nexport function resolveAcpTransportMode(\n sandbox: SandboxHandle,\n preference: AcpTransportPreference = 'auto',\n): 'stdio' | 'websocket' {\n if (preference === 'stdio') {\n if (sandbox.capabilities.writableStdin !== true) {\n throw new Error(\n `${sandbox.provider}: ACP stdio transport requires capabilities.writableStdin, but this sandbox cannot write to process stdin. Use transport: 'websocket' or 'auto'.`,\n )\n }\n return 'stdio'\n }\n\n if (preference === 'websocket') {\n if (sandbox.capabilities.ports !== true) {\n throw new Error(\n `${sandbox.provider}: ACP WebSocket transport requires capabilities.ports so the harness server can be reached from the orchestrator.`,\n )\n }\n if (sandbox.capabilities.backgroundProcesses !== true) {\n throw new Error(\n `${sandbox.provider}: ACP WebSocket transport requires capabilities.backgroundProcesses to run the in-sandbox harness server.`,\n )\n }\n return 'websocket'\n }\n\n if (sandbox.capabilities.writableStdin === true) return 'stdio'\n if (\n sandbox.capabilities.ports === true &&\n sandbox.capabilities.backgroundProcesses === true\n ) {\n return 'websocket'\n }\n\n throw new Error(\n `${sandbox.provider}: cannot run ACP — no writable stdin and no port-exposed background-process support. Use a Docker/local-process sandbox or enable ports on the provider.`,\n )\n}\n"],"names":[],"mappings":"AAOO,SAAS,wBACd,SACA,aAAqC,QACd;AACvB,MAAI,eAAe,SAAS;AAC1B,QAAI,QAAQ,aAAa,kBAAkB,MAAM;AAC/C,YAAM,IAAI;AAAA,QACR,GAAG,QAAQ,QAAQ;AAAA,MAAA;AAAA,IAEvB;AACA,WAAO;AAAA,EACT;AAEA,MAAI,eAAe,aAAa;AAC9B,QAAI,QAAQ,aAAa,UAAU,MAAM;AACvC,YAAM,IAAI;AAAA,QACR,GAAG,QAAQ,QAAQ;AAAA,MAAA;AAAA,IAEvB;AACA,QAAI,QAAQ,aAAa,wBAAwB,MAAM;AACrD,YAAM,IAAI;AAAA,QACR,GAAG,QAAQ,QAAQ;AAAA,MAAA;AAAA,IAEvB;AACA,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,aAAa,kBAAkB,KAAM,QAAO;AACxD,MACE,QAAQ,aAAa,UAAU,QAC/B,QAAQ,aAAa,wBAAwB,MAC7C;AACA,WAAO;AAAA,EACT;AAEA,QAAM,IAAI;AAAA,IACR,GAAG,QAAQ,QAAQ;AAAA,EAAA;AAEvB;"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
function spawnHandleToAcpTransport(handle) {
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
const decoder = new TextDecoder();
|
|
4
|
+
const readable = new ReadableStream({
|
|
5
|
+
async start(controller) {
|
|
6
|
+
try {
|
|
7
|
+
for await (const chunk of handle.stdout) {
|
|
8
|
+
controller.enqueue(encoder.encode(chunk));
|
|
9
|
+
}
|
|
10
|
+
controller.close();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
controller.error(error);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
const writable = new WritableStream({
|
|
17
|
+
write(chunk) {
|
|
18
|
+
return handle.stdin.write(decoder.decode(chunk));
|
|
19
|
+
},
|
|
20
|
+
close() {
|
|
21
|
+
return handle.stdin.end();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
let tail = "";
|
|
25
|
+
void (async () => {
|
|
26
|
+
try {
|
|
27
|
+
for await (const chunk of handle.stderr) {
|
|
28
|
+
tail = (tail + chunk).slice(-4096);
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
const exited = handle.wait().then((code) => {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`ACP harness process exited unexpectedly (code ${code}).${tail.trim() !== "" ? `
|
|
36
|
+
stderr: ${tail.trim()}` : ""}`
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
void exited.catch(() => void 0);
|
|
40
|
+
return {
|
|
41
|
+
writable,
|
|
42
|
+
readable,
|
|
43
|
+
exited,
|
|
44
|
+
stderrTail: () => tail,
|
|
45
|
+
kill: () => handle.kill()
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
spawnHandleToAcpTransport
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=stdio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.js","sources":["../../../src/transport/stdio.ts"],"sourcesContent":["/**\n * Adapt a sandbox {@link SpawnHandle} into byte streams for ACP stdio\n * (newline-delimited JSON-RPC).\n */\nimport type { SpawnHandle } from '@tanstack/ai-sandbox'\nimport type { AcpByteTransport } from './types'\n\nexport function spawnHandleToAcpTransport(\n handle: SpawnHandle,\n): AcpByteTransport {\n const encoder = new TextEncoder()\n const decoder = new TextDecoder()\n\n const readable = new ReadableStream<Uint8Array>({\n async start(controller) {\n try {\n for await (const chunk of handle.stdout) {\n controller.enqueue(encoder.encode(chunk))\n }\n controller.close()\n } catch (error) {\n controller.error(error)\n }\n },\n })\n\n const writable = new WritableStream<Uint8Array>({\n write(chunk) {\n return handle.stdin.write(decoder.decode(chunk))\n },\n close() {\n return handle.stdin.end()\n },\n })\n\n let tail = ''\n void (async () => {\n try {\n for await (const chunk of handle.stderr) {\n tail = (tail + chunk).slice(-4096)\n }\n } catch {\n // stderr closed\n }\n })()\n\n const exited: Promise<never> = handle.wait().then((code) => {\n throw new Error(\n `ACP harness process exited unexpectedly (code ${code}).${\n tail.trim() !== '' ? `\\nstderr: ${tail.trim()}` : ''\n }`,\n )\n })\n void exited.catch(() => undefined)\n\n return {\n writable,\n readable,\n exited,\n stderrTail: () => tail,\n kill: () => handle.kill(),\n }\n}\n"],"names":[],"mappings":"AAOO,SAAS,0BACd,QACkB;AAClB,QAAM,UAAU,IAAI,YAAA;AACpB,QAAM,UAAU,IAAI,YAAA;AAEpB,QAAM,WAAW,IAAI,eAA2B;AAAA,IAC9C,MAAM,MAAM,YAAY;AACtB,UAAI;AACF,yBAAiB,SAAS,OAAO,QAAQ;AACvC,qBAAW,QAAQ,QAAQ,OAAO,KAAK,CAAC;AAAA,QAC1C;AACA,mBAAW,MAAA;AAAA,MACb,SAAS,OAAO;AACd,mBAAW,MAAM,KAAK;AAAA,MACxB;AAAA,IACF;AAAA,EAAA,CACD;AAED,QAAM,WAAW,IAAI,eAA2B;AAAA,IAC9C,MAAM,OAAO;AACX,aAAO,OAAO,MAAM,MAAM,QAAQ,OAAO,KAAK,CAAC;AAAA,IACjD;AAAA,IACA,QAAQ;AACN,aAAO,OAAO,MAAM,IAAA;AAAA,IACtB;AAAA,EAAA,CACD;AAED,MAAI,OAAO;AACX,QAAM,YAAY;AAChB,QAAI;AACF,uBAAiB,SAAS,OAAO,QAAQ;AACvC,gBAAQ,OAAO,OAAO,MAAM,KAAK;AAAA,MACnC;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF,GAAA;AAEA,QAAM,SAAyB,OAAO,KAAA,EAAO,KAAK,CAAC,SAAS;AAC1D,UAAM,IAAI;AAAA,MACR,iDAAiD,IAAI,KACnD,KAAK,KAAA,MAAW,KAAK;AAAA,UAAa,KAAK,MAAM,KAAK,EACpD;AAAA,IAAA;AAAA,EAEJ,CAAC;AACD,OAAK,OAAO,MAAM,MAAM,MAAS;AAEjC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY,MAAM;AAAA,IAClB,MAAM,MAAM,OAAO,KAAA;AAAA,EAAK;AAE5B;"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ndJsonStream } from '@agentclientprotocol/sdk';
|
|
2
|
+
import { SpawnHandle } from '@tanstack/ai-sandbox';
|
|
3
|
+
/** How JSON-RPC messages are framed on the wire. */
|
|
4
|
+
export type AcpMessageFraming = 'ndjson' | 'frame';
|
|
5
|
+
/**
|
|
6
|
+
* Byte-level duplex transport consumed by {@link ndJsonStream} for stdio
|
|
7
|
+
* harnesses.
|
|
8
|
+
*/
|
|
9
|
+
export interface AcpByteTransport {
|
|
10
|
+
writable: WritableStream<Uint8Array>;
|
|
11
|
+
readable: ReadableStream<Uint8Array>;
|
|
12
|
+
/** Resolves (throws) when the underlying connection exits unexpectedly. */
|
|
13
|
+
exited: Promise<never>;
|
|
14
|
+
/** Last bytes of stderr, for error messages. */
|
|
15
|
+
stderrTail: () => string;
|
|
16
|
+
kill: () => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/** Parsed JSON-RPC object streams passed to {@link ClientSideConnection}. */
|
|
19
|
+
export type AcpJsonRpcStream = ReturnType<typeof ndJsonStream>;
|
|
20
|
+
export type AcpSessionTransport = {
|
|
21
|
+
kind: 'stdio';
|
|
22
|
+
process: SpawnHandle;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'stream';
|
|
25
|
+
stream: AcpJsonRpcStream;
|
|
26
|
+
dispose: () => Promise<void>;
|
|
27
|
+
stderrTail?: () => string;
|
|
28
|
+
};
|
|
29
|
+
export type AcpTransportPreference = 'auto' | 'stdio' | 'websocket';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AcpJsonRpcStream, AcpMessageFraming } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* One JSON-RPC object per WebSocket text frame (e.g. `grok agent serve`).
|
|
4
|
+
*/
|
|
5
|
+
export declare function webSocketFrameToAcpStream(ws: WebSocket): AcpJsonRpcStream;
|
|
6
|
+
export interface ConnectAcpWebSocketOptions {
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
framing?: AcpMessageFraming;
|
|
10
|
+
}
|
|
11
|
+
export interface AcpWebSocketConnection {
|
|
12
|
+
stream: AcpJsonRpcStream;
|
|
13
|
+
close: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function connectAcpWebSocket(url: string, options?: ConnectAcpWebSocketOptions): Promise<AcpWebSocketConnection>;
|
|
16
|
+
/** Convert an HTTP sandbox channel URL to a WebSocket base URL. */
|
|
17
|
+
export declare function httpChannelUrlToWsBase(channelUrl: string): string;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { ndJsonStream } from "@agentclientprotocol/sdk";
|
|
2
|
+
function waitForWebSocketOpen(ws, signal) {
|
|
3
|
+
if (ws.readyState === WebSocket.OPEN) return Promise.resolve();
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const onOpen = () => {
|
|
6
|
+
cleanup();
|
|
7
|
+
resolve();
|
|
8
|
+
};
|
|
9
|
+
const onError = () => {
|
|
10
|
+
cleanup();
|
|
11
|
+
reject(new Error("WebSocket connection failed"));
|
|
12
|
+
};
|
|
13
|
+
const onAbort = () => {
|
|
14
|
+
cleanup();
|
|
15
|
+
ws.close();
|
|
16
|
+
reject(signal?.reason ?? new Error("WebSocket connection aborted"));
|
|
17
|
+
};
|
|
18
|
+
const cleanup = () => {
|
|
19
|
+
ws.removeEventListener("open", onOpen);
|
|
20
|
+
ws.removeEventListener("error", onError);
|
|
21
|
+
signal?.removeEventListener("abort", onAbort);
|
|
22
|
+
};
|
|
23
|
+
ws.addEventListener("open", onOpen);
|
|
24
|
+
ws.addEventListener("error", onError);
|
|
25
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function idempotentController(controller) {
|
|
29
|
+
let settled = false;
|
|
30
|
+
return {
|
|
31
|
+
enqueue: (chunk) => {
|
|
32
|
+
if (!settled) controller.enqueue(chunk);
|
|
33
|
+
},
|
|
34
|
+
close: () => {
|
|
35
|
+
if (settled) return;
|
|
36
|
+
settled = true;
|
|
37
|
+
controller.close();
|
|
38
|
+
},
|
|
39
|
+
error: (reason) => {
|
|
40
|
+
if (settled) return;
|
|
41
|
+
settled = true;
|
|
42
|
+
controller.error(reason);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function webSocketFrameToAcpStream(ws) {
|
|
47
|
+
const decoder = new TextDecoder();
|
|
48
|
+
const readable = new ReadableStream({
|
|
49
|
+
start(rawController) {
|
|
50
|
+
const controller = idempotentController(rawController);
|
|
51
|
+
ws.addEventListener("message", (event) => {
|
|
52
|
+
const text = typeof event.data === "string" ? event.data : decoder.decode(event.data);
|
|
53
|
+
const trimmed = text.trim();
|
|
54
|
+
if (trimmed === "") return;
|
|
55
|
+
try {
|
|
56
|
+
controller.enqueue(JSON.parse(trimmed));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
controller.error(
|
|
59
|
+
error instanceof Error ? error : new Error(String(error))
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
ws.addEventListener("close", () => controller.close());
|
|
64
|
+
ws.addEventListener(
|
|
65
|
+
"error",
|
|
66
|
+
() => controller.error(new Error("WebSocket connection error"))
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
cancel() {
|
|
70
|
+
ws.close();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const writable = new WritableStream({
|
|
74
|
+
write(message) {
|
|
75
|
+
ws.send(JSON.stringify(message));
|
|
76
|
+
},
|
|
77
|
+
close() {
|
|
78
|
+
ws.close();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return { readable, writable };
|
|
82
|
+
}
|
|
83
|
+
function webSocketNdjsonToAcpStream(ws) {
|
|
84
|
+
const encoder = new TextEncoder();
|
|
85
|
+
const decoder = new TextDecoder();
|
|
86
|
+
const readable = new ReadableStream({
|
|
87
|
+
start(rawController) {
|
|
88
|
+
const controller = idempotentController(rawController);
|
|
89
|
+
ws.addEventListener("message", (event) => {
|
|
90
|
+
const text = typeof event.data === "string" ? event.data : decoder.decode(event.data);
|
|
91
|
+
controller.enqueue(encoder.encode(text));
|
|
92
|
+
});
|
|
93
|
+
ws.addEventListener("close", () => controller.close());
|
|
94
|
+
ws.addEventListener(
|
|
95
|
+
"error",
|
|
96
|
+
() => controller.error(new Error("WebSocket connection error"))
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
cancel() {
|
|
100
|
+
ws.close();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const writable = new WritableStream({
|
|
104
|
+
write(chunk) {
|
|
105
|
+
ws.send(chunk);
|
|
106
|
+
},
|
|
107
|
+
close() {
|
|
108
|
+
ws.close();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return ndJsonStream(writable, readable);
|
|
112
|
+
}
|
|
113
|
+
function openWebSocket(url, headers) {
|
|
114
|
+
if (headers === void 0) return new WebSocket(url);
|
|
115
|
+
return Reflect.construct(WebSocket, [url, { headers }]);
|
|
116
|
+
}
|
|
117
|
+
async function connectAcpWebSocket(url, options = {}) {
|
|
118
|
+
const ws = openWebSocket(url, options.headers);
|
|
119
|
+
await waitForWebSocketOpen(ws, options.signal);
|
|
120
|
+
const framing = options.framing ?? "frame";
|
|
121
|
+
const stream = framing === "ndjson" ? webSocketNdjsonToAcpStream(ws) : webSocketFrameToAcpStream(ws);
|
|
122
|
+
return {
|
|
123
|
+
stream,
|
|
124
|
+
close: () => ws.close()
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function httpChannelUrlToWsBase(channelUrl) {
|
|
128
|
+
return channelUrl.replace(/^http/i, "ws").replace(/\/$/, "");
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
connectAcpWebSocket,
|
|
132
|
+
httpChannelUrlToWsBase,
|
|
133
|
+
webSocketFrameToAcpStream
|
|
134
|
+
};
|
|
135
|
+
//# sourceMappingURL=websocket.js.map
|