@wrongstack/acp 0.10.2 → 0.24.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/dist/agent.d.ts +92 -0
- package/dist/agent.js +420 -0
- package/dist/agent.js.map +1 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +315 -0
- package/dist/client.js.map +1 -0
- package/dist/{client/tool-translator.d.ts → index-CncECXiF.d.ts} +41 -17
- package/dist/index.d.ts +5 -24
- package/dist/index.js +841 -21
- package/dist/index.js.map +1 -1
- package/dist/stdio-transport-DqWq-2iP.d.ts +198 -0
- package/package.json +7 -7
- package/dist/agent/index.d.ts +0 -7
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/index.js +0 -5
- package/dist/agent/index.js.map +0 -1
- package/dist/agent/protocol-handler.d.ts +0 -39
- package/dist/agent/protocol-handler.d.ts.map +0 -1
- package/dist/agent/protocol-handler.js +0 -142
- package/dist/agent/protocol-handler.js.map +0 -1
- package/dist/agent/stdio-transport.d.ts +0 -64
- package/dist/agent/stdio-transport.d.ts.map +0 -1
- package/dist/agent/stdio-transport.js +0 -250
- package/dist/agent/stdio-transport.js.map +0 -1
- package/dist/agent/tools-registry.d.ts +0 -33
- package/dist/agent/tools-registry.d.ts.map +0 -1
- package/dist/agent/tools-registry.js +0 -131
- package/dist/agent/tools-registry.js.map +0 -1
- package/dist/agent/wrongstack-acp-agent.d.ts +0 -32
- package/dist/agent/wrongstack-acp-agent.d.ts.map +0 -1
- package/dist/agent/wrongstack-acp-agent.js +0 -90
- package/dist/agent/wrongstack-acp-agent.js.map +0 -1
- package/dist/client/index.d.ts +0 -6
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/index.js +0 -4
- package/dist/client/index.js.map +0 -1
- package/dist/client/tool-translator.d.ts.map +0 -1
- package/dist/client/tool-translator.js +0 -112
- package/dist/client/tool-translator.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/integration/acp-subagent-runner.d.ts +0 -34
- package/dist/integration/acp-subagent-runner.d.ts.map +0 -1
- package/dist/integration/acp-subagent-runner.js +0 -218
- package/dist/integration/acp-subagent-runner.js.map +0 -1
- package/dist/types/acp-messages.d.ts +0 -133
- package/dist/types/acp-messages.d.ts.map +0 -1
- package/dist/types/acp-messages.js +0 -2
- package/dist/types/acp-messages.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,22 +1,842 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
import { buildChildEnv } from '@wrongstack/core';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
// src/agent/stdio-transport.ts
|
|
6
|
+
var StdioTransport = class {
|
|
7
|
+
stdin = process.stdin;
|
|
8
|
+
stdout = process.stdout;
|
|
9
|
+
stderr = process.stderr;
|
|
10
|
+
buffer = "";
|
|
11
|
+
handlers = /* @__PURE__ */ new Set();
|
|
12
|
+
closed = false;
|
|
13
|
+
resolveRead = null;
|
|
14
|
+
messageQueue = [];
|
|
15
|
+
constructor() {
|
|
16
|
+
this.stdin.resume();
|
|
17
|
+
this.stdin.setEncoding("utf8");
|
|
18
|
+
this.stdin.on("data", (chunk) => this.onData(chunk));
|
|
19
|
+
this.stdin.on("end", () => this.handleClose());
|
|
20
|
+
this.stdin.on("error", (err) => this.failAll(err));
|
|
21
|
+
}
|
|
22
|
+
sendStartupMarker() {
|
|
23
|
+
this.stdout.write("[wstack-acp]\n", "utf8");
|
|
24
|
+
}
|
|
25
|
+
send(msg) {
|
|
26
|
+
if (this.closed) return Promise.resolve();
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const line = JSON.stringify(msg) + "\n";
|
|
29
|
+
this.stdout.write(line, "utf8", () => resolve());
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
sendRaw(chunk) {
|
|
33
|
+
this.stdout.write(chunk, "utf8");
|
|
34
|
+
}
|
|
35
|
+
read() {
|
|
36
|
+
if (this.messageQueue.length > 0) return Promise.resolve(this.messageQueue.shift());
|
|
37
|
+
if (this.closed) return Promise.resolve(null);
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
this.resolveRead = resolve;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
onMessage(handler) {
|
|
43
|
+
this.handlers.add(handler);
|
|
44
|
+
return () => this.handlers.delete(handler);
|
|
45
|
+
}
|
|
46
|
+
close() {
|
|
47
|
+
this.closed = true;
|
|
48
|
+
this.stdin.pause();
|
|
49
|
+
this.resolveRead?.(null);
|
|
50
|
+
this.resolveRead = null;
|
|
51
|
+
}
|
|
52
|
+
onData(chunk) {
|
|
53
|
+
this.buffer += chunk;
|
|
54
|
+
const lines = this.buffer.split("\n");
|
|
55
|
+
this.buffer = lines.pop() ?? "";
|
|
56
|
+
for (const raw of lines) {
|
|
57
|
+
if (!raw.trim()) continue;
|
|
58
|
+
try {
|
|
59
|
+
this.dispatch(JSON.parse(raw));
|
|
60
|
+
} catch (err) {
|
|
61
|
+
this.stderr.write(`[wstack-acp parse error] ${err}
|
|
62
|
+
`, "utf8");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
dispatch(msg) {
|
|
67
|
+
if (this.resolveRead) {
|
|
68
|
+
const resolve = this.resolveRead;
|
|
69
|
+
this.resolveRead = null;
|
|
70
|
+
resolve(msg);
|
|
71
|
+
} else {
|
|
72
|
+
this.messageQueue.push(msg);
|
|
73
|
+
}
|
|
74
|
+
for (const handler of this.handlers) {
|
|
75
|
+
try {
|
|
76
|
+
handler(msg);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
this.stderr.write(`[wstack-acp handler error] ${err}
|
|
79
|
+
`, "utf8");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
handleClose() {
|
|
84
|
+
this.closed = true;
|
|
85
|
+
this.resolveRead?.(null);
|
|
86
|
+
this.resolveRead = null;
|
|
87
|
+
}
|
|
88
|
+
failAll(err) {
|
|
89
|
+
this.stderr.write(`[wstack-acp stdin error] ${err.message}
|
|
90
|
+
`, "utf8");
|
|
91
|
+
this.close();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var ClientTransport = class {
|
|
95
|
+
child = null;
|
|
96
|
+
buffer = "";
|
|
97
|
+
handlers = /* @__PURE__ */ new Set();
|
|
98
|
+
closed = false;
|
|
99
|
+
resolveRead = null;
|
|
100
|
+
messageQueue = [];
|
|
101
|
+
opts;
|
|
102
|
+
constructor(options) {
|
|
103
|
+
this.opts = {
|
|
104
|
+
handshakeTimeoutMs: 3e4,
|
|
105
|
+
...options
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async start() {
|
|
109
|
+
if (this.child) return;
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const timeout = setTimeout(() => {
|
|
112
|
+
reject(
|
|
113
|
+
new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
|
|
114
|
+
);
|
|
115
|
+
}, this.opts.handshakeTimeoutMs);
|
|
116
|
+
try {
|
|
117
|
+
this.child = spawn(this.opts.command, this.opts.args ?? [], {
|
|
118
|
+
env: { ...buildChildEnv(), ...this.opts.env },
|
|
119
|
+
cwd: this.opts.cwd,
|
|
120
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
121
|
+
});
|
|
122
|
+
} catch (err) {
|
|
123
|
+
clearTimeout(timeout);
|
|
124
|
+
reject(err);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const child = this.child;
|
|
128
|
+
child.stdout.setEncoding("utf8");
|
|
129
|
+
const waitForMarker = (chunk) => {
|
|
130
|
+
this.buffer += chunk;
|
|
131
|
+
const idx = this.buffer.indexOf("[wstack-acp]\n");
|
|
132
|
+
if (idx !== -1) {
|
|
133
|
+
this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
|
|
134
|
+
child.stdout.removeListener("data", waitForMarker);
|
|
135
|
+
child.stdout.on("data", (c) => this.onChildData(c));
|
|
136
|
+
child.stderr.on("data", (c) => this.onChildError(c));
|
|
137
|
+
child.on("close", (code) => this.onChildClose(code));
|
|
138
|
+
clearTimeout(timeout);
|
|
139
|
+
resolve();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
child.stdout.on("data", waitForMarker);
|
|
143
|
+
child.stdout.on("error", (err) => {
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
reject(err);
|
|
146
|
+
});
|
|
147
|
+
child.on("error", (err) => {
|
|
148
|
+
clearTimeout(timeout);
|
|
149
|
+
reject(err);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
send(msg) {
|
|
154
|
+
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
const line = JSON.stringify(msg) + "\n";
|
|
157
|
+
this.child.stdin.write(line, "utf8", (err) => {
|
|
158
|
+
if (err) reject(err);
|
|
159
|
+
else resolve();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
read() {
|
|
164
|
+
if (this.messageQueue.length > 0) return Promise.resolve(this.messageQueue.shift());
|
|
165
|
+
if (this.closed) return Promise.resolve(null);
|
|
166
|
+
return new Promise((resolve) => {
|
|
167
|
+
this.resolveRead = resolve;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
onMessage(handler) {
|
|
171
|
+
this.handlers.add(handler);
|
|
172
|
+
return () => this.handlers.delete(handler);
|
|
173
|
+
}
|
|
174
|
+
stop() {
|
|
175
|
+
if (!this.child) return;
|
|
176
|
+
this.closed = true;
|
|
177
|
+
try {
|
|
178
|
+
this.child.kill();
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
this.child = null;
|
|
182
|
+
}
|
|
183
|
+
onChildData(chunk) {
|
|
184
|
+
this.buffer += chunk;
|
|
185
|
+
const lines = this.buffer.split("\n");
|
|
186
|
+
this.buffer = lines.pop() ?? "";
|
|
187
|
+
for (const raw of lines) {
|
|
188
|
+
if (!raw.trim()) continue;
|
|
189
|
+
try {
|
|
190
|
+
this.dispatch(JSON.parse(raw));
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
onChildError(chunk) {
|
|
196
|
+
process.stderr.write(`[acp-child stderr] ${chunk}`, "utf8");
|
|
197
|
+
}
|
|
198
|
+
onChildClose(code) {
|
|
199
|
+
this.closed = true;
|
|
200
|
+
this.resolveRead?.(null);
|
|
201
|
+
this.resolveRead = null;
|
|
202
|
+
if (code !== 0 && code !== null) {
|
|
203
|
+
process.stderr.write(`[acp-child exited with code ${code}]
|
|
204
|
+
`, "utf8");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
dispatch(msg) {
|
|
208
|
+
if (this.resolveRead) {
|
|
209
|
+
const resolve = this.resolveRead;
|
|
210
|
+
this.resolveRead = null;
|
|
211
|
+
resolve(msg);
|
|
212
|
+
} else {
|
|
213
|
+
this.messageQueue.push(msg);
|
|
214
|
+
}
|
|
215
|
+
for (const handler of this.handlers) {
|
|
216
|
+
try {
|
|
217
|
+
handler(msg);
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// src/agent/tools-registry.ts
|
|
225
|
+
var ACPToolsRegistry = class {
|
|
226
|
+
tools = /* @__PURE__ */ new Map();
|
|
227
|
+
owner;
|
|
228
|
+
constructor(owner = "wrongstack") {
|
|
229
|
+
this.owner = owner;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Register one or more tools.
|
|
233
|
+
* Throws on duplicate name unless force=true.
|
|
234
|
+
*/
|
|
235
|
+
register(tools) {
|
|
236
|
+
for (const tool of tools) {
|
|
237
|
+
this.tools.set(tool.name, tool);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Replace the current tool set.
|
|
242
|
+
*/
|
|
243
|
+
setTools(tools) {
|
|
244
|
+
this.tools.clear();
|
|
245
|
+
for (const tool of tools) this.tools.set(tool.name, tool);
|
|
246
|
+
}
|
|
247
|
+
get(name) {
|
|
248
|
+
return this.tools.get(name);
|
|
249
|
+
}
|
|
250
|
+
has(name) {
|
|
251
|
+
return this.tools.has(name);
|
|
252
|
+
}
|
|
253
|
+
list() {
|
|
254
|
+
return Array.from(this.tools.values());
|
|
255
|
+
}
|
|
256
|
+
/** Build the ACP tools/list payload from registered tools. */
|
|
257
|
+
buildToolList() {
|
|
258
|
+
return {
|
|
259
|
+
tools: Array.from(this.tools.values()).map(
|
|
260
|
+
(t) => toACPToolDefinition(t, this.owner)
|
|
261
|
+
)
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Execute a tool by name and return ACP-formatted result.
|
|
266
|
+
* Returns null if the tool is not found.
|
|
267
|
+
*/
|
|
268
|
+
async execute(name, args, ctx, signal) {
|
|
269
|
+
const tool = this.tools.get(name);
|
|
270
|
+
if (!tool) return null;
|
|
271
|
+
try {
|
|
272
|
+
const result = await tool.execute(args, ctx, {
|
|
273
|
+
signal
|
|
274
|
+
});
|
|
275
|
+
return toACPToolResult(result);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
278
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
function toACPToolDefinition(tool, _owner) {
|
|
283
|
+
return {
|
|
284
|
+
name: tool.name,
|
|
285
|
+
description: tool.description,
|
|
286
|
+
inputSchema: toACPInputSchema(tool.inputSchema),
|
|
287
|
+
annotations: {
|
|
288
|
+
title: tool.name,
|
|
289
|
+
description: tool.usageHint ?? tool.description,
|
|
290
|
+
priority: toolToPriority(tool),
|
|
291
|
+
alwaysAccept: tool.permission === "auto"
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function toACPInputSchema(src) {
|
|
296
|
+
if (!src || typeof src !== "object") {
|
|
297
|
+
return {};
|
|
298
|
+
}
|
|
299
|
+
const s = src;
|
|
300
|
+
if (s.properties && typeof s.properties === "object") {
|
|
301
|
+
const props = {};
|
|
302
|
+
for (const [k, v] of Object.entries(s.properties)) {
|
|
303
|
+
props[k] = toACPInputSchema(v);
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
type: typeof s.type === "string" ? s.type : void 0,
|
|
307
|
+
properties: props,
|
|
308
|
+
required: Array.isArray(s.required) ? s.required : void 0,
|
|
309
|
+
items: s.items ? toACPInputSchema(s.items) : void 0,
|
|
310
|
+
enum: Array.isArray(s.enum) ? s.enum : void 0,
|
|
311
|
+
description: typeof s.description === "string" ? s.description : void 0,
|
|
312
|
+
default: s.default,
|
|
313
|
+
minimum: typeof s.minimum === "number" ? s.minimum : void 0,
|
|
314
|
+
maximum: typeof s.maximum === "number" ? s.maximum : void 0
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
type: typeof s.type === "string" ? s.type : void 0,
|
|
319
|
+
items: s.items ? toACPInputSchema(s.items) : void 0,
|
|
320
|
+
enum: Array.isArray(s.enum) ? s.enum : void 0,
|
|
321
|
+
description: typeof s.description === "string" ? s.description : void 0,
|
|
322
|
+
default: s.default,
|
|
323
|
+
minimum: typeof s.minimum === "number" ? s.minimum : void 0,
|
|
324
|
+
maximum: typeof s.maximum === "number" ? s.maximum : void 0
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function toACPToolResult(result) {
|
|
328
|
+
const blocks = [];
|
|
329
|
+
if (result === void 0 || result === null) {
|
|
330
|
+
return { content: [{ type: "text", text: "ok" }] };
|
|
331
|
+
}
|
|
332
|
+
if (typeof result === "string") {
|
|
333
|
+
blocks.push({ type: "text", text: result });
|
|
334
|
+
} else if (typeof result === "object") {
|
|
335
|
+
blocks.push({ type: "text", text: JSON.stringify(result, null, 2) });
|
|
336
|
+
} else {
|
|
337
|
+
blocks.push({ type: "text", text: String(result) });
|
|
338
|
+
}
|
|
339
|
+
return { content: blocks };
|
|
340
|
+
}
|
|
341
|
+
function toolToPriority(tool) {
|
|
342
|
+
if (tool.riskTier === "destructive") return "high";
|
|
343
|
+
if (tool.riskTier === "standard" || tool.permission === "confirm") return "medium";
|
|
344
|
+
return "low";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/agent/protocol-handler.ts
|
|
348
|
+
var WRONGSTACK_VERSION = "0.1.0";
|
|
349
|
+
var WRONGSTACK_CAPABILITIES = [
|
|
350
|
+
"code-generation",
|
|
351
|
+
"async-tools",
|
|
352
|
+
"streaming",
|
|
353
|
+
"progress"
|
|
354
|
+
];
|
|
355
|
+
var ACPProtocolHandler = class {
|
|
356
|
+
constructor(transport, registry, context) {
|
|
357
|
+
this.transport = transport;
|
|
358
|
+
this.registry = registry;
|
|
359
|
+
this.context = context;
|
|
360
|
+
}
|
|
361
|
+
transport;
|
|
362
|
+
registry;
|
|
363
|
+
context;
|
|
364
|
+
initialized = false;
|
|
365
|
+
signal = new AbortController();
|
|
366
|
+
pendingCalls = /* @__PURE__ */ new Map();
|
|
367
|
+
/** Wire an external abort signal from the ACP client */
|
|
368
|
+
wireAbortController(abortController) {
|
|
369
|
+
abortController.signal.addEventListener("abort", () => {
|
|
370
|
+
for (const id of this.pendingCalls.keys()) {
|
|
371
|
+
this.transport.send({ id, method: "cancel", result: { ok: true } }).catch(() => {
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/** Process one inbound message. Returns true if this was a terminal message. */
|
|
377
|
+
async handleMessage(msg) {
|
|
378
|
+
if (msg.id !== void 0) {
|
|
379
|
+
return this.handleRequest(msg);
|
|
380
|
+
}
|
|
381
|
+
return this.handleNotification(msg);
|
|
382
|
+
}
|
|
383
|
+
async handleRequest(req) {
|
|
384
|
+
if (req.method !== "initialize" && !this.initialized) {
|
|
385
|
+
await this.sendError(req.id ?? null, -32e3, "Not initialized");
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
const id = req.id;
|
|
389
|
+
switch (req.method) {
|
|
390
|
+
case "initialize":
|
|
391
|
+
return this.handleInitialize(req, id);
|
|
392
|
+
case "ping":
|
|
393
|
+
await this.transport.send({ id, method: "ping", result: { pong: true } });
|
|
394
|
+
return false;
|
|
395
|
+
case "tools/call":
|
|
396
|
+
return this.handleToolCall(req, id);
|
|
397
|
+
case "tools/list":
|
|
398
|
+
return this.handleToolsList(id);
|
|
399
|
+
case "cancel":
|
|
400
|
+
return this.handleCancel(id);
|
|
401
|
+
case "session/list":
|
|
402
|
+
return this.handleSessionList(id);
|
|
403
|
+
case "sessionInfoUpdate":
|
|
404
|
+
await this.transport.send({ id, method: "sessionInfoUpdate", result: { ok: true } });
|
|
405
|
+
return false;
|
|
406
|
+
default:
|
|
407
|
+
await this.sendError(id, -32601, `Unknown method: ${req.method}`);
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async handleNotification(n) {
|
|
412
|
+
if (n.method === "cancel") {
|
|
413
|
+
this.handleCancelNotification(n);
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
async handleInitialize(req, id) {
|
|
418
|
+
this.initialized = true;
|
|
419
|
+
const result = {
|
|
420
|
+
capabilities: WRONGSTACK_CAPABILITIES,
|
|
421
|
+
agentName: "WrongStack",
|
|
422
|
+
agentVersion: WRONGSTACK_VERSION,
|
|
423
|
+
protocolVersion: req.params?.protocolVersion ?? "2024-11",
|
|
424
|
+
...this.registry.buildToolList()
|
|
425
|
+
};
|
|
426
|
+
await this.transport.send({ id, method: "initialize", result });
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
async handleToolsList(id) {
|
|
430
|
+
await this.transport.send({
|
|
431
|
+
id,
|
|
432
|
+
method: "tools/list",
|
|
433
|
+
result: this.registry.buildToolList()
|
|
434
|
+
});
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
async handleToolCall(req, id) {
|
|
438
|
+
const { name, arguments: args } = req.params;
|
|
439
|
+
const runPromise = (async () => {
|
|
440
|
+
if (!this.registry.has(name)) {
|
|
441
|
+
return {
|
|
442
|
+
content: [{ type: "text", text: `Tool not found: ${name}` }],
|
|
443
|
+
isError: true
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const result = await this.registry.execute(
|
|
447
|
+
name,
|
|
448
|
+
args,
|
|
449
|
+
this.context,
|
|
450
|
+
this.signal.signal
|
|
451
|
+
);
|
|
452
|
+
return result ?? { content: [{ type: "text", text: "Tool returned null" }], isError: false };
|
|
453
|
+
})();
|
|
454
|
+
this.pendingCalls.set(id, runPromise);
|
|
455
|
+
try {
|
|
456
|
+
const toolResult = await runPromise;
|
|
457
|
+
this.pendingCalls.delete(id);
|
|
458
|
+
const response = { method: "tools/call", id, result: toolResult };
|
|
459
|
+
await this.transport.send(response);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
this.pendingCalls.delete(id);
|
|
462
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
463
|
+
await this.transport.send({
|
|
464
|
+
id,
|
|
465
|
+
method: "tools/call",
|
|
466
|
+
result: { content: [{ type: "text", text: msg }], isError: true }
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
async handleCancel(id) {
|
|
472
|
+
this.pendingCalls.delete(id);
|
|
473
|
+
await this.transport.send({ id, method: "cancel", result: { ok: true } });
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
handleCancelNotification(_n) {
|
|
477
|
+
}
|
|
478
|
+
async handleSessionList(id) {
|
|
479
|
+
await this.transport.send({
|
|
480
|
+
id,
|
|
481
|
+
method: "session/list",
|
|
482
|
+
result: { sessions: [] }
|
|
483
|
+
});
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
async sendError(id, code, message) {
|
|
487
|
+
if (id === null) return;
|
|
488
|
+
await this.transport.send({ id, method: "", error: { code, message } });
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
var WrongStackACPServer = class {
|
|
492
|
+
transport;
|
|
493
|
+
registry;
|
|
494
|
+
handler;
|
|
495
|
+
running = false;
|
|
496
|
+
constructor(opts) {
|
|
497
|
+
this.transport = new StdioTransport();
|
|
498
|
+
this.registry = new ACPToolsRegistry(opts.owner);
|
|
499
|
+
this.registry.register(opts.tools);
|
|
500
|
+
this.handler = new ACPProtocolHandler(
|
|
501
|
+
this.transport,
|
|
502
|
+
this.registry,
|
|
503
|
+
/* TODO: load WrongStack Context */
|
|
504
|
+
{}
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Start the server. Blocks until the client disconnects.
|
|
509
|
+
*
|
|
510
|
+
* 1. Send the startup marker `[wstack-acp]` so the client
|
|
511
|
+
* knows which stdout line is the protocol boundary.
|
|
512
|
+
* 2. Loop: read messages, dispatch to handler, until EOF or error.
|
|
513
|
+
*/
|
|
514
|
+
async start() {
|
|
515
|
+
this.transport.sendStartupMarker();
|
|
516
|
+
this.running = true;
|
|
517
|
+
this.transport.onMessage((msg) => {
|
|
518
|
+
void this.handler.handleMessage(msg);
|
|
519
|
+
});
|
|
520
|
+
while (this.running) {
|
|
521
|
+
const msg = await this.transport.read();
|
|
522
|
+
if (!msg) break;
|
|
523
|
+
const terminal = await this.handler.handleMessage(msg);
|
|
524
|
+
if (terminal) break;
|
|
525
|
+
}
|
|
526
|
+
this.transport.close();
|
|
527
|
+
}
|
|
528
|
+
/** Stop the server. */
|
|
529
|
+
stop() {
|
|
530
|
+
this.running = false;
|
|
531
|
+
this.transport.close();
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
async function main() {
|
|
535
|
+
const server = new WrongStackACPServer({ tools: [] });
|
|
536
|
+
await server.start();
|
|
537
|
+
}
|
|
538
|
+
var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
|
|
539
|
+
if (isEntrypoint) {
|
|
540
|
+
main().catch((err) => {
|
|
541
|
+
process.stderr.write(`[wstack-acp fatal] ${err}
|
|
542
|
+
`);
|
|
543
|
+
process.exit(1);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/client/tool-translator.ts
|
|
548
|
+
var DEFAULT_OPTIONS = {
|
|
549
|
+
asyncTools: true,
|
|
550
|
+
pollIntervalMs: 500,
|
|
551
|
+
totalTimeoutMs: 12e4
|
|
552
|
+
};
|
|
553
|
+
function extractTextFromContent(blocks) {
|
|
554
|
+
const parts = [];
|
|
555
|
+
for (const b of blocks) {
|
|
556
|
+
if (b.type === "text") parts.push(b.text);
|
|
557
|
+
else if (b.type === "resource") parts.push(`[resource: ${b.resource.uri}]`);
|
|
558
|
+
else if (b.type === "image") parts.push(`[image: ${b.data.slice(0, 20)}...]`);
|
|
559
|
+
else if (b.type === "progress") {
|
|
560
|
+
if (b.messages?.length) parts.push(b.messages.join("\n"));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return parts.join("\n");
|
|
564
|
+
}
|
|
565
|
+
function parseToolResponse(taskId, subagentId, response) {
|
|
566
|
+
const blocks = response.result.content;
|
|
567
|
+
const text = extractTextFromContent(blocks);
|
|
568
|
+
const isError = response.result.isError || text.toLowerCase().includes("error") || text.toLowerCase().includes("failed");
|
|
569
|
+
return {
|
|
570
|
+
taskId,
|
|
571
|
+
subagentId,
|
|
572
|
+
status: isError ? "failed" : "success",
|
|
573
|
+
result: text,
|
|
574
|
+
iterations: 1,
|
|
575
|
+
toolCalls: 1,
|
|
576
|
+
durationMs: 0
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
var ToolTranslator = class {
|
|
580
|
+
opts;
|
|
581
|
+
pending = /* @__PURE__ */ new Map();
|
|
582
|
+
constructor(opts = {}) {
|
|
583
|
+
this.opts = { ...DEFAULT_OPTIONS, ...opts };
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Start listening to a transport for tool responses and cancellations.
|
|
587
|
+
* Call this once after constructing the translator and before sending tasks.
|
|
588
|
+
*/
|
|
589
|
+
attachToTransport(transport) {
|
|
590
|
+
transport.onMessage((msg) => {
|
|
591
|
+
if (msg.method === "tools/call" && msg.id !== void 0) {
|
|
592
|
+
const pending = this.pending.get(msg.id);
|
|
593
|
+
if (pending) {
|
|
594
|
+
clearTimeout(pending.timeout);
|
|
595
|
+
this.pending.delete(msg.id);
|
|
596
|
+
pending.resolve(msg);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (msg.method === "cancel" && msg.id !== void 0) {
|
|
600
|
+
const pending = this.pending.get(msg.id);
|
|
601
|
+
if (pending) {
|
|
602
|
+
clearTimeout(pending.timeout);
|
|
603
|
+
this.pending.delete(msg.id);
|
|
604
|
+
pending.reject(new Error("Call cancelled by client"));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Send a tool call over the transport and wait for a response.
|
|
611
|
+
* If asyncTools is true, polls for progress and resolves when the final
|
|
612
|
+
* response arrives.
|
|
613
|
+
*/
|
|
614
|
+
async callTool(transport, name, args, callId = crypto.randomUUID()) {
|
|
615
|
+
await transport.send({
|
|
616
|
+
method: "tools/call",
|
|
617
|
+
id: callId,
|
|
618
|
+
params: { name, arguments: args }
|
|
619
|
+
});
|
|
620
|
+
return new Promise((resolve, reject) => {
|
|
621
|
+
const timeout = setTimeout(() => {
|
|
622
|
+
this.pending.delete(callId);
|
|
623
|
+
reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
|
|
624
|
+
}, this.opts.totalTimeoutMs);
|
|
625
|
+
this.pending.set(callId, { resolve, reject, timeout });
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
cancelAll() {
|
|
629
|
+
for (const [, p] of this.pending) {
|
|
630
|
+
clearTimeout(p.timeout);
|
|
631
|
+
}
|
|
632
|
+
this.pending.clear();
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// src/integration/acp-subagent-runner.ts
|
|
637
|
+
var ACP_AGENT_COMMANDS = {
|
|
638
|
+
cline: {
|
|
639
|
+
command: "npx",
|
|
640
|
+
args: ["-y", "@agentify/cline"],
|
|
641
|
+
role: "cline"
|
|
642
|
+
},
|
|
643
|
+
"gemini-cli": {
|
|
644
|
+
command: "gemini",
|
|
645
|
+
role: "gemini-cli"
|
|
646
|
+
},
|
|
647
|
+
copilot: {
|
|
648
|
+
command: "gh",
|
|
649
|
+
args: ["copilot", "agent"],
|
|
650
|
+
role: "copilot"
|
|
651
|
+
},
|
|
652
|
+
openhands: {
|
|
653
|
+
command: "openhands",
|
|
654
|
+
role: "openhands"
|
|
655
|
+
},
|
|
656
|
+
goose: {
|
|
657
|
+
command: "goose",
|
|
658
|
+
role: "goose"
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
async function makeACPSubagentRunner(options) {
|
|
662
|
+
const transport = new ClientTransport({
|
|
663
|
+
command: options.command,
|
|
664
|
+
args: options.args,
|
|
665
|
+
env: options.env,
|
|
666
|
+
cwd: options.cwd,
|
|
667
|
+
handshakeTimeoutMs: 3e4
|
|
668
|
+
});
|
|
669
|
+
const translator = new ToolTranslator(options.toolTranslatorOpts);
|
|
670
|
+
const activeAbort = new AbortController();
|
|
671
|
+
let sessionStarted = false;
|
|
672
|
+
const startSession = async () => {
|
|
673
|
+
if (sessionStarted) return;
|
|
674
|
+
await transport.start();
|
|
675
|
+
await transport.send({
|
|
676
|
+
method: "initialize",
|
|
677
|
+
id: "1",
|
|
678
|
+
params: {
|
|
679
|
+
capabilities: ["code-generation", "async-tools", "streaming", "progress"],
|
|
680
|
+
protocolVersion: "2024-11",
|
|
681
|
+
sessionId: options.role ?? "wrongstack-subagent"
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
const initResp = await transport.read();
|
|
685
|
+
if (!initResp || initResp.error) {
|
|
686
|
+
throw new Error(`ACP initialize failed: ${initResp?.error?.message ?? "no response"}`);
|
|
687
|
+
}
|
|
688
|
+
translator.attachToTransport({
|
|
689
|
+
onMessage: (h) => transport.onMessage(h),
|
|
690
|
+
send: (m) => transport.send(m)
|
|
691
|
+
});
|
|
692
|
+
sessionStarted = true;
|
|
693
|
+
};
|
|
694
|
+
const runner = async (task, ctx) => {
|
|
695
|
+
ctx.signal.addEventListener("abort", () => {
|
|
696
|
+
activeAbort.abort();
|
|
697
|
+
transport.stop();
|
|
698
|
+
});
|
|
699
|
+
await startSession();
|
|
700
|
+
const callId = crypto.randomUUID();
|
|
701
|
+
let toolResult = null;
|
|
702
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
703
|
+
const budgetMs = ctx.budget.limits.timeoutMs ?? 3e5;
|
|
704
|
+
const timeout = setTimeout(() => {
|
|
705
|
+
reject(new Error(`ACP task timed out for subagent ${ctx.subagentId} (${budgetMs}ms budget)`));
|
|
706
|
+
}, budgetMs);
|
|
707
|
+
transport.onMessage((msg) => {
|
|
708
|
+
if (msg.method === "tools/call" && msg.id !== void 0) {
|
|
709
|
+
clearTimeout(timeout);
|
|
710
|
+
resolve(msg);
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
ctx.signal.addEventListener("abort", () => {
|
|
714
|
+
clearTimeout(timeout);
|
|
715
|
+
reject(new Error("Task aborted by parent"));
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
try {
|
|
719
|
+
await transport.send({
|
|
720
|
+
method: "agent/run",
|
|
721
|
+
id: callId,
|
|
722
|
+
params: {
|
|
723
|
+
task: task.description,
|
|
724
|
+
sessionId: ctx.subagentId
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
toolResult = await resultPromise;
|
|
728
|
+
} catch (err) {
|
|
729
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
730
|
+
return {
|
|
731
|
+
result: `ACP subagent error: ${msg}`,
|
|
732
|
+
iterations: 0,
|
|
733
|
+
toolCalls: 0
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
if (!toolResult) {
|
|
737
|
+
return { result: "ACP subagent returned no result", iterations: 1, toolCalls: 1 };
|
|
738
|
+
}
|
|
739
|
+
const parsed = parseToolResponse(task.id, ctx.subagentId, toolResult);
|
|
740
|
+
return {
|
|
741
|
+
result: parsed.result ?? parsed.error,
|
|
742
|
+
iterations: parsed.iterations,
|
|
743
|
+
toolCalls: parsed.toolCalls
|
|
744
|
+
};
|
|
745
|
+
};
|
|
746
|
+
return runner;
|
|
747
|
+
}
|
|
748
|
+
async function makeACPSubagentRunnerWithStop(options) {
|
|
749
|
+
const transport = new ClientTransport({
|
|
750
|
+
command: options.command,
|
|
751
|
+
args: options.args,
|
|
752
|
+
env: options.env,
|
|
753
|
+
cwd: options.cwd,
|
|
754
|
+
handshakeTimeoutMs: 3e4
|
|
755
|
+
});
|
|
756
|
+
const translator = new ToolTranslator(options.toolTranslatorOpts);
|
|
757
|
+
const activeAbort = new AbortController();
|
|
758
|
+
let sessionStarted = false;
|
|
759
|
+
const startSession = async () => {
|
|
760
|
+
if (sessionStarted) return;
|
|
761
|
+
await transport.start();
|
|
762
|
+
await transport.send({
|
|
763
|
+
method: "initialize",
|
|
764
|
+
id: "1",
|
|
765
|
+
params: {
|
|
766
|
+
capabilities: ["code-generation", "async-tools", "streaming", "progress"],
|
|
767
|
+
protocolVersion: "2024-11",
|
|
768
|
+
sessionId: options.role ?? "wrongstack-subagent"
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
const initResp = await transport.read();
|
|
772
|
+
if (!initResp || initResp.error) {
|
|
773
|
+
throw new Error(`ACP initialize failed: ${initResp?.error?.message ?? "no response"}`);
|
|
774
|
+
}
|
|
775
|
+
translator.attachToTransport({
|
|
776
|
+
onMessage: (h) => transport.onMessage(h),
|
|
777
|
+
send: (m) => transport.send(m)
|
|
778
|
+
});
|
|
779
|
+
sessionStarted = true;
|
|
780
|
+
};
|
|
781
|
+
const stop = () => {
|
|
782
|
+
activeAbort.abort();
|
|
783
|
+
transport.stop();
|
|
784
|
+
};
|
|
785
|
+
const runner = async (task, ctx) => {
|
|
786
|
+
ctx.signal.addEventListener("abort", () => {
|
|
787
|
+
activeAbort.abort();
|
|
788
|
+
transport.stop();
|
|
789
|
+
});
|
|
790
|
+
await startSession();
|
|
791
|
+
const callId = crypto.randomUUID();
|
|
792
|
+
let toolResult = null;
|
|
793
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
794
|
+
const budgetMs = ctx.budget.limits.timeoutMs ?? 3e5;
|
|
795
|
+
const timeout = setTimeout(() => {
|
|
796
|
+
reject(new Error(`ACP task timed out for subagent ${ctx.subagentId} (${budgetMs}ms budget)`));
|
|
797
|
+
}, budgetMs);
|
|
798
|
+
transport.onMessage((msg) => {
|
|
799
|
+
if (msg.method === "tools/call" && msg.id !== void 0) {
|
|
800
|
+
clearTimeout(timeout);
|
|
801
|
+
resolve(msg);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
ctx.signal.addEventListener("abort", () => {
|
|
805
|
+
clearTimeout(timeout);
|
|
806
|
+
reject(new Error("Task aborted by parent"));
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
try {
|
|
810
|
+
await transport.send({
|
|
811
|
+
method: "agent/run",
|
|
812
|
+
id: callId,
|
|
813
|
+
params: {
|
|
814
|
+
task: task.description,
|
|
815
|
+
sessionId: ctx.subagentId
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
toolResult = await resultPromise;
|
|
819
|
+
} catch (err) {
|
|
820
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
821
|
+
return {
|
|
822
|
+
result: `ACP subagent error: ${msg}`,
|
|
823
|
+
iterations: 0,
|
|
824
|
+
toolCalls: 0
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
if (!toolResult) {
|
|
828
|
+
return { result: "ACP subagent returned no result", iterations: 1, toolCalls: 1 };
|
|
829
|
+
}
|
|
830
|
+
const parsed = parseToolResponse(task.id, ctx.subagentId, toolResult);
|
|
831
|
+
return {
|
|
832
|
+
result: parsed.result ?? parsed.error,
|
|
833
|
+
iterations: parsed.iterations,
|
|
834
|
+
toolCalls: parsed.toolCalls
|
|
835
|
+
};
|
|
836
|
+
};
|
|
837
|
+
return { runner, stop };
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export { ACPProtocolHandler, ACPToolsRegistry, ACP_AGENT_COMMANDS, ClientTransport, StdioTransport, ToolTranslator, WrongStackACPServer, makeACPSubagentRunner, makeACPSubagentRunnerWithStop };
|
|
841
|
+
//# sourceMappingURL=index.js.map
|
|
22
842
|
//# sourceMappingURL=index.js.map
|