@band-ai/sdk 0.3.4 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -1
- package/dist/{ClaudeSDKAdapter-CXud2DBE.d.ts → CopilotACPAdapter-B_AVyAl6.d.ts} +249 -19
- package/dist/{ClaudeSDKAdapter-Cx6zhSaG.d.cts → CopilotACPAdapter-Clid9xGR.d.cts} +249 -19
- package/dist/{opencode-2YdscIc1.d.cts → acp-client-D-I_5lK-.d.cts} +23 -4
- package/dist/{opencode-Bld62x5t.d.ts → acp-client-DUXyczlF.d.ts} +23 -4
- package/dist/acp-server-BlcYMxKt.d.ts +17 -0
- package/dist/acp-server-CJhclo6P.d.cts +17 -0
- package/dist/adapters.cjs +2257 -969
- package/dist/adapters.d.cts +21 -132
- package/dist/adapters.d.ts +21 -132
- package/dist/adapters.js +26 -1922
- package/dist/{backends-D-dBdDm-.d.ts → backends-CgsPQa9B.d.ts} +2 -2
- package/dist/{backends-ZLxmQrTZ.d.cts → backends-Dh2WXcHQ.d.cts} +2 -2
- package/dist/{chunk-JY75VSQK.js → chunk-4OVNFDD7.js} +14 -1
- package/dist/chunk-6BE2EG5P.js +128 -0
- package/dist/{chunk-U6AFCLYZ.js → chunk-6TGXV5TE.js} +1824 -514
- package/dist/{chunk-OH6VVVKT.js → chunk-CBCHTTUY.js} +10 -0
- package/dist/{chunk-K5KAEYPF.js → chunk-HNWZZXAE.js} +288 -120
- package/dist/chunk-I34SXMTJ.js +6 -0
- package/dist/{chunk-XWHWJP4I.js → chunk-JDW5WSGF.js} +2 -2
- package/dist/{chunk-KXJ74J73.js → chunk-JIZE4EC6.js} +4 -10
- package/dist/chunk-OGOXY43L.js +119 -0
- package/dist/{chunk-W5IAI6Z4.js → chunk-OVXVZUPO.js} +5 -5
- package/dist/{chunk-YJ42AZO4.js → chunk-RJNJUISR.js} +5 -2
- package/dist/{chunk-MQY74R7D.js → chunk-SHZBLLDQ.js} +21 -9
- package/dist/{chunk-JADCAMZY.js → chunk-TGDDFBYS.js} +45 -0
- package/dist/{chunk-US226AS4.js → chunk-UENIC5FJ.js} +14 -11
- package/dist/{chunk-MQBBWYRP.js → chunk-UFO6XMCV.js} +3404 -572
- package/dist/{chunk-WA7CHXUS.js → chunk-UL3Y5C4J.js} +2 -2
- package/dist/{chunk-WL2HNJZ6.js → chunk-VOMJRQOM.js} +3 -3
- package/dist/{chunk-OD2G5LFQ.js → chunk-ZYOXI4JL.js} +41 -2
- package/dist/config.cjs +2 -12
- package/dist/config.js +2 -2
- package/dist/converters.cjs +36 -11
- package/dist/converters.d.cts +7 -5
- package/dist/converters.d.ts +7 -5
- package/dist/converters.js +11 -13
- package/dist/core.cjs +239 -5
- package/dist/core.d.cts +71 -6
- package/dist/core.d.ts +71 -6
- package/dist/core.js +31 -6
- package/dist/{customTools-BzF0IISO.d.cts → customTools-Bfecd0mJ.d.cts} +1 -1
- package/dist/{customTools-BzF0IISO.d.ts → customTools-Bfecd0mJ.d.ts} +1 -1
- package/dist/{disconnectReason-IuADl6TH.d.ts → disconnectReason-Cctmg1SN.d.cts} +1 -1
- package/dist/{disconnectReason-IuADl6TH.d.cts → disconnectReason-Cctmg1SN.d.ts} +1 -1
- package/dist/{dtos-B8KU_q4d.d.ts → dtos-CjQsN1_h.d.cts} +5 -12
- package/dist/{dtos-B8KU_q4d.d.cts → dtos-CjQsN1_h.d.ts} +5 -12
- package/dist/index.cjs +5289 -1474
- package/dist/index.d.cts +61 -22
- package/dist/index.d.ts +61 -22
- package/dist/index.js +151 -43
- package/dist/linear.cjs +45 -7
- package/dist/linear.d.cts +4 -3
- package/dist/linear.d.ts +4 -3
- package/dist/linear.js +10 -10
- package/dist/mcp-claude.cjs +11 -6
- package/dist/mcp-claude.d.cts +4 -3
- package/dist/mcp-claude.d.ts +4 -3
- package/dist/mcp-claude.js +4 -5
- package/dist/mcp.cjs +13 -7
- package/dist/mcp.d.cts +6 -5
- package/dist/mcp.d.ts +6 -5
- package/dist/mcp.js +5 -6
- package/dist/{pagination-DqdkQArT.d.cts → pagination-BZDhB_nO.d.cts} +2 -2
- package/dist/{pagination-BgEx-HeL.d.ts → pagination-CPz0J_vQ.d.ts} +2 -2
- package/dist/{protocols-Bz1rxcUg.d.cts → protocols-CabgO52c.d.cts} +38 -3
- package/dist/{protocols-Bwwcql0h.d.ts → protocols-Dpci6uae.d.ts} +38 -3
- package/dist/rest.cjs +43 -1
- package/dist/rest.d.cts +6 -5
- package/dist/rest.d.ts +6 -5
- package/dist/rest.js +6 -5
- package/dist/runtime.cjs +2113 -597
- package/dist/runtime.d.cts +44 -25
- package/dist/runtime.d.ts +44 -25
- package/dist/runtime.js +11 -11
- package/dist/{schemas-BgJhlqqO.d.cts → schemas-BKnh4fKR.d.cts} +209 -24
- package/dist/{schemas-C4mK1WSs.d.ts → schemas-CDa3V_I5.d.ts} +209 -24
- package/dist/{sdk-BTciSLRQ.d.cts → sdk-CXNqzoY1.d.cts} +1 -1
- package/dist/{sdk-qKJtMma_.d.ts → sdk-Cin80BTC.d.ts} +1 -1
- package/dist/{simpleAdapter-BpT4XbZC.d.cts → simpleAdapter-D75rcz9n.d.cts} +3 -1
- package/dist/{simpleAdapter--wznuoOw.d.ts → simpleAdapter-eLvsAQNo.d.ts} +3 -1
- package/dist/testing.cjs +25 -7
- package/dist/testing.d.cts +5 -3
- package/dist/testing.d.ts +5 -3
- package/dist/testing.js +9 -6
- package/dist/{types-BivGO7I9.d.ts → types-42-HWeX8.d.cts} +10 -4
- package/dist/types-CKU1N0SK.d.ts +137 -0
- package/dist/{types-BgYFd_Yw.d.cts → types-CjxZekMH.d.ts} +10 -4
- package/dist/types-DtcOLALn.d.cts +137 -0
- package/package.json +25 -26
- package/dist/acp-server-CiUqN3G3.d.ts +0 -24
- package/dist/acp-server-Dlj7D563.d.cts +0 -24
- package/dist/chunk-AVVDPXP4.js +0 -39
- package/dist/chunk-F63TPOI3.js +0 -29
- package/dist/chunk-V5TSWS7P.js +0 -62
- package/dist/types-BNNtOXu3.d.cts +0 -446
- package/dist/types-DFUtpxah.d.ts +0 -446
package/dist/adapters.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ACPClientAdapter,
|
|
3
|
+
ACPServer,
|
|
2
4
|
AnthropicAdapter,
|
|
3
5
|
AnthropicToolCallingModel,
|
|
6
|
+
BandACPServerAdapter,
|
|
4
7
|
CODEX_REASONING_EFFORTS,
|
|
5
8
|
CODEX_REASONING_SUMMARIES,
|
|
6
9
|
CODEX_WEB_SEARCH_MODES,
|
|
@@ -8,6 +11,9 @@ import {
|
|
|
8
11
|
CodexAdapter,
|
|
9
12
|
CodexAppServerStdioClient,
|
|
10
13
|
CodexJsonRpcError,
|
|
14
|
+
CopilotACPAdapter,
|
|
15
|
+
DEFAULT_COPILOT_ACP_COMMAND,
|
|
16
|
+
DEFAULT_OMP_ACP_COMMAND,
|
|
11
17
|
GeminiAdapter,
|
|
12
18
|
GeminiToolCallingModel,
|
|
13
19
|
GenericAdapter,
|
|
@@ -17,1943 +23,37 @@ import {
|
|
|
17
23
|
LangGraphAdapter,
|
|
18
24
|
LettaAdapter,
|
|
19
25
|
LettaHistoryConverter,
|
|
26
|
+
OmpACPAdapter,
|
|
20
27
|
OpenAIAdapter,
|
|
21
28
|
OpenAIToolCallingModel,
|
|
22
29
|
OpencodeAdapter,
|
|
23
30
|
ToolCallingAdapter,
|
|
24
31
|
VercelAISDKAdapter,
|
|
25
32
|
VercelAISDKToolCallingModel,
|
|
26
|
-
runSingleToolRound
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
import
|
|
30
|
-
BandMcpServer,
|
|
31
|
-
BandMcpSseServer,
|
|
32
|
-
generateAuthToken
|
|
33
|
-
} from "./chunk-WA7CHXUS.js";
|
|
34
|
-
import "./chunk-XWHWJP4I.js";
|
|
35
|
-
import {
|
|
36
|
-
ACPClientHistoryConverter,
|
|
37
|
-
ACPServerHistoryConverter
|
|
38
|
-
} from "./chunk-V5TSWS7P.js";
|
|
33
|
+
runSingleToolRound
|
|
34
|
+
} from "./chunk-UFO6XMCV.js";
|
|
35
|
+
import "./chunk-UL3Y5C4J.js";
|
|
36
|
+
import "./chunk-JDW5WSGF.js";
|
|
39
37
|
import {
|
|
40
38
|
A2AAdapter,
|
|
41
39
|
A2AGatewayAdapter,
|
|
42
40
|
A2AHistoryConverter,
|
|
43
41
|
GatewayHistoryConverter,
|
|
44
42
|
GatewayServer,
|
|
45
|
-
LazyAsyncValue,
|
|
46
43
|
ParlantAdapter,
|
|
47
44
|
ParlantHistoryConverter,
|
|
48
45
|
buildA2AAuthHeaders,
|
|
49
|
-
createGatewayServer
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
import
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
import
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} from "./chunk-OH6VVVKT.js";
|
|
61
|
-
import {
|
|
62
|
-
renderSystemPrompt
|
|
63
|
-
} from "./chunk-YJ42AZO4.js";
|
|
64
|
-
import {
|
|
65
|
-
MCP_SERVER_NAME
|
|
66
|
-
} from "./chunk-MQY74R7D.js";
|
|
67
|
-
import {
|
|
68
|
-
isBlankEventContent
|
|
69
|
-
} from "./chunk-F63TPOI3.js";
|
|
70
|
-
import "./chunk-KXJ74J73.js";
|
|
71
|
-
import "./chunk-JADCAMZY.js";
|
|
72
|
-
import {
|
|
73
|
-
NoopLogger
|
|
74
|
-
} from "./chunk-OD2G5LFQ.js";
|
|
75
|
-
import {
|
|
76
|
-
UnsupportedFeatureError,
|
|
77
|
-
ValidationError
|
|
78
|
-
} from "./chunk-JY75VSQK.js";
|
|
79
|
-
|
|
80
|
-
// src/adapters/acp/ACPClientAdapter.ts
|
|
81
|
-
import { spawn } from "child_process";
|
|
82
|
-
import { Readable, Writable } from "stream";
|
|
83
|
-
|
|
84
|
-
// src/adapters/shared/withTimeout.ts
|
|
85
|
-
async function withTimeout(promise, timeoutMs, message) {
|
|
86
|
-
let timer;
|
|
87
|
-
try {
|
|
88
|
-
return await Promise.race([
|
|
89
|
-
promise,
|
|
90
|
-
new Promise((_, reject) => {
|
|
91
|
-
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
92
|
-
})
|
|
93
|
-
]);
|
|
94
|
-
} finally {
|
|
95
|
-
clearTimeout(timer);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// src/adapters/acp/types.ts
|
|
100
|
-
var DEFAULT_ACP_SERVER_MODES = [
|
|
101
|
-
{
|
|
102
|
-
id: "default",
|
|
103
|
-
name: "Default",
|
|
104
|
-
description: "General-purpose chat mode"
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
id: "code",
|
|
108
|
-
name: "Code",
|
|
109
|
-
description: "Route prompts toward coding peers when available"
|
|
110
|
-
}
|
|
111
|
-
];
|
|
112
|
-
function createPendingPrompt(sessionId) {
|
|
113
|
-
let markDone = () => void 0;
|
|
114
|
-
const done = new Promise((resolve) => {
|
|
115
|
-
markDone = resolve;
|
|
116
|
-
});
|
|
117
|
-
return {
|
|
118
|
-
sessionId,
|
|
119
|
-
done,
|
|
120
|
-
markDone,
|
|
121
|
-
terminalMessageSeen: false,
|
|
122
|
-
completionTimer: null
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function choosePermissionOption(options) {
|
|
126
|
-
if (options.length === 0) {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
return options.find((option) => option.kind === "allow_once") ?? options.find((option) => option.kind === "allow_always") ?? options[0];
|
|
130
|
-
}
|
|
131
|
-
function asJsonSafe(value) {
|
|
132
|
-
if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
133
|
-
return value;
|
|
134
|
-
}
|
|
135
|
-
if (Array.isArray(value)) {
|
|
136
|
-
return value.map((item) => asJsonSafe(item));
|
|
137
|
-
}
|
|
138
|
-
if (typeof value === "object") {
|
|
139
|
-
if ("model_dump" in value && typeof value.model_dump === "function") {
|
|
140
|
-
return asJsonSafe(value.model_dump());
|
|
141
|
-
}
|
|
142
|
-
if ("toJSON" in value && typeof value.toJSON === "function") {
|
|
143
|
-
return asJsonSafe(value.toJSON());
|
|
144
|
-
}
|
|
145
|
-
return Object.fromEntries(
|
|
146
|
-
Object.entries(value).map(([key, item]) => [key, asJsonSafe(item)])
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
return String(value);
|
|
150
|
-
}
|
|
151
|
-
function normalizeMcpServers(mcpServers) {
|
|
152
|
-
if (!mcpServers) {
|
|
153
|
-
return [];
|
|
154
|
-
}
|
|
155
|
-
return mcpServers.map((server) => asJsonSafe(server)).filter((server) => !!server && typeof server === "object" && !Array.isArray(server));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// src/adapters/acp/client.ts
|
|
159
|
-
var BandACPClient = class {
|
|
160
|
-
sessionChunks = /* @__PURE__ */ new Map();
|
|
161
|
-
permissionHandler;
|
|
162
|
-
// The handler is connection-scoped and required at construction, so it is
|
|
163
|
-
// already in place before the agent process is spawned: there is no window
|
|
164
|
-
// in which a `session/request_permission` has nowhere to go.
|
|
165
|
-
constructor(permissionHandler) {
|
|
166
|
-
this.permissionHandler = permissionHandler;
|
|
167
|
-
}
|
|
168
|
-
async sessionUpdate(params) {
|
|
169
|
-
const chunk = toCollectedChunk(params.update);
|
|
170
|
-
if (!chunk) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const existing = this.sessionChunks.get(params.sessionId) ?? [];
|
|
174
|
-
existing.push(chunk);
|
|
175
|
-
this.sessionChunks.set(params.sessionId, existing);
|
|
176
|
-
}
|
|
177
|
-
async requestPermission(params) {
|
|
178
|
-
return this.permissionHandler(params);
|
|
179
|
-
}
|
|
180
|
-
// Named for the one thing it clears: collected chunks are per-turn, and a
|
|
181
|
-
// per-turn caller must not be able to reach anything with a longer life.
|
|
182
|
-
resetChunks(sessionId) {
|
|
183
|
-
this.sessionChunks.delete(sessionId);
|
|
184
|
-
}
|
|
185
|
-
getCollectedText(sessionId) {
|
|
186
|
-
return this.getCollectedChunks(sessionId).filter((chunk) => chunk.chunkType === "text").map((chunk) => chunk.content).join("");
|
|
187
|
-
}
|
|
188
|
-
getCollectedChunks(sessionId) {
|
|
189
|
-
if (sessionId) {
|
|
190
|
-
return coalesceChunks(this.sessionChunks.get(sessionId) ?? []);
|
|
191
|
-
}
|
|
192
|
-
return [...this.sessionChunks.values()].flatMap((chunks) => coalesceChunks(chunks));
|
|
193
|
-
}
|
|
194
|
-
async extMethod(method, params) {
|
|
195
|
-
if (method === "cursor/ask_question") {
|
|
196
|
-
const options = Array.isArray(params.options) ? params.options : [];
|
|
197
|
-
const selected = choosePermissionOption(
|
|
198
|
-
options.filter((option) => !!option && typeof option === "object")
|
|
199
|
-
);
|
|
200
|
-
if (!selected) {
|
|
201
|
-
return {
|
|
202
|
-
outcome: {
|
|
203
|
-
type: "cancelled"
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
return {
|
|
208
|
-
outcome: {
|
|
209
|
-
type: "selected",
|
|
210
|
-
optionId: selected.optionId
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
if (method === "cursor/create_plan") {
|
|
215
|
-
return {
|
|
216
|
-
outcome: {
|
|
217
|
-
type: "approved"
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
return {};
|
|
222
|
-
}
|
|
223
|
-
async extNotification(method, params) {
|
|
224
|
-
const sessionId = toOptionalString(params.sessionId) ?? toOptionalString(params.session_id);
|
|
225
|
-
if (!sessionId) {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
if (method === "cursor/update_todos") {
|
|
229
|
-
const todos = Array.isArray(params.todos) ? params.todos : [];
|
|
230
|
-
const lines = todos.filter((todo) => !!todo && typeof todo === "object").map((todo) => `- [${todo.completed === true ? "x" : " "}] ${String(todo.content ?? "")}`).filter((line) => line.trim().length > 0);
|
|
231
|
-
if (lines.length > 0) {
|
|
232
|
-
this.appendChunk(sessionId, {
|
|
233
|
-
chunkType: "plan",
|
|
234
|
-
content: lines.join("\n"),
|
|
235
|
-
metadata: {},
|
|
236
|
-
streamed: false
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
if (method === "cursor/task") {
|
|
242
|
-
const result = toOptionalString(params.result);
|
|
243
|
-
if (result) {
|
|
244
|
-
this.appendChunk(sessionId, {
|
|
245
|
-
chunkType: "text",
|
|
246
|
-
content: `[Task completed] ${result}`,
|
|
247
|
-
metadata: {},
|
|
248
|
-
streamed: false
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
appendChunk(sessionId, chunk) {
|
|
254
|
-
const existing = this.sessionChunks.get(sessionId) ?? [];
|
|
255
|
-
existing.push(chunk);
|
|
256
|
-
this.sessionChunks.set(sessionId, existing);
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
function coalesceChunks(chunks) {
|
|
260
|
-
const result = [];
|
|
261
|
-
const openRuns = /* @__PURE__ */ new Map();
|
|
262
|
-
for (const chunk of chunks) {
|
|
263
|
-
if (!chunk.streamed) {
|
|
264
|
-
openRuns.clear();
|
|
265
|
-
result.push({ ...chunk });
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
const openRun = openRuns.get(chunk.chunkType);
|
|
269
|
-
if (openRun) {
|
|
270
|
-
openRun.content += chunk.content;
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
const clone = { ...chunk };
|
|
274
|
-
openRuns.set(chunk.chunkType, clone);
|
|
275
|
-
result.push(clone);
|
|
276
|
-
}
|
|
277
|
-
return result;
|
|
278
|
-
}
|
|
279
|
-
function toCollectedChunk(update) {
|
|
280
|
-
switch (update.sessionUpdate) {
|
|
281
|
-
case "agent_message_chunk":
|
|
282
|
-
return {
|
|
283
|
-
chunkType: "text",
|
|
284
|
-
content: extractTextFromContent(update.content),
|
|
285
|
-
metadata: {},
|
|
286
|
-
streamed: true
|
|
287
|
-
};
|
|
288
|
-
case "agent_thought_chunk":
|
|
289
|
-
return {
|
|
290
|
-
chunkType: "thought",
|
|
291
|
-
content: extractTextFromContent(update.content),
|
|
292
|
-
metadata: {},
|
|
293
|
-
streamed: true
|
|
294
|
-
};
|
|
295
|
-
case "tool_call":
|
|
296
|
-
return {
|
|
297
|
-
chunkType: "tool_call",
|
|
298
|
-
content: update.title,
|
|
299
|
-
metadata: {
|
|
300
|
-
tool_call_id: update.toolCallId,
|
|
301
|
-
raw_input: update.rawInput,
|
|
302
|
-
status: update.status ?? "pending"
|
|
303
|
-
},
|
|
304
|
-
streamed: false
|
|
305
|
-
};
|
|
306
|
-
case "tool_call_update":
|
|
307
|
-
return {
|
|
308
|
-
chunkType: "tool_result",
|
|
309
|
-
content: extractToolOutput(update),
|
|
310
|
-
metadata: {
|
|
311
|
-
tool_call_id: update.toolCallId,
|
|
312
|
-
status: update.status ?? "completed"
|
|
313
|
-
},
|
|
314
|
-
streamed: false
|
|
315
|
-
};
|
|
316
|
-
case "plan":
|
|
317
|
-
return {
|
|
318
|
-
chunkType: "plan",
|
|
319
|
-
content: update.entries.map((entry) => entry.content).join("\n"),
|
|
320
|
-
metadata: {},
|
|
321
|
-
streamed: false
|
|
322
|
-
};
|
|
323
|
-
default:
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
function extractToolOutput(update) {
|
|
328
|
-
if (typeof update.rawOutput === "string") {
|
|
329
|
-
return update.rawOutput;
|
|
330
|
-
}
|
|
331
|
-
if (update.rawOutput !== void 0 && update.rawOutput !== null) {
|
|
332
|
-
try {
|
|
333
|
-
return JSON.stringify(update.rawOutput);
|
|
334
|
-
} catch {
|
|
335
|
-
return String(update.rawOutput);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const parts = (update.content ?? []).map(extractTextFromToolContent).filter((text) => text.length > 0);
|
|
339
|
-
return parts.join("\n");
|
|
340
|
-
}
|
|
341
|
-
function extractTextFromToolContent(content) {
|
|
342
|
-
if (content.type === "content") {
|
|
343
|
-
return extractTextFromContent(content.content);
|
|
344
|
-
}
|
|
345
|
-
if (content.type === "diff") {
|
|
346
|
-
return content.newText;
|
|
347
|
-
}
|
|
348
|
-
if (content.type === "terminal") {
|
|
349
|
-
return `[Terminal: ${content.terminalId}]`;
|
|
350
|
-
}
|
|
351
|
-
return "";
|
|
352
|
-
}
|
|
353
|
-
function extractTextFromContent(content) {
|
|
354
|
-
switch (content.type) {
|
|
355
|
-
case "text":
|
|
356
|
-
return content.text;
|
|
357
|
-
case "resource_link":
|
|
358
|
-
return `[Resource: ${content.title ?? content.name ?? content.uri}]`;
|
|
359
|
-
case "resource": {
|
|
360
|
-
const resource = content.resource;
|
|
361
|
-
if ("text" in resource && typeof resource.text === "string") {
|
|
362
|
-
return resource.text;
|
|
363
|
-
}
|
|
364
|
-
return `[Resource: ${resource.uri ?? "embedded"}]`;
|
|
365
|
-
}
|
|
366
|
-
case "image":
|
|
367
|
-
return `[Image: ${content.mimeType ?? "image"}]`;
|
|
368
|
-
case "audio":
|
|
369
|
-
return `[Audio: ${content.mimeType ?? "audio"}]`;
|
|
370
|
-
default:
|
|
371
|
-
return "";
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
function toOptionalString(value) {
|
|
375
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// src/adapters/acp/loader.ts
|
|
379
|
-
var acpModule = new LazyAsyncValue({
|
|
380
|
-
load: async () => {
|
|
381
|
-
return await import("@agentclientprotocol/sdk").catch((error) => {
|
|
382
|
-
throw new UnsupportedFeatureError(
|
|
383
|
-
`ACP adapters require optional dependency "@agentclientprotocol/sdk". Install it with "pnpm add @agentclientprotocol/sdk". (${error instanceof Error ? error.message : String(error)})`
|
|
384
|
-
);
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
// src/adapters/acp/ACPClientAdapter.ts
|
|
390
|
-
var DEFAULT_PERMISSION_TIMEOUT_MS = 5 * 6e4;
|
|
391
|
-
var SET_SESSION_MODE_TIMEOUT_MS = 1e4;
|
|
392
|
-
var ACPClientAdapter = class extends SimpleAdapter {
|
|
393
|
-
command;
|
|
394
|
-
cwd;
|
|
395
|
-
env;
|
|
396
|
-
mcpServers;
|
|
397
|
-
authMethod;
|
|
398
|
-
enableMemoryTools;
|
|
399
|
-
enableMcpTools;
|
|
400
|
-
additionalMcpTools;
|
|
401
|
-
clientCapabilities;
|
|
402
|
-
connectionFactory;
|
|
403
|
-
roomToSession = /* @__PURE__ */ new Map();
|
|
404
|
-
sessionToRoom = /* @__PURE__ */ new Map();
|
|
405
|
-
roomTools = /* @__PURE__ */ new Map();
|
|
406
|
-
activeSessions = /* @__PURE__ */ new Set();
|
|
407
|
-
bootstrappedSessions = /* @__PURE__ */ new Set();
|
|
408
|
-
pendingPermissions = /* @__PURE__ */ new Map();
|
|
409
|
-
sessionsInFlight = /* @__PURE__ */ new Map();
|
|
410
|
-
roomTurnLocks = /* @__PURE__ */ new Map();
|
|
411
|
-
// Bumped each time a room starts a *new* establishment (never on a
|
|
412
|
-
// coalesced reuse) and whenever a room is torn down. An establishment
|
|
413
|
-
// captures its own value at the start; if the room has moved on by the
|
|
414
|
-
// time it would link/activate a session, it was superseded and must not.
|
|
415
|
-
roomGeneration = /* @__PURE__ */ new Map();
|
|
416
|
-
resolvePermission;
|
|
417
|
-
resolveSessionMode;
|
|
418
|
-
permissionTimeoutMs;
|
|
419
|
-
logger;
|
|
420
|
-
backend = null;
|
|
421
|
-
backendPromise = null;
|
|
422
|
-
client = null;
|
|
423
|
-
connectionHandle = null;
|
|
424
|
-
connection = null;
|
|
425
|
-
connectionState = null;
|
|
426
|
-
started = false;
|
|
427
|
-
systemPrompt = "";
|
|
428
|
-
spawnPromise = null;
|
|
429
|
-
constructor(options) {
|
|
430
|
-
super({
|
|
431
|
-
historyConverter: new ACPClientHistoryConverter()
|
|
432
|
-
});
|
|
433
|
-
this.command = Array.isArray(options.command) ? [...options.command] : [options.command];
|
|
434
|
-
if (this.command.length === 0 || this.command[0].length === 0) {
|
|
435
|
-
throw new Error("ACPClientAdapter requires a command");
|
|
436
|
-
}
|
|
437
|
-
this.cwd = options.cwd ?? process.cwd();
|
|
438
|
-
this.env = options.env;
|
|
439
|
-
this.mcpServers = [...options.mcpServers ?? []];
|
|
440
|
-
this.authMethod = options.authMethod;
|
|
441
|
-
this.enableMemoryTools = options.enableMemoryTools ?? false;
|
|
442
|
-
this.enableMcpTools = options.enableMcpTools ?? true;
|
|
443
|
-
this.additionalMcpTools = [...options.additionalMcpTools ?? []];
|
|
444
|
-
this.clientCapabilities = options.clientCapabilities;
|
|
445
|
-
this.connectionFactory = options.connectionFactory ?? createSubprocessConnection;
|
|
446
|
-
this.resolvePermission = options.resolvePermission;
|
|
447
|
-
this.resolveSessionMode = options.resolveSessionMode;
|
|
448
|
-
this.logger = options.logger ?? new NoopLogger();
|
|
449
|
-
this.permissionTimeoutMs = options.permissionTimeoutMs ?? DEFAULT_PERMISSION_TIMEOUT_MS;
|
|
450
|
-
if ((this.resolvePermission || this.resolveSessionMode) && (!Number.isFinite(this.permissionTimeoutMs) || this.permissionTimeoutMs <= 0)) {
|
|
451
|
-
throw new ValidationError(`permissionTimeoutMs must be a positive finite number, got ${options.permissionTimeoutMs}`);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
async onStarted(agentName, agentDescription) {
|
|
455
|
-
await super.onStarted(agentName, agentDescription);
|
|
456
|
-
this.started = true;
|
|
457
|
-
this.systemPrompt = renderSystemPrompt({
|
|
458
|
-
agentName,
|
|
459
|
-
agentDescription,
|
|
460
|
-
includeBaseInstructions: false
|
|
461
|
-
});
|
|
462
|
-
await this.ensureConnection();
|
|
463
|
-
}
|
|
464
|
-
async onMessage(message, tools, history, participantsMessage, contactsMessage, context) {
|
|
465
|
-
if (context.isSessionBootstrap) {
|
|
466
|
-
this.rehydrate(history);
|
|
467
|
-
}
|
|
468
|
-
this.roomTools.set(context.roomId, tools);
|
|
469
|
-
await this.withRoomTurnLock(context.roomId, () => this.runTurn(message, tools, participantsMessage, contactsMessage, context));
|
|
470
|
-
}
|
|
471
|
-
async runTurn(message, tools, participantsMessage, contactsMessage, context) {
|
|
472
|
-
const connection = await this.ensureConnection();
|
|
473
|
-
const client = this.client;
|
|
474
|
-
if (!client) {
|
|
475
|
-
throw new Error("ACP client was not initialized");
|
|
476
|
-
}
|
|
477
|
-
const sessionId = await this.getOrCreateSession(context.roomId, connection);
|
|
478
|
-
client.resetChunks(sessionId);
|
|
479
|
-
const content = replaceUuidMentions(message.content, mentionSubjectsFromMetadata(message.metadata));
|
|
480
|
-
const messageWithContext = [...systemUpdateParts(participantsMessage, contactsMessage), content].join("\n\n");
|
|
481
|
-
const promptText = this.bootstrappedSessions.has(sessionId) ? messageWithContext : `${this.buildSystemContext(context.roomId, message)}
|
|
482
|
-
|
|
483
|
-
${messageWithContext}`;
|
|
484
|
-
this.bootstrappedSessions.add(sessionId);
|
|
485
|
-
try {
|
|
486
|
-
await connection.prompt({
|
|
487
|
-
sessionId,
|
|
488
|
-
prompt: [{
|
|
489
|
-
type: "text",
|
|
490
|
-
text: promptText
|
|
491
|
-
}]
|
|
492
|
-
});
|
|
493
|
-
} catch (error) {
|
|
494
|
-
await this.stop();
|
|
495
|
-
await tools.sendEvent(`ACP agent error: ${toErrorMessage(error)}`, "error", {
|
|
496
|
-
acp_error: toErrorMessage(error)
|
|
497
|
-
});
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
await this.flushChunks({
|
|
501
|
-
tools,
|
|
502
|
-
sessionId,
|
|
503
|
-
senderId: message.senderId,
|
|
504
|
-
senderHandle: message.senderName ?? message.senderType
|
|
505
|
-
});
|
|
506
|
-
await tools.sendEvent("ACP client session", "task", {
|
|
507
|
-
acp_client_session_id: sessionId,
|
|
508
|
-
acp_client_room_id: context.roomId
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
// A per-room async mutex: `fn` for a given `roomId` never overlaps another
|
|
512
|
-
// call for that same room, while different rooms stay fully concurrent.
|
|
513
|
-
// The tracked tail (`this.roomTurnLocks`) always settles — via the
|
|
514
|
-
// trailing `.catch` — so one turn's failure can't wedge every later turn
|
|
515
|
-
// for the room; the real result/rejection is still `run`, returned to this
|
|
516
|
-
// call's own caller.
|
|
517
|
-
async withRoomTurnLock(roomId, fn) {
|
|
518
|
-
const previous = this.roomTurnLocks.get(roomId) ?? Promise.resolve();
|
|
519
|
-
const run = previous.then(fn, fn);
|
|
520
|
-
this.roomTurnLocks.set(roomId, run.catch(() => void 0));
|
|
521
|
-
return run;
|
|
522
|
-
}
|
|
523
|
-
async onCleanup(roomId) {
|
|
524
|
-
const sessionId = this.unlinkRoom(roomId);
|
|
525
|
-
this.roomTools.delete(roomId);
|
|
526
|
-
this.sessionsInFlight.delete(roomId);
|
|
527
|
-
this.roomTurnLocks.delete(roomId);
|
|
528
|
-
this.nextRoomGeneration(roomId);
|
|
529
|
-
if (sessionId) {
|
|
530
|
-
this.activeSessions.delete(sessionId);
|
|
531
|
-
this.bootstrappedSessions.delete(sessionId);
|
|
532
|
-
this.cancelPendingPermissions(sessionId, "room-closed");
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
async onRuntimeStop() {
|
|
536
|
-
await this.stop();
|
|
537
|
-
}
|
|
538
|
-
async stop() {
|
|
539
|
-
this.spawnPromise = null;
|
|
540
|
-
this.connectionState = null;
|
|
541
|
-
this.activeSessions.clear();
|
|
542
|
-
this.bootstrappedSessions.clear();
|
|
543
|
-
this.roomToSession.clear();
|
|
544
|
-
this.sessionToRoom.clear();
|
|
545
|
-
this.roomTools.clear();
|
|
546
|
-
this.sessionsInFlight.clear();
|
|
547
|
-
this.roomTurnLocks.clear();
|
|
548
|
-
for (const roomId of this.roomGeneration.keys()) {
|
|
549
|
-
this.nextRoomGeneration(roomId);
|
|
550
|
-
}
|
|
551
|
-
this.cancelAllPendingPermissions("adapter-stopped");
|
|
552
|
-
this.client = null;
|
|
553
|
-
this.connection = null;
|
|
554
|
-
if (this.backend) {
|
|
555
|
-
const backend = this.backend;
|
|
556
|
-
this.backend = null;
|
|
557
|
-
await backend.stop();
|
|
558
|
-
}
|
|
559
|
-
if (this.connectionHandle) {
|
|
560
|
-
const handle = this.connectionHandle;
|
|
561
|
-
this.connectionHandle = null;
|
|
562
|
-
await handle.stop();
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
rehydrate(history) {
|
|
566
|
-
for (const [roomId, sessionId] of Object.entries(history.roomToSession)) {
|
|
567
|
-
if (!this.roomToSession.has(roomId)) {
|
|
568
|
-
this.linkSession(roomId, sessionId);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
// The only writer of both session maps, so they cannot drift: replacing a
|
|
573
|
-
// room's session drops the old session's route, and a session id already
|
|
574
|
-
// routed to another room is refused rather than silently re-pointed — two
|
|
575
|
-
// rooms sharing one session id would make its permission requests
|
|
576
|
-
// unattributable. Returns whether the link was made — a caller that goes
|
|
577
|
-
// on to activate/configure/prompt a session regardless of a `false` here
|
|
578
|
-
// would use a session this room was refused, not just fail to route its
|
|
579
|
-
// permissions.
|
|
580
|
-
linkSession(roomId, sessionId) {
|
|
581
|
-
const routedRoomId = this.sessionToRoom.get(sessionId);
|
|
582
|
-
if (routedRoomId !== void 0 && routedRoomId !== roomId) {
|
|
583
|
-
this.safeWarn("refusing to route one ACP session to a second room", {
|
|
584
|
-
sessionId,
|
|
585
|
-
roomId,
|
|
586
|
-
routedRoomId
|
|
587
|
-
});
|
|
588
|
-
return false;
|
|
589
|
-
}
|
|
590
|
-
const replacedSessionId = this.roomToSession.get(roomId);
|
|
591
|
-
if (replacedSessionId !== void 0 && replacedSessionId !== sessionId) {
|
|
592
|
-
this.sessionToRoom.delete(replacedSessionId);
|
|
593
|
-
}
|
|
594
|
-
this.roomToSession.set(roomId, sessionId);
|
|
595
|
-
this.sessionToRoom.set(sessionId, roomId);
|
|
596
|
-
return true;
|
|
597
|
-
}
|
|
598
|
-
nextRoomGeneration(roomId) {
|
|
599
|
-
const next = (this.roomGeneration.get(roomId) ?? 0) + 1;
|
|
600
|
-
this.roomGeneration.set(roomId, next);
|
|
601
|
-
return next;
|
|
602
|
-
}
|
|
603
|
-
isCurrentGeneration(roomId, generation) {
|
|
604
|
-
return this.roomGeneration.get(roomId) === generation;
|
|
605
|
-
}
|
|
606
|
-
// The installed ACP SDK's `sendRequest` never rejects a pending call when
|
|
607
|
-
// its connection closes (no server response ever arrives to reject it
|
|
608
|
-
// with) — so a session-establishment RPC in flight when the subprocess
|
|
609
|
-
// dies would otherwise hang forever, wedging the room's `sessionsInFlight`
|
|
610
|
-
// entry along with it. Racing every such RPC against the connection's own
|
|
611
|
-
// `closed` promise gives it a real, prompt failure instead.
|
|
612
|
-
raceAgainstConnectionClose(connection, operation) {
|
|
613
|
-
let reject = () => void 0;
|
|
614
|
-
const closedRejection = new Promise((_resolve, rejectFn) => {
|
|
615
|
-
reject = rejectFn;
|
|
616
|
-
});
|
|
617
|
-
void connection.closed.then(() => reject(new Error("ACP connection closed while a session operation was still in flight")));
|
|
618
|
-
return Promise.race([operation, closedRejection]);
|
|
619
|
-
}
|
|
620
|
-
unlinkRoom(roomId) {
|
|
621
|
-
const sessionId = this.roomToSession.get(roomId);
|
|
622
|
-
this.roomToSession.delete(roomId);
|
|
623
|
-
if (sessionId !== void 0) {
|
|
624
|
-
this.sessionToRoom.delete(sessionId);
|
|
625
|
-
}
|
|
626
|
-
return sessionId;
|
|
627
|
-
}
|
|
628
|
-
async ensureConnection() {
|
|
629
|
-
if (this.connection && !this.connection.signal.aborted) {
|
|
630
|
-
return this.connection;
|
|
631
|
-
}
|
|
632
|
-
if (!this.started) {
|
|
633
|
-
throw new Error("ACPClientAdapter was not started");
|
|
634
|
-
}
|
|
635
|
-
const isCreator = !this.spawnPromise;
|
|
636
|
-
if (isCreator) {
|
|
637
|
-
this.spawnPromise = this.spawnConnection();
|
|
638
|
-
}
|
|
639
|
-
try {
|
|
640
|
-
return await this.spawnPromise;
|
|
641
|
-
} finally {
|
|
642
|
-
if (isCreator) this.spawnPromise = null;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
async spawnConnection() {
|
|
646
|
-
const acp = await acpModule.get();
|
|
647
|
-
const client = new BandACPClient((params) => this.routePermissionRequest(params));
|
|
648
|
-
const handle = await this.connectionFactory(client, {
|
|
649
|
-
command: this.command,
|
|
650
|
-
cwd: this.cwd,
|
|
651
|
-
env: this.env
|
|
652
|
-
});
|
|
653
|
-
const connection = handle.connection;
|
|
654
|
-
const initializeResult = await connection.initialize({
|
|
655
|
-
protocolVersion: acp.PROTOCOL_VERSION,
|
|
656
|
-
clientCapabilities: this.clientCapabilities ?? {}
|
|
657
|
-
});
|
|
658
|
-
if (this.authMethod) {
|
|
659
|
-
await connection.authenticate({
|
|
660
|
-
methodId: this.authMethod
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
this.client = client;
|
|
664
|
-
this.connection = connection;
|
|
665
|
-
this.connectionHandle = handle;
|
|
666
|
-
this.connectionState = initializeResult;
|
|
667
|
-
void connection.closed.finally(() => {
|
|
668
|
-
if (this.connection === connection) {
|
|
669
|
-
this.connection = null;
|
|
670
|
-
this.connectionHandle = null;
|
|
671
|
-
this.connectionState = null;
|
|
672
|
-
this.activeSessions.clear();
|
|
673
|
-
this.cancelAllPendingPermissions("connection-lost");
|
|
674
|
-
}
|
|
675
|
-
});
|
|
676
|
-
return connection;
|
|
677
|
-
}
|
|
678
|
-
async getOrCreateSession(roomId, connection) {
|
|
679
|
-
const existingSessionId = this.roomToSession.get(roomId);
|
|
680
|
-
if (existingSessionId && this.activeSessions.has(existingSessionId)) {
|
|
681
|
-
return existingSessionId;
|
|
682
|
-
}
|
|
683
|
-
const inFlight = this.sessionsInFlight.get(roomId);
|
|
684
|
-
if (inFlight) {
|
|
685
|
-
return inFlight;
|
|
686
|
-
}
|
|
687
|
-
const generation = this.nextRoomGeneration(roomId);
|
|
688
|
-
const establishing = this.establishSession(roomId, existingSessionId, connection, generation);
|
|
689
|
-
establishing.finally(() => {
|
|
690
|
-
if (this.sessionsInFlight.get(roomId) === establishing) {
|
|
691
|
-
this.sessionsInFlight.delete(roomId);
|
|
692
|
-
}
|
|
693
|
-
}).catch(() => void 0);
|
|
694
|
-
this.sessionsInFlight.set(roomId, establishing);
|
|
695
|
-
return establishing;
|
|
696
|
-
}
|
|
697
|
-
async establishSession(roomId, existingSessionId, connection, generation) {
|
|
698
|
-
const mcpServers = await this.buildSessionMcpServers();
|
|
699
|
-
if (existingSessionId) {
|
|
700
|
-
const restored = await this.tryRestoreSession(connection, existingSessionId, mcpServers);
|
|
701
|
-
if (restored.ok) {
|
|
702
|
-
this.linkOrAbandon(roomId, existingSessionId, generation);
|
|
703
|
-
this.activeSessions.add(existingSessionId);
|
|
704
|
-
this.bootstrappedSessions.add(existingSessionId);
|
|
705
|
-
await this.configureSessionMode(roomId, existingSessionId, restored.modes, connection);
|
|
706
|
-
return existingSessionId;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
const created = await this.raceAgainstConnectionClose(connection, connection.newSession({
|
|
710
|
-
cwd: this.cwd,
|
|
711
|
-
mcpServers
|
|
712
|
-
}));
|
|
713
|
-
this.linkOrAbandon(roomId, created.sessionId, generation);
|
|
714
|
-
this.activeSessions.add(created.sessionId);
|
|
715
|
-
await this.configureSessionMode(roomId, created.sessionId, created.modes, connection);
|
|
716
|
-
return created.sessionId;
|
|
717
|
-
}
|
|
718
|
-
// The single gate an establishment must pass before it's allowed to claim
|
|
719
|
-
// the room: it must still be the room's current generation (not
|
|
720
|
-
// superseded by a teardown or a fresher establishment while this one was
|
|
721
|
-
// awaiting an RPC), and its session id must not already belong to another
|
|
722
|
-
// room. Either failure throws — this establishment cannot silently
|
|
723
|
-
// continue to activate, configure, and prompt a session it has no right
|
|
724
|
-
// to use for this room.
|
|
725
|
-
linkOrAbandon(roomId, sessionId, generation) {
|
|
726
|
-
if (!this.isCurrentGeneration(roomId, generation)) {
|
|
727
|
-
throw new Error(`ACP session establishment for room "${roomId}" was superseded before it could be linked`);
|
|
728
|
-
}
|
|
729
|
-
if (!this.linkSession(roomId, sessionId)) {
|
|
730
|
-
throw new Error(`ACP session "${sessionId}" could not be linked to room "${roomId}": already routed elsewhere`);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
// Best-effort: never throws, so a mode switch going wrong can't take a
|
|
734
|
-
// session establishment down with it.
|
|
735
|
-
async configureSessionMode(roomId, sessionId, modes, connection) {
|
|
736
|
-
if (!this.resolveSessionMode || !modes) {
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
const availableModes = Array.isArray(modes.availableModes) ? modes.availableModes : [];
|
|
740
|
-
if (availableModes.length === 0) {
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
const selectedModeId = await this.resolveSessionModeManually(
|
|
744
|
-
(signal) => this.resolveSessionMode({
|
|
745
|
-
roomId,
|
|
746
|
-
sessionId,
|
|
747
|
-
currentModeId: modes.currentModeId,
|
|
748
|
-
modes: availableModes
|
|
749
|
-
}, signal),
|
|
750
|
-
connection.signal
|
|
751
|
-
);
|
|
752
|
-
if (!selectedModeId || selectedModeId === modes.currentModeId) {
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
if (!availableModes.some((mode) => mode?.id === selectedModeId)) {
|
|
756
|
-
this.safeWarn("resolveSessionMode selected a mode id this session does not advertise", {
|
|
757
|
-
sessionId,
|
|
758
|
-
selectedModeId,
|
|
759
|
-
availableModeIds: availableModes.map((mode) => mode?.id)
|
|
760
|
-
});
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
try {
|
|
764
|
-
await withTimeout(
|
|
765
|
-
connection.setSessionMode({ sessionId, modeId: selectedModeId }),
|
|
766
|
-
SET_SESSION_MODE_TIMEOUT_MS,
|
|
767
|
-
`setSessionMode did not respond within ${SET_SESSION_MODE_TIMEOUT_MS}ms`
|
|
768
|
-
);
|
|
769
|
-
} catch (error) {
|
|
770
|
-
this.safeWarn("failed to switch session into the selected mode", {
|
|
771
|
-
sessionId,
|
|
772
|
-
selectedModeId,
|
|
773
|
-
error: String(error)
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
async resolveSessionModeManually(resolver, signal) {
|
|
778
|
-
const controller = new AbortController();
|
|
779
|
-
const abort = () => controller.abort();
|
|
780
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
781
|
-
let timer;
|
|
782
|
-
try {
|
|
783
|
-
const cancelled = new Promise((resolve) => {
|
|
784
|
-
controller.signal.addEventListener("abort", () => resolve(void 0), { once: true });
|
|
785
|
-
});
|
|
786
|
-
const timeout = new Promise((resolve) => {
|
|
787
|
-
timer = setTimeout(() => resolve(void 0), this.permissionTimeoutMs);
|
|
788
|
-
});
|
|
789
|
-
return await Promise.race([
|
|
790
|
-
Promise.resolve().then(() => resolver(controller.signal)).catch((error) => {
|
|
791
|
-
this.safeWarn("resolveSessionMode threw; preserving the harness default", { error: String(error) });
|
|
792
|
-
return void 0;
|
|
793
|
-
}),
|
|
794
|
-
timeout,
|
|
795
|
-
cancelled
|
|
796
|
-
]);
|
|
797
|
-
} finally {
|
|
798
|
-
clearTimeout(timer);
|
|
799
|
-
signal.removeEventListener("abort", abort);
|
|
800
|
-
controller.abort();
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
async tryRestoreSession(connection, sessionId, mcpServers) {
|
|
804
|
-
const capabilities = this.connectionState?.agentCapabilities;
|
|
805
|
-
const params = { cwd: this.cwd, mcpServers, sessionId };
|
|
806
|
-
const restore = capabilities?.loadSession ? () => connection.loadSession(params) : capabilities?.sessionCapabilities?.resume ? () => connection.unstable_resumeSession(params) : null;
|
|
807
|
-
if (!restore) {
|
|
808
|
-
return { ok: false };
|
|
809
|
-
}
|
|
810
|
-
try {
|
|
811
|
-
const restored = await this.raceAgainstConnectionClose(connection, restore());
|
|
812
|
-
return { ok: true, modes: restored?.modes };
|
|
813
|
-
} catch {
|
|
814
|
-
return { ok: false };
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
async buildSessionMcpServers() {
|
|
818
|
-
const mcpServers = [...this.mcpServers];
|
|
819
|
-
if (!this.enableMcpTools) {
|
|
820
|
-
return mcpServers;
|
|
821
|
-
}
|
|
822
|
-
const backend = await this.getOrCreateBackend();
|
|
823
|
-
if (backend.kind === "http") {
|
|
824
|
-
const url = backend.server.url;
|
|
825
|
-
if (!url) {
|
|
826
|
-
throw new Error("Band MCP HTTP backend did not expose a URL");
|
|
827
|
-
}
|
|
828
|
-
mcpServers.push({
|
|
829
|
-
type: "http",
|
|
830
|
-
name: MCP_SERVER_NAME,
|
|
831
|
-
url,
|
|
832
|
-
headers: [{ name: "Authorization", value: `Bearer ${backend.authToken}` }]
|
|
833
|
-
});
|
|
834
|
-
return mcpServers;
|
|
835
|
-
}
|
|
836
|
-
if (backend.kind === "sse") {
|
|
837
|
-
const url = backend.server.sseUrl;
|
|
838
|
-
if (!url) {
|
|
839
|
-
throw new Error("Band MCP SSE backend did not expose a URL");
|
|
840
|
-
}
|
|
841
|
-
mcpServers.push({
|
|
842
|
-
type: "sse",
|
|
843
|
-
name: MCP_SERVER_NAME,
|
|
844
|
-
url,
|
|
845
|
-
headers: [{ name: "Authorization", value: `Bearer ${backend.authToken}` }]
|
|
846
|
-
});
|
|
847
|
-
return mcpServers;
|
|
848
|
-
}
|
|
849
|
-
return mcpServers;
|
|
850
|
-
}
|
|
851
|
-
async getOrCreateBackend() {
|
|
852
|
-
if (this.backend) {
|
|
853
|
-
return this.backend;
|
|
854
|
-
}
|
|
855
|
-
this.backendPromise ??= this.createBackend().finally(() => {
|
|
856
|
-
this.backendPromise = null;
|
|
857
|
-
});
|
|
858
|
-
return this.backendPromise;
|
|
859
|
-
}
|
|
860
|
-
async createBackend() {
|
|
861
|
-
const mcpCapabilities = this.connectionState?.agentCapabilities?.mcpCapabilities;
|
|
862
|
-
const transport = mcpCapabilities?.http ? "http" : mcpCapabilities?.sse ? "sse" : null;
|
|
863
|
-
if (transport === null) {
|
|
864
|
-
throw new Error(
|
|
865
|
-
"ACP agent does not advertise MCP transport support: its initialize response has mcpCapabilities.http and .sse both false or missing, so Band tools cannot be exposed to it over MCP."
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
const authToken = generateAuthToken();
|
|
869
|
-
if (transport === "sse") {
|
|
870
|
-
const server2 = new BandMcpSseServer({
|
|
871
|
-
tools: (roomId) => this.roomTools.get(roomId),
|
|
872
|
-
enableMemoryTools: this.enableMemoryTools,
|
|
873
|
-
enableContactTools: true,
|
|
874
|
-
additionalTools: this.additionalMcpTools,
|
|
875
|
-
authToken
|
|
876
|
-
});
|
|
877
|
-
await server2.start();
|
|
878
|
-
this.backend = {
|
|
879
|
-
kind: "sse",
|
|
880
|
-
server: server2,
|
|
881
|
-
authToken,
|
|
882
|
-
stop: async () => {
|
|
883
|
-
await server2.stop();
|
|
884
|
-
}
|
|
885
|
-
};
|
|
886
|
-
return this.backend;
|
|
887
|
-
}
|
|
888
|
-
const server = new BandMcpServer({
|
|
889
|
-
tools: (roomId) => this.roomTools.get(roomId),
|
|
890
|
-
enableMemoryTools: this.enableMemoryTools,
|
|
891
|
-
enableContactTools: true,
|
|
892
|
-
additionalTools: this.additionalMcpTools,
|
|
893
|
-
authToken
|
|
894
|
-
});
|
|
895
|
-
await server.start();
|
|
896
|
-
this.backend = {
|
|
897
|
-
kind: "http",
|
|
898
|
-
server,
|
|
899
|
-
authToken,
|
|
900
|
-
stop: async () => {
|
|
901
|
-
await server.stop();
|
|
902
|
-
}
|
|
903
|
-
};
|
|
904
|
-
return this.backend;
|
|
905
|
-
}
|
|
906
|
-
buildSystemContext(roomId, message) {
|
|
907
|
-
const requesterName = message.senderName ?? message.senderId;
|
|
908
|
-
const requesterId = message.senderId;
|
|
909
|
-
return [
|
|
910
|
-
"[System Context]",
|
|
911
|
-
this.systemPrompt,
|
|
912
|
-
"",
|
|
913
|
-
"## Room Context",
|
|
914
|
-
"You are connected to Band using Band MCP tools.",
|
|
915
|
-
"Use the Band tools for any visible room action. Plain text output is not posted back to the room.",
|
|
916
|
-
"",
|
|
917
|
-
`Current room_id: ${roomId}`,
|
|
918
|
-
`Current requester name: ${requesterName}`,
|
|
919
|
-
`Current requester id: ${requesterId}`,
|
|
920
|
-
"",
|
|
921
|
-
"All Band MCP tool calls must include room_id."
|
|
922
|
-
].join("\n");
|
|
923
|
-
}
|
|
924
|
-
// The connection's single permission entry point, and total by
|
|
925
|
-
// construction: every path resolves, nothing throws, and a request that
|
|
926
|
-
// can't be attributed to a live room is cancelled *and* warned rather than
|
|
927
|
-
// silently declined. `activeSessions` is the gate that keeps a dead
|
|
928
|
-
// session from raising a live prompt — `roomToSession` deliberately
|
|
929
|
-
// outlives a dropped connection so the session can be restored later.
|
|
930
|
-
async routePermissionRequest(params) {
|
|
931
|
-
const isActive = this.activeSessions.has(params.sessionId);
|
|
932
|
-
const roomId = isActive ? this.sessionToRoom.get(params.sessionId) : void 0;
|
|
933
|
-
const tools = roomId === void 0 ? void 0 : this.roomTools.get(roomId);
|
|
934
|
-
if (roomId === void 0 || !tools) {
|
|
935
|
-
this.safeWarn("cancelling a permission request that maps to no live room", {
|
|
936
|
-
sessionId: params.sessionId,
|
|
937
|
-
toolName: params.toolCall?.title,
|
|
938
|
-
sessionActive: isActive,
|
|
939
|
-
roomId
|
|
940
|
-
});
|
|
941
|
-
return { outcome: { outcome: "cancelled" } };
|
|
942
|
-
}
|
|
943
|
-
try {
|
|
944
|
-
return await this.handlePermissionRequest(tools, roomId, params);
|
|
945
|
-
} catch (error) {
|
|
946
|
-
this.safeWarn("permission handling failed; cancelling the request", {
|
|
947
|
-
sessionId: params.sessionId,
|
|
948
|
-
roomId,
|
|
949
|
-
error: String(error)
|
|
950
|
-
});
|
|
951
|
-
return { outcome: { outcome: "cancelled" } };
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
async handlePermissionRequest(tools, roomId, params) {
|
|
955
|
-
const toolName = params.toolCall.title ?? "unknown";
|
|
956
|
-
const autoSelection = this.resolvePermission ? void 0 : choosePermissionOption(params.options);
|
|
957
|
-
const controller = this.resolvePermission ? new AbortController() : void 0;
|
|
958
|
-
if (controller) {
|
|
959
|
-
this.trackPending(params.sessionId, controller);
|
|
960
|
-
}
|
|
961
|
-
let requestEventFailed = false;
|
|
962
|
-
const [, resolvedChosenId] = await Promise.all([
|
|
963
|
-
// Started immediately rather than serialized in front of a manual
|
|
964
|
-
// wait that can take up to `permissionTimeoutMs`.
|
|
965
|
-
tools.sendEvent(`Permission requested: ${toolName}`, "tool_call", {
|
|
966
|
-
permission_request: true,
|
|
967
|
-
tool_name: toolName,
|
|
968
|
-
tool_call_id: params.toolCall.toolCallId,
|
|
969
|
-
acp_session_id: params.sessionId,
|
|
970
|
-
auto_allowed: autoSelection !== void 0 && autoSelection !== null
|
|
971
|
-
}).catch((error) => {
|
|
972
|
-
requestEventFailed = true;
|
|
973
|
-
this.safeWarn("failed to post the permission-requested event; cancelling the request", {
|
|
974
|
-
roomId,
|
|
975
|
-
sessionId: params.sessionId,
|
|
976
|
-
error: String(error)
|
|
977
|
-
});
|
|
978
|
-
if (controller) {
|
|
979
|
-
this.abandon(controller, "no-answer");
|
|
980
|
-
}
|
|
981
|
-
}),
|
|
982
|
-
controller ? this.resolveManually(roomId, params, controller) : Promise.resolve(autoSelection?.optionId)
|
|
983
|
-
]);
|
|
984
|
-
const chosenId = requestEventFailed ? void 0 : resolvedChosenId;
|
|
985
|
-
const response = this.toResponse(chosenId, params.options, { roomId, sessionId: params.sessionId });
|
|
986
|
-
if (controller && !controller.signal.aborted) {
|
|
987
|
-
this.abandon(controller, response.outcome.outcome === "selected" ? "settled" : "no-answer");
|
|
988
|
-
}
|
|
989
|
-
return response;
|
|
990
|
-
}
|
|
991
|
-
// `undefined`, or an id absent from this request's own `options` (a buggy
|
|
992
|
-
// or stale caller), both map to `cancelled` — never silently treated as a
|
|
993
|
-
// deny. A real match, reject-kind options included, maps to `selected`.
|
|
994
|
-
toResponse(chosenId, options, context) {
|
|
995
|
-
if (chosenId === void 0) {
|
|
996
|
-
return { outcome: { outcome: "cancelled" } };
|
|
997
|
-
}
|
|
998
|
-
if (!options.some((option) => option.optionId === chosenId)) {
|
|
999
|
-
this.safeWarn("resolvePermission chose an option this request does not offer", {
|
|
1000
|
-
...context,
|
|
1001
|
-
chosenId,
|
|
1002
|
-
optionIds: options.map((option) => option.optionId)
|
|
1003
|
-
});
|
|
1004
|
-
return { outcome: { outcome: "cancelled" } };
|
|
1005
|
-
}
|
|
1006
|
-
return { outcome: { outcome: "selected", optionId: chosenId } };
|
|
1007
|
-
}
|
|
1008
|
-
// A caller-supplied `Logger` isn't guaranteed to be synchronous or
|
|
1009
|
-
// non-throwing. Every best-effort warning in this file routes through here
|
|
1010
|
-
// so one failing sink — a synchronous throw, or an `async` implementation
|
|
1011
|
-
// rejecting (the `Logger` interface's `void` return type permits either;
|
|
1012
|
-
// a bare try/catch only ever catches the former) — can't turn a warning
|
|
1013
|
-
// into an unhandled rejection in its place.
|
|
1014
|
-
safeWarn(message, context) {
|
|
1015
|
-
try {
|
|
1016
|
-
Promise.resolve(this.logger.warn(message, context)).catch(() => void 0);
|
|
1017
|
-
} catch {
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
// Races the caller-supplied resolver against `controller`'s abort signal,
|
|
1021
|
-
// which is the request's single termination channel: the timeout below
|
|
1022
|
-
// fires it with `"timeout"`, and `cancelPendingPermissions` /
|
|
1023
|
-
// `cancelAllPendingPermissions` fire it with the reason their caller
|
|
1024
|
-
// supplies. `controller` is the same object tracked in `pendingPermissions`,
|
|
1025
|
-
// so there is exactly one cancellation channel here, not a second
|
|
1026
|
-
// hand-rolled one alongside it.
|
|
1027
|
-
async resolveManually(roomId, params, controller) {
|
|
1028
|
-
const abandoned = new Promise((resolve) => {
|
|
1029
|
-
controller.signal.addEventListener("abort", () => resolve(void 0));
|
|
1030
|
-
});
|
|
1031
|
-
const timer = setTimeout(() => this.abandon(controller, "timeout"), this.permissionTimeoutMs);
|
|
1032
|
-
try {
|
|
1033
|
-
return await Promise.race([
|
|
1034
|
-
// `resolvePermission` is caller-supplied; nothing guarantees it's
|
|
1035
|
-
// `async` or otherwise well-behaved. `Promise.resolve().then(...)`
|
|
1036
|
-
// normalizes a synchronous throw the same way it normalizes a
|
|
1037
|
-
// rejected promise, so both land in the `.catch` below rather than
|
|
1038
|
-
// escaping this race uncaught.
|
|
1039
|
-
Promise.resolve().then(() => this.resolvePermission({ ...params, roomId }, controller.signal)).then((chosenId) => this.discardLateAnswer(chosenId, controller, roomId, params.sessionId)).catch((error) => {
|
|
1040
|
-
this.safeWarn("resolvePermission threw; treating as no answer", { error: String(error) });
|
|
1041
|
-
return void 0;
|
|
1042
|
-
}),
|
|
1043
|
-
abandoned
|
|
1044
|
-
]);
|
|
1045
|
-
} finally {
|
|
1046
|
-
clearTimeout(timer);
|
|
1047
|
-
this.untrackPending(params.sessionId, controller);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
// An answer that lands after the request was given up on can no longer be
|
|
1051
|
-
// honoured — the response has already gone back to the agent. It is dropped
|
|
1052
|
-
// either way; warning is what makes "my click did nothing" explicable.
|
|
1053
|
-
discardLateAnswer(chosenId, controller, roomId, sessionId) {
|
|
1054
|
-
if (!controller.signal.aborted || chosenId === void 0) {
|
|
1055
|
-
return chosenId;
|
|
1056
|
-
}
|
|
1057
|
-
this.safeWarn("resolvePermission answered after the request was abandoned; discarding", {
|
|
1058
|
-
roomId,
|
|
1059
|
-
sessionId,
|
|
1060
|
-
chosenId,
|
|
1061
|
-
reason: String(controller.signal.reason)
|
|
1062
|
-
});
|
|
1063
|
-
return void 0;
|
|
1064
|
-
}
|
|
1065
|
-
// The only place a permission's controller is ever aborted, so every
|
|
1066
|
-
// `signal.reason` a consumer can observe comes from the documented union.
|
|
1067
|
-
abandon(controller, reason) {
|
|
1068
|
-
controller.abort(reason);
|
|
1069
|
-
}
|
|
1070
|
-
trackPending(sessionId, controller) {
|
|
1071
|
-
const pending = this.pendingPermissions.get(sessionId) ?? /* @__PURE__ */ new Set();
|
|
1072
|
-
pending.add(controller);
|
|
1073
|
-
this.pendingPermissions.set(sessionId, pending);
|
|
1074
|
-
}
|
|
1075
|
-
untrackPending(sessionId, controller) {
|
|
1076
|
-
const pending = this.pendingPermissions.get(sessionId);
|
|
1077
|
-
pending?.delete(controller);
|
|
1078
|
-
if (pending?.size === 0) {
|
|
1079
|
-
this.pendingPermissions.delete(sessionId);
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
cancelPendingPermissions(sessionId, reason) {
|
|
1083
|
-
for (const controller of this.pendingPermissions.get(sessionId) ?? []) {
|
|
1084
|
-
this.abandon(controller, reason);
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
cancelAllPendingPermissions(reason) {
|
|
1088
|
-
for (const sessionId of this.pendingPermissions.keys()) {
|
|
1089
|
-
this.cancelPendingPermissions(sessionId, reason);
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
async flushChunks(input) {
|
|
1093
|
-
const client = this.client;
|
|
1094
|
-
if (!client) {
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
for (const chunk of client.getCollectedChunks(input.sessionId)) {
|
|
1098
|
-
if (isBlankEventContent(chunk.content)) {
|
|
1099
|
-
continue;
|
|
1100
|
-
}
|
|
1101
|
-
if (chunk.chunkType === "text") {
|
|
1102
|
-
await input.tools.sendMessage(chunk.content, [{
|
|
1103
|
-
id: input.senderId,
|
|
1104
|
-
handle: input.senderHandle
|
|
1105
|
-
}]);
|
|
1106
|
-
continue;
|
|
1107
|
-
}
|
|
1108
|
-
const messageType = chunk.chunkType === "plan" ? "task" : chunk.chunkType;
|
|
1109
|
-
await input.tools.sendEvent(
|
|
1110
|
-
chunk.content,
|
|
1111
|
-
messageType,
|
|
1112
|
-
chunk.metadata
|
|
1113
|
-
);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
};
|
|
1117
|
-
async function createSubprocessConnection(client, options) {
|
|
1118
|
-
const acp = await acpModule.get();
|
|
1119
|
-
const child = spawn(options.command[0], options.command.slice(1), {
|
|
1120
|
-
cwd: options.cwd,
|
|
1121
|
-
env: {
|
|
1122
|
-
...process.env,
|
|
1123
|
-
...options.env
|
|
1124
|
-
},
|
|
1125
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1126
|
-
});
|
|
1127
|
-
if (!child.stdin || !child.stdout) {
|
|
1128
|
-
throw new Error("ACP subprocess did not expose stdio pipes");
|
|
1129
|
-
}
|
|
1130
|
-
const stream = acp.ndJsonStream(
|
|
1131
|
-
Writable.toWeb(child.stdin),
|
|
1132
|
-
Readable.toWeb(child.stdout)
|
|
1133
|
-
);
|
|
1134
|
-
const connection = new acp.ClientSideConnection(() => client, stream);
|
|
1135
|
-
return {
|
|
1136
|
-
connection,
|
|
1137
|
-
stop: async () => {
|
|
1138
|
-
await new Promise((resolve) => {
|
|
1139
|
-
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1140
|
-
resolve();
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
let settled = false;
|
|
1144
|
-
const finish = () => {
|
|
1145
|
-
if (settled) {
|
|
1146
|
-
return;
|
|
1147
|
-
}
|
|
1148
|
-
settled = true;
|
|
1149
|
-
child.off("exit", finish);
|
|
1150
|
-
child.off("close", finish);
|
|
1151
|
-
resolve();
|
|
1152
|
-
};
|
|
1153
|
-
child.once("exit", finish);
|
|
1154
|
-
child.once("close", finish);
|
|
1155
|
-
if (!child.killed) {
|
|
1156
|
-
child.kill();
|
|
1157
|
-
}
|
|
1158
|
-
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1159
|
-
finish();
|
|
1160
|
-
}
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1165
|
-
function toErrorMessage(error) {
|
|
1166
|
-
if (error instanceof Error) {
|
|
1167
|
-
return error.message;
|
|
1168
|
-
}
|
|
1169
|
-
return String(error);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
// src/adapters/acp/ACPServer.ts
|
|
1173
|
-
import { Readable as Readable2, Writable as Writable2 } from "stream";
|
|
1174
|
-
|
|
1175
|
-
// src/adapters/acp/BandACPServerAdapter.ts
|
|
1176
|
-
import { randomUUID } from "crypto";
|
|
1177
|
-
|
|
1178
|
-
// src/adapters/acp/eventConverter.ts
|
|
1179
|
-
var EventConverter = class {
|
|
1180
|
-
static convert(message) {
|
|
1181
|
-
switch (message.messageType) {
|
|
1182
|
-
case "text":
|
|
1183
|
-
return {
|
|
1184
|
-
sessionUpdate: "agent_message_chunk",
|
|
1185
|
-
content: {
|
|
1186
|
-
type: "text",
|
|
1187
|
-
text: message.content
|
|
1188
|
-
}
|
|
1189
|
-
};
|
|
1190
|
-
case "thought":
|
|
1191
|
-
return {
|
|
1192
|
-
sessionUpdate: "agent_thought_chunk",
|
|
1193
|
-
content: {
|
|
1194
|
-
type: "text",
|
|
1195
|
-
text: message.content
|
|
1196
|
-
}
|
|
1197
|
-
};
|
|
1198
|
-
case "tool_call":
|
|
1199
|
-
return this.convertToolCall(message.content);
|
|
1200
|
-
case "tool_result":
|
|
1201
|
-
return this.convertToolResult(message.content);
|
|
1202
|
-
case "error":
|
|
1203
|
-
return {
|
|
1204
|
-
sessionUpdate: "agent_message_chunk",
|
|
1205
|
-
content: {
|
|
1206
|
-
type: "text",
|
|
1207
|
-
text: `[Error] ${message.content}`
|
|
1208
|
-
}
|
|
1209
|
-
};
|
|
1210
|
-
case "task":
|
|
1211
|
-
return {
|
|
1212
|
-
sessionUpdate: "plan",
|
|
1213
|
-
entries: [{
|
|
1214
|
-
content: message.content,
|
|
1215
|
-
priority: "medium",
|
|
1216
|
-
status: "in_progress"
|
|
1217
|
-
}]
|
|
1218
|
-
};
|
|
1219
|
-
default:
|
|
1220
|
-
return null;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
static convertToolCall(value) {
|
|
1224
|
-
const parsed = parseToolCall(value);
|
|
1225
|
-
if (!parsed) {
|
|
1226
|
-
return {
|
|
1227
|
-
sessionUpdate: "agent_message_chunk",
|
|
1228
|
-
content: {
|
|
1229
|
-
type: "text",
|
|
1230
|
-
text: value
|
|
1231
|
-
}
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
return {
|
|
1235
|
-
sessionUpdate: "tool_call",
|
|
1236
|
-
toolCallId: parsed.toolCallId,
|
|
1237
|
-
title: parsed.name,
|
|
1238
|
-
kind: "other",
|
|
1239
|
-
status: "in_progress",
|
|
1240
|
-
rawInput: parsed.args
|
|
1241
|
-
};
|
|
1242
|
-
}
|
|
1243
|
-
static convertToolResult(value) {
|
|
1244
|
-
const parsed = parseToolResult(value);
|
|
1245
|
-
if (!parsed) {
|
|
1246
|
-
return {
|
|
1247
|
-
sessionUpdate: "agent_message_chunk",
|
|
1248
|
-
content: {
|
|
1249
|
-
type: "text",
|
|
1250
|
-
text: value
|
|
1251
|
-
}
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
const textBlock = {
|
|
1255
|
-
type: "text",
|
|
1256
|
-
text: parsed.output
|
|
1257
|
-
};
|
|
1258
|
-
const content = [{
|
|
1259
|
-
type: "content",
|
|
1260
|
-
content: textBlock
|
|
1261
|
-
}];
|
|
1262
|
-
return {
|
|
1263
|
-
sessionUpdate: "tool_call_update",
|
|
1264
|
-
toolCallId: parsed.toolCallId,
|
|
1265
|
-
status: parsed.isError ? "failed" : "completed",
|
|
1266
|
-
rawOutput: parsed.output,
|
|
1267
|
-
content
|
|
1268
|
-
};
|
|
1269
|
-
}
|
|
1270
|
-
};
|
|
1271
|
-
|
|
1272
|
-
// src/adapters/acp/pushHandler.ts
|
|
1273
|
-
var ACPPushHandler = class {
|
|
1274
|
-
adapter;
|
|
1275
|
-
constructor(adapter) {
|
|
1276
|
-
this.adapter = adapter;
|
|
1277
|
-
}
|
|
1278
|
-
async handlePushEvent(message, roomId) {
|
|
1279
|
-
const sessionId = this.adapter.getSessionForRoom(roomId);
|
|
1280
|
-
const connection = this.adapter.getConnection();
|
|
1281
|
-
if (!sessionId || !connection) {
|
|
1282
|
-
return;
|
|
1283
|
-
}
|
|
1284
|
-
const update = EventConverter.convert(message);
|
|
1285
|
-
if (!update) {
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
await connection.sessionUpdate({
|
|
1289
|
-
sessionId,
|
|
1290
|
-
update
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1293
|
-
};
|
|
1294
|
-
|
|
1295
|
-
// src/adapters/acp/router.ts
|
|
1296
|
-
var AgentRouter = class {
|
|
1297
|
-
modeToPeer;
|
|
1298
|
-
slashCommands;
|
|
1299
|
-
constructor(options) {
|
|
1300
|
-
this.modeToPeer = { ...options?.modeToPeer ?? {} };
|
|
1301
|
-
this.slashCommands = Object.fromEntries(
|
|
1302
|
-
Object.entries(options?.slashCommands ?? {}).map(([command, peer]) => [command.toLowerCase(), peer])
|
|
1303
|
-
);
|
|
1304
|
-
}
|
|
1305
|
-
resolve(text, currentMode) {
|
|
1306
|
-
if (text.startsWith("/")) {
|
|
1307
|
-
const [command, ...rest] = text.slice(1).split(/\s+/);
|
|
1308
|
-
const peer = this.slashCommands[command?.toLowerCase() ?? ""];
|
|
1309
|
-
if (peer) {
|
|
1310
|
-
return {
|
|
1311
|
-
text: rest.join(" ").trim(),
|
|
1312
|
-
targetPeer: peer
|
|
1313
|
-
};
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
if (currentMode && this.modeToPeer[currentMode]) {
|
|
1317
|
-
return {
|
|
1318
|
-
text,
|
|
1319
|
-
targetPeer: this.modeToPeer[currentMode]
|
|
1320
|
-
};
|
|
1321
|
-
}
|
|
1322
|
-
return {
|
|
1323
|
-
text,
|
|
1324
|
-
targetPeer: null
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
};
|
|
1328
|
-
|
|
1329
|
-
// src/adapters/acp/BandACPServerAdapter.ts
|
|
1330
|
-
var DEFAULT_MAX_SESSIONS = 100;
|
|
1331
|
-
var DEFAULT_PROMPT_TIMEOUT_MS = 3e5;
|
|
1332
|
-
var DEFAULT_PROMPT_COMPLETION_GRACE_MS = 250;
|
|
1333
|
-
var BandACPServerAdapter = class extends SimpleAdapter {
|
|
1334
|
-
bandRest;
|
|
1335
|
-
maxSessions;
|
|
1336
|
-
responseTimeoutMs;
|
|
1337
|
-
promptCompletionGraceMs;
|
|
1338
|
-
sessionModes;
|
|
1339
|
-
sessionToRoom = /* @__PURE__ */ new Map();
|
|
1340
|
-
roomToSession = /* @__PURE__ */ new Map();
|
|
1341
|
-
pendingPrompts = /* @__PURE__ */ new Map();
|
|
1342
|
-
sessionModeIds = /* @__PURE__ */ new Map();
|
|
1343
|
-
sessionModelIds = /* @__PURE__ */ new Map();
|
|
1344
|
-
sessionCwds = /* @__PURE__ */ new Map();
|
|
1345
|
-
sessionMcpServers = /* @__PURE__ */ new Map();
|
|
1346
|
-
connection = null;
|
|
1347
|
-
agentId = null;
|
|
1348
|
-
sessionsInFlight = 0;
|
|
1349
|
-
router;
|
|
1350
|
-
pushHandler;
|
|
1351
|
-
constructor(options) {
|
|
1352
|
-
super({
|
|
1353
|
-
historyConverter: new ACPServerHistoryConverter()
|
|
1354
|
-
});
|
|
1355
|
-
this.bandRest = options.bandRest;
|
|
1356
|
-
this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
1357
|
-
this.responseTimeoutMs = options.responseTimeoutMs ?? DEFAULT_PROMPT_TIMEOUT_MS;
|
|
1358
|
-
this.promptCompletionGraceMs = options.promptCompletionGraceMs ?? DEFAULT_PROMPT_COMPLETION_GRACE_MS;
|
|
1359
|
-
this.sessionModes = options.sessionModes?.length ? [...options.sessionModes] : [...DEFAULT_ACP_SERVER_MODES];
|
|
1360
|
-
this.router = new AgentRouter({
|
|
1361
|
-
modeToPeer: options.modeToPeer,
|
|
1362
|
-
slashCommands: options.slashCommands
|
|
1363
|
-
});
|
|
1364
|
-
this.pushHandler = new ACPPushHandler(this);
|
|
1365
|
-
}
|
|
1366
|
-
bindConnection(connection) {
|
|
1367
|
-
this.connection = connection;
|
|
1368
|
-
}
|
|
1369
|
-
getConnection() {
|
|
1370
|
-
return this.connection;
|
|
1371
|
-
}
|
|
1372
|
-
hasSession(sessionId) {
|
|
1373
|
-
return this.sessionToRoom.has(sessionId);
|
|
1374
|
-
}
|
|
1375
|
-
getSessionIds() {
|
|
1376
|
-
return [...this.sessionToRoom.keys()];
|
|
1377
|
-
}
|
|
1378
|
-
getSessionForRoom(roomId) {
|
|
1379
|
-
return this.roomToSession.get(roomId) ?? null;
|
|
1380
|
-
}
|
|
1381
|
-
get displayName() {
|
|
1382
|
-
return this.agentName;
|
|
1383
|
-
}
|
|
1384
|
-
getSessionCwd(sessionId) {
|
|
1385
|
-
return this.sessionCwds.get(sessionId) ?? ".";
|
|
1386
|
-
}
|
|
1387
|
-
getModeState(sessionId) {
|
|
1388
|
-
return {
|
|
1389
|
-
availableModes: this.sessionModes,
|
|
1390
|
-
currentModeId: this.sessionModeIds.get(sessionId) ?? this.sessionModes[0]?.id ?? "default"
|
|
1391
|
-
};
|
|
1392
|
-
}
|
|
1393
|
-
setSessionModes(modes) {
|
|
1394
|
-
this.sessionModes.splice(0, this.sessionModes.length, ...modes);
|
|
1395
|
-
}
|
|
1396
|
-
setSessionMode(sessionId, modeId) {
|
|
1397
|
-
this.sessionModeIds.set(sessionId, modeId);
|
|
1398
|
-
}
|
|
1399
|
-
setSessionModel(sessionId, modelId) {
|
|
1400
|
-
this.sessionModelIds.set(sessionId, modelId);
|
|
1401
|
-
}
|
|
1402
|
-
updateSessionContext(sessionId, input) {
|
|
1403
|
-
if (input.cwd) {
|
|
1404
|
-
this.sessionCwds.set(sessionId, input.cwd);
|
|
1405
|
-
}
|
|
1406
|
-
if (input.mcpServers) {
|
|
1407
|
-
const normalized = normalizeMcpServers(input.mcpServers);
|
|
1408
|
-
if (normalized.length > 0) {
|
|
1409
|
-
this.sessionMcpServers.set(sessionId, normalized);
|
|
1410
|
-
} else {
|
|
1411
|
-
this.sessionMcpServers.delete(sessionId);
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
async verifyCredentials() {
|
|
1416
|
-
try {
|
|
1417
|
-
await this.bandRest.getAgentMe();
|
|
1418
|
-
return true;
|
|
1419
|
-
} catch {
|
|
1420
|
-
return false;
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
async onStarted(agentName, agentDescription) {
|
|
1424
|
-
await super.onStarted(agentName, agentDescription);
|
|
1425
|
-
try {
|
|
1426
|
-
const identity = await this.bandRest.getAgentMe();
|
|
1427
|
-
this.agentId = identity.id;
|
|
1428
|
-
} catch {
|
|
1429
|
-
this.agentId = null;
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
async createSession(input = {}) {
|
|
1433
|
-
this.sessionsInFlight += 1;
|
|
1434
|
-
const activeCount = this.sessionToRoom.size + this.sessionsInFlight;
|
|
1435
|
-
if (activeCount > this.maxSessions) {
|
|
1436
|
-
this.sessionsInFlight = Math.max(0, this.sessionsInFlight - 1);
|
|
1437
|
-
throw new Error(`Maximum ACP sessions (${this.maxSessions}) reached`);
|
|
1438
|
-
}
|
|
1439
|
-
try {
|
|
1440
|
-
const room = await this.bandRest.createChat();
|
|
1441
|
-
const sessionId = randomUUID();
|
|
1442
|
-
const normalizedMcpServers = normalizeMcpServers(input.mcpServers);
|
|
1443
|
-
this.sessionToRoom.set(sessionId, room.id);
|
|
1444
|
-
this.roomToSession.set(room.id, sessionId);
|
|
1445
|
-
this.sessionCwds.set(sessionId, input.cwd ?? ".");
|
|
1446
|
-
if (normalizedMcpServers.length > 0) {
|
|
1447
|
-
this.sessionMcpServers.set(sessionId, normalizedMcpServers);
|
|
1448
|
-
}
|
|
1449
|
-
try {
|
|
1450
|
-
await this.bandRest.createChatEvent(room.id, {
|
|
1451
|
-
content: "ACP session context",
|
|
1452
|
-
messageType: "task",
|
|
1453
|
-
metadata: {
|
|
1454
|
-
acp_session_id: sessionId,
|
|
1455
|
-
acp_room_id: room.id,
|
|
1456
|
-
acp_cwd: input.cwd ?? ".",
|
|
1457
|
-
acp_mcp_servers: normalizedMcpServers
|
|
1458
|
-
}
|
|
1459
|
-
});
|
|
1460
|
-
} catch (error) {
|
|
1461
|
-
this.sessionToRoom.delete(sessionId);
|
|
1462
|
-
this.roomToSession.delete(room.id);
|
|
1463
|
-
this.sessionCwds.delete(sessionId);
|
|
1464
|
-
this.sessionMcpServers.delete(sessionId);
|
|
1465
|
-
this.sessionModeIds.delete(sessionId);
|
|
1466
|
-
this.sessionModelIds.delete(sessionId);
|
|
1467
|
-
throw error;
|
|
1468
|
-
}
|
|
1469
|
-
return sessionId;
|
|
1470
|
-
} finally {
|
|
1471
|
-
this.sessionsInFlight = Math.max(0, this.sessionsInFlight - 1);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
async handlePrompt(sessionId, text) {
|
|
1475
|
-
const roomId = this.sessionToRoom.get(sessionId);
|
|
1476
|
-
if (!roomId) {
|
|
1477
|
-
throw new Error(`Unknown ACP session: ${sessionId}`);
|
|
1478
|
-
}
|
|
1479
|
-
const pending = createPendingPrompt(sessionId);
|
|
1480
|
-
this.pendingPrompts.set(roomId, pending);
|
|
1481
|
-
const resolved = this.router.resolve(text, this.sessionModeIds.get(sessionId));
|
|
1482
|
-
const promptText = this.prependSessionContext(sessionId, resolved.text);
|
|
1483
|
-
const participants = await this.bandRest.listChatParticipants(roomId);
|
|
1484
|
-
const mentions = this.resolveMentions(participants, resolved.targetPeer);
|
|
1485
|
-
try {
|
|
1486
|
-
await this.bandRest.createChatMessage(roomId, {
|
|
1487
|
-
content: promptText,
|
|
1488
|
-
mentions
|
|
1489
|
-
});
|
|
1490
|
-
await Promise.race([
|
|
1491
|
-
pending.done,
|
|
1492
|
-
new Promise((_, reject) => {
|
|
1493
|
-
const timer = setTimeout(() => {
|
|
1494
|
-
reject(new Error(`ACP prompt timed out after ${this.responseTimeoutMs}ms`));
|
|
1495
|
-
}, this.responseTimeoutMs);
|
|
1496
|
-
pending.done.finally(() => clearTimeout(timer)).catch(() => void 0);
|
|
1497
|
-
})
|
|
1498
|
-
]);
|
|
1499
|
-
} finally {
|
|
1500
|
-
this.finishPendingPrompt(roomId, pending, false);
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
async onMessage(message, _tools, history, _participantsMessage, _contactsMessage, context) {
|
|
1504
|
-
if (context.isSessionBootstrap) {
|
|
1505
|
-
this.rehydrate(history);
|
|
1506
|
-
}
|
|
1507
|
-
if (this.agentId && message.senderId === this.agentId) {
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1510
|
-
const pending = this.pendingPrompts.get(context.roomId);
|
|
1511
|
-
if (pending && this.connection) {
|
|
1512
|
-
const update = EventConverter.convert(message);
|
|
1513
|
-
if (update) {
|
|
1514
|
-
await this.connection.sessionUpdate({
|
|
1515
|
-
sessionId: pending.sessionId,
|
|
1516
|
-
update
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1519
|
-
if (message.messageType === "text" || message.messageType === "error") {
|
|
1520
|
-
pending.terminalMessageSeen = true;
|
|
1521
|
-
}
|
|
1522
|
-
if (update || pending.terminalMessageSeen) {
|
|
1523
|
-
this.schedulePromptCompletion(context.roomId, pending);
|
|
1524
|
-
}
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
|
-
if (this.connection) {
|
|
1528
|
-
await this.pushHandler.handlePushEvent(message, context.roomId);
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
async onCleanup(roomId) {
|
|
1532
|
-
const sessionId = this.roomToSession.get(roomId);
|
|
1533
|
-
if (sessionId) {
|
|
1534
|
-
this.roomToSession.delete(roomId);
|
|
1535
|
-
this.sessionToRoom.delete(sessionId);
|
|
1536
|
-
this.sessionModeIds.delete(sessionId);
|
|
1537
|
-
this.sessionModelIds.delete(sessionId);
|
|
1538
|
-
this.sessionCwds.delete(sessionId);
|
|
1539
|
-
this.sessionMcpServers.delete(sessionId);
|
|
1540
|
-
}
|
|
1541
|
-
this.finishPendingPrompt(roomId, this.pendingPrompts.get(roomId) ?? null, true);
|
|
1542
|
-
}
|
|
1543
|
-
async onRuntimeStop() {
|
|
1544
|
-
for (const roomId of [...this.roomToSession.keys()]) {
|
|
1545
|
-
await this.onCleanup(roomId);
|
|
1546
|
-
}
|
|
1547
|
-
this.connection = null;
|
|
1548
|
-
}
|
|
1549
|
-
async cancelPrompt(sessionId) {
|
|
1550
|
-
const roomId = this.sessionToRoom.get(sessionId);
|
|
1551
|
-
if (!roomId) {
|
|
1552
|
-
return;
|
|
1553
|
-
}
|
|
1554
|
-
this.finishPendingPrompt(roomId, this.pendingPrompts.get(roomId) ?? null, true);
|
|
1555
|
-
}
|
|
1556
|
-
async closeSession(sessionId) {
|
|
1557
|
-
const roomId = this.sessionToRoom.get(sessionId);
|
|
1558
|
-
if (!roomId) {
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
await this.onCleanup(roomId);
|
|
1562
|
-
}
|
|
1563
|
-
rehydrate(history) {
|
|
1564
|
-
for (const [sessionId, roomId] of Object.entries(history.sessionToRoom)) {
|
|
1565
|
-
if (!this.sessionToRoom.has(sessionId)) {
|
|
1566
|
-
this.sessionToRoom.set(sessionId, roomId);
|
|
1567
|
-
}
|
|
1568
|
-
if (!this.roomToSession.has(roomId)) {
|
|
1569
|
-
this.roomToSession.set(roomId, sessionId);
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
for (const [sessionId, cwd] of Object.entries(history.sessionCwd)) {
|
|
1573
|
-
if (!this.sessionCwds.has(sessionId)) {
|
|
1574
|
-
this.sessionCwds.set(sessionId, cwd);
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
for (const [sessionId, mcpServers] of Object.entries(history.sessionMcpServers)) {
|
|
1578
|
-
if (!this.sessionMcpServers.has(sessionId)) {
|
|
1579
|
-
this.sessionMcpServers.set(sessionId, [...mcpServers]);
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
prependSessionContext(sessionId, text) {
|
|
1584
|
-
const cwd = this.sessionCwds.get(sessionId);
|
|
1585
|
-
const mcpServers = this.sessionMcpServers.get(sessionId) ?? [];
|
|
1586
|
-
if (!cwd && mcpServers.length === 0) {
|
|
1587
|
-
return text;
|
|
1588
|
-
}
|
|
1589
|
-
const lines = ["[ACP Session Context]"];
|
|
1590
|
-
if (cwd) {
|
|
1591
|
-
lines.push(`Editor cwd: ${cwd}`);
|
|
1592
|
-
}
|
|
1593
|
-
if (mcpServers.length > 0) {
|
|
1594
|
-
lines.push("Editor MCP servers:");
|
|
1595
|
-
lines.push(...mcpServers.map((server) => formatMcpServerLine(server)));
|
|
1596
|
-
lines.push("These MCP servers belong to the connected editor session.");
|
|
1597
|
-
}
|
|
1598
|
-
lines.push("");
|
|
1599
|
-
lines.push(text);
|
|
1600
|
-
return lines.join("\n");
|
|
1601
|
-
}
|
|
1602
|
-
resolveMentions(participants, targetPeer) {
|
|
1603
|
-
const filtered = participants.filter((participant) => {
|
|
1604
|
-
if (this.agentId && participant.id === this.agentId) {
|
|
1605
|
-
return false;
|
|
1606
|
-
}
|
|
1607
|
-
if (!targetPeer) {
|
|
1608
|
-
return true;
|
|
1609
|
-
}
|
|
1610
|
-
const normalizedTarget = targetPeer.startsWith("@") ? targetPeer.slice(1) : targetPeer;
|
|
1611
|
-
const participantHandles = [
|
|
1612
|
-
participant.handle,
|
|
1613
|
-
ensureHandlePrefix(participant.handle)?.slice(1)
|
|
1614
|
-
].filter((value) => typeof value === "string" && value.length > 0);
|
|
1615
|
-
return participant.name === targetPeer || participant.name === normalizedTarget || participantHandles.includes(targetPeer) || participantHandles.includes(normalizedTarget);
|
|
1616
|
-
});
|
|
1617
|
-
return filtered.map((participant) => ({
|
|
1618
|
-
id: participant.id,
|
|
1619
|
-
handle: participant.handle ?? void 0,
|
|
1620
|
-
name: participant.name
|
|
1621
|
-
}));
|
|
1622
|
-
}
|
|
1623
|
-
schedulePromptCompletion(roomId, pending) {
|
|
1624
|
-
if (this.pendingPrompts.get(roomId) !== pending) {
|
|
1625
|
-
return;
|
|
1626
|
-
}
|
|
1627
|
-
if (pending.completionTimer) {
|
|
1628
|
-
clearTimeout(pending.completionTimer);
|
|
1629
|
-
}
|
|
1630
|
-
pending.completionTimer = setTimeout(() => {
|
|
1631
|
-
this.finishPendingPrompt(roomId, pending, true);
|
|
1632
|
-
}, this.promptCompletionGraceMs);
|
|
1633
|
-
}
|
|
1634
|
-
finishPendingPrompt(roomId, pending, setDone) {
|
|
1635
|
-
const current = this.pendingPrompts.get(roomId);
|
|
1636
|
-
if (!current || pending && current !== pending) {
|
|
1637
|
-
return;
|
|
1638
|
-
}
|
|
1639
|
-
this.pendingPrompts.delete(roomId);
|
|
1640
|
-
if (current.completionTimer) {
|
|
1641
|
-
clearTimeout(current.completionTimer);
|
|
1642
|
-
current.completionTimer = null;
|
|
1643
|
-
}
|
|
1644
|
-
if (setDone) {
|
|
1645
|
-
current.markDone();
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
};
|
|
1649
|
-
function formatMcpServerLine(server) {
|
|
1650
|
-
const type = typeof server.type === "string" ? server.type : "unknown";
|
|
1651
|
-
const name = typeof server.name === "string" ? server.name : typeof server.command === "string" ? server.command : typeof server.url === "string" ? server.url : "unnamed";
|
|
1652
|
-
const details = [type];
|
|
1653
|
-
if (type === "stdio" && typeof server.command === "string") {
|
|
1654
|
-
details.push(`command=${server.command}`);
|
|
1655
|
-
}
|
|
1656
|
-
if (type !== "stdio" && typeof server.url === "string") {
|
|
1657
|
-
details.push(`url=${server.url}`);
|
|
1658
|
-
}
|
|
1659
|
-
return `- ${name} (${details.join(", ")})`;
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
// src/adapters/acp/cursorExtensions.ts
|
|
1663
|
-
var CursorExtensionHandler = class {
|
|
1664
|
-
async extMethod(method, params) {
|
|
1665
|
-
if (method === "cursor/ask_question") {
|
|
1666
|
-
const options = Array.isArray(params.options) ? params.options : [];
|
|
1667
|
-
const first = options.find((option) => !!option && typeof option === "object");
|
|
1668
|
-
if (!first) {
|
|
1669
|
-
return {
|
|
1670
|
-
outcome: {
|
|
1671
|
-
type: "cancelled"
|
|
1672
|
-
}
|
|
1673
|
-
};
|
|
1674
|
-
}
|
|
1675
|
-
return {
|
|
1676
|
-
outcome: {
|
|
1677
|
-
type: "selected",
|
|
1678
|
-
optionId: String(first.optionId ?? first.id ?? "0")
|
|
1679
|
-
}
|
|
1680
|
-
};
|
|
1681
|
-
}
|
|
1682
|
-
if (method === "cursor/create_plan") {
|
|
1683
|
-
return {
|
|
1684
|
-
outcome: {
|
|
1685
|
-
type: "approved"
|
|
1686
|
-
}
|
|
1687
|
-
};
|
|
1688
|
-
}
|
|
1689
|
-
return null;
|
|
1690
|
-
}
|
|
1691
|
-
async extNotification(method, params, context) {
|
|
1692
|
-
if (method === "cursor/update_todos") {
|
|
1693
|
-
const todos = Array.isArray(params.todos) ? params.todos : [];
|
|
1694
|
-
const text = todos.filter((todo) => !!todo && typeof todo === "object").map((todo) => `- [${todo.completed === true ? "x" : " "}] ${String(todo.content ?? "")}`).join("\n");
|
|
1695
|
-
if (text.length > 0) {
|
|
1696
|
-
await context.connection.sessionUpdate({
|
|
1697
|
-
sessionId: context.sessionId,
|
|
1698
|
-
update: {
|
|
1699
|
-
sessionUpdate: "agent_message_chunk",
|
|
1700
|
-
content: {
|
|
1701
|
-
type: "text",
|
|
1702
|
-
text
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
});
|
|
1706
|
-
}
|
|
1707
|
-
return;
|
|
1708
|
-
}
|
|
1709
|
-
if (method === "cursor/task" && typeof params.result === "string" && params.result.length > 0) {
|
|
1710
|
-
await context.connection.sessionUpdate({
|
|
1711
|
-
sessionId: context.sessionId,
|
|
1712
|
-
update: {
|
|
1713
|
-
sessionUpdate: "agent_message_chunk",
|
|
1714
|
-
content: {
|
|
1715
|
-
type: "text",
|
|
1716
|
-
text: `[Task completed] ${params.result}`
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
});
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
};
|
|
1723
|
-
|
|
1724
|
-
// src/adapters/acp/ACPServer.ts
|
|
1725
|
-
var ACPServer = class {
|
|
1726
|
-
adapter;
|
|
1727
|
-
authMethods;
|
|
1728
|
-
agentInfo;
|
|
1729
|
-
extensionHandler;
|
|
1730
|
-
connection = null;
|
|
1731
|
-
constructor(adapter, options) {
|
|
1732
|
-
this.adapter = adapter;
|
|
1733
|
-
if (options?.modes?.length) {
|
|
1734
|
-
this.adapter.setSessionModes([...options.modes]);
|
|
1735
|
-
}
|
|
1736
|
-
this.authMethods = options?.authMethods ?? [{
|
|
1737
|
-
id: "api_key",
|
|
1738
|
-
name: "API Key",
|
|
1739
|
-
description: "Authenticate with BAND_API_KEY (legacy THENVOI_API_KEY)."
|
|
1740
|
-
}];
|
|
1741
|
-
this.agentInfo = options?.agentInfo ?? {};
|
|
1742
|
-
this.extensionHandler = options?.extensionHandler ?? new CursorExtensionHandler();
|
|
1743
|
-
}
|
|
1744
|
-
async connectStream(stream) {
|
|
1745
|
-
if (this.connection && !this.connection.signal.aborted) {
|
|
1746
|
-
return this.connection;
|
|
1747
|
-
}
|
|
1748
|
-
const acp = await acpModule.get();
|
|
1749
|
-
this.connection = new acp.AgentSideConnection((connection) => {
|
|
1750
|
-
this.adapter.bindConnection(connection);
|
|
1751
|
-
return this;
|
|
1752
|
-
}, stream);
|
|
1753
|
-
void this.connection.closed.finally(() => {
|
|
1754
|
-
if (this.connection?.signal.aborted) {
|
|
1755
|
-
this.adapter.bindConnection(null);
|
|
1756
|
-
}
|
|
1757
|
-
});
|
|
1758
|
-
return this.connection;
|
|
1759
|
-
}
|
|
1760
|
-
connectStdio(options) {
|
|
1761
|
-
const stdin = options?.stdin ?? process.stdin;
|
|
1762
|
-
const stdout = options?.stdout ?? process.stdout;
|
|
1763
|
-
return acpModule.get().then((acp) => this.connectStream(
|
|
1764
|
-
acp.ndJsonStream(
|
|
1765
|
-
Writable2.toWeb(stdout),
|
|
1766
|
-
Readable2.toWeb(stdin)
|
|
1767
|
-
)
|
|
1768
|
-
));
|
|
1769
|
-
}
|
|
1770
|
-
get closed() {
|
|
1771
|
-
return this.connection?.closed ?? Promise.resolve();
|
|
1772
|
-
}
|
|
1773
|
-
async initialize(params) {
|
|
1774
|
-
const acp = await acpModule.get();
|
|
1775
|
-
return {
|
|
1776
|
-
protocolVersion: params.protocolVersion === acp.PROTOCOL_VERSION ? params.protocolVersion : acp.PROTOCOL_VERSION,
|
|
1777
|
-
agentCapabilities: {
|
|
1778
|
-
loadSession: true,
|
|
1779
|
-
promptCapabilities: {
|
|
1780
|
-
image: false,
|
|
1781
|
-
audio: false,
|
|
1782
|
-
embeddedContext: true
|
|
1783
|
-
},
|
|
1784
|
-
sessionCapabilities: {
|
|
1785
|
-
list: {},
|
|
1786
|
-
resume: {},
|
|
1787
|
-
fork: {},
|
|
1788
|
-
close: {}
|
|
1789
|
-
}
|
|
1790
|
-
},
|
|
1791
|
-
agentInfo: {
|
|
1792
|
-
name: this.agentInfo.name ?? "band-agent",
|
|
1793
|
-
title: this.agentInfo.title ?? this.adapter.displayName ?? "Band Agent",
|
|
1794
|
-
version: this.agentInfo.version ?? "0.1.0"
|
|
1795
|
-
},
|
|
1796
|
-
authMethods: this.authMethods
|
|
1797
|
-
};
|
|
1798
|
-
}
|
|
1799
|
-
async newSession(params) {
|
|
1800
|
-
const sessionId = await this.adapter.createSession({
|
|
1801
|
-
cwd: params.cwd,
|
|
1802
|
-
mcpServers: params.mcpServers
|
|
1803
|
-
});
|
|
1804
|
-
return {
|
|
1805
|
-
sessionId,
|
|
1806
|
-
modes: this.adapter.getModeState(sessionId)
|
|
1807
|
-
};
|
|
1808
|
-
}
|
|
1809
|
-
async loadSession(params) {
|
|
1810
|
-
if (!this.adapter.hasSession(params.sessionId)) {
|
|
1811
|
-
const acp = await acpModule.get();
|
|
1812
|
-
throw acp.RequestError.resourceNotFound(params.sessionId);
|
|
1813
|
-
}
|
|
1814
|
-
this.adapter.updateSessionContext(params.sessionId, {
|
|
1815
|
-
cwd: params.cwd,
|
|
1816
|
-
mcpServers: params.mcpServers
|
|
1817
|
-
});
|
|
1818
|
-
return {
|
|
1819
|
-
modes: this.adapter.getModeState(params.sessionId)
|
|
1820
|
-
};
|
|
1821
|
-
}
|
|
1822
|
-
async listSessions(params) {
|
|
1823
|
-
const sessions = this.adapter.getSessionIds().filter((sessionId) => !params.cwd || this.adapter.getSessionCwd(sessionId) === params.cwd).map((sessionId) => ({
|
|
1824
|
-
sessionId,
|
|
1825
|
-
cwd: this.adapter.getSessionCwd(sessionId)
|
|
1826
|
-
}));
|
|
1827
|
-
return { sessions };
|
|
1828
|
-
}
|
|
1829
|
-
async unstable_forkSession(params) {
|
|
1830
|
-
if (!this.adapter.hasSession(params.sessionId)) {
|
|
1831
|
-
const acp = await acpModule.get();
|
|
1832
|
-
throw acp.RequestError.resourceNotFound(params.sessionId);
|
|
1833
|
-
}
|
|
1834
|
-
const sessionId = await this.adapter.createSession({
|
|
1835
|
-
cwd: params.cwd,
|
|
1836
|
-
mcpServers: params.mcpServers
|
|
1837
|
-
});
|
|
1838
|
-
return {
|
|
1839
|
-
sessionId
|
|
1840
|
-
};
|
|
1841
|
-
}
|
|
1842
|
-
async unstable_resumeSession(params) {
|
|
1843
|
-
if (!this.adapter.hasSession(params.sessionId)) {
|
|
1844
|
-
const acp = await acpModule.get();
|
|
1845
|
-
throw acp.RequestError.resourceNotFound(params.sessionId);
|
|
1846
|
-
}
|
|
1847
|
-
this.adapter.updateSessionContext(params.sessionId, {
|
|
1848
|
-
cwd: params.cwd,
|
|
1849
|
-
mcpServers: params.mcpServers
|
|
1850
|
-
});
|
|
1851
|
-
return {
|
|
1852
|
-
modes: this.adapter.getModeState(params.sessionId)
|
|
1853
|
-
};
|
|
1854
|
-
}
|
|
1855
|
-
async unstable_closeSession(params) {
|
|
1856
|
-
await this.adapter.closeSession(params.sessionId);
|
|
1857
|
-
return {};
|
|
1858
|
-
}
|
|
1859
|
-
async setSessionMode(params) {
|
|
1860
|
-
if (!this.adapter.hasSession(params.sessionId)) {
|
|
1861
|
-
const acp = await acpModule.get();
|
|
1862
|
-
throw acp.RequestError.resourceNotFound(params.sessionId);
|
|
1863
|
-
}
|
|
1864
|
-
this.adapter.setSessionMode(params.sessionId, params.modeId);
|
|
1865
|
-
return {};
|
|
1866
|
-
}
|
|
1867
|
-
async unstable_setSessionModel(params) {
|
|
1868
|
-
if (!this.adapter.hasSession(params.sessionId)) {
|
|
1869
|
-
const acp = await acpModule.get();
|
|
1870
|
-
throw acp.RequestError.resourceNotFound(params.sessionId);
|
|
1871
|
-
}
|
|
1872
|
-
this.adapter.setSessionModel(params.sessionId, params.modelId);
|
|
1873
|
-
return {};
|
|
1874
|
-
}
|
|
1875
|
-
async setSessionConfigOption(_params) {
|
|
1876
|
-
return {
|
|
1877
|
-
configOptions: []
|
|
1878
|
-
};
|
|
1879
|
-
}
|
|
1880
|
-
async authenticate(params) {
|
|
1881
|
-
if (params.methodId !== "api_key" && params.methodId !== "cursor_login") {
|
|
1882
|
-
const acp = await acpModule.get();
|
|
1883
|
-
throw acp.RequestError.authRequired(void 0, `Unsupported auth method: ${params.methodId}`);
|
|
1884
|
-
}
|
|
1885
|
-
const ok = await this.adapter.verifyCredentials();
|
|
1886
|
-
if (!ok) {
|
|
1887
|
-
const acp = await acpModule.get();
|
|
1888
|
-
throw acp.RequestError.authRequired(void 0, "Authentication failed");
|
|
1889
|
-
}
|
|
1890
|
-
return {};
|
|
1891
|
-
}
|
|
1892
|
-
async prompt(params) {
|
|
1893
|
-
const text = extractPromptText(params.prompt);
|
|
1894
|
-
await this.adapter.handlePrompt(params.sessionId, text);
|
|
1895
|
-
return {
|
|
1896
|
-
stopReason: "end_turn"
|
|
1897
|
-
};
|
|
1898
|
-
}
|
|
1899
|
-
async cancel(params) {
|
|
1900
|
-
await this.adapter.cancelPrompt(params.sessionId);
|
|
1901
|
-
}
|
|
1902
|
-
async extMethod(method, params) {
|
|
1903
|
-
if (this.extensionHandler.extMethod) {
|
|
1904
|
-
const result = await this.extensionHandler.extMethod(method, params);
|
|
1905
|
-
if (result) {
|
|
1906
|
-
return result;
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
return {
|
|
1910
|
-
error: `Unknown extension method: ${method}`
|
|
1911
|
-
};
|
|
1912
|
-
}
|
|
1913
|
-
async extNotification(method, params) {
|
|
1914
|
-
const sessionId = typeof params.sessionId === "string" ? params.sessionId : typeof params.session_id === "string" ? params.session_id : null;
|
|
1915
|
-
if (!sessionId || !this.adapter.hasSession(sessionId)) {
|
|
1916
|
-
return;
|
|
1917
|
-
}
|
|
1918
|
-
const connection = this.adapter.getConnection();
|
|
1919
|
-
if (!connection) {
|
|
1920
|
-
return;
|
|
1921
|
-
}
|
|
1922
|
-
if (this.extensionHandler.extNotification) {
|
|
1923
|
-
await this.extensionHandler.extNotification(method, params, { sessionId, connection });
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
};
|
|
1927
|
-
function extractPromptText(prompt) {
|
|
1928
|
-
const parts = [];
|
|
1929
|
-
for (const block of prompt) {
|
|
1930
|
-
if (block.type === "text") {
|
|
1931
|
-
parts.push(block.text);
|
|
1932
|
-
continue;
|
|
1933
|
-
}
|
|
1934
|
-
if (block.type === "image") {
|
|
1935
|
-
parts.push(block.uri ? `[Image: ${block.uri}]` : "[Image]");
|
|
1936
|
-
continue;
|
|
1937
|
-
}
|
|
1938
|
-
if (block.type === "audio") {
|
|
1939
|
-
parts.push("[Audio]");
|
|
1940
|
-
continue;
|
|
1941
|
-
}
|
|
1942
|
-
if (block.type === "resource_link") {
|
|
1943
|
-
parts.push(`[Resource: ${block.title ?? block.name ?? block.uri}]`);
|
|
1944
|
-
continue;
|
|
1945
|
-
}
|
|
1946
|
-
if (block.type === "resource") {
|
|
1947
|
-
const resource = block.resource;
|
|
1948
|
-
if ("text" in resource && typeof resource.text === "string") {
|
|
1949
|
-
parts.push(resource.text);
|
|
1950
|
-
} else {
|
|
1951
|
-
parts.push(`[Resource: ${resource.uri ?? "embedded"}]`);
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
return parts.join("\n").trim();
|
|
1956
|
-
}
|
|
46
|
+
createGatewayServer
|
|
47
|
+
} from "./chunk-HNWZZXAE.js";
|
|
48
|
+
import "./chunk-OGOXY43L.js";
|
|
49
|
+
import "./chunk-CBCHTTUY.js";
|
|
50
|
+
import "./chunk-RJNJUISR.js";
|
|
51
|
+
import "./chunk-SHZBLLDQ.js";
|
|
52
|
+
import "./chunk-JIZE4EC6.js";
|
|
53
|
+
import "./chunk-6BE2EG5P.js";
|
|
54
|
+
import "./chunk-TGDDFBYS.js";
|
|
55
|
+
import "./chunk-ZYOXI4JL.js";
|
|
56
|
+
import "./chunk-4OVNFDD7.js";
|
|
1957
57
|
export {
|
|
1958
58
|
A2AAdapter,
|
|
1959
59
|
A2AGatewayAdapter,
|
|
@@ -1970,6 +70,9 @@ export {
|
|
|
1970
70
|
CodexAdapter,
|
|
1971
71
|
CodexAppServerStdioClient,
|
|
1972
72
|
CodexJsonRpcError,
|
|
73
|
+
CopilotACPAdapter,
|
|
74
|
+
DEFAULT_COPILOT_ACP_COMMAND,
|
|
75
|
+
DEFAULT_OMP_ACP_COMMAND,
|
|
1973
76
|
GatewayHistoryConverter,
|
|
1974
77
|
GatewayServer,
|
|
1975
78
|
GeminiAdapter,
|
|
@@ -1981,6 +84,7 @@ export {
|
|
|
1981
84
|
LangGraphAdapter,
|
|
1982
85
|
LettaAdapter,
|
|
1983
86
|
LettaHistoryConverter,
|
|
87
|
+
OmpACPAdapter,
|
|
1984
88
|
OpenAIAdapter,
|
|
1985
89
|
OpenAIToolCallingModel,
|
|
1986
90
|
OpencodeAdapter,
|