@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.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
- /** Abort all active turns and drop session state. */
803
- close() {
804
- for (const [sessionId, session] of this.sessions) {
805
- session.abort.abort();
806
- this.disposeSession(sessionId);
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
- disposeSession(sessionId) {
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.disposeFor?.(sessionId);
751
+ await this.store.save(state, history ?? this.replayFor?.(state.id));
818
752
  } catch {
819
753
  }
820
754
  }
821
- sessionContext() {
822
- return {
823
- sessions: this.sessions,
824
- maxSessions: this.maxSessions,
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
- // Requests
844
- // ────────────────────────────────────────────────────────────────────
845
- async handleRequest(id, method, params) {
846
- if (method !== "initialize" && !this.initialized) {
847
- await this.sendError(id, -32e3, "Not initialized");
848
- return false;
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
- switch (method) {
852
- case "initialize":
853
- return await this.handleInitialize(id, params);
854
- case "authenticate":
855
- return await this.handleAuthenticate(id, params);
856
- case "logout":
857
- return await this.handleLogout(id, params);
858
- case "session/new":
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
- } catch (err) {
891
- const { code, message, data } = errorToJsonRpc(err);
892
- await this.sendError(id, code, message, data);
893
- return false;
894
- }
895
- }
896
- async handleInitialize(id, params) {
897
- const p = params ?? {};
898
- if (p.clientCapabilities && typeof p.clientCapabilities === "object") {
899
- this.clientCapabilities = p.clientCapabilities;
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
- this.initialized = true;
902
- await this.sendResult(
903
- id,
904
- buildInitializeResult(this.agentName, this.modes, this.configOptions)
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
- return false;
907
- }
908
- async handleAuthenticate(id, _params) {
909
- await this.sendResult(id, { outcome: "unauthenticated" });
910
- return false;
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
- async handleLogout(id, _params) {
913
- await this.sendResult(id, {});
914
- return false;
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
- async handleSessionResume(id, params) {
917
- const p = params ?? {};
918
- const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
919
- const existing = sessionId ? this.sessions.get(sessionId) : void 0;
920
- if (existing) {
921
- existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
922
- await this.sendResult(id, {
923
- initialMode: {
924
- currentModeId: existing.modeId,
925
- availableModes: this.modes
926
- }
927
- });
928
- return false;
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
- async handleSessionClose(id, params) {
934
- const p = params ?? {};
935
- const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
936
- const session = sessionId ? this.sessions.get(sessionId) : void 0;
937
- if (!session) {
938
- await this.sendError(id, -32e3, `session not found: ${sessionId}`);
939
- return false;
940
- }
941
- session.abort.abort();
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
- async handleSessionDelete(id, params) {
948
- const p = params ?? {};
949
- const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
950
- if (!sessionId) {
951
- await this.sendError(id, -32e3, `session not found: ${sessionId}`);
952
- return false;
953
- }
954
- if (!this.sessions.has(sessionId)) {
955
- await this.sendResult(id, { configOptions: [...this.configOptions] });
956
- return false;
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
- async handleSessionList(id) {
989
- const sessions = Array.from(this.sessions.values()).map((s) => {
990
- const out = {
991
- sessionId: s.id,
992
- cwd: s.cwd,
993
- updatedAt: s.updatedAt
994
- };
995
- if (s.title !== void 0) out.title = s.title;
996
- return out;
997
- });
998
- await this.sendResult(id, { sessions });
999
- return false;
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
- // Notifications
1003
- // ────────────────────────────────────────────────────────────────────
1004
- async handleNotification(method, params) {
1005
- switch (method) {
1006
- case "session/cancel": {
1007
- const p = params ?? {};
1008
- const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
1009
- const session = sessionId ? this.sessions.get(sessionId) : void 0;
1010
- if (session) {
1011
- session.abort.abort();
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
- // Wire helpers
1027
- // ────────────────────────────────────────────────────────────────────
1028
- async sendNotification(params) {
1029
- await this.transport.send(toWire({ jsonrpc: "2.0", method: "session/update", params }));
1030
- }
1031
- async sendResult(id, result) {
1032
- await this.transport.send(toWire({ jsonrpc: "2.0", id, result }));
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
- async persist(state, history = void 0) {
1035
- if (!this.store) return;
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
- async sendError(id, code, message, data) {
1042
- const error = { code, message };
1043
- if (data !== void 0) error.data = data;
1044
- await this.transport.send(toWire({ jsonrpc: "2.0", id, error }));
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
- allocId() {
1047
- return `${this.nextId++}_${randomUUID().replaceAll("-", "")}`;
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/ws-bridge-transport.ts
1315
- var WsBridgeTransport = class {
1316
- /** @param sink Called with each outbound message to write to the socket. */
1317
- constructor(sink) {
1318
- this.sink = sink;
1319
- }
1320
- sink;
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
- try {
1326
- this.sink(msg);
1327
- } catch {
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
- sendRaw() {
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
- read() {
1334
- return Promise.resolve(null);
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
- onMessage(handler) {
1337
- this.handlers.add(handler);
1338
- return () => this.handlers.delete(handler);
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
- close() {
1341
- this.closed = true;
1342
- this.handlers.clear();
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
- * Feed one inbound message from the socket. Fires the registered
1346
- * `onMessage` handlers (which route JSON-RPC responses to pending
1347
- * outbound requests inside the handler). Inbound *requests* are processed
1348
- * by the caller via `handler.handleMessage(msg)` — call both per message.
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
- const result_out = {
1707
- // `turnAbort.signal` covers both client cancellation and the
1708
- // wall-clock timeout, so either maps to stopReason 'cancelled'.
1709
- stopReason: pickStopReason(result, turnAbort.signal)
1710
- };
1711
- if (text) result_out.text = text;
1712
- const runTurnPlan = extractPlan(result);
1713
- if (runTurnPlan.length > 0) result_out.plan = runTurnPlan;
1714
- if (usage) result_out.usage = usage;
1715
- return result_out;
1716
- } finally {
1717
- clearTimeout(timer);
1718
- timeouts.delete(input.sessionId);
1719
- input.signal.removeEventListener("abort", onParentAbort);
1720
- for (const u of unsub) u();
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
- return Math.max(0, retainedBytes);
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 replayEntryBytes(entry) {
1762
- return Buffer.byteLength(JSON.stringify(entry), "utf8");
1813
+ function isLoopbackPeer(req) {
1814
+ const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
1815
+ return address !== void 0 && isLoopbackHost(address);
1763
1816
  }
1764
- function seedAgentContext(agent, history) {
1765
- const state = agent.ctx?.state;
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 toolNameToKind(name) {
1775
- const n = name.toLowerCase();
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 toolTitle(name, input) {
1787
- if (isRecord(input)) {
1788
- const path3 = input.path ?? input.file ?? input.filePath ?? input.pattern ?? input.command;
1789
- if (typeof path3 === "string" && path3.length > 0) {
1790
- return `${name}: ${path3.length > 80 ? `${path3.slice(0, 77)}\u2026` : path3}`;
1791
- }
1792
- }
1793
- return name;
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 isRecord(v) {
1796
- return typeof v === "object" && v !== null && !Array.isArray(v);
1833
+ async function main() {
1834
+ const server = new WrongStackACPServer();
1835
+ await server.start();
1797
1836
  }
1798
- function promptToAgentInput(blocks) {
1799
- const hasImage = blocks.some((b) => b.type === "image");
1800
- if (!hasImage) {
1801
- return promptToText(blocks);
1802
- }
1803
- const out = [];
1804
- for (const b of blocks) {
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
- function promptToText(blocks) {
1824
- const parts = [];
1825
- for (const b of blocks) {
1826
- if (b.type === "text") {
1827
- parts.push(b.text);
1828
- } else if (b.type === "image") {
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
- return parts.join("\n").trim();
1839
- }
1840
- function extractText(result) {
1841
- if (typeof result !== "object" || result === null) return "";
1842
- const r = result;
1843
- if (typeof r.text === "string") return r.text;
1844
- if (Array.isArray(r.content)) {
1845
- const parts = [];
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 parts.join("");
1861
+ return Promise.resolve();
1853
1862
  }
1854
- return "";
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
- if (typeof r.stopReason === "string" && r.stopReason) {
1864
- return r.stopReason;
1865
+ read() {
1866
+ return Promise.resolve(null);
1865
1867
  }
1866
- return "end_turn";
1867
- }
1868
- function extractPlan(result) {
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
- return [];
1877
- }
1878
- function extractUsage(result) {
1879
- if (typeof result !== "object" || result === null) return null;
1880
- const r = result;
1881
- if (typeof r.usage === "object" && r.usage !== null) {
1882
- const u = r.usage;
1883
- if (typeof u.used === "number" && typeof u.size === "number") {
1884
- return {
1885
- used: u.used,
1886
- size: u.size,
1887
- ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
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
- return null;
1892
- }
1891
+ };
1893
1892
  export {
1894
1893
  ACPProtocolHandler,
1895
1894
  ACPSessionStore,