@wrongstack/acp 0.309.1 → 0.310.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/index.d.ts +9 -9
- package/dist/agent/protocol-contract.d.ts +1 -1
- package/dist/agent.js +897 -898
- package/dist/client/acp-session.d.ts +3 -3
- package/dist/client/tool-translator.d.ts +2 -2
- package/dist/client.js +1005 -1023
- package/dist/index.d.ts +2 -1
- package/dist/index.js +997 -1023
- package/dist/integration/acp-subagent-runner.d.ts +1 -1
- package/dist/integration/run-one-acp-task.d.ts +6 -0
- package/dist/registry/acp-registry-fetch.d.ts +1 -1
- package/dist/registry/agents.catalog.d.ts +1 -1
- package/dist/registry/contracts.d.ts +54 -0
- package/dist/registry/ensemble-registry.d.ts +3 -42
- package/dist/v1.js +1 -3
- package/package.json +2 -2
package/dist/agent.js
CHANGED
|
@@ -1,289 +1,3 @@
|
|
|
1
|
-
// src/agent/stdio-transport.ts
|
|
2
|
-
import { expectDefined, writeErr } from "@wrongstack/core/utils";
|
|
3
|
-
import { treeKill } from "@wrongstack/core/utils/tree-kill";
|
|
4
|
-
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
5
|
-
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
6
|
-
var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
|
|
7
|
-
function positiveLimit(value, fallback) {
|
|
8
|
-
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
9
|
-
}
|
|
10
|
-
var StdioTransport = class {
|
|
11
|
-
stdin = process.stdin;
|
|
12
|
-
stdout = process.stdout;
|
|
13
|
-
stderr = process.stderr;
|
|
14
|
-
buffer = "";
|
|
15
|
-
handlers = /* @__PURE__ */ new Set();
|
|
16
|
-
claimHandlers = /* @__PURE__ */ new Set();
|
|
17
|
-
closed = false;
|
|
18
|
-
resolveRead = null;
|
|
19
|
-
messageQueue = [];
|
|
20
|
-
queuedChars = 0;
|
|
21
|
-
maxFrameChars;
|
|
22
|
-
maxQueuedMessages;
|
|
23
|
-
maxQueuedChars;
|
|
24
|
-
onStdinData = (chunk) => this.onData(chunk);
|
|
25
|
-
onStdinEnd = () => this.handleClose();
|
|
26
|
-
onStdinError = (err) => this.failAll(err);
|
|
27
|
-
constructor(opts = {}) {
|
|
28
|
-
this.maxFrameChars = positiveLimit(opts.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
29
|
-
this.maxQueuedMessages = positiveLimit(opts.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
|
|
30
|
-
this.maxQueuedChars = positiveLimit(opts.maxQueuedChars, DEFAULT_MAX_QUEUED_CHARS);
|
|
31
|
-
this.stdin.resume();
|
|
32
|
-
this.stdin.setEncoding("utf8");
|
|
33
|
-
this.stdin.on("data", this.onStdinData);
|
|
34
|
-
this.stdin.on("end", this.onStdinEnd);
|
|
35
|
-
this.stdin.on("error", this.onStdinError);
|
|
36
|
-
}
|
|
37
|
-
sendStartupMarker() {
|
|
38
|
-
this.stdout.write("[wstack-acp]\n", "utf8");
|
|
39
|
-
}
|
|
40
|
-
send(msg) {
|
|
41
|
-
if (this.closed) return Promise.resolve();
|
|
42
|
-
return new Promise((resolve2) => {
|
|
43
|
-
const line = JSON.stringify(msg) + "\n";
|
|
44
|
-
this.stdout.write(line, "utf8", () => resolve2());
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
sendRaw(chunk) {
|
|
48
|
-
this.stdout.write(chunk, "utf8");
|
|
49
|
-
}
|
|
50
|
-
read() {
|
|
51
|
-
if (this.messageQueue.length > 0) {
|
|
52
|
-
const queued = expectDefined(this.messageQueue.shift());
|
|
53
|
-
this.queuedChars = Math.max(0, this.queuedChars - queued.chars);
|
|
54
|
-
return Promise.resolve(queued.message);
|
|
55
|
-
}
|
|
56
|
-
if (this.closed) return Promise.resolve(null);
|
|
57
|
-
return new Promise((resolve2) => {
|
|
58
|
-
this.resolveRead = resolve2;
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
onMessage(handler) {
|
|
62
|
-
this.handlers.add(handler);
|
|
63
|
-
return () => this.handlers.delete(handler);
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Register a handler that MAY claim a message by returning `true`.
|
|
67
|
-
* Claimed messages are NOT enqueued for the read() loop — they are
|
|
68
|
-
* considered fully consumed by the handler. This is how the ACP
|
|
69
|
-
* server transport prevents the correlation handler (which always
|
|
70
|
-
* fires on every message but only actually correlates responses)
|
|
71
|
-
* from starving the read() loop of pipelined requests, notifications,
|
|
72
|
-
* and any message the handler chose to ignore. See dispatch() for
|
|
73
|
-
* the gating logic.
|
|
74
|
-
*/
|
|
75
|
-
onMessageClaim(handler) {
|
|
76
|
-
this.claimHandlers.add(handler);
|
|
77
|
-
return () => this.claimHandlers.delete(handler);
|
|
78
|
-
}
|
|
79
|
-
close() {
|
|
80
|
-
this.closed = true;
|
|
81
|
-
this.stdin.off("data", this.onStdinData);
|
|
82
|
-
this.stdin.off("end", this.onStdinEnd);
|
|
83
|
-
this.stdin.off("error", this.onStdinError);
|
|
84
|
-
this.stdin.pause();
|
|
85
|
-
this.resolveRead?.(null);
|
|
86
|
-
this.resolveRead = null;
|
|
87
|
-
this.buffer = "";
|
|
88
|
-
this.messageQueue.length = 0;
|
|
89
|
-
this.queuedChars = 0;
|
|
90
|
-
this.handlers.clear();
|
|
91
|
-
this.claimHandlers.clear();
|
|
92
|
-
}
|
|
93
|
-
onData(chunk) {
|
|
94
|
-
this.buffer += chunk;
|
|
95
|
-
const lines = this.buffer.split("\n");
|
|
96
|
-
this.buffer = lines.pop() ?? "";
|
|
97
|
-
if (this.buffer.length > this.maxFrameChars) {
|
|
98
|
-
this.stderr.write(
|
|
99
|
-
`[wstack-acp frame error] pending frame exceeds ${this.maxFrameChars} characters
|
|
100
|
-
`,
|
|
101
|
-
"utf8"
|
|
102
|
-
);
|
|
103
|
-
this.close();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
for (const raw of lines) {
|
|
107
|
-
if (!raw.trim()) continue;
|
|
108
|
-
if (raw.length > this.maxFrameChars) {
|
|
109
|
-
this.stderr.write(
|
|
110
|
-
`[wstack-acp frame error] frame exceeds ${this.maxFrameChars} characters
|
|
111
|
-
`,
|
|
112
|
-
"utf8"
|
|
113
|
-
);
|
|
114
|
-
this.close();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
this.dispatch(JSON.parse(raw), raw.length);
|
|
119
|
-
} catch (err) {
|
|
120
|
-
this.stderr.write(`[wstack-acp parse error] ${err}
|
|
121
|
-
`, "utf8");
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
126
|
-
if (this.resolveRead) {
|
|
127
|
-
const resolve2 = this.resolveRead;
|
|
128
|
-
this.resolveRead = null;
|
|
129
|
-
resolve2(msg);
|
|
130
|
-
} else {
|
|
131
|
-
let claimed = false;
|
|
132
|
-
for (const handler of this.claimHandlers) {
|
|
133
|
-
try {
|
|
134
|
-
if (handler(msg)) claimed = true;
|
|
135
|
-
} catch (err) {
|
|
136
|
-
this.stderr.write(`[wstack-acp handler error] ${err}
|
|
137
|
-
`, "utf8");
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (!claimed) {
|
|
141
|
-
if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
|
|
142
|
-
this.stderr.write(
|
|
143
|
-
`[wstack-acp queue error] pending message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters
|
|
144
|
-
`,
|
|
145
|
-
"utf8"
|
|
146
|
-
);
|
|
147
|
-
this.close();
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
this.messageQueue.push({ message: msg, chars });
|
|
151
|
-
this.queuedChars += chars;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
for (const handler of this.handlers) {
|
|
155
|
-
try {
|
|
156
|
-
handler(msg);
|
|
157
|
-
} catch (err) {
|
|
158
|
-
this.stderr.write(`[wstack-acp handler error] ${err}
|
|
159
|
-
`, "utf8");
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
handleClose() {
|
|
164
|
-
this.close();
|
|
165
|
-
}
|
|
166
|
-
failAll(err) {
|
|
167
|
-
this.stderr.write(`[wstack-acp stdin error] ${err.message}
|
|
168
|
-
`, "utf8");
|
|
169
|
-
this.close();
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// src/agent/tools-registry.ts
|
|
174
|
-
var ACPToolsRegistry = class {
|
|
175
|
-
tools = /* @__PURE__ */ new Map();
|
|
176
|
-
owner;
|
|
177
|
-
constructor(owner = "wrongstack") {
|
|
178
|
-
this.owner = owner;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Register one or more tools.
|
|
182
|
-
* Throws on duplicate name unless force=true.
|
|
183
|
-
*/
|
|
184
|
-
register(tools) {
|
|
185
|
-
for (const tool of tools) {
|
|
186
|
-
this.tools.set(tool.name, tool);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Replace the current tool set.
|
|
191
|
-
*/
|
|
192
|
-
setTools(tools) {
|
|
193
|
-
this.tools.clear();
|
|
194
|
-
for (const tool of tools) this.tools.set(tool.name, tool);
|
|
195
|
-
}
|
|
196
|
-
get(name) {
|
|
197
|
-
return this.tools.get(name);
|
|
198
|
-
}
|
|
199
|
-
has(name) {
|
|
200
|
-
return this.tools.has(name);
|
|
201
|
-
}
|
|
202
|
-
list() {
|
|
203
|
-
return Array.from(this.tools.values());
|
|
204
|
-
}
|
|
205
|
-
/** Build the ACP tools/list payload from registered tools. */
|
|
206
|
-
buildToolList() {
|
|
207
|
-
return {
|
|
208
|
-
tools: Array.from(this.tools.values()).map(
|
|
209
|
-
(t) => toACPToolDefinition(t, this.owner)
|
|
210
|
-
)
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Execute a tool by name and return ACP-formatted result.
|
|
215
|
-
* Returns null if the tool is not found.
|
|
216
|
-
*/
|
|
217
|
-
async execute(name, args, ctx, signal) {
|
|
218
|
-
const tool = this.tools.get(name);
|
|
219
|
-
if (!tool) return null;
|
|
220
|
-
try {
|
|
221
|
-
const result = await tool.execute(args, ctx, {
|
|
222
|
-
signal
|
|
223
|
-
});
|
|
224
|
-
return toACPToolResult(result);
|
|
225
|
-
} catch (err) {
|
|
226
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
227
|
-
return { content: [{ type: "text", text: msg }], isError: true };
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
function toACPToolDefinition(tool, _owner) {
|
|
232
|
-
return {
|
|
233
|
-
name: tool.name,
|
|
234
|
-
description: tool.description,
|
|
235
|
-
inputSchema: toACPInputSchema(tool.inputSchema),
|
|
236
|
-
annotations: {
|
|
237
|
-
title: tool.name,
|
|
238
|
-
description: tool.usageHint ?? tool.description,
|
|
239
|
-
priority: toolToPriority(tool),
|
|
240
|
-
alwaysAccept: tool.permission === "auto"
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
function toACPInputSchema(src) {
|
|
245
|
-
if (!src || typeof src !== "object") {
|
|
246
|
-
return {};
|
|
247
|
-
}
|
|
248
|
-
const s = src;
|
|
249
|
-
const out = {};
|
|
250
|
-
if (typeof s.type === "string") out.type = s.type;
|
|
251
|
-
if (Array.isArray(s.enum)) out.enum = s.enum;
|
|
252
|
-
if (typeof s.description === "string") out.description = s.description;
|
|
253
|
-
if ("default" in s) out.default = s.default;
|
|
254
|
-
if (typeof s.minimum === "number") out.minimum = s.minimum;
|
|
255
|
-
if (typeof s.maximum === "number") out.maximum = s.maximum;
|
|
256
|
-
if (s.items) out.items = toACPInputSchema(s.items);
|
|
257
|
-
if (s.properties && typeof s.properties === "object") {
|
|
258
|
-
const props = {};
|
|
259
|
-
for (const [k, v] of Object.entries(s.properties)) {
|
|
260
|
-
props[k] = toACPInputSchema(v);
|
|
261
|
-
}
|
|
262
|
-
out.properties = props;
|
|
263
|
-
if (Array.isArray(s.required)) out.required = s.required;
|
|
264
|
-
}
|
|
265
|
-
return out;
|
|
266
|
-
}
|
|
267
|
-
function toACPToolResult(result) {
|
|
268
|
-
const blocks = [];
|
|
269
|
-
if (result === void 0 || result === null) {
|
|
270
|
-
return { content: [{ type: "text", text: "ok" }] };
|
|
271
|
-
}
|
|
272
|
-
if (typeof result === "string") {
|
|
273
|
-
blocks.push({ type: "text", text: result });
|
|
274
|
-
} else if (typeof result === "object") {
|
|
275
|
-
blocks.push({ type: "text", text: JSON.stringify(result, null, 2) });
|
|
276
|
-
} else {
|
|
277
|
-
blocks.push({ type: "text", text: String(result) });
|
|
278
|
-
}
|
|
279
|
-
return { content: blocks };
|
|
280
|
-
}
|
|
281
|
-
function toolToPriority(tool) {
|
|
282
|
-
if (tool.riskTier === "destructive") return "high";
|
|
283
|
-
if (tool.riskTier === "standard" || tool.permission === "confirm") return "medium";
|
|
284
|
-
return "low";
|
|
285
|
-
}
|
|
286
|
-
|
|
287
1
|
// src/agent/protocol-handler.ts
|
|
288
2
|
import { randomUUID } from "node:crypto";
|
|
289
3
|
|
|
@@ -797,256 +511,564 @@ var ACPProtocolHandler = class {
|
|
|
797
511
|
if (typeof m.method === "string") {
|
|
798
512
|
return this.handleNotification(m.method, m.params);
|
|
799
513
|
}
|
|
800
|
-
return false;
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
/** Abort all active turns and drop session state. */
|
|
517
|
+
close() {
|
|
518
|
+
for (const [sessionId, session] of this.sessions) {
|
|
519
|
+
session.abort.abort();
|
|
520
|
+
this.disposeSession(sessionId);
|
|
521
|
+
}
|
|
522
|
+
this.sessions.clear();
|
|
523
|
+
for (const [, p] of this.pendingOut) {
|
|
524
|
+
clearTimeout(p.timer);
|
|
525
|
+
p.reject(new Error("protocol handler closed"));
|
|
526
|
+
}
|
|
527
|
+
this.pendingOut.clear();
|
|
528
|
+
}
|
|
529
|
+
disposeSession(sessionId) {
|
|
530
|
+
try {
|
|
531
|
+
this.disposeFor?.(sessionId);
|
|
532
|
+
} catch {
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
sessionContext() {
|
|
536
|
+
return {
|
|
537
|
+
sessions: this.sessions,
|
|
538
|
+
maxSessions: this.maxSessions,
|
|
539
|
+
defaultCwd: this.defaultCwd,
|
|
540
|
+
modes: this.modes,
|
|
541
|
+
configOptions: this.configOptions,
|
|
542
|
+
store: this.store,
|
|
543
|
+
replayFor: this.replayFor,
|
|
544
|
+
seedFor: this.seedFor,
|
|
545
|
+
onSessionNew: this.onSessionNew,
|
|
546
|
+
allocId: () => this.allocId(),
|
|
547
|
+
persist: (state, history) => this.persist(state, history),
|
|
548
|
+
sendNotification: (params) => this.sendNotification(params),
|
|
549
|
+
sendError: (id, code, message, data) => this.sendError(id, code, message, data),
|
|
550
|
+
sendResult: (id, result) => this.sendResult(id, result),
|
|
551
|
+
request: (method, params, timeoutMs) => this.request(method, params, timeoutMs),
|
|
552
|
+
runTurn: this.runTurn,
|
|
553
|
+
clientCapabilities: this.clientCapabilities
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
// ────────────────────────────────────────────────────────────────────
|
|
557
|
+
// Requests
|
|
558
|
+
// ────────────────────────────────────────────────────────────────────
|
|
559
|
+
async handleRequest(id, method, params) {
|
|
560
|
+
if (method !== "initialize" && !this.initialized) {
|
|
561
|
+
await this.sendError(id, -32e3, "Not initialized");
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
switch (method) {
|
|
566
|
+
case "initialize":
|
|
567
|
+
return await this.handleInitialize(id, params);
|
|
568
|
+
case "authenticate":
|
|
569
|
+
return await this.handleAuthenticate(id, params);
|
|
570
|
+
case "logout":
|
|
571
|
+
return await this.handleLogout(id, params);
|
|
572
|
+
case "session/new":
|
|
573
|
+
return await handleSessionNewOp(this.sessionContext(), id, params);
|
|
574
|
+
case "session/load":
|
|
575
|
+
return await handleSessionLoadOp(this.sessionContext(), id, params);
|
|
576
|
+
case "session/resume":
|
|
577
|
+
return await this.handleSessionResume(id, params);
|
|
578
|
+
case "session/close":
|
|
579
|
+
return await this.handleSessionClose(id, params);
|
|
580
|
+
case "session/delete":
|
|
581
|
+
return await this.handleSessionDelete(id, params);
|
|
582
|
+
case "session/prompt":
|
|
583
|
+
return await handleSessionPromptOp(this.sessionContext(), id, params);
|
|
584
|
+
case "session/set_mode":
|
|
585
|
+
return await handleSetModeOp(this.sessionContext(), id, params);
|
|
586
|
+
case "session/set_config_option":
|
|
587
|
+
return await handleSetConfigOptionOp(this.sessionContext(), id, params);
|
|
588
|
+
case "session/list":
|
|
589
|
+
return await this.handleSessionList(id);
|
|
590
|
+
case "session/fork":
|
|
591
|
+
return await handleSessionForkOp(this.sessionContext(), id, params);
|
|
592
|
+
case "providers/list":
|
|
593
|
+
return await this.handleProvidersList(id, params);
|
|
594
|
+
case "providers/set":
|
|
595
|
+
return await this.handleProvidersSet(id, params);
|
|
596
|
+
case "providers/disable":
|
|
597
|
+
return await this.handleProvidersDisable(id, params);
|
|
598
|
+
case "mcp/message":
|
|
599
|
+
return await this.handleMcpMessage(id, params);
|
|
600
|
+
default:
|
|
601
|
+
await this.sendError(id, -32601, `Unknown method: ${method}`);
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
} catch (err) {
|
|
605
|
+
const { code, message, data } = errorToJsonRpc(err);
|
|
606
|
+
await this.sendError(id, code, message, data);
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async handleInitialize(id, params) {
|
|
611
|
+
const p = params ?? {};
|
|
612
|
+
if (p.clientCapabilities && typeof p.clientCapabilities === "object") {
|
|
613
|
+
this.clientCapabilities = p.clientCapabilities;
|
|
614
|
+
}
|
|
615
|
+
this.initialized = true;
|
|
616
|
+
await this.sendResult(
|
|
617
|
+
id,
|
|
618
|
+
buildInitializeResult(this.agentName, this.modes, this.configOptions)
|
|
619
|
+
);
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
async handleAuthenticate(id, _params) {
|
|
623
|
+
await this.sendResult(id, { outcome: "unauthenticated" });
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
async handleLogout(id, _params) {
|
|
627
|
+
await this.sendResult(id, {});
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
async handleSessionResume(id, params) {
|
|
631
|
+
const p = params ?? {};
|
|
632
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
633
|
+
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
634
|
+
if (existing) {
|
|
635
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
636
|
+
await this.sendResult(id, {
|
|
637
|
+
initialMode: {
|
|
638
|
+
currentModeId: existing.modeId,
|
|
639
|
+
availableModes: this.modes
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
async handleSessionClose(id, params) {
|
|
648
|
+
const p = params ?? {};
|
|
649
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
650
|
+
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
651
|
+
if (!session) {
|
|
652
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
session.abort.abort();
|
|
656
|
+
this.sessions.delete(sessionId);
|
|
657
|
+
this.disposeSession(sessionId);
|
|
658
|
+
await this.sendResult(id, {});
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
async handleSessionDelete(id, params) {
|
|
662
|
+
const p = params ?? {};
|
|
663
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
664
|
+
if (!sessionId) {
|
|
665
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
if (!this.sessions.has(sessionId)) {
|
|
669
|
+
await this.sendResult(id, { configOptions: [...this.configOptions] });
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
const session = this.sessions.get(sessionId);
|
|
673
|
+
session.abort.abort();
|
|
674
|
+
this.sessions.delete(sessionId);
|
|
675
|
+
this.disposeSession(sessionId);
|
|
676
|
+
await this.sendResult(id, {});
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
async handleProvidersList(id, _params) {
|
|
680
|
+
await this.sendResult(id, {
|
|
681
|
+
providers: [],
|
|
682
|
+
currentProviderId: null
|
|
683
|
+
});
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
async handleProvidersSet(id, _params) {
|
|
687
|
+
await this.sendError(
|
|
688
|
+
id,
|
|
689
|
+
-32e3,
|
|
690
|
+
"provider configuration not available through ACP; use wstack auth"
|
|
691
|
+
);
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
async handleProvidersDisable(id, _params) {
|
|
695
|
+
await this.sendResult(id, {});
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
async handleMcpMessage(id, _params) {
|
|
699
|
+
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
async handleSessionList(id) {
|
|
703
|
+
const sessions = Array.from(this.sessions.values()).map((s) => {
|
|
704
|
+
const out = {
|
|
705
|
+
sessionId: s.id,
|
|
706
|
+
cwd: s.cwd,
|
|
707
|
+
updatedAt: s.updatedAt
|
|
708
|
+
};
|
|
709
|
+
if (s.title !== void 0) out.title = s.title;
|
|
710
|
+
return out;
|
|
711
|
+
});
|
|
712
|
+
await this.sendResult(id, { sessions });
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
// ────────────────────────────────────────────────────────────────────
|
|
716
|
+
// Notifications
|
|
717
|
+
// ────────────────────────────────────────────────────────────────────
|
|
718
|
+
async handleNotification(method, params) {
|
|
719
|
+
switch (method) {
|
|
720
|
+
case "session/cancel": {
|
|
721
|
+
const p = params ?? {};
|
|
722
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
723
|
+
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
724
|
+
if (session) {
|
|
725
|
+
session.abort.abort();
|
|
726
|
+
}
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
case "$/cancel_request": {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
case "exit":
|
|
733
|
+
this.close();
|
|
734
|
+
return true;
|
|
735
|
+
default:
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
801
738
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
}
|
|
808
|
-
this.sessions.clear();
|
|
809
|
-
for (const [, p] of this.pendingOut) {
|
|
810
|
-
clearTimeout(p.timer);
|
|
811
|
-
p.reject(new Error("protocol handler closed"));
|
|
812
|
-
}
|
|
813
|
-
this.pendingOut.clear();
|
|
739
|
+
// ────────────────────────────────────────────────────────────────────
|
|
740
|
+
// Wire helpers
|
|
741
|
+
// ────────────────────────────────────────────────────────────────────
|
|
742
|
+
async sendNotification(params) {
|
|
743
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", method: "session/update", params }));
|
|
814
744
|
}
|
|
815
|
-
|
|
745
|
+
async sendResult(id, result) {
|
|
746
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result }));
|
|
747
|
+
}
|
|
748
|
+
async persist(state, history = void 0) {
|
|
749
|
+
if (!this.store) return;
|
|
816
750
|
try {
|
|
817
|
-
this.
|
|
751
|
+
await this.store.save(state, history ?? this.replayFor?.(state.id));
|
|
818
752
|
} catch {
|
|
819
753
|
}
|
|
820
754
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
defaultCwd: this.defaultCwd,
|
|
826
|
-
modes: this.modes,
|
|
827
|
-
configOptions: this.configOptions,
|
|
828
|
-
store: this.store,
|
|
829
|
-
replayFor: this.replayFor,
|
|
830
|
-
seedFor: this.seedFor,
|
|
831
|
-
onSessionNew: this.onSessionNew,
|
|
832
|
-
allocId: () => this.allocId(),
|
|
833
|
-
persist: (state, history) => this.persist(state, history),
|
|
834
|
-
sendNotification: (params) => this.sendNotification(params),
|
|
835
|
-
sendError: (id, code, message, data) => this.sendError(id, code, message, data),
|
|
836
|
-
sendResult: (id, result) => this.sendResult(id, result),
|
|
837
|
-
request: (method, params, timeoutMs) => this.request(method, params, timeoutMs),
|
|
838
|
-
runTurn: this.runTurn,
|
|
839
|
-
clientCapabilities: this.clientCapabilities
|
|
840
|
-
};
|
|
755
|
+
async sendError(id, code, message, data) {
|
|
756
|
+
const error = { code, message };
|
|
757
|
+
if (data !== void 0) error.data = data;
|
|
758
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, error }));
|
|
841
759
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
760
|
+
allocId() {
|
|
761
|
+
return `${this.nextId++}_${randomUUID().replaceAll("-", "")}`;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
// src/agent/server-agent-turn.ts
|
|
766
|
+
function makeACPServerAgentTurn(opts) {
|
|
767
|
+
const agents = /* @__PURE__ */ new Map();
|
|
768
|
+
const timeouts = /* @__PURE__ */ new Map();
|
|
769
|
+
const history = /* @__PURE__ */ new Map();
|
|
770
|
+
const historyBytes = /* @__PURE__ */ new Map();
|
|
771
|
+
const pendingSeed = /* @__PURE__ */ new Set();
|
|
772
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
773
|
+
const maxHistoryEntries = finitePositiveLimit(opts.maxHistoryEntries, 1e3);
|
|
774
|
+
const maxHistoryBytes = finitePositiveLimit(opts.maxHistoryBytes, 8 * 1024 * 1024);
|
|
775
|
+
const turn = async (input, emit, api) => {
|
|
776
|
+
let agent = agents.get(input.sessionId);
|
|
777
|
+
if (!agent) {
|
|
778
|
+
agent = await opts.agentFor(input.sessionId, process.cwd(), api);
|
|
779
|
+
agents.set(input.sessionId, agent);
|
|
780
|
+
if (pendingSeed.has(input.sessionId)) {
|
|
781
|
+
pendingSeed.delete(input.sessionId);
|
|
782
|
+
seedAgentContext(agent, history.get(input.sessionId));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const turnAbort = new AbortController();
|
|
786
|
+
const abortForTimeout = () => turnAbort.abort();
|
|
787
|
+
const onParentAbort = () => turnAbort.abort();
|
|
788
|
+
if (input.signal.aborted) {
|
|
789
|
+
turnAbort.abort();
|
|
790
|
+
} else {
|
|
791
|
+
input.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
792
|
+
}
|
|
793
|
+
const timer = setTimeout(() => {
|
|
794
|
+
timeouts.delete(input.sessionId);
|
|
795
|
+
abortForTimeout();
|
|
796
|
+
}, timeoutMs);
|
|
797
|
+
timeouts.set(input.sessionId, timer);
|
|
798
|
+
const unsub = [];
|
|
799
|
+
const bus = agent.events;
|
|
800
|
+
if (bus?.on) {
|
|
801
|
+
unsub.push(
|
|
802
|
+
bus.on("tool.started", (e) => {
|
|
803
|
+
emit({
|
|
804
|
+
sessionUpdate: "tool_call",
|
|
805
|
+
toolCallId: e.id,
|
|
806
|
+
title: toolTitle(e.name, e.input),
|
|
807
|
+
kind: toolNameToKind(e.name),
|
|
808
|
+
status: "in_progress",
|
|
809
|
+
...isRecord(e.input) ? { rawInput: e.input } : {}
|
|
810
|
+
});
|
|
811
|
+
}),
|
|
812
|
+
bus.on("tool.executed", (e) => {
|
|
813
|
+
emit({
|
|
814
|
+
sessionUpdate: "tool_call_update",
|
|
815
|
+
toolCallId: e.id ?? e.name,
|
|
816
|
+
status: e.ok ? "completed" : "failed",
|
|
817
|
+
...e.output !== void 0 ? {
|
|
818
|
+
content: [{ type: "content", content: { type: "text", text: e.output } }]
|
|
819
|
+
} : {}
|
|
820
|
+
});
|
|
821
|
+
})
|
|
822
|
+
);
|
|
849
823
|
}
|
|
850
824
|
try {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
return await handleSessionNewOp(this.sessionContext(), id, params);
|
|
860
|
-
case "session/load":
|
|
861
|
-
return await handleSessionLoadOp(this.sessionContext(), id, params);
|
|
862
|
-
case "session/resume":
|
|
863
|
-
return await this.handleSessionResume(id, params);
|
|
864
|
-
case "session/close":
|
|
865
|
-
return await this.handleSessionClose(id, params);
|
|
866
|
-
case "session/delete":
|
|
867
|
-
return await this.handleSessionDelete(id, params);
|
|
868
|
-
case "session/prompt":
|
|
869
|
-
return await handleSessionPromptOp(this.sessionContext(), id, params);
|
|
870
|
-
case "session/set_mode":
|
|
871
|
-
return await handleSetModeOp(this.sessionContext(), id, params);
|
|
872
|
-
case "session/set_config_option":
|
|
873
|
-
return await handleSetConfigOptionOp(this.sessionContext(), id, params);
|
|
874
|
-
case "session/list":
|
|
875
|
-
return await this.handleSessionList(id);
|
|
876
|
-
case "session/fork":
|
|
877
|
-
return await handleSessionForkOp(this.sessionContext(), id, params);
|
|
878
|
-
case "providers/list":
|
|
879
|
-
return await this.handleProvidersList(id, params);
|
|
880
|
-
case "providers/set":
|
|
881
|
-
return await this.handleProvidersSet(id, params);
|
|
882
|
-
case "providers/disable":
|
|
883
|
-
return await this.handleProvidersDisable(id, params);
|
|
884
|
-
case "mcp/message":
|
|
885
|
-
return await this.handleMcpMessage(id, params);
|
|
886
|
-
default:
|
|
887
|
-
await this.sendError(id, -32601, `Unknown method: ${method}`);
|
|
888
|
-
return false;
|
|
825
|
+
const userInput = promptToAgentInput(input.prompt);
|
|
826
|
+
const result = await agent.run(userInput, { signal: turnAbort.signal });
|
|
827
|
+
const text = extractText(result);
|
|
828
|
+
if (text) {
|
|
829
|
+
emit({
|
|
830
|
+
sessionUpdate: "agent_message_chunk",
|
|
831
|
+
content: { type: "text", text }
|
|
832
|
+
});
|
|
889
833
|
}
|
|
890
|
-
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
834
|
+
const userText = promptToText(input.prompt);
|
|
835
|
+
const hist = history.get(input.sessionId) ?? [];
|
|
836
|
+
let retainedHistoryBytes = historyBytes.get(input.sessionId) ?? 0;
|
|
837
|
+
if (userText) {
|
|
838
|
+
const update = {
|
|
839
|
+
sessionUpdate: "user_message_chunk",
|
|
840
|
+
content: { type: "text", text: userText }
|
|
841
|
+
};
|
|
842
|
+
hist.push(update);
|
|
843
|
+
retainedHistoryBytes += replayEntryBytes(update);
|
|
844
|
+
}
|
|
845
|
+
if (text) {
|
|
846
|
+
const update = {
|
|
847
|
+
sessionUpdate: "agent_message_chunk",
|
|
848
|
+
content: { type: "text", text }
|
|
849
|
+
};
|
|
850
|
+
hist.push(update);
|
|
851
|
+
retainedHistoryBytes += replayEntryBytes(update);
|
|
852
|
+
}
|
|
853
|
+
retainedHistoryBytes = trimHistory(
|
|
854
|
+
hist,
|
|
855
|
+
retainedHistoryBytes,
|
|
856
|
+
maxHistoryEntries,
|
|
857
|
+
maxHistoryBytes
|
|
858
|
+
);
|
|
859
|
+
if (hist.length > 0) {
|
|
860
|
+
history.set(input.sessionId, hist);
|
|
861
|
+
historyBytes.set(input.sessionId, retainedHistoryBytes);
|
|
862
|
+
} else {
|
|
863
|
+
history.delete(input.sessionId);
|
|
864
|
+
historyBytes.delete(input.sessionId);
|
|
865
|
+
}
|
|
866
|
+
const plan = extractPlan(result);
|
|
867
|
+
if (plan.length > 0) {
|
|
868
|
+
emit({
|
|
869
|
+
sessionUpdate: "plan",
|
|
870
|
+
entries: plan
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
const usage = extractUsage(result);
|
|
874
|
+
if (usage) {
|
|
875
|
+
emit({
|
|
876
|
+
sessionUpdate: "usage_update",
|
|
877
|
+
used: usage.used,
|
|
878
|
+
size: usage.size,
|
|
879
|
+
...usage.cost ? { cost: usage.cost } : {}
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
const result_out = {
|
|
883
|
+
// `turnAbort.signal` covers both client cancellation and the
|
|
884
|
+
// wall-clock timeout, so either maps to stopReason 'cancelled'.
|
|
885
|
+
stopReason: pickStopReason(result, turnAbort.signal)
|
|
886
|
+
};
|
|
887
|
+
if (text) result_out.text = text;
|
|
888
|
+
const runTurnPlan = extractPlan(result);
|
|
889
|
+
if (runTurnPlan.length > 0) result_out.plan = runTurnPlan;
|
|
890
|
+
if (usage) result_out.usage = usage;
|
|
891
|
+
return result_out;
|
|
892
|
+
} finally {
|
|
893
|
+
clearTimeout(timer);
|
|
894
|
+
timeouts.delete(input.sessionId);
|
|
895
|
+
input.signal.removeEventListener("abort", onParentAbort);
|
|
896
|
+
for (const u of unsub) u();
|
|
900
897
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
898
|
+
};
|
|
899
|
+
const replay = (sessionId) => [...history.get(sessionId) ?? []];
|
|
900
|
+
const seed = (sessionId, incoming) => {
|
|
901
|
+
if (incoming.length === 0) return;
|
|
902
|
+
const seeded = [...incoming];
|
|
903
|
+
const retainedBytes = trimHistory(
|
|
904
|
+
seeded,
|
|
905
|
+
seeded.reduce((total, entry) => total + replayEntryBytes(entry), 0),
|
|
906
|
+
maxHistoryEntries,
|
|
907
|
+
maxHistoryBytes
|
|
905
908
|
);
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
909
|
+
history.set(sessionId, seeded);
|
|
910
|
+
historyBytes.set(sessionId, retainedBytes);
|
|
911
|
+
pendingSeed.add(sessionId);
|
|
912
|
+
};
|
|
913
|
+
const dispose = (sessionId) => {
|
|
914
|
+
const timer = timeouts.get(sessionId);
|
|
915
|
+
if (timer) clearTimeout(timer);
|
|
916
|
+
timeouts.delete(sessionId);
|
|
917
|
+
agents.delete(sessionId);
|
|
918
|
+
history.delete(sessionId);
|
|
919
|
+
historyBytes.delete(sessionId);
|
|
920
|
+
pendingSeed.delete(sessionId);
|
|
921
|
+
};
|
|
922
|
+
return Object.assign(turn, { replay, seed, dispose });
|
|
923
|
+
}
|
|
924
|
+
function finitePositiveLimit(value, fallback) {
|
|
925
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
926
|
+
}
|
|
927
|
+
function trimHistory(entries, retainedBytes, maxEntries, maxBytes) {
|
|
928
|
+
while (entries.length > maxEntries || retainedBytes > maxBytes) {
|
|
929
|
+
const removed = entries.shift();
|
|
930
|
+
if (!removed) break;
|
|
931
|
+
retainedBytes -= replayEntryBytes(removed);
|
|
911
932
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
933
|
+
return Math.max(0, retainedBytes);
|
|
934
|
+
}
|
|
935
|
+
function replayEntryBytes(entry) {
|
|
936
|
+
return Buffer.byteLength(JSON.stringify(entry), "utf8");
|
|
937
|
+
}
|
|
938
|
+
function seedAgentContext(agent, history) {
|
|
939
|
+
const state = agent.ctx?.state;
|
|
940
|
+
if (!state?.appendMessage) return;
|
|
941
|
+
for (const u of history) {
|
|
942
|
+
const text = u.content?.text;
|
|
943
|
+
if (typeof text !== "string" || text.length === 0) continue;
|
|
944
|
+
const role = u.sessionUpdate === "user_message_chunk" ? "user" : "assistant";
|
|
945
|
+
state.appendMessage({ role, content: text });
|
|
915
946
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
947
|
+
}
|
|
948
|
+
function toolNameToKind(name) {
|
|
949
|
+
const n = name.toLowerCase();
|
|
950
|
+
if (n.includes("read") || n.includes("cat")) return "read";
|
|
951
|
+
if (n.includes("write") || n.includes("edit") || n.includes("apply") || n.includes("patch"))
|
|
952
|
+
return "edit";
|
|
953
|
+
if (n.includes("delete") || n === "rm" || n.startsWith("rm_") || n.endsWith("_rm"))
|
|
954
|
+
return "delete";
|
|
955
|
+
if (n.includes("move") || n.includes("rename") || n.includes("mv")) return "move";
|
|
956
|
+
if (n.includes("grep") || n.includes("glob") || n.includes("search") || n.includes("find"))
|
|
957
|
+
return "search";
|
|
958
|
+
if (n.includes("bash") || n.includes("shell") || n.includes("exec") || n.includes("run") || n.includes("terminal"))
|
|
959
|
+
return "execute";
|
|
960
|
+
if (n.includes("fetch") || n.includes("http") || n.includes("web") || n.includes("url"))
|
|
961
|
+
return "fetch";
|
|
962
|
+
if (n.includes("think") || n.includes("plan")) return "think";
|
|
963
|
+
return "other";
|
|
964
|
+
}
|
|
965
|
+
function toolTitle(name, input) {
|
|
966
|
+
if (isRecord(input)) {
|
|
967
|
+
const path3 = input.path ?? input.file ?? input.filePath ?? input.pattern ?? input.command;
|
|
968
|
+
if (typeof path3 === "string" && path3.length > 0) {
|
|
969
|
+
return `${name}: ${path3.length > 80 ? `${path3.slice(0, 77)}\u2026` : path3}`;
|
|
929
970
|
}
|
|
930
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
931
|
-
return false;
|
|
932
971
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
this.sessions.delete(sessionId);
|
|
943
|
-
this.disposeSession(sessionId);
|
|
944
|
-
await this.sendResult(id, {});
|
|
945
|
-
return false;
|
|
972
|
+
return name;
|
|
973
|
+
}
|
|
974
|
+
function isRecord(v) {
|
|
975
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
976
|
+
}
|
|
977
|
+
function promptToAgentInput(blocks) {
|
|
978
|
+
const hasImage = blocks.some((b) => b.type === "image");
|
|
979
|
+
if (!hasImage) {
|
|
980
|
+
return promptToText(blocks);
|
|
946
981
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
982
|
+
const out = [];
|
|
983
|
+
for (const b of blocks) {
|
|
984
|
+
if (b.type === "text") {
|
|
985
|
+
out.push({ type: "text", text: b.text });
|
|
986
|
+
} else if (b.type === "image") {
|
|
987
|
+
out.push({
|
|
988
|
+
type: "image",
|
|
989
|
+
source: { type: "base64", media_type: b.mimeType, data: b.data }
|
|
990
|
+
});
|
|
991
|
+
} else if (b.type === "audio") {
|
|
992
|
+
out.push({ type: "text", text: `[audio: ${b.mimeType}]` });
|
|
993
|
+
} else if (b.type === "resource") {
|
|
994
|
+
const text = "text" in b.resource && typeof b.resource.text === "string" ? b.resource.text : `[embedded resource: ${b.resource.uri}]`;
|
|
995
|
+
out.push({ type: "text", text });
|
|
996
|
+
} else if (b.type === "resource_link") {
|
|
997
|
+
out.push({ type: "text", text: `[resource link: ${b.uri}]` });
|
|
957
998
|
}
|
|
958
|
-
const session = this.sessions.get(sessionId);
|
|
959
|
-
session.abort.abort();
|
|
960
|
-
this.sessions.delete(sessionId);
|
|
961
|
-
this.disposeSession(sessionId);
|
|
962
|
-
await this.sendResult(id, {});
|
|
963
|
-
return false;
|
|
964
|
-
}
|
|
965
|
-
async handleProvidersList(id, _params) {
|
|
966
|
-
await this.sendResult(id, {
|
|
967
|
-
providers: [],
|
|
968
|
-
currentProviderId: null
|
|
969
|
-
});
|
|
970
|
-
return false;
|
|
971
|
-
}
|
|
972
|
-
async handleProvidersSet(id, _params) {
|
|
973
|
-
await this.sendError(
|
|
974
|
-
id,
|
|
975
|
-
-32e3,
|
|
976
|
-
"provider configuration not available through ACP; use wstack auth"
|
|
977
|
-
);
|
|
978
|
-
return false;
|
|
979
|
-
}
|
|
980
|
-
async handleProvidersDisable(id, _params) {
|
|
981
|
-
await this.sendResult(id, {});
|
|
982
|
-
return false;
|
|
983
|
-
}
|
|
984
|
-
async handleMcpMessage(id, _params) {
|
|
985
|
-
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
986
|
-
return false;
|
|
987
999
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
})
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
+
return out;
|
|
1001
|
+
}
|
|
1002
|
+
function promptToText(blocks) {
|
|
1003
|
+
const parts = [];
|
|
1004
|
+
for (const b of blocks) {
|
|
1005
|
+
if (b.type === "text") {
|
|
1006
|
+
parts.push(b.text);
|
|
1007
|
+
} else if (b.type === "image") {
|
|
1008
|
+
parts.push(`[image: ${b.mimeType}]`);
|
|
1009
|
+
} else if (b.type === "audio") {
|
|
1010
|
+
parts.push(`[audio: ${b.mimeType}]`);
|
|
1011
|
+
} else if (b.type === "resource") {
|
|
1012
|
+
parts.push(`[embedded resource: ${b.resource.uri}]`);
|
|
1013
|
+
} else if (b.type === "resource_link") {
|
|
1014
|
+
parts.push(`[resource link: ${b.uri}]`);
|
|
1015
|
+
}
|
|
1000
1016
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
return false;
|
|
1014
|
-
}
|
|
1015
|
-
case "$/cancel_request": {
|
|
1016
|
-
return false;
|
|
1017
|
+
return parts.join("\n").trim();
|
|
1018
|
+
}
|
|
1019
|
+
function extractText(result) {
|
|
1020
|
+
if (typeof result !== "object" || result === null) return "";
|
|
1021
|
+
const r = result;
|
|
1022
|
+
if (typeof r.text === "string") return r.text;
|
|
1023
|
+
if (Array.isArray(r.content)) {
|
|
1024
|
+
const parts = [];
|
|
1025
|
+
for (const c of r.content) {
|
|
1026
|
+
if (typeof c === "object" && c !== null) {
|
|
1027
|
+
const cb = c;
|
|
1028
|
+
if (cb.type === "text" && typeof cb.text === "string") parts.push(cb.text);
|
|
1017
1029
|
}
|
|
1018
|
-
case "exit":
|
|
1019
|
-
this.close();
|
|
1020
|
-
return true;
|
|
1021
|
-
default:
|
|
1022
|
-
return false;
|
|
1023
1030
|
}
|
|
1031
|
+
return parts.join("");
|
|
1024
1032
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
+
return "";
|
|
1034
|
+
}
|
|
1035
|
+
function pickStopReason(result, signal) {
|
|
1036
|
+
if (signal.aborted) return "cancelled";
|
|
1037
|
+
if (typeof result !== "object" || result === null) return "end_turn";
|
|
1038
|
+
const r = result;
|
|
1039
|
+
if (r.error) {
|
|
1040
|
+
return "end_turn";
|
|
1033
1041
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
try {
|
|
1037
|
-
await this.store.save(state, history ?? this.replayFor?.(state.id));
|
|
1038
|
-
} catch {
|
|
1039
|
-
}
|
|
1042
|
+
if (typeof r.stopReason === "string" && r.stopReason) {
|
|
1043
|
+
return r.stopReason;
|
|
1040
1044
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
+
return "end_turn";
|
|
1046
|
+
}
|
|
1047
|
+
function extractPlan(result) {
|
|
1048
|
+
if (typeof result !== "object" || result === null) return [];
|
|
1049
|
+
const r = result;
|
|
1050
|
+
if (Array.isArray(r.plan)) {
|
|
1051
|
+
return r.plan.filter(
|
|
1052
|
+
(e) => typeof e === "object" && e !== null && typeof e.content === "string"
|
|
1053
|
+
);
|
|
1045
1054
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1055
|
+
return [];
|
|
1056
|
+
}
|
|
1057
|
+
function extractUsage(result) {
|
|
1058
|
+
if (typeof result !== "object" || result === null) return null;
|
|
1059
|
+
const r = result;
|
|
1060
|
+
if (typeof r.usage === "object" && r.usage !== null) {
|
|
1061
|
+
const u = r.usage;
|
|
1062
|
+
if (typeof u.used === "number" && typeof u.size === "number") {
|
|
1063
|
+
return {
|
|
1064
|
+
used: u.used,
|
|
1065
|
+
size: u.size,
|
|
1066
|
+
...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1048
1069
|
}
|
|
1049
|
-
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1050
1072
|
|
|
1051
1073
|
// src/agent/session-store.ts
|
|
1052
1074
|
import * as fsp2 from "node:fs/promises";
|
|
@@ -1311,52 +1333,289 @@ var ACPSessionStore = class {
|
|
|
1311
1333
|
}
|
|
1312
1334
|
};
|
|
1313
1335
|
|
|
1314
|
-
// src/agent/
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1336
|
+
// src/agent/stdio-transport.ts
|
|
1337
|
+
import { expectDefined, writeErr } from "@wrongstack/core/utils";
|
|
1338
|
+
import { treeKill } from "@wrongstack/core/utils/tree-kill";
|
|
1339
|
+
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
1340
|
+
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
1341
|
+
var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
|
|
1342
|
+
function positiveLimit(value, fallback) {
|
|
1343
|
+
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
1344
|
+
}
|
|
1345
|
+
var StdioTransport = class {
|
|
1346
|
+
stdin = process.stdin;
|
|
1347
|
+
stdout = process.stdout;
|
|
1348
|
+
stderr = process.stderr;
|
|
1349
|
+
buffer = "";
|
|
1321
1350
|
handlers = /* @__PURE__ */ new Set();
|
|
1351
|
+
claimHandlers = /* @__PURE__ */ new Set();
|
|
1322
1352
|
closed = false;
|
|
1353
|
+
resolveRead = null;
|
|
1354
|
+
messageQueue = [];
|
|
1355
|
+
queuedChars = 0;
|
|
1356
|
+
maxFrameChars;
|
|
1357
|
+
maxQueuedMessages;
|
|
1358
|
+
maxQueuedChars;
|
|
1359
|
+
onStdinData = (chunk) => this.onData(chunk);
|
|
1360
|
+
onStdinEnd = () => this.handleClose();
|
|
1361
|
+
onStdinError = (err) => this.failAll(err);
|
|
1362
|
+
constructor(opts = {}) {
|
|
1363
|
+
this.maxFrameChars = positiveLimit(opts.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
1364
|
+
this.maxQueuedMessages = positiveLimit(opts.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
|
|
1365
|
+
this.maxQueuedChars = positiveLimit(opts.maxQueuedChars, DEFAULT_MAX_QUEUED_CHARS);
|
|
1366
|
+
this.stdin.resume();
|
|
1367
|
+
this.stdin.setEncoding("utf8");
|
|
1368
|
+
this.stdin.on("data", this.onStdinData);
|
|
1369
|
+
this.stdin.on("end", this.onStdinEnd);
|
|
1370
|
+
this.stdin.on("error", this.onStdinError);
|
|
1371
|
+
}
|
|
1372
|
+
sendStartupMarker() {
|
|
1373
|
+
this.stdout.write("[wstack-acp]\n", "utf8");
|
|
1374
|
+
}
|
|
1323
1375
|
send(msg) {
|
|
1324
1376
|
if (this.closed) return Promise.resolve();
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1377
|
+
return new Promise((resolve2) => {
|
|
1378
|
+
const line = JSON.stringify(msg) + "\n";
|
|
1379
|
+
this.stdout.write(line, "utf8", () => resolve2());
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
sendRaw(chunk) {
|
|
1383
|
+
this.stdout.write(chunk, "utf8");
|
|
1384
|
+
}
|
|
1385
|
+
read() {
|
|
1386
|
+
if (this.messageQueue.length > 0) {
|
|
1387
|
+
const queued = expectDefined(this.messageQueue.shift());
|
|
1388
|
+
this.queuedChars = Math.max(0, this.queuedChars - queued.chars);
|
|
1389
|
+
return Promise.resolve(queued.message);
|
|
1328
1390
|
}
|
|
1329
|
-
return Promise.resolve();
|
|
1391
|
+
if (this.closed) return Promise.resolve(null);
|
|
1392
|
+
return new Promise((resolve2) => {
|
|
1393
|
+
this.resolveRead = resolve2;
|
|
1394
|
+
});
|
|
1330
1395
|
}
|
|
1331
|
-
|
|
1396
|
+
onMessage(handler) {
|
|
1397
|
+
this.handlers.add(handler);
|
|
1398
|
+
return () => this.handlers.delete(handler);
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Register a handler that MAY claim a message by returning `true`.
|
|
1402
|
+
* Claimed messages are NOT enqueued for the read() loop — they are
|
|
1403
|
+
* considered fully consumed by the handler. This is how the ACP
|
|
1404
|
+
* server transport prevents the correlation handler (which always
|
|
1405
|
+
* fires on every message but only actually correlates responses)
|
|
1406
|
+
* from starving the read() loop of pipelined requests, notifications,
|
|
1407
|
+
* and any message the handler chose to ignore. See dispatch() for
|
|
1408
|
+
* the gating logic.
|
|
1409
|
+
*/
|
|
1410
|
+
onMessageClaim(handler) {
|
|
1411
|
+
this.claimHandlers.add(handler);
|
|
1412
|
+
return () => this.claimHandlers.delete(handler);
|
|
1413
|
+
}
|
|
1414
|
+
close() {
|
|
1415
|
+
this.closed = true;
|
|
1416
|
+
this.stdin.off("data", this.onStdinData);
|
|
1417
|
+
this.stdin.off("end", this.onStdinEnd);
|
|
1418
|
+
this.stdin.off("error", this.onStdinError);
|
|
1419
|
+
this.stdin.pause();
|
|
1420
|
+
this.resolveRead?.(null);
|
|
1421
|
+
this.resolveRead = null;
|
|
1422
|
+
this.buffer = "";
|
|
1423
|
+
this.messageQueue.length = 0;
|
|
1424
|
+
this.queuedChars = 0;
|
|
1425
|
+
this.handlers.clear();
|
|
1426
|
+
this.claimHandlers.clear();
|
|
1427
|
+
}
|
|
1428
|
+
onData(chunk) {
|
|
1429
|
+
this.buffer += chunk;
|
|
1430
|
+
const lines = this.buffer.split("\n");
|
|
1431
|
+
this.buffer = lines.pop() ?? "";
|
|
1432
|
+
if (this.buffer.length > this.maxFrameChars) {
|
|
1433
|
+
this.stderr.write(
|
|
1434
|
+
`[wstack-acp frame error] pending frame exceeds ${this.maxFrameChars} characters
|
|
1435
|
+
`,
|
|
1436
|
+
"utf8"
|
|
1437
|
+
);
|
|
1438
|
+
this.close();
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
for (const raw of lines) {
|
|
1442
|
+
if (!raw.trim()) continue;
|
|
1443
|
+
if (raw.length > this.maxFrameChars) {
|
|
1444
|
+
this.stderr.write(
|
|
1445
|
+
`[wstack-acp frame error] frame exceeds ${this.maxFrameChars} characters
|
|
1446
|
+
`,
|
|
1447
|
+
"utf8"
|
|
1448
|
+
);
|
|
1449
|
+
this.close();
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
try {
|
|
1453
|
+
this.dispatch(JSON.parse(raw), raw.length);
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
this.stderr.write(`[wstack-acp parse error] ${err}
|
|
1456
|
+
`, "utf8");
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
1461
|
+
if (this.resolveRead) {
|
|
1462
|
+
const resolve2 = this.resolveRead;
|
|
1463
|
+
this.resolveRead = null;
|
|
1464
|
+
resolve2(msg);
|
|
1465
|
+
} else {
|
|
1466
|
+
let claimed = false;
|
|
1467
|
+
for (const handler of this.claimHandlers) {
|
|
1468
|
+
try {
|
|
1469
|
+
if (handler(msg)) claimed = true;
|
|
1470
|
+
} catch (err) {
|
|
1471
|
+
this.stderr.write(`[wstack-acp handler error] ${err}
|
|
1472
|
+
`, "utf8");
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (!claimed) {
|
|
1476
|
+
if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
|
|
1477
|
+
this.stderr.write(
|
|
1478
|
+
`[wstack-acp queue error] pending message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters
|
|
1479
|
+
`,
|
|
1480
|
+
"utf8"
|
|
1481
|
+
);
|
|
1482
|
+
this.close();
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
this.messageQueue.push({ message: msg, chars });
|
|
1486
|
+
this.queuedChars += chars;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
for (const handler of this.handlers) {
|
|
1490
|
+
try {
|
|
1491
|
+
handler(msg);
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
this.stderr.write(`[wstack-acp handler error] ${err}
|
|
1494
|
+
`, "utf8");
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
handleClose() {
|
|
1499
|
+
this.close();
|
|
1500
|
+
}
|
|
1501
|
+
failAll(err) {
|
|
1502
|
+
this.stderr.write(`[wstack-acp stdin error] ${err.message}
|
|
1503
|
+
`, "utf8");
|
|
1504
|
+
this.close();
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
// src/agent/tools-registry.ts
|
|
1509
|
+
var ACPToolsRegistry = class {
|
|
1510
|
+
tools = /* @__PURE__ */ new Map();
|
|
1511
|
+
owner;
|
|
1512
|
+
constructor(owner = "wrongstack") {
|
|
1513
|
+
this.owner = owner;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Register one or more tools.
|
|
1517
|
+
* Throws on duplicate name unless force=true.
|
|
1518
|
+
*/
|
|
1519
|
+
register(tools) {
|
|
1520
|
+
for (const tool of tools) {
|
|
1521
|
+
this.tools.set(tool.name, tool);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Replace the current tool set.
|
|
1526
|
+
*/
|
|
1527
|
+
setTools(tools) {
|
|
1528
|
+
this.tools.clear();
|
|
1529
|
+
for (const tool of tools) this.tools.set(tool.name, tool);
|
|
1530
|
+
}
|
|
1531
|
+
get(name) {
|
|
1532
|
+
return this.tools.get(name);
|
|
1533
|
+
}
|
|
1534
|
+
has(name) {
|
|
1535
|
+
return this.tools.has(name);
|
|
1536
|
+
}
|
|
1537
|
+
list() {
|
|
1538
|
+
return Array.from(this.tools.values());
|
|
1539
|
+
}
|
|
1540
|
+
/** Build the ACP tools/list payload from registered tools. */
|
|
1541
|
+
buildToolList() {
|
|
1542
|
+
return {
|
|
1543
|
+
tools: Array.from(this.tools.values()).map((t) => toACPToolDefinition(t, this.owner))
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Execute a tool by name and return ACP-formatted result.
|
|
1548
|
+
* Returns null if the tool is not found.
|
|
1549
|
+
*/
|
|
1550
|
+
async execute(name, args, ctx, signal) {
|
|
1551
|
+
const tool = this.tools.get(name);
|
|
1552
|
+
if (!tool) return null;
|
|
1553
|
+
try {
|
|
1554
|
+
const result = await tool.execute(args, ctx, {
|
|
1555
|
+
signal
|
|
1556
|
+
});
|
|
1557
|
+
return toACPToolResult(result);
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1560
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
1561
|
+
}
|
|
1332
1562
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1563
|
+
};
|
|
1564
|
+
function toACPToolDefinition(tool, _owner) {
|
|
1565
|
+
return {
|
|
1566
|
+
name: tool.name,
|
|
1567
|
+
description: tool.description,
|
|
1568
|
+
inputSchema: toACPInputSchema(tool.inputSchema),
|
|
1569
|
+
annotations: {
|
|
1570
|
+
title: tool.name,
|
|
1571
|
+
description: tool.usageHint ?? tool.description,
|
|
1572
|
+
priority: toolToPriority(tool),
|
|
1573
|
+
alwaysAccept: tool.permission === "auto"
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
function toACPInputSchema(src) {
|
|
1578
|
+
if (!src || typeof src !== "object") {
|
|
1579
|
+
return {};
|
|
1335
1580
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1581
|
+
const s = src;
|
|
1582
|
+
const out = {};
|
|
1583
|
+
if (typeof s.type === "string") out.type = s.type;
|
|
1584
|
+
if (Array.isArray(s.enum)) out.enum = s.enum;
|
|
1585
|
+
if (typeof s.description === "string") out.description = s.description;
|
|
1586
|
+
if ("default" in s) out.default = s.default;
|
|
1587
|
+
if (typeof s.minimum === "number") out.minimum = s.minimum;
|
|
1588
|
+
if (typeof s.maximum === "number") out.maximum = s.maximum;
|
|
1589
|
+
if (s.items) out.items = toACPInputSchema(s.items);
|
|
1590
|
+
if (s.properties && typeof s.properties === "object") {
|
|
1591
|
+
const props = {};
|
|
1592
|
+
for (const [k, v] of Object.entries(s.properties)) {
|
|
1593
|
+
props[k] = toACPInputSchema(v);
|
|
1594
|
+
}
|
|
1595
|
+
out.properties = props;
|
|
1596
|
+
if (Array.isArray(s.required)) out.required = s.required;
|
|
1339
1597
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1598
|
+
return out;
|
|
1599
|
+
}
|
|
1600
|
+
function toACPToolResult(result) {
|
|
1601
|
+
const blocks = [];
|
|
1602
|
+
if (result === void 0 || result === null) {
|
|
1603
|
+
return { content: [{ type: "text", text: "ok" }] };
|
|
1343
1604
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
receive(msg) {
|
|
1351
|
-
if (this.closed) return;
|
|
1352
|
-
for (const handler of [...this.handlers]) {
|
|
1353
|
-
try {
|
|
1354
|
-
handler(msg);
|
|
1355
|
-
} catch {
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1605
|
+
if (typeof result === "string") {
|
|
1606
|
+
blocks.push({ type: "text", text: result });
|
|
1607
|
+
} else if (typeof result === "object") {
|
|
1608
|
+
blocks.push({ type: "text", text: JSON.stringify(result, null, 2) });
|
|
1609
|
+
} else {
|
|
1610
|
+
blocks.push({ type: "text", text: String(result) });
|
|
1358
1611
|
}
|
|
1359
|
-
};
|
|
1612
|
+
return { content: blocks };
|
|
1613
|
+
}
|
|
1614
|
+
function toolToPriority(tool) {
|
|
1615
|
+
if (tool.riskTier === "destructive") return "high";
|
|
1616
|
+
if (tool.riskTier === "standard" || tool.permission === "confirm") return "medium";
|
|
1617
|
+
return "low";
|
|
1618
|
+
}
|
|
1360
1619
|
|
|
1361
1620
|
// src/agent/wrongstack-acp-agent.ts
|
|
1362
1621
|
import { timingSafeEqual } from "node:crypto";
|
|
@@ -1518,378 +1777,118 @@ var WrongStackACPServer = class {
|
|
|
1518
1777
|
await requestPromise;
|
|
1519
1778
|
} catch {
|
|
1520
1779
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1521
|
-
res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
|
|
1522
|
-
}
|
|
1523
|
-
});
|
|
1524
|
-
return new Promise((resolve2) => {
|
|
1525
|
-
this.httpServer.listen(port, host, () => {
|
|
1526
|
-
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
|
|
1527
|
-
`);
|
|
1528
|
-
this.running = true;
|
|
1529
|
-
resolve2();
|
|
1530
|
-
});
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1533
|
-
/** Stop the server. */
|
|
1534
|
-
stop() {
|
|
1535
|
-
this.running = false;
|
|
1536
|
-
this.handler.close();
|
|
1537
|
-
this.transport.close();
|
|
1538
|
-
if (this.httpServer) {
|
|
1539
|
-
this.httpServer.close();
|
|
1540
|
-
this.httpServer = null;
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
};
|
|
1544
|
-
var defaultEchoRunTurn = async (_input, _emit) => {
|
|
1545
|
-
return { stopReason: "end_turn" };
|
|
1546
|
-
};
|
|
1547
|
-
function timingSafeTokenEqual(supplied, expected) {
|
|
1548
|
-
if (!supplied || !expected) return false;
|
|
1549
|
-
const a = Buffer.from(supplied);
|
|
1550
|
-
const b = Buffer.from(expected);
|
|
1551
|
-
if (a.length !== b.length) return false;
|
|
1552
|
-
return timingSafeEqual(a, b);
|
|
1553
|
-
}
|
|
1554
|
-
function isLoopbackPeer(req) {
|
|
1555
|
-
const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
|
|
1556
|
-
return address !== void 0 && isLoopbackHost(address);
|
|
1557
|
-
}
|
|
1558
|
-
function headerValue(value) {
|
|
1559
|
-
return Array.isArray(value) ? value[0] : value;
|
|
1560
|
-
}
|
|
1561
|
-
function requestPath(value) {
|
|
1562
|
-
return value ?? "/";
|
|
1563
|
-
}
|
|
1564
|
-
function isLoopbackHost(host) {
|
|
1565
|
-
const normalized = host.trim().toLowerCase();
|
|
1566
|
-
if (normalized === "localhost") return true;
|
|
1567
|
-
const literal = normalized.startsWith("[") && normalized.endsWith("]") ? normalized.slice(1, -1) : normalized;
|
|
1568
|
-
const version = isIP(literal);
|
|
1569
|
-
if (version === 4) return literal.startsWith("127.");
|
|
1570
|
-
if (version !== 6) return false;
|
|
1571
|
-
const groups = expandIPv6(literal);
|
|
1572
|
-
return groups !== null && groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1;
|
|
1573
|
-
}
|
|
1574
|
-
async function main() {
|
|
1575
|
-
const server = new WrongStackACPServer();
|
|
1576
|
-
await server.start();
|
|
1577
|
-
}
|
|
1578
|
-
var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
|
|
1579
|
-
if (isEntrypoint) {
|
|
1580
|
-
main().catch((err) => {
|
|
1581
|
-
writeErr2(`[wstack-acp fatal] ${err}
|
|
1582
|
-
`);
|
|
1583
|
-
process.exit(1);
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
// src/agent/server-agent-turn.ts
|
|
1588
|
-
function makeACPServerAgentTurn(opts) {
|
|
1589
|
-
const agents = /* @__PURE__ */ new Map();
|
|
1590
|
-
const timeouts = /* @__PURE__ */ new Map();
|
|
1591
|
-
const history = /* @__PURE__ */ new Map();
|
|
1592
|
-
const historyBytes = /* @__PURE__ */ new Map();
|
|
1593
|
-
const pendingSeed = /* @__PURE__ */ new Set();
|
|
1594
|
-
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
1595
|
-
const maxHistoryEntries = finitePositiveLimit(opts.maxHistoryEntries, 1e3);
|
|
1596
|
-
const maxHistoryBytes = finitePositiveLimit(opts.maxHistoryBytes, 8 * 1024 * 1024);
|
|
1597
|
-
const turn = async (input, emit, api) => {
|
|
1598
|
-
let agent = agents.get(input.sessionId);
|
|
1599
|
-
if (!agent) {
|
|
1600
|
-
agent = await opts.agentFor(input.sessionId, process.cwd(), api);
|
|
1601
|
-
agents.set(input.sessionId, agent);
|
|
1602
|
-
if (pendingSeed.has(input.sessionId)) {
|
|
1603
|
-
pendingSeed.delete(input.sessionId);
|
|
1604
|
-
seedAgentContext(agent, history.get(input.sessionId));
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
const turnAbort = new AbortController();
|
|
1608
|
-
const abortForTimeout = () => turnAbort.abort();
|
|
1609
|
-
const onParentAbort = () => turnAbort.abort();
|
|
1610
|
-
if (input.signal.aborted) {
|
|
1611
|
-
turnAbort.abort();
|
|
1612
|
-
} else {
|
|
1613
|
-
input.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
1614
|
-
}
|
|
1615
|
-
const timer = setTimeout(() => {
|
|
1616
|
-
timeouts.delete(input.sessionId);
|
|
1617
|
-
abortForTimeout();
|
|
1618
|
-
}, timeoutMs);
|
|
1619
|
-
timeouts.set(input.sessionId, timer);
|
|
1620
|
-
const unsub = [];
|
|
1621
|
-
const bus = agent.events;
|
|
1622
|
-
if (bus?.on) {
|
|
1623
|
-
unsub.push(
|
|
1624
|
-
bus.on("tool.started", (e) => {
|
|
1625
|
-
emit({
|
|
1626
|
-
sessionUpdate: "tool_call",
|
|
1627
|
-
toolCallId: e.id,
|
|
1628
|
-
title: toolTitle(e.name, e.input),
|
|
1629
|
-
kind: toolNameToKind(e.name),
|
|
1630
|
-
status: "in_progress",
|
|
1631
|
-
...isRecord(e.input) ? { rawInput: e.input } : {}
|
|
1632
|
-
});
|
|
1633
|
-
}),
|
|
1634
|
-
bus.on("tool.executed", (e) => {
|
|
1635
|
-
emit({
|
|
1636
|
-
sessionUpdate: "tool_call_update",
|
|
1637
|
-
toolCallId: e.id ?? e.name,
|
|
1638
|
-
status: e.ok ? "completed" : "failed",
|
|
1639
|
-
...e.output !== void 0 ? {
|
|
1640
|
-
content: [
|
|
1641
|
-
{ type: "content", content: { type: "text", text: e.output } }
|
|
1642
|
-
]
|
|
1643
|
-
} : {}
|
|
1644
|
-
});
|
|
1645
|
-
})
|
|
1646
|
-
);
|
|
1647
|
-
}
|
|
1648
|
-
try {
|
|
1649
|
-
const userInput = promptToAgentInput(input.prompt);
|
|
1650
|
-
const result = await agent.run(userInput, { signal: turnAbort.signal });
|
|
1651
|
-
const text = extractText(result);
|
|
1652
|
-
if (text) {
|
|
1653
|
-
emit({
|
|
1654
|
-
sessionUpdate: "agent_message_chunk",
|
|
1655
|
-
content: { type: "text", text }
|
|
1656
|
-
});
|
|
1657
|
-
}
|
|
1658
|
-
const userText = promptToText(input.prompt);
|
|
1659
|
-
const hist = history.get(input.sessionId) ?? [];
|
|
1660
|
-
let retainedHistoryBytes = historyBytes.get(input.sessionId) ?? 0;
|
|
1661
|
-
if (userText) {
|
|
1662
|
-
const update = {
|
|
1663
|
-
sessionUpdate: "user_message_chunk",
|
|
1664
|
-
content: { type: "text", text: userText }
|
|
1665
|
-
};
|
|
1666
|
-
hist.push(update);
|
|
1667
|
-
retainedHistoryBytes += replayEntryBytes(update);
|
|
1668
|
-
}
|
|
1669
|
-
if (text) {
|
|
1670
|
-
const update = {
|
|
1671
|
-
sessionUpdate: "agent_message_chunk",
|
|
1672
|
-
content: { type: "text", text }
|
|
1673
|
-
};
|
|
1674
|
-
hist.push(update);
|
|
1675
|
-
retainedHistoryBytes += replayEntryBytes(update);
|
|
1676
|
-
}
|
|
1677
|
-
retainedHistoryBytes = trimHistory(
|
|
1678
|
-
hist,
|
|
1679
|
-
retainedHistoryBytes,
|
|
1680
|
-
maxHistoryEntries,
|
|
1681
|
-
maxHistoryBytes
|
|
1682
|
-
);
|
|
1683
|
-
if (hist.length > 0) {
|
|
1684
|
-
history.set(input.sessionId, hist);
|
|
1685
|
-
historyBytes.set(input.sessionId, retainedHistoryBytes);
|
|
1686
|
-
} else {
|
|
1687
|
-
history.delete(input.sessionId);
|
|
1688
|
-
historyBytes.delete(input.sessionId);
|
|
1689
|
-
}
|
|
1690
|
-
const plan = extractPlan(result);
|
|
1691
|
-
if (plan.length > 0) {
|
|
1692
|
-
emit({
|
|
1693
|
-
sessionUpdate: "plan",
|
|
1694
|
-
entries: plan
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1697
|
-
const usage = extractUsage(result);
|
|
1698
|
-
if (usage) {
|
|
1699
|
-
emit({
|
|
1700
|
-
sessionUpdate: "usage_update",
|
|
1701
|
-
used: usage.used,
|
|
1702
|
-
size: usage.size,
|
|
1703
|
-
...usage.cost ? { cost: usage.cost } : {}
|
|
1704
|
-
});
|
|
1780
|
+
res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
|
|
1705
1781
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1782
|
+
});
|
|
1783
|
+
return new Promise((resolve2) => {
|
|
1784
|
+
this.httpServer.listen(port, host, () => {
|
|
1785
|
+
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
|
|
1786
|
+
`);
|
|
1787
|
+
this.running = true;
|
|
1788
|
+
resolve2();
|
|
1789
|
+
});
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
/** Stop the server. */
|
|
1793
|
+
stop() {
|
|
1794
|
+
this.running = false;
|
|
1795
|
+
this.handler.close();
|
|
1796
|
+
this.transport.close();
|
|
1797
|
+
if (this.httpServer) {
|
|
1798
|
+
this.httpServer.close();
|
|
1799
|
+
this.httpServer = null;
|
|
1721
1800
|
}
|
|
1722
|
-
};
|
|
1723
|
-
const replay = (sessionId) => [
|
|
1724
|
-
...history.get(sessionId) ?? []
|
|
1725
|
-
];
|
|
1726
|
-
const seed = (sessionId, incoming) => {
|
|
1727
|
-
if (incoming.length === 0) return;
|
|
1728
|
-
const seeded = [...incoming];
|
|
1729
|
-
const retainedBytes = trimHistory(
|
|
1730
|
-
seeded,
|
|
1731
|
-
seeded.reduce((total, entry) => total + replayEntryBytes(entry), 0),
|
|
1732
|
-
maxHistoryEntries,
|
|
1733
|
-
maxHistoryBytes
|
|
1734
|
-
);
|
|
1735
|
-
history.set(sessionId, seeded);
|
|
1736
|
-
historyBytes.set(sessionId, retainedBytes);
|
|
1737
|
-
pendingSeed.add(sessionId);
|
|
1738
|
-
};
|
|
1739
|
-
const dispose = (sessionId) => {
|
|
1740
|
-
const timer = timeouts.get(sessionId);
|
|
1741
|
-
if (timer) clearTimeout(timer);
|
|
1742
|
-
timeouts.delete(sessionId);
|
|
1743
|
-
agents.delete(sessionId);
|
|
1744
|
-
history.delete(sessionId);
|
|
1745
|
-
historyBytes.delete(sessionId);
|
|
1746
|
-
pendingSeed.delete(sessionId);
|
|
1747
|
-
};
|
|
1748
|
-
return Object.assign(turn, { replay, seed, dispose });
|
|
1749
|
-
}
|
|
1750
|
-
function finitePositiveLimit(value, fallback) {
|
|
1751
|
-
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
1752
|
-
}
|
|
1753
|
-
function trimHistory(entries, retainedBytes, maxEntries, maxBytes) {
|
|
1754
|
-
while (entries.length > maxEntries || retainedBytes > maxBytes) {
|
|
1755
|
-
const removed = entries.shift();
|
|
1756
|
-
if (!removed) break;
|
|
1757
|
-
retainedBytes -= replayEntryBytes(removed);
|
|
1758
1801
|
}
|
|
1759
|
-
|
|
1802
|
+
};
|
|
1803
|
+
var defaultEchoRunTurn = async (_input, _emit) => {
|
|
1804
|
+
return { stopReason: "end_turn" };
|
|
1805
|
+
};
|
|
1806
|
+
function timingSafeTokenEqual(supplied, expected) {
|
|
1807
|
+
if (!supplied || !expected) return false;
|
|
1808
|
+
const a = Buffer.from(supplied);
|
|
1809
|
+
const b = Buffer.from(expected);
|
|
1810
|
+
if (a.length !== b.length) return false;
|
|
1811
|
+
return timingSafeEqual(a, b);
|
|
1760
1812
|
}
|
|
1761
|
-
function
|
|
1762
|
-
|
|
1813
|
+
function isLoopbackPeer(req) {
|
|
1814
|
+
const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
|
|
1815
|
+
return address !== void 0 && isLoopbackHost(address);
|
|
1763
1816
|
}
|
|
1764
|
-
function
|
|
1765
|
-
|
|
1766
|
-
if (!state?.appendMessage) return;
|
|
1767
|
-
for (const u of history) {
|
|
1768
|
-
const text = u.content?.text;
|
|
1769
|
-
if (typeof text !== "string" || text.length === 0) continue;
|
|
1770
|
-
const role = u.sessionUpdate === "user_message_chunk" ? "user" : "assistant";
|
|
1771
|
-
state.appendMessage({ role, content: text });
|
|
1772
|
-
}
|
|
1817
|
+
function headerValue(value) {
|
|
1818
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1773
1819
|
}
|
|
1774
|
-
function
|
|
1775
|
-
|
|
1776
|
-
if (n.includes("read") || n.includes("cat")) return "read";
|
|
1777
|
-
if (n.includes("write") || n.includes("edit") || n.includes("apply") || n.includes("patch")) return "edit";
|
|
1778
|
-
if (n.includes("delete") || n === "rm" || n.startsWith("rm_") || n.endsWith("_rm")) return "delete";
|
|
1779
|
-
if (n.includes("move") || n.includes("rename") || n.includes("mv")) return "move";
|
|
1780
|
-
if (n.includes("grep") || n.includes("glob") || n.includes("search") || n.includes("find")) return "search";
|
|
1781
|
-
if (n.includes("bash") || n.includes("shell") || n.includes("exec") || n.includes("run") || n.includes("terminal")) return "execute";
|
|
1782
|
-
if (n.includes("fetch") || n.includes("http") || n.includes("web") || n.includes("url")) return "fetch";
|
|
1783
|
-
if (n.includes("think") || n.includes("plan")) return "think";
|
|
1784
|
-
return "other";
|
|
1820
|
+
function requestPath(value) {
|
|
1821
|
+
return value ?? "/";
|
|
1785
1822
|
}
|
|
1786
|
-
function
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1823
|
+
function isLoopbackHost(host) {
|
|
1824
|
+
const normalized = host.trim().toLowerCase();
|
|
1825
|
+
if (normalized === "localhost") return true;
|
|
1826
|
+
const literal = normalized.startsWith("[") && normalized.endsWith("]") ? normalized.slice(1, -1) : normalized;
|
|
1827
|
+
const version = isIP(literal);
|
|
1828
|
+
if (version === 4) return literal.startsWith("127.");
|
|
1829
|
+
if (version !== 6) return false;
|
|
1830
|
+
const groups = expandIPv6(literal);
|
|
1831
|
+
return groups !== null && groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1;
|
|
1794
1832
|
}
|
|
1795
|
-
function
|
|
1796
|
-
|
|
1833
|
+
async function main() {
|
|
1834
|
+
const server = new WrongStackACPServer();
|
|
1835
|
+
await server.start();
|
|
1797
1836
|
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
if (b.type === "text") {
|
|
1806
|
-
out.push({ type: "text", text: b.text });
|
|
1807
|
-
} else if (b.type === "image") {
|
|
1808
|
-
out.push({
|
|
1809
|
-
type: "image",
|
|
1810
|
-
source: { type: "base64", media_type: b.mimeType, data: b.data }
|
|
1811
|
-
});
|
|
1812
|
-
} else if (b.type === "audio") {
|
|
1813
|
-
out.push({ type: "text", text: `[audio: ${b.mimeType}]` });
|
|
1814
|
-
} else if (b.type === "resource") {
|
|
1815
|
-
const text = "text" in b.resource && typeof b.resource.text === "string" ? b.resource.text : `[embedded resource: ${b.resource.uri}]`;
|
|
1816
|
-
out.push({ type: "text", text });
|
|
1817
|
-
} else if (b.type === "resource_link") {
|
|
1818
|
-
out.push({ type: "text", text: `[resource link: ${b.uri}]` });
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
return out;
|
|
1837
|
+
var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
|
|
1838
|
+
if (isEntrypoint) {
|
|
1839
|
+
main().catch((err) => {
|
|
1840
|
+
writeErr2(`[wstack-acp fatal] ${err}
|
|
1841
|
+
`);
|
|
1842
|
+
process.exit(1);
|
|
1843
|
+
});
|
|
1822
1844
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
parts.push(`[image: ${b.mimeType}]`);
|
|
1830
|
-
} else if (b.type === "audio") {
|
|
1831
|
-
parts.push(`[audio: ${b.mimeType}]`);
|
|
1832
|
-
} else if (b.type === "resource") {
|
|
1833
|
-
parts.push(`[embedded resource: ${b.resource.uri}]`);
|
|
1834
|
-
} else if (b.type === "resource_link") {
|
|
1835
|
-
parts.push(`[resource link: ${b.uri}]`);
|
|
1836
|
-
}
|
|
1845
|
+
|
|
1846
|
+
// src/agent/ws-bridge-transport.ts
|
|
1847
|
+
var WsBridgeTransport = class {
|
|
1848
|
+
/** @param sink Called with each outbound message to write to the socket. */
|
|
1849
|
+
constructor(sink) {
|
|
1850
|
+
this.sink = sink;
|
|
1837
1851
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
for (const c of r.content) {
|
|
1847
|
-
if (typeof c === "object" && c !== null) {
|
|
1848
|
-
const cb = c;
|
|
1849
|
-
if (cb.type === "text" && typeof cb.text === "string") parts.push(cb.text);
|
|
1850
|
-
}
|
|
1852
|
+
sink;
|
|
1853
|
+
handlers = /* @__PURE__ */ new Set();
|
|
1854
|
+
closed = false;
|
|
1855
|
+
send(msg) {
|
|
1856
|
+
if (this.closed) return Promise.resolve();
|
|
1857
|
+
try {
|
|
1858
|
+
this.sink(msg);
|
|
1859
|
+
} catch {
|
|
1851
1860
|
}
|
|
1852
|
-
return
|
|
1861
|
+
return Promise.resolve();
|
|
1853
1862
|
}
|
|
1854
|
-
|
|
1855
|
-
}
|
|
1856
|
-
function pickStopReason(result, signal) {
|
|
1857
|
-
if (signal.aborted) return "cancelled";
|
|
1858
|
-
if (typeof result !== "object" || result === null) return "end_turn";
|
|
1859
|
-
const r = result;
|
|
1860
|
-
if (r.error) {
|
|
1861
|
-
return "end_turn";
|
|
1863
|
+
sendRaw() {
|
|
1862
1864
|
}
|
|
1863
|
-
|
|
1864
|
-
return
|
|
1865
|
+
read() {
|
|
1866
|
+
return Promise.resolve(null);
|
|
1865
1867
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
if (typeof result !== "object" || result === null) return [];
|
|
1870
|
-
const r = result;
|
|
1871
|
-
if (Array.isArray(r.plan)) {
|
|
1872
|
-
return r.plan.filter(
|
|
1873
|
-
(e) => typeof e === "object" && e !== null && typeof e.content === "string"
|
|
1874
|
-
);
|
|
1868
|
+
onMessage(handler) {
|
|
1869
|
+
this.handlers.add(handler);
|
|
1870
|
+
return () => this.handlers.delete(handler);
|
|
1875
1871
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1872
|
+
close() {
|
|
1873
|
+
this.closed = true;
|
|
1874
|
+
this.handlers.clear();
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Feed one inbound message from the socket. Fires the registered
|
|
1878
|
+
* `onMessage` handlers (which route JSON-RPC responses to pending
|
|
1879
|
+
* outbound requests inside the handler). Inbound *requests* are processed
|
|
1880
|
+
* by the caller via `handler.handleMessage(msg)` — call both per message.
|
|
1881
|
+
*/
|
|
1882
|
+
receive(msg) {
|
|
1883
|
+
if (this.closed) return;
|
|
1884
|
+
for (const handler of [...this.handlers]) {
|
|
1885
|
+
try {
|
|
1886
|
+
handler(msg);
|
|
1887
|
+
} catch {
|
|
1888
|
+
}
|
|
1889
1889
|
}
|
|
1890
1890
|
}
|
|
1891
|
-
|
|
1892
|
-
}
|
|
1891
|
+
};
|
|
1893
1892
|
export {
|
|
1894
1893
|
ACPProtocolHandler,
|
|
1895
1894
|
ACPSessionStore,
|