@pwrdrvr/agent-acp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +568 -0
- package/dist/index.js +1973 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1973 @@
|
|
|
1
|
+
// src/acp-stdio-transport.ts
|
|
2
|
+
import { noopLogger } from "@pwrdrvr/agent-core";
|
|
3
|
+
import {
|
|
4
|
+
JsonRpcConnection,
|
|
5
|
+
StdioJsonRpcTransport
|
|
6
|
+
} from "@pwrdrvr/agent-transport";
|
|
7
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 10 * 6e4;
|
|
8
|
+
var AcpStdioJsonRpcTransport = class {
|
|
9
|
+
connection;
|
|
10
|
+
notificationListeners = /* @__PURE__ */ new Set();
|
|
11
|
+
requestHandler;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
const logger = options.logger ?? noopLogger;
|
|
14
|
+
const transport = options.transport ?? new StdioJsonRpcTransport({
|
|
15
|
+
command: options.command,
|
|
16
|
+
args: options.args,
|
|
17
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
18
|
+
logger
|
|
19
|
+
});
|
|
20
|
+
this.connection = new JsonRpcConnection(
|
|
21
|
+
transport,
|
|
22
|
+
options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
23
|
+
options.observer,
|
|
24
|
+
{ logger, logContext: { owner: "acp-stdio-transport" } }
|
|
25
|
+
);
|
|
26
|
+
this.connection.setNotificationHandler((method, params) => {
|
|
27
|
+
const normalized = asRecord(params) ?? {};
|
|
28
|
+
for (const listener of this.notificationListeners) {
|
|
29
|
+
listener(method, normalized);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
this.connection.setRequestHandler(async (method, params, id) => {
|
|
33
|
+
if (!this.requestHandler) {
|
|
34
|
+
throw new Error(`ACP request handler unavailable for ${method}`);
|
|
35
|
+
}
|
|
36
|
+
return await this.requestHandler(method, asRecord(params) ?? {}, id);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async connect() {
|
|
40
|
+
await this.connection.connect();
|
|
41
|
+
}
|
|
42
|
+
async request(method, params, timeoutMs) {
|
|
43
|
+
await this.connection.connect();
|
|
44
|
+
return await this.connection.request(method, params, timeoutMs);
|
|
45
|
+
}
|
|
46
|
+
async notify(method, params) {
|
|
47
|
+
await this.connection.connect();
|
|
48
|
+
await this.connection.notify(method, params);
|
|
49
|
+
}
|
|
50
|
+
async close() {
|
|
51
|
+
await this.connection.close();
|
|
52
|
+
}
|
|
53
|
+
onNotification(listener) {
|
|
54
|
+
this.notificationListeners.add(listener);
|
|
55
|
+
return () => {
|
|
56
|
+
this.notificationListeners.delete(listener);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
onRequest(listener) {
|
|
60
|
+
this.requestHandler = listener;
|
|
61
|
+
return () => {
|
|
62
|
+
if (this.requestHandler === listener) {
|
|
63
|
+
this.requestHandler = void 0;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
function asRecord(value) {
|
|
69
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/acp-client.ts
|
|
73
|
+
import {
|
|
74
|
+
noopLogger as noopLogger2
|
|
75
|
+
} from "@pwrdrvr/agent-core";
|
|
76
|
+
import { readFile } from "fs/promises";
|
|
77
|
+
import { extname } from "path";
|
|
78
|
+
|
|
79
|
+
// src/normalizer/acp-normalizer.ts
|
|
80
|
+
import {
|
|
81
|
+
mergeToolCall,
|
|
82
|
+
preferSpecificLabel
|
|
83
|
+
} from "@pwrdrvr/agent-core";
|
|
84
|
+
|
|
85
|
+
// src/normalizer/content.ts
|
|
86
|
+
function asRecord2(value) {
|
|
87
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
88
|
+
}
|
|
89
|
+
function readString(record, key) {
|
|
90
|
+
const value = record?.[key];
|
|
91
|
+
return typeof value === "string" ? value : void 0;
|
|
92
|
+
}
|
|
93
|
+
function readNonEmptyString(record, key) {
|
|
94
|
+
const value = record?.[key];
|
|
95
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
96
|
+
}
|
|
97
|
+
function readNumber(record, key) {
|
|
98
|
+
const value = record?.[key];
|
|
99
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
100
|
+
}
|
|
101
|
+
function readBoolean(record, key) {
|
|
102
|
+
const value = record?.[key];
|
|
103
|
+
return typeof value === "boolean" ? value : void 0;
|
|
104
|
+
}
|
|
105
|
+
function readFirstString(record, ...keys) {
|
|
106
|
+
for (const key of keys) {
|
|
107
|
+
const value = readString(record, key);
|
|
108
|
+
if (value !== void 0) return value;
|
|
109
|
+
}
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
function readKind(update) {
|
|
113
|
+
return readString(update, "sessionUpdate") ?? readString(update, "session_update") ?? readString(update, "kind") ?? readString(update, "type") ?? "unknown";
|
|
114
|
+
}
|
|
115
|
+
function readAcpContentText(value) {
|
|
116
|
+
if (typeof value === "string") {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
const parts = value.map((item) => readAcpContentText(item)).filter((item) => Boolean(item));
|
|
121
|
+
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
122
|
+
}
|
|
123
|
+
const content = asRecord2(value);
|
|
124
|
+
if (!content) {
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
if (content.type === "text" && typeof content.text === "string") {
|
|
128
|
+
return content.text;
|
|
129
|
+
}
|
|
130
|
+
return readAcpContentText(content.content) ?? readAcpContentText(content.text) ?? readAcpContentText(content.output) ?? readAcpContentText(content.result);
|
|
131
|
+
}
|
|
132
|
+
function readContentText(record, key) {
|
|
133
|
+
return readAcpContentText(record[key]);
|
|
134
|
+
}
|
|
135
|
+
function readToolOutput(record) {
|
|
136
|
+
return readString(record, "output") ?? readString(record, "stdout") ?? readString(record, "stderr") ?? readString(record, "result") ?? readContentText(record, "content");
|
|
137
|
+
}
|
|
138
|
+
function readFirstLocationPath(record) {
|
|
139
|
+
const locations = record.locations ?? record.location;
|
|
140
|
+
if (!Array.isArray(locations)) {
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
for (const location of locations) {
|
|
144
|
+
const path3 = readString(asRecord2(location), "path");
|
|
145
|
+
if (path3 && path3.trim()) {
|
|
146
|
+
return path3;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
function readUpdateText(update) {
|
|
152
|
+
return readString(update, "text") ?? readString(update, "outputText") ?? readString(update, "output_text") ?? readContentText(update, "content");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/normalizer/tool-activity.ts
|
|
156
|
+
import {
|
|
157
|
+
inferToolKind
|
|
158
|
+
} from "@pwrdrvr/agent-core";
|
|
159
|
+
function readToolCallId(update, kind, sessionId) {
|
|
160
|
+
return readFirstString(
|
|
161
|
+
update,
|
|
162
|
+
"toolCallId",
|
|
163
|
+
"tool_call_id",
|
|
164
|
+
"id",
|
|
165
|
+
"itemId",
|
|
166
|
+
"item_id"
|
|
167
|
+
) ?? `${kind}:${sessionId}`;
|
|
168
|
+
}
|
|
169
|
+
function normalizeStatus(status) {
|
|
170
|
+
switch (status) {
|
|
171
|
+
case "completed":
|
|
172
|
+
case "failed":
|
|
173
|
+
case "cancelled":
|
|
174
|
+
case "in_progress":
|
|
175
|
+
return status;
|
|
176
|
+
case "pending":
|
|
177
|
+
return "in_progress";
|
|
178
|
+
default:
|
|
179
|
+
return void 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function toolKindFor(acpKind, command, path3, label) {
|
|
183
|
+
switch (acpKind) {
|
|
184
|
+
case "edit":
|
|
185
|
+
case "write":
|
|
186
|
+
return "write";
|
|
187
|
+
case "execute":
|
|
188
|
+
case "exec":
|
|
189
|
+
case "shell":
|
|
190
|
+
return "command";
|
|
191
|
+
case "read":
|
|
192
|
+
return "read";
|
|
193
|
+
case "search":
|
|
194
|
+
return "search";
|
|
195
|
+
case "fetch":
|
|
196
|
+
return "fetch";
|
|
197
|
+
default:
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
if (command) return "command";
|
|
201
|
+
const inferred = inferToolKind(label);
|
|
202
|
+
if (inferred !== "other") return inferred;
|
|
203
|
+
return path3 ? "read" : "other";
|
|
204
|
+
}
|
|
205
|
+
function toolCallFromUpdate(update, kind, sessionId) {
|
|
206
|
+
const id = readToolCallId(update, kind, sessionId);
|
|
207
|
+
const label = readString(update, "title") ?? readString(update, "name") ?? readString(update, "kind") ?? kind.replaceAll("_", " ");
|
|
208
|
+
const acpKind = readString(update, "kind");
|
|
209
|
+
const path3 = readString(update, "path") ?? readFirstLocationPath(update);
|
|
210
|
+
const command = readString(update, "command");
|
|
211
|
+
const output = readToolOutput(update);
|
|
212
|
+
const exitCode = readNumber(update, "exitCode") ?? readNumber(update, "exit_code");
|
|
213
|
+
const status = normalizeStatus(readString(update, "status"));
|
|
214
|
+
const toolKind = toolKindFor(acpKind, command, path3, label);
|
|
215
|
+
const call = {
|
|
216
|
+
id,
|
|
217
|
+
name: readString(update, "name") ?? acpKind ?? label,
|
|
218
|
+
kind: toolKind,
|
|
219
|
+
label,
|
|
220
|
+
status: status ?? "in_progress",
|
|
221
|
+
args: readToolArgs(update)
|
|
222
|
+
};
|
|
223
|
+
if (output !== void 0) {
|
|
224
|
+
call.result = output;
|
|
225
|
+
}
|
|
226
|
+
const explicitDisplay = command ?? readString(update, "title");
|
|
227
|
+
if (command || output !== void 0 || exitCode !== void 0) {
|
|
228
|
+
const detail = {
|
|
229
|
+
displayCommand: explicitDisplay ?? label
|
|
230
|
+
};
|
|
231
|
+
if (command !== void 0) detail.rawCommand = command;
|
|
232
|
+
if (output !== void 0) detail.output = output;
|
|
233
|
+
if (exitCode !== void 0) detail.exitCode = exitCode;
|
|
234
|
+
call.command = detail;
|
|
235
|
+
}
|
|
236
|
+
if ((toolKind === "write" || toolKind === "read") && path3) {
|
|
237
|
+
if (!call.command) {
|
|
238
|
+
call.command = { displayCommand: label };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return call;
|
|
242
|
+
}
|
|
243
|
+
function readToolArgs(update) {
|
|
244
|
+
const raw = update.rawInput ?? update.raw_input ?? update.input ?? update.arguments ?? update.args;
|
|
245
|
+
return asRecord2(raw) ?? (raw === void 0 ? void 0 : raw);
|
|
246
|
+
}
|
|
247
|
+
function readToolContentText(value) {
|
|
248
|
+
return readContentText({ content: value }, "content");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/normalizer/acp-normalizer.ts
|
|
252
|
+
var EMPTY_RESULT = { events: [] };
|
|
253
|
+
var AcpSessionNormalizer = class {
|
|
254
|
+
quirks;
|
|
255
|
+
// Coalescing state. One "live" assistant message bubble at a time; a tool
|
|
256
|
+
// call (or any non-text update) clears it so the next text starts a new one.
|
|
257
|
+
activeAssistantItemId;
|
|
258
|
+
assistantText = "";
|
|
259
|
+
assistantSequence = 0;
|
|
260
|
+
// Tool-call merge state: id → last fully-merged NormalizedToolCall, so a
|
|
261
|
+
// tool_call_update reconciles labels / fills command output against it.
|
|
262
|
+
toolCalls = /* @__PURE__ */ new Map();
|
|
263
|
+
constructor(options) {
|
|
264
|
+
this.quirks = options.quirks;
|
|
265
|
+
}
|
|
266
|
+
/** Reset coalescing state at a turn boundary (new prompt / turn finished). */
|
|
267
|
+
resetTurn() {
|
|
268
|
+
this.activeAssistantItemId = void 0;
|
|
269
|
+
this.assistantText = "";
|
|
270
|
+
}
|
|
271
|
+
/** Finalize the in-flight assistant bubble into a terminal `agent_message`, if any. */
|
|
272
|
+
finalizeAssistantMessage(ctx) {
|
|
273
|
+
if (this.activeAssistantItemId === void 0 || this.assistantText === "") {
|
|
274
|
+
this.resetTurn();
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
const message = {
|
|
278
|
+
id: this.activeAssistantItemId,
|
|
279
|
+
role: "assistant",
|
|
280
|
+
text: this.assistantText
|
|
281
|
+
};
|
|
282
|
+
this.resetTurn();
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
kind: "agent_message",
|
|
286
|
+
threadId: ctx.threadId,
|
|
287
|
+
turnId: ctx.turnId,
|
|
288
|
+
message
|
|
289
|
+
}
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
/** Normalize one ACP session/update into neutral events (+ optional title). */
|
|
293
|
+
apply(update, ctx) {
|
|
294
|
+
const kind = readKind(update);
|
|
295
|
+
const title = this.extractTitle(update, kind);
|
|
296
|
+
if (title !== void 0) {
|
|
297
|
+
return { events: [], title };
|
|
298
|
+
}
|
|
299
|
+
switch (kind) {
|
|
300
|
+
case "agent_message_chunk":
|
|
301
|
+
return { events: this.applyAgentMessageChunk(update, ctx) };
|
|
302
|
+
case "agent_thought_chunk":
|
|
303
|
+
return { events: this.applyThoughtChunk(update, ctx) };
|
|
304
|
+
case "user_message_chunk":
|
|
305
|
+
return { events: this.applyUserMessageChunk(update, ctx) };
|
|
306
|
+
case "plan":
|
|
307
|
+
this.activeAssistantItemId = void 0;
|
|
308
|
+
return { events: this.applyPlan(update, ctx) };
|
|
309
|
+
case "tool_call":
|
|
310
|
+
case "tool_call_update":
|
|
311
|
+
case "file":
|
|
312
|
+
case "terminal":
|
|
313
|
+
this.activeAssistantItemId = void 0;
|
|
314
|
+
return { events: this.applyToolCall(update, kind, ctx) };
|
|
315
|
+
case "available_commands_update":
|
|
316
|
+
case "current_mode_update":
|
|
317
|
+
case "config_option_update":
|
|
318
|
+
return EMPTY_RESULT;
|
|
319
|
+
default:
|
|
320
|
+
this.activeAssistantItemId = void 0;
|
|
321
|
+
return EMPTY_RESULT;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
applyAgentMessageChunk(update, ctx) {
|
|
325
|
+
const text = readUpdateText(update) ?? "";
|
|
326
|
+
if (text === "" || isModeUpdateMarker(text)) {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
const itemId = this.assistantItemIdForChunk(update, ctx);
|
|
330
|
+
this.assistantText = appendTranscriptChunk(this.assistantText, text);
|
|
331
|
+
return [
|
|
332
|
+
{
|
|
333
|
+
kind: "agent_message_delta",
|
|
334
|
+
threadId: ctx.threadId,
|
|
335
|
+
turnId: ctx.turnId,
|
|
336
|
+
itemId,
|
|
337
|
+
delta: text
|
|
338
|
+
}
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
applyThoughtChunk(update, ctx) {
|
|
342
|
+
if (!this.quirks.surfaceThoughts) {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
const text = readUpdateText(update) ?? "";
|
|
346
|
+
if (text === "") {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
const itemId = this.assistantItemIdForChunk(update, ctx);
|
|
350
|
+
this.assistantText = appendTranscriptChunk(this.assistantText, text);
|
|
351
|
+
return [
|
|
352
|
+
{
|
|
353
|
+
kind: "reasoning_delta",
|
|
354
|
+
threadId: ctx.threadId,
|
|
355
|
+
turnId: ctx.turnId,
|
|
356
|
+
itemId,
|
|
357
|
+
delta: text
|
|
358
|
+
}
|
|
359
|
+
];
|
|
360
|
+
}
|
|
361
|
+
applyUserMessageChunk(update, ctx) {
|
|
362
|
+
this.activeAssistantItemId = void 0;
|
|
363
|
+
const text = readUpdateText(update) ?? "";
|
|
364
|
+
if (text === "") {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
const id = readFirstString(update, "messageId", "message_id", "id") ?? `user:${ctx.threadId}:${ctx.turnId}`;
|
|
368
|
+
const message = { id, role: "user", text };
|
|
369
|
+
return [
|
|
370
|
+
{
|
|
371
|
+
kind: "agent_message",
|
|
372
|
+
threadId: ctx.threadId,
|
|
373
|
+
turnId: ctx.turnId,
|
|
374
|
+
message
|
|
375
|
+
}
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
applyPlan(update, ctx) {
|
|
379
|
+
const id = readString(update, "planId") ?? `plan:${ctx.threadId}`;
|
|
380
|
+
const plan = {
|
|
381
|
+
id,
|
|
382
|
+
steps: readPlanSteps(update)
|
|
383
|
+
};
|
|
384
|
+
const explanation = readString(update, "explanation");
|
|
385
|
+
if (explanation !== void 0) plan.explanation = explanation;
|
|
386
|
+
const markdown = readString(update, "markdown");
|
|
387
|
+
if (markdown !== void 0) plan.markdown = markdown;
|
|
388
|
+
return [{ kind: "plan_update", threadId: ctx.threadId, turnId: ctx.turnId, plan }];
|
|
389
|
+
}
|
|
390
|
+
applyToolCall(update, kind, ctx) {
|
|
391
|
+
const incoming = toolCallFromUpdate(update, kind, ctx.threadId);
|
|
392
|
+
const prev = this.toolCalls.get(incoming.id);
|
|
393
|
+
if (prev === void 0) {
|
|
394
|
+
this.toolCalls.set(incoming.id, incoming);
|
|
395
|
+
return [
|
|
396
|
+
{ kind: "tool_call", threadId: ctx.threadId, turnId: ctx.turnId, toolCall: incoming }
|
|
397
|
+
];
|
|
398
|
+
}
|
|
399
|
+
const merged = mergeToolCall(prev, incoming);
|
|
400
|
+
if (merged.command && prev.command) {
|
|
401
|
+
merged.command = {
|
|
402
|
+
...merged.command,
|
|
403
|
+
displayCommand: preferSpecificLabel(
|
|
404
|
+
prev.command.displayCommand,
|
|
405
|
+
merged.command.displayCommand
|
|
406
|
+
)
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
this.toolCalls.set(incoming.id, merged);
|
|
410
|
+
return [
|
|
411
|
+
{
|
|
412
|
+
kind: "tool_call_update",
|
|
413
|
+
threadId: ctx.threadId,
|
|
414
|
+
turnId: ctx.turnId,
|
|
415
|
+
toolCall: merged
|
|
416
|
+
}
|
|
417
|
+
];
|
|
418
|
+
}
|
|
419
|
+
assistantItemIdForChunk(update, ctx) {
|
|
420
|
+
const explicitId = readFirstString(update, "messageId", "message_id");
|
|
421
|
+
if (explicitId) {
|
|
422
|
+
if (explicitId !== this.activeAssistantItemId) {
|
|
423
|
+
this.activeAssistantItemId = explicitId;
|
|
424
|
+
this.assistantText = "";
|
|
425
|
+
}
|
|
426
|
+
return explicitId;
|
|
427
|
+
}
|
|
428
|
+
if (this.activeAssistantItemId === void 0) {
|
|
429
|
+
this.activeAssistantItemId = `assistant:${ctx.turnId}:${this.assistantSequence++}`;
|
|
430
|
+
this.assistantText = "";
|
|
431
|
+
}
|
|
432
|
+
return this.activeAssistantItemId;
|
|
433
|
+
}
|
|
434
|
+
/** Strategy-driven title extraction. NO agent-id literal — reads quirks.titleFrom. */
|
|
435
|
+
extractTitle(update, kind) {
|
|
436
|
+
const wantsSummary = this.quirks.titleFrom === "session-summary" || this.quirks.titleFrom === "both";
|
|
437
|
+
const wantsTopic = this.quirks.titleFrom === "topic-update" || this.quirks.titleFrom === "both";
|
|
438
|
+
if (wantsSummary && kind === "session_summary_generated") {
|
|
439
|
+
const summary = (readString(update, "session_summary") ?? readString(update, "sessionSummary"))?.trim();
|
|
440
|
+
return summary || void 0;
|
|
441
|
+
}
|
|
442
|
+
if (wantsTopic) {
|
|
443
|
+
const isToolish = kind === "tool_call" || kind === "tool_call_update" || kind === "think";
|
|
444
|
+
if (!isToolish) {
|
|
445
|
+
return void 0;
|
|
446
|
+
}
|
|
447
|
+
const titleText = readString(update, "title")?.trim();
|
|
448
|
+
if (!titleText) {
|
|
449
|
+
return void 0;
|
|
450
|
+
}
|
|
451
|
+
const quoted = /^Update topic to:\s*["“](.+?)["”]\s*$/iu.exec(titleText);
|
|
452
|
+
const fallback = /^Update topic to:\s*(.+)$/iu.exec(titleText);
|
|
453
|
+
const topic = (quoted?.[1] ?? fallback?.[1])?.trim();
|
|
454
|
+
return topic || void 0;
|
|
455
|
+
}
|
|
456
|
+
return void 0;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
function appendTranscriptChunk(existing, next) {
|
|
460
|
+
if (!existing || !next) {
|
|
461
|
+
return `${existing}${next}`;
|
|
462
|
+
}
|
|
463
|
+
if (shouldSeparateTranscriptChunks(existing, next)) {
|
|
464
|
+
return `${existing}
|
|
465
|
+
|
|
466
|
+
${next}`;
|
|
467
|
+
}
|
|
468
|
+
return `${existing}${next}`;
|
|
469
|
+
}
|
|
470
|
+
function shouldSeparateTranscriptChunks(existing, next) {
|
|
471
|
+
if (/\s$/.test(existing)) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
return /^(?:#{1,6}\s|\*\*[^*]+?\*\*(?:\s|$))/.test(next);
|
|
475
|
+
}
|
|
476
|
+
function isModeUpdateMarker(text) {
|
|
477
|
+
return /^\[MODE_UPDATE\]\s*[A-Za-z0-9_-]+\s*$/.test(text.trim());
|
|
478
|
+
}
|
|
479
|
+
function readPlanSteps(record) {
|
|
480
|
+
const steps = Array.isArray(record.steps) ? record.steps : [];
|
|
481
|
+
return steps.flatMap((step) => {
|
|
482
|
+
if (typeof step === "string") {
|
|
483
|
+
return [{ step, status: "pending" }];
|
|
484
|
+
}
|
|
485
|
+
const stepRecord = asRecord2(step);
|
|
486
|
+
if (!stepRecord) {
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
const text = readString(stepRecord, "step") ?? readString(stepRecord, "content");
|
|
490
|
+
if (!text) {
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
const status = readString(stepRecord, "status");
|
|
494
|
+
return [
|
|
495
|
+
{
|
|
496
|
+
step: text,
|
|
497
|
+
status: status === "in_progress" || status === "completed" ? status : "pending"
|
|
498
|
+
}
|
|
499
|
+
];
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
function readPromptText(content) {
|
|
503
|
+
return readContentText({ content }, "content");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/normalizer/runtime-capabilities.ts
|
|
507
|
+
function normalizeAcpRuntimeCapabilities(params) {
|
|
508
|
+
const record = asRecord2(params.value);
|
|
509
|
+
if (!record) {
|
|
510
|
+
return params.initialize;
|
|
511
|
+
}
|
|
512
|
+
const configOptions = readConfigOptions(record.configOptions ?? record.config_options);
|
|
513
|
+
const modes = readModes(record.modes);
|
|
514
|
+
const models = readModels(record.models);
|
|
515
|
+
const agentCapabilities = readAgentCapabilities(
|
|
516
|
+
record.agentCapabilities ?? record.agent_capabilities ?? record.capabilities,
|
|
517
|
+
record.sessionCapabilities ?? record.session_capabilities
|
|
518
|
+
);
|
|
519
|
+
const agentInfo = readAgentInfo(record.agentInfo ?? record.agent_info);
|
|
520
|
+
const protocolVersion = typeof record.protocolVersion === "number" ? record.protocolVersion : typeof record.protocol_version === "number" ? record.protocol_version : params.initialize?.protocolVersion;
|
|
521
|
+
const hasRuntimeData = configOptions.length > 0 || Boolean(modes) || Boolean(models) || Boolean(agentCapabilities) || Boolean(agentInfo) || typeof protocolVersion === "number";
|
|
522
|
+
if (!hasRuntimeData && !params.initialize) {
|
|
523
|
+
return void 0;
|
|
524
|
+
}
|
|
525
|
+
const merged = {
|
|
526
|
+
source: params.source,
|
|
527
|
+
discoveredAt: params.initialize?.discoveredAt ?? params.now,
|
|
528
|
+
checkedAt: params.now
|
|
529
|
+
};
|
|
530
|
+
if (typeof protocolVersion === "number") merged.protocolVersion = protocolVersion;
|
|
531
|
+
if (agentInfo ?? params.initialize?.agentInfo) {
|
|
532
|
+
merged.agentInfo = { ...params.initialize?.agentInfo, ...agentInfo };
|
|
533
|
+
}
|
|
534
|
+
if (agentCapabilities ?? params.initialize?.agentCapabilities) {
|
|
535
|
+
merged.agentCapabilities = {
|
|
536
|
+
...params.initialize?.agentCapabilities,
|
|
537
|
+
...agentCapabilities
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
if (configOptions.length > 0) {
|
|
541
|
+
merged.configOptions = configOptions;
|
|
542
|
+
} else if (params.initialize?.configOptions) {
|
|
543
|
+
merged.configOptions = params.initialize.configOptions;
|
|
544
|
+
}
|
|
545
|
+
if (modes) {
|
|
546
|
+
merged.modes = modes;
|
|
547
|
+
} else if (params.initialize?.modes) {
|
|
548
|
+
merged.modes = params.initialize.modes;
|
|
549
|
+
}
|
|
550
|
+
if (models) {
|
|
551
|
+
merged.models = models;
|
|
552
|
+
} else if (params.initialize?.models) {
|
|
553
|
+
merged.models = params.initialize.models;
|
|
554
|
+
}
|
|
555
|
+
return merged;
|
|
556
|
+
}
|
|
557
|
+
function acpRuntimeSupportsSessionLoad(capabilities) {
|
|
558
|
+
return capabilities?.agentCapabilities?.loadSession !== false;
|
|
559
|
+
}
|
|
560
|
+
function acpSessionRuntimeStateFromCapabilities(capabilities, now) {
|
|
561
|
+
if (!capabilities) {
|
|
562
|
+
return void 0;
|
|
563
|
+
}
|
|
564
|
+
const configValues = Object.fromEntries(
|
|
565
|
+
(capabilities.configOptions ?? []).flatMap(
|
|
566
|
+
(option) => typeof option.currentValue === "string" ? [[option.id, option.currentValue]] : []
|
|
567
|
+
)
|
|
568
|
+
);
|
|
569
|
+
const state = { updatedAt: now };
|
|
570
|
+
if (Object.keys(configValues).length > 0) state.configValues = configValues;
|
|
571
|
+
if (capabilities.modes?.currentModeId) {
|
|
572
|
+
state.currentModeId = capabilities.modes.currentModeId;
|
|
573
|
+
}
|
|
574
|
+
if (capabilities.models?.currentModelId) {
|
|
575
|
+
state.currentModelId = capabilities.models.currentModelId;
|
|
576
|
+
}
|
|
577
|
+
return Object.keys(state).length > 1 ? state : void 0;
|
|
578
|
+
}
|
|
579
|
+
function acpSessionRuntimeStateFromUpdate(update, now) {
|
|
580
|
+
const kind = readString(update, "sessionUpdate") ?? readString(update, "session_update") ?? readString(update, "kind") ?? readString(update, "type");
|
|
581
|
+
if (kind === "agent_message_chunk") {
|
|
582
|
+
const modeId = readModeUpdateMarker(update);
|
|
583
|
+
return modeId ? { currentModeId: modeId, updatedAt: now } : void 0;
|
|
584
|
+
}
|
|
585
|
+
if (kind === "current_mode_update") {
|
|
586
|
+
const currentModeId = readString(update, "currentModeId") ?? readString(update, "current_mode_id") ?? readString(update, "modeId") ?? readString(update, "mode_id") ?? readString(update, "id");
|
|
587
|
+
return currentModeId ? { currentModeId, updatedAt: now } : void 0;
|
|
588
|
+
}
|
|
589
|
+
if (kind === "config_option_update") {
|
|
590
|
+
const configOption = asRecord2(update.configOption ?? update.config_option) ?? update;
|
|
591
|
+
const id = readString(configOption, "id") ?? readString(configOption, "configOptionId") ?? readString(configOption, "configId");
|
|
592
|
+
const value = readString(configOption, "currentValue") ?? readString(configOption, "value");
|
|
593
|
+
return id && value ? { configValues: { [id]: value }, updatedAt: now } : void 0;
|
|
594
|
+
}
|
|
595
|
+
return void 0;
|
|
596
|
+
}
|
|
597
|
+
function mergeAcpRuntimeState(existing, update) {
|
|
598
|
+
return {
|
|
599
|
+
...existing,
|
|
600
|
+
...update,
|
|
601
|
+
configValues: {
|
|
602
|
+
...existing?.configValues ?? {},
|
|
603
|
+
...update.configValues ?? {}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function modeLabelFor(capabilities, modeId) {
|
|
608
|
+
return capabilities?.modes?.availableModes.find((mode) => mode.id === modeId)?.label;
|
|
609
|
+
}
|
|
610
|
+
function readModeUpdateMarker(update) {
|
|
611
|
+
const text = readString(update, "content") ?? readString(update, "text");
|
|
612
|
+
const match = text?.trim().match(/^\[MODE_UPDATE\]\s*([A-Za-z0-9_-]+)\s*$/);
|
|
613
|
+
return match?.[1];
|
|
614
|
+
}
|
|
615
|
+
function readConfigOptions(value) {
|
|
616
|
+
if (!Array.isArray(value)) {
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
return value.flatMap((item) => {
|
|
620
|
+
const record = asRecord2(item);
|
|
621
|
+
const id = readString(record, "id") ?? readString(record, "configOptionId") ?? readString(record, "configId");
|
|
622
|
+
if (!record || !id) {
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
const values = readConfigOptionValues(record.values ?? record.options);
|
|
626
|
+
if (values.length === 0) {
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
629
|
+
const option = {
|
|
630
|
+
id,
|
|
631
|
+
label: readString(record, "name") ?? readString(record, "label") ?? readString(record, "title") ?? id,
|
|
632
|
+
type: "select",
|
|
633
|
+
values
|
|
634
|
+
};
|
|
635
|
+
const description = readString(record, "description");
|
|
636
|
+
if (description !== void 0) option.description = description;
|
|
637
|
+
const category = readString(record, "category");
|
|
638
|
+
if (category !== void 0) option.category = category;
|
|
639
|
+
const currentValue = readString(record, "currentValue") ?? readString(record, "value");
|
|
640
|
+
if (currentValue !== void 0) option.currentValue = currentValue;
|
|
641
|
+
return [option];
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function readConfigOptionValues(value) {
|
|
645
|
+
if (!Array.isArray(value)) {
|
|
646
|
+
return [];
|
|
647
|
+
}
|
|
648
|
+
return value.flatMap((item) => {
|
|
649
|
+
const record = asRecord2(item);
|
|
650
|
+
const optionValue = readString(record, "value") ?? readString(record, "id") ?? readString(record, "optionId");
|
|
651
|
+
if (!record || !optionValue) {
|
|
652
|
+
return [];
|
|
653
|
+
}
|
|
654
|
+
const normalized = { value: optionValue };
|
|
655
|
+
const label = readString(record, "name") ?? readString(record, "label") ?? readString(record, "title");
|
|
656
|
+
if (label !== void 0) normalized.label = label;
|
|
657
|
+
const description = readString(record, "description");
|
|
658
|
+
if (description !== void 0) normalized.description = description;
|
|
659
|
+
return [normalized];
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
function readModes(value) {
|
|
663
|
+
const record = asRecord2(value);
|
|
664
|
+
const modes = Array.isArray(record?.availableModes) ? record.availableModes.flatMap(readMode) : [];
|
|
665
|
+
if (modes.length === 0) {
|
|
666
|
+
return void 0;
|
|
667
|
+
}
|
|
668
|
+
const result = { availableModes: modes };
|
|
669
|
+
const currentModeId = readString(record, "currentModeId");
|
|
670
|
+
if (currentModeId !== void 0) result.currentModeId = currentModeId;
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
function readMode(value) {
|
|
674
|
+
const record = asRecord2(value);
|
|
675
|
+
const id = readString(record, "id") ?? readString(record, "modeId");
|
|
676
|
+
if (!record || !id) {
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
const mode = {
|
|
680
|
+
id,
|
|
681
|
+
label: readString(record, "name") ?? readString(record, "label") ?? id
|
|
682
|
+
};
|
|
683
|
+
const description = readString(record, "description");
|
|
684
|
+
if (description !== void 0) mode.description = description;
|
|
685
|
+
return [mode];
|
|
686
|
+
}
|
|
687
|
+
function readModels(value) {
|
|
688
|
+
const record = asRecord2(value);
|
|
689
|
+
const models = Array.isArray(record?.availableModels) ? record.availableModels.flatMap(readModel) : [];
|
|
690
|
+
if (models.length === 0) {
|
|
691
|
+
return void 0;
|
|
692
|
+
}
|
|
693
|
+
const result = { availableModels: models };
|
|
694
|
+
const currentModelId = readString(record, "currentModelId") ?? readString(record, "modelId");
|
|
695
|
+
if (currentModelId !== void 0) result.currentModelId = currentModelId;
|
|
696
|
+
return result;
|
|
697
|
+
}
|
|
698
|
+
function readModel(value) {
|
|
699
|
+
const record = asRecord2(value);
|
|
700
|
+
const id = readString(record, "modelId") ?? readString(record, "id");
|
|
701
|
+
if (!record || !id) {
|
|
702
|
+
return [];
|
|
703
|
+
}
|
|
704
|
+
const model = { id };
|
|
705
|
+
const label = readString(record, "name") ?? readString(record, "label");
|
|
706
|
+
if (label !== void 0) model.label = label;
|
|
707
|
+
const description = readString(record, "description");
|
|
708
|
+
if (description !== void 0) model.description = description;
|
|
709
|
+
return [model];
|
|
710
|
+
}
|
|
711
|
+
function readAgentCapabilities(value, sessionCapabilitiesValue) {
|
|
712
|
+
const record = asRecord2(value);
|
|
713
|
+
const sessionCapabilities = asRecord2(sessionCapabilitiesValue);
|
|
714
|
+
const sessionMeta = asRecord2(sessionCapabilities?._meta);
|
|
715
|
+
const kimiMeta = asRecord2(sessionMeta?.kimi);
|
|
716
|
+
if (!record && !sessionCapabilities) {
|
|
717
|
+
return void 0;
|
|
718
|
+
}
|
|
719
|
+
const loadSession = readBoolean(record, "loadSession") ?? readBoolean(record, "load_session") ?? readBoolean(asRecord2(record?.session), "load");
|
|
720
|
+
const close = readBoolean(asRecord2(record?.session), "close");
|
|
721
|
+
const cancel = readBoolean(asRecord2(record?.session), "cancel");
|
|
722
|
+
const sessionHistoryReplay = readBoolean(kimiMeta, "sessionHistoryReplay") ?? readBoolean(kimiMeta, "session_history_replay");
|
|
723
|
+
const capabilities = {
|
|
724
|
+
raw: record ?? sessionCapabilities
|
|
725
|
+
};
|
|
726
|
+
if (loadSession !== void 0) capabilities.loadSession = loadSession;
|
|
727
|
+
if (sessionHistoryReplay !== void 0) {
|
|
728
|
+
capabilities.sessionHistoryReplay = sessionHistoryReplay;
|
|
729
|
+
}
|
|
730
|
+
if (close !== void 0 || cancel !== void 0) {
|
|
731
|
+
capabilities.session = {
|
|
732
|
+
...close !== void 0 ? { close } : {},
|
|
733
|
+
...cancel !== void 0 ? { cancel } : {}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
return capabilities;
|
|
737
|
+
}
|
|
738
|
+
function readAgentInfo(value) {
|
|
739
|
+
const record = asRecord2(value);
|
|
740
|
+
if (!record) {
|
|
741
|
+
return void 0;
|
|
742
|
+
}
|
|
743
|
+
const agentInfo = {};
|
|
744
|
+
const name = readString(record, "name");
|
|
745
|
+
if (name !== void 0) agentInfo.name = name;
|
|
746
|
+
const title = readString(record, "title");
|
|
747
|
+
if (title !== void 0) agentInfo.title = title;
|
|
748
|
+
const version = readString(record, "version");
|
|
749
|
+
if (version !== void 0) agentInfo.version = version;
|
|
750
|
+
return Object.keys(agentInfo).length > 0 ? agentInfo : void 0;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/acp-client.ts
|
|
754
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
755
|
+
var ACP_PROMPT_REQUEST_TIMEOUT_MS = 60 * 6e4;
|
|
756
|
+
var ACP_REQUEST_TIMEOUT_MS = 3e4;
|
|
757
|
+
var DEFAULT_CLIENT_NAME = "agent-kit";
|
|
758
|
+
function permissionDecisionToken(decision) {
|
|
759
|
+
switch (decision) {
|
|
760
|
+
case "approved":
|
|
761
|
+
return "approve";
|
|
762
|
+
case "abort":
|
|
763
|
+
case "denied":
|
|
764
|
+
default:
|
|
765
|
+
return "reject";
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
var AcpAgentClient = class {
|
|
769
|
+
transport;
|
|
770
|
+
strategy;
|
|
771
|
+
now;
|
|
772
|
+
logger;
|
|
773
|
+
eventListeners = /* @__PURE__ */ new Set();
|
|
774
|
+
toolCallHandler = null;
|
|
775
|
+
approvalHandler = null;
|
|
776
|
+
titleHandler = null;
|
|
777
|
+
runtimeCapabilitiesHandler = null;
|
|
778
|
+
// threadId ↔ protocol sessionId mapping + per-session state.
|
|
779
|
+
sessions = /* @__PURE__ */ new Map();
|
|
780
|
+
threadIdByProtocolId = /* @__PURE__ */ new Map();
|
|
781
|
+
unsubscribeNotification = void 0;
|
|
782
|
+
unsubscribeRequest = void 0;
|
|
783
|
+
initialized = false;
|
|
784
|
+
runtimeCapabilities;
|
|
785
|
+
threadSequence = 0;
|
|
786
|
+
constructor(options) {
|
|
787
|
+
this.transport = options.transport;
|
|
788
|
+
this.strategy = options.strategy;
|
|
789
|
+
this.now = options.now ?? Date.now;
|
|
790
|
+
this.logger = options.logger ?? noopLogger2;
|
|
791
|
+
this.clientName = options.clientName ?? DEFAULT_CLIENT_NAME;
|
|
792
|
+
this.clientTitle = options.clientTitle ?? this.clientName;
|
|
793
|
+
this.clientVersion = options.clientVersion ?? "0.0.0";
|
|
794
|
+
this.defaultCwd = options.cwd;
|
|
795
|
+
this.defaultMcpServers = options.mcpServers ?? [];
|
|
796
|
+
}
|
|
797
|
+
clientName;
|
|
798
|
+
clientTitle;
|
|
799
|
+
clientVersion;
|
|
800
|
+
defaultCwd;
|
|
801
|
+
defaultMcpServers;
|
|
802
|
+
// ---- subscriptions (mirror CodexThreadClient) ----
|
|
803
|
+
onEvent(cb) {
|
|
804
|
+
this.eventListeners.add(cb);
|
|
805
|
+
return () => {
|
|
806
|
+
this.eventListeners.delete(cb);
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
onToolCall(handler) {
|
|
810
|
+
this.toolCallHandler = handler;
|
|
811
|
+
return () => {
|
|
812
|
+
if (this.toolCallHandler === handler) this.toolCallHandler = null;
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
onApprovalRequest(handler) {
|
|
816
|
+
this.approvalHandler = handler;
|
|
817
|
+
return () => {
|
|
818
|
+
if (this.approvalHandler === handler) this.approvalHandler = null;
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/** Subscribe to title extraction (topic-update / session-summary). */
|
|
822
|
+
onTitle(handler) {
|
|
823
|
+
this.titleHandler = handler;
|
|
824
|
+
return () => {
|
|
825
|
+
if (this.titleHandler === handler) this.titleHandler = null;
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
/** Subscribe to runtime-capabilities (models/modes/config-options) changes. */
|
|
829
|
+
onRuntimeCapabilities(handler) {
|
|
830
|
+
this.runtimeCapabilitiesHandler = handler;
|
|
831
|
+
return () => {
|
|
832
|
+
if (this.runtimeCapabilitiesHandler === handler) {
|
|
833
|
+
this.runtimeCapabilitiesHandler = null;
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
// ---- lifecycle ----
|
|
838
|
+
/**
|
|
839
|
+
* Public `AgentBackend.startThread`: accepts NEUTRAL `AgentStartThreadOptions`
|
|
840
|
+
* and maps them onto an ACP `session/new`. ACP supports:
|
|
841
|
+
* • `cwd` → `session/new.cwd`.
|
|
842
|
+
* • `model` → applied via `session/set_model` after the session opens, when
|
|
843
|
+
* the agent advertises model selection (best-effort; debug-logged if not).
|
|
844
|
+
* • `instructions` is NOT injected here — ACP `session/new` has no base-
|
|
845
|
+
* instructions slot, matching the adapter's existing behavior. A host that
|
|
846
|
+
* wants system framing sends it as leading turn text.
|
|
847
|
+
* Codex-only fields (`approvalPolicy`, `sandbox`, `config`, `environments`,
|
|
848
|
+
* `tools`, `serviceName`, `modelProvider`, `serviceTier`, `workspaceRoots`) are
|
|
849
|
+
* IGNORED — logged at debug so it's visible the backend doesn't honor them.
|
|
850
|
+
*/
|
|
851
|
+
async startThread(options = {}) {
|
|
852
|
+
const ignored = [];
|
|
853
|
+
for (const key of [
|
|
854
|
+
"instructions",
|
|
855
|
+
"approvalPolicy",
|
|
856
|
+
"sandbox",
|
|
857
|
+
"config",
|
|
858
|
+
"environments",
|
|
859
|
+
"tools",
|
|
860
|
+
"serviceName",
|
|
861
|
+
"modelProvider",
|
|
862
|
+
"serviceTier",
|
|
863
|
+
"workspaceRoots"
|
|
864
|
+
]) {
|
|
865
|
+
if (options[key] !== void 0) ignored.push(key);
|
|
866
|
+
}
|
|
867
|
+
if (ignored.length > 0) {
|
|
868
|
+
this.logger.debug("acp startThread: ignoring Codex-only neutral options", {
|
|
869
|
+
ignored
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
const native = {};
|
|
873
|
+
if (options.cwd !== void 0) native.cwd = options.cwd;
|
|
874
|
+
const result = await this.startThreadNative(native);
|
|
875
|
+
if (options.model !== void 0) {
|
|
876
|
+
await this.setModel(result.threadId, options.model).catch((cause) => {
|
|
877
|
+
this.logger.debug("acp startThread: model selection not applied", {
|
|
878
|
+
model: options.model,
|
|
879
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
return result;
|
|
884
|
+
}
|
|
885
|
+
/** ACP-native `session/new`. The neutral `startThread` delegates here after
|
|
886
|
+
* mapping cwd + dropping Codex-only fields. Exposed for hosts that need
|
|
887
|
+
* ACP-specific control (per-thread `mcpServers`). */
|
|
888
|
+
async startThreadNative(options = {}) {
|
|
889
|
+
await this.initialize();
|
|
890
|
+
const cwd = options.cwd ?? this.defaultCwd ?? process.cwd();
|
|
891
|
+
const mcpServers = options.mcpServers ?? this.defaultMcpServers;
|
|
892
|
+
const result = await this.transport.request("session/new", {
|
|
893
|
+
cwd,
|
|
894
|
+
mcpServers
|
|
895
|
+
});
|
|
896
|
+
const record = asRecord3(result);
|
|
897
|
+
const protocolSessionId = readString2(record, "sessionId") ?? readString2(record, "session_id");
|
|
898
|
+
if (!protocolSessionId) {
|
|
899
|
+
throw new Error("ACP session/new did not return a session id");
|
|
900
|
+
}
|
|
901
|
+
const threadId = `acp:${this.strategy.id}:${++this.threadSequence}`;
|
|
902
|
+
const session = {
|
|
903
|
+
threadId,
|
|
904
|
+
protocolSessionId,
|
|
905
|
+
normalizer: new AcpSessionNormalizer({ quirks: this.strategy.quirks }),
|
|
906
|
+
turnId: void 0,
|
|
907
|
+
runtimeState: void 0
|
|
908
|
+
};
|
|
909
|
+
this.sessions.set(threadId, session);
|
|
910
|
+
this.threadIdByProtocolId.set(protocolSessionId, threadId);
|
|
911
|
+
const runtimeCapabilities = this.captureRuntimeCapabilities("session-new", result);
|
|
912
|
+
if (runtimeCapabilities) {
|
|
913
|
+
const runtimeState = acpSessionRuntimeStateFromCapabilities(
|
|
914
|
+
runtimeCapabilities,
|
|
915
|
+
this.now()
|
|
916
|
+
);
|
|
917
|
+
if (runtimeState) session.runtimeState = runtimeState;
|
|
918
|
+
this.notifyRuntimeCapabilities({ threadId, runtimeCapabilities, runtimeState });
|
|
919
|
+
this.emitThreadSettings(session, runtimeCapabilities, runtimeState);
|
|
920
|
+
}
|
|
921
|
+
this.logger.debug("acp thread started", { threadId, protocolSessionId });
|
|
922
|
+
const out = { threadId };
|
|
923
|
+
const model = runtimeCapabilities?.models?.currentModelId;
|
|
924
|
+
if (model !== void 0) out.model = model;
|
|
925
|
+
out.modelProvider = this.strategy.id;
|
|
926
|
+
return out;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Public `AgentBackend.startTurn`: accepts NEUTRAL `AgentStartTurnOptions` and
|
|
930
|
+
* maps them onto ACP prompt content blocks — a leading `text` block from
|
|
931
|
+
* `input.text`, then one `image` block per `input.imagePaths` entry (read from
|
|
932
|
+
* disk, base64-encoded, mimeType inferred from extension). `reasoning` maps to
|
|
933
|
+
* an ACP mode/config when the session advertises a matching option, else it is
|
|
934
|
+
* IGNORED (debug-logged).
|
|
935
|
+
*/
|
|
936
|
+
async startTurn(options) {
|
|
937
|
+
const promptContent = [
|
|
938
|
+
{ type: "text", text: options.input.text }
|
|
939
|
+
];
|
|
940
|
+
for (const imagePath of options.input.imagePaths ?? []) {
|
|
941
|
+
const block = await this.imageBlockFromPath(imagePath).catch((cause) => {
|
|
942
|
+
this.logger.debug("acp startTurn: skipping unreadable image", {
|
|
943
|
+
imagePath,
|
|
944
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
945
|
+
});
|
|
946
|
+
return void 0;
|
|
947
|
+
});
|
|
948
|
+
if (block !== void 0) promptContent.push(block);
|
|
949
|
+
}
|
|
950
|
+
if (options.reasoning !== void 0) {
|
|
951
|
+
await this.applyReasoning(options.threadId, options.reasoning).catch((cause) => {
|
|
952
|
+
this.logger.debug("acp startTurn: reasoning not applied", {
|
|
953
|
+
reasoning: options.reasoning,
|
|
954
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
return this.startTurnNative({ threadId: options.threadId, promptContent });
|
|
959
|
+
}
|
|
960
|
+
/** ACP-native `session/prompt`. Takes a plain prompt or pre-built content
|
|
961
|
+
* blocks. The neutral `startTurn` delegates here after building blocks. */
|
|
962
|
+
async startTurnNative(options) {
|
|
963
|
+
const session = this.requireSession(options.threadId);
|
|
964
|
+
if (session.turnId !== void 0) {
|
|
965
|
+
throw new Error("A turn is already active for this ACP session.");
|
|
966
|
+
}
|
|
967
|
+
const turnId = `turn:${session.threadId}:${this.now()}`;
|
|
968
|
+
session.turnId = turnId;
|
|
969
|
+
session.normalizer.resetTurn();
|
|
970
|
+
this.emit({ kind: "turn_started", threadId: session.threadId, turnId });
|
|
971
|
+
const prompt = options.promptContent ?? textPrompt(options.prompt ?? "");
|
|
972
|
+
try {
|
|
973
|
+
await this.transport.request(
|
|
974
|
+
"session/prompt",
|
|
975
|
+
{
|
|
976
|
+
sessionId: session.protocolSessionId,
|
|
977
|
+
prompt
|
|
978
|
+
},
|
|
979
|
+
ACP_PROMPT_REQUEST_TIMEOUT_MS
|
|
980
|
+
);
|
|
981
|
+
} catch (error) {
|
|
982
|
+
session.turnId = void 0;
|
|
983
|
+
this.emit({
|
|
984
|
+
kind: "turn_completed",
|
|
985
|
+
threadId: session.threadId,
|
|
986
|
+
turnId,
|
|
987
|
+
status: "failed"
|
|
988
|
+
});
|
|
989
|
+
this.emit({
|
|
990
|
+
kind: "error",
|
|
991
|
+
threadId: session.threadId,
|
|
992
|
+
turnId,
|
|
993
|
+
message: errorMessage(error)
|
|
994
|
+
});
|
|
995
|
+
throw error;
|
|
996
|
+
}
|
|
997
|
+
for (const event of session.normalizer.finalizeAssistantMessage({
|
|
998
|
+
threadId: session.threadId,
|
|
999
|
+
turnId
|
|
1000
|
+
})) {
|
|
1001
|
+
this.emit(event);
|
|
1002
|
+
}
|
|
1003
|
+
session.turnId = void 0;
|
|
1004
|
+
this.emit({
|
|
1005
|
+
kind: "turn_completed",
|
|
1006
|
+
threadId: session.threadId,
|
|
1007
|
+
turnId,
|
|
1008
|
+
status: "completed"
|
|
1009
|
+
});
|
|
1010
|
+
return { turnId };
|
|
1011
|
+
}
|
|
1012
|
+
async interruptTurn(threadId) {
|
|
1013
|
+
const session = this.requireSession(threadId);
|
|
1014
|
+
if (this.transport.notify) {
|
|
1015
|
+
await this.transport.notify("session/cancel", {
|
|
1016
|
+
sessionId: session.protocolSessionId
|
|
1017
|
+
});
|
|
1018
|
+
} else {
|
|
1019
|
+
await this.transport.request("session/cancel", {
|
|
1020
|
+
sessionId: session.protocolSessionId
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
async setMode(threadId, modeId) {
|
|
1025
|
+
await this.setRuntimeOption(threadId, "mode", modeId, modeId);
|
|
1026
|
+
}
|
|
1027
|
+
async setModel(threadId, modelId) {
|
|
1028
|
+
await this.setRuntimeOption(threadId, "model", modelId, modelId);
|
|
1029
|
+
}
|
|
1030
|
+
async setConfigOption(threadId, optionId, value) {
|
|
1031
|
+
await this.setRuntimeOption(threadId, "configOption", optionId, value);
|
|
1032
|
+
}
|
|
1033
|
+
async close() {
|
|
1034
|
+
this.unsubscribeNotification?.();
|
|
1035
|
+
this.unsubscribeNotification = void 0;
|
|
1036
|
+
this.unsubscribeRequest?.();
|
|
1037
|
+
this.unsubscribeRequest = void 0;
|
|
1038
|
+
this.sessions.clear();
|
|
1039
|
+
this.threadIdByProtocolId.clear();
|
|
1040
|
+
this.initialized = false;
|
|
1041
|
+
await this.transport.close?.();
|
|
1042
|
+
}
|
|
1043
|
+
// ---- internals ----
|
|
1044
|
+
emit(event) {
|
|
1045
|
+
for (const listener of this.eventListeners) listener(event);
|
|
1046
|
+
}
|
|
1047
|
+
async initialize() {
|
|
1048
|
+
if (this.initialized) return;
|
|
1049
|
+
this.unsubscribeNotification = this.transport.onNotification((method, params) => {
|
|
1050
|
+
this.handleNotification(method, params);
|
|
1051
|
+
});
|
|
1052
|
+
this.unsubscribeRequest = this.transport.onRequest?.(
|
|
1053
|
+
async (method, params, id) => await this.handleAcpRequest(method, params, id)
|
|
1054
|
+
);
|
|
1055
|
+
const result = await this.transport.request(
|
|
1056
|
+
"initialize",
|
|
1057
|
+
{
|
|
1058
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
1059
|
+
clientCapabilities: {
|
|
1060
|
+
auth: { terminal: false },
|
|
1061
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
1062
|
+
terminal: false
|
|
1063
|
+
},
|
|
1064
|
+
clientInfo: {
|
|
1065
|
+
name: this.clientName,
|
|
1066
|
+
title: this.clientTitle,
|
|
1067
|
+
version: this.clientVersion
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
1070
|
+
ACP_REQUEST_TIMEOUT_MS
|
|
1071
|
+
);
|
|
1072
|
+
const runtimeCapabilities = this.captureRuntimeCapabilities("initialize", result);
|
|
1073
|
+
if (runtimeCapabilities) {
|
|
1074
|
+
this.notifyRuntimeCapabilities({ runtimeCapabilities });
|
|
1075
|
+
}
|
|
1076
|
+
this.initialized = true;
|
|
1077
|
+
}
|
|
1078
|
+
handleNotification(method, params) {
|
|
1079
|
+
const vendorMethods = this.strategy.quirks.vendorNotificationMethods ?? [];
|
|
1080
|
+
if (method !== "session/update" && !vendorMethods.includes(method)) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const record = asRecord3(params);
|
|
1084
|
+
if (!record) return;
|
|
1085
|
+
this.applySessionUpdate(record);
|
|
1086
|
+
}
|
|
1087
|
+
applySessionUpdate(params) {
|
|
1088
|
+
const protocolSessionId = readString2(params, "sessionId") ?? readString2(params, "session_id");
|
|
1089
|
+
const update = asRecord3(params.update);
|
|
1090
|
+
if (!protocolSessionId || !update) {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const threadId = this.threadIdByProtocolId.get(protocolSessionId);
|
|
1094
|
+
const session = threadId ? this.sessions.get(threadId) : void 0;
|
|
1095
|
+
if (!session) {
|
|
1096
|
+
this.logger.debug("acp session/update for unknown session", { protocolSessionId });
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
const runtimeState = acpSessionRuntimeStateFromUpdate(update, this.now());
|
|
1100
|
+
if (runtimeState) {
|
|
1101
|
+
session.runtimeState = mergeAcpRuntimeState(session.runtimeState, runtimeState);
|
|
1102
|
+
if (this.runtimeCapabilities) {
|
|
1103
|
+
this.notifyRuntimeCapabilities({
|
|
1104
|
+
threadId: session.threadId,
|
|
1105
|
+
runtimeCapabilities: this.runtimeCapabilities,
|
|
1106
|
+
runtimeState: session.runtimeState
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
this.emitThreadSettings(session, this.runtimeCapabilities, session.runtimeState);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const ctx = {
|
|
1113
|
+
threadId: session.threadId,
|
|
1114
|
+
turnId: session.turnId ?? `turn:${session.threadId}:detached`
|
|
1115
|
+
};
|
|
1116
|
+
const result = session.normalizer.apply(update, ctx);
|
|
1117
|
+
if (result.title !== void 0) {
|
|
1118
|
+
this.titleHandler?.({ threadId: session.threadId, title: result.title });
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
for (const event of result.events) {
|
|
1122
|
+
this.emit(event);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
async handleAcpRequest(method, params, id) {
|
|
1126
|
+
if (method === "session/request_permission") {
|
|
1127
|
+
return await this.handlePermissionRequest(params, id);
|
|
1128
|
+
}
|
|
1129
|
+
if (this.toolCallHandler) {
|
|
1130
|
+
const call = { method, params };
|
|
1131
|
+
return await this.toolCallHandler(call);
|
|
1132
|
+
}
|
|
1133
|
+
throw new Error(`Unsupported ACP request: ${method}`);
|
|
1134
|
+
}
|
|
1135
|
+
async handlePermissionRequest(params, id) {
|
|
1136
|
+
const protocolSessionId = readString2(params, "sessionId") ?? readString2(params, "session_id");
|
|
1137
|
+
const threadId = protocolSessionId ? this.threadIdByProtocolId.get(protocolSessionId) : void 0;
|
|
1138
|
+
const options = readPermissionOptions(params.options);
|
|
1139
|
+
const handler = this.approvalHandler;
|
|
1140
|
+
if (!handler) {
|
|
1141
|
+
return cancelledPermissionOutcome();
|
|
1142
|
+
}
|
|
1143
|
+
const approval = buildApprovalRequest({
|
|
1144
|
+
params,
|
|
1145
|
+
id,
|
|
1146
|
+
threadId,
|
|
1147
|
+
session: threadId ? this.sessions.get(threadId) : void 0,
|
|
1148
|
+
now: this.now
|
|
1149
|
+
});
|
|
1150
|
+
if (threadId) {
|
|
1151
|
+
this.emit({ kind: "approval_request", threadId, approval });
|
|
1152
|
+
}
|
|
1153
|
+
const decision = await handler("session/request_permission", params);
|
|
1154
|
+
return permissionOutcomeFromDecision(decision, options);
|
|
1155
|
+
}
|
|
1156
|
+
async setRuntimeOption(threadId, source, optionId, value) {
|
|
1157
|
+
const session = this.requireSession(threadId);
|
|
1158
|
+
const result = await this.setRuntimeOptionOnTransport(
|
|
1159
|
+
session.protocolSessionId,
|
|
1160
|
+
source,
|
|
1161
|
+
optionId,
|
|
1162
|
+
value
|
|
1163
|
+
);
|
|
1164
|
+
const runtimeCapabilities = this.captureRuntimeCapabilities("session-load", result);
|
|
1165
|
+
const requested = source === "configOption" ? { configValues: { [optionId]: value }, updatedAt: this.now() } : source === "mode" ? { currentModeId: value, updatedAt: this.now() } : { currentModelId: value, updatedAt: this.now() };
|
|
1166
|
+
session.runtimeState = mergeAcpRuntimeState(session.runtimeState, requested);
|
|
1167
|
+
const effectiveCapabilities = runtimeCapabilities ?? this.runtimeCapabilities;
|
|
1168
|
+
if (effectiveCapabilities) {
|
|
1169
|
+
this.notifyRuntimeCapabilities({
|
|
1170
|
+
threadId,
|
|
1171
|
+
runtimeCapabilities: effectiveCapabilities,
|
|
1172
|
+
...session.runtimeState !== void 0 ? { runtimeState: session.runtimeState } : {}
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
this.emitThreadSettings(session, effectiveCapabilities, session.runtimeState);
|
|
1176
|
+
}
|
|
1177
|
+
async setRuntimeOptionOnTransport(protocolSessionId, source, optionId, value) {
|
|
1178
|
+
if (source === "configOption") {
|
|
1179
|
+
return await this.transport.request("session/set_config_option", {
|
|
1180
|
+
sessionId: protocolSessionId,
|
|
1181
|
+
configId: optionId,
|
|
1182
|
+
value
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
if (source === "mode") {
|
|
1186
|
+
return await this.transport.request("session/set_mode", {
|
|
1187
|
+
sessionId: protocolSessionId,
|
|
1188
|
+
modeId: value
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
return await this.transport.request("session/set_model", {
|
|
1192
|
+
sessionId: protocolSessionId,
|
|
1193
|
+
modelId: value
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
/** Map a neutral `reasoning` token onto an ACP runtime option. We try to match
|
|
1197
|
+
* it to an available MODE (by id or label, case-insensitively) and switch via
|
|
1198
|
+
* `session/set_mode`. ACP has no first-class "reasoning effort" concept, so if
|
|
1199
|
+
* no mode matches we leave it alone — the caller's `.catch` debug-logs. */
|
|
1200
|
+
async applyReasoning(threadId, reasoning) {
|
|
1201
|
+
const modes = this.runtimeCapabilities?.modes?.availableModes ?? [];
|
|
1202
|
+
const target = reasoning.toLowerCase();
|
|
1203
|
+
const match = modes.find(
|
|
1204
|
+
(mode) => mode.id.toLowerCase() === target || typeof mode.label === "string" && mode.label.toLowerCase() === target
|
|
1205
|
+
);
|
|
1206
|
+
if (match === void 0) {
|
|
1207
|
+
this.logger.debug("acp reasoning has no matching mode \u2014 ignored", { reasoning });
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
await this.setMode(threadId, match.id);
|
|
1211
|
+
}
|
|
1212
|
+
/** Read an image file and build an ACP `image` content block (base64 + inferred
|
|
1213
|
+
* mimeType). Throws on read failure; the caller catches + skips. */
|
|
1214
|
+
async imageBlockFromPath(imagePath) {
|
|
1215
|
+
const data = await readFile(imagePath);
|
|
1216
|
+
return {
|
|
1217
|
+
type: "image",
|
|
1218
|
+
mimeType: mimeTypeForImagePath(imagePath),
|
|
1219
|
+
data: data.toString("base64")
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
captureRuntimeCapabilities(source, result) {
|
|
1223
|
+
const runtimeCapabilities = normalizeAcpRuntimeCapabilities({
|
|
1224
|
+
value: result,
|
|
1225
|
+
now: this.now(),
|
|
1226
|
+
source,
|
|
1227
|
+
...this.runtimeCapabilities !== void 0 ? { initialize: this.runtimeCapabilities } : {}
|
|
1228
|
+
});
|
|
1229
|
+
if (runtimeCapabilities) {
|
|
1230
|
+
this.runtimeCapabilities = runtimeCapabilities;
|
|
1231
|
+
}
|
|
1232
|
+
return runtimeCapabilities;
|
|
1233
|
+
}
|
|
1234
|
+
notifyRuntimeCapabilities(event) {
|
|
1235
|
+
this.runtimeCapabilitiesHandler?.({
|
|
1236
|
+
...event.threadId !== void 0 ? { threadId: event.threadId } : {},
|
|
1237
|
+
runtimeCapabilities: event.runtimeCapabilities,
|
|
1238
|
+
...event.runtimeState !== void 0 ? { runtimeState: event.runtimeState } : {}
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
emitThreadSettings(session, capabilities, runtimeState) {
|
|
1242
|
+
const settings = { threadId: session.threadId };
|
|
1243
|
+
const model = runtimeState?.currentModelId ?? capabilities?.models?.currentModelId;
|
|
1244
|
+
if (model !== void 0) settings.model = model;
|
|
1245
|
+
settings.modelProvider = this.strategy.id;
|
|
1246
|
+
const modeId = runtimeState?.currentModeId ?? capabilities?.modes?.currentModeId;
|
|
1247
|
+
if (modeId !== void 0) {
|
|
1248
|
+
settings.modeId = modeId;
|
|
1249
|
+
const label = modeLabelFor(capabilities, modeId);
|
|
1250
|
+
if (label !== void 0) settings.modeLabel = label;
|
|
1251
|
+
}
|
|
1252
|
+
this.emit({ kind: "thread_settings", settings });
|
|
1253
|
+
}
|
|
1254
|
+
requireSession(threadId) {
|
|
1255
|
+
const session = this.sessions.get(threadId);
|
|
1256
|
+
if (!session) {
|
|
1257
|
+
throw new Error(`Unknown ACP thread: ${threadId}`);
|
|
1258
|
+
}
|
|
1259
|
+
return session;
|
|
1260
|
+
}
|
|
1261
|
+
/** Whether the agent advertises session/load support (for hosts that resume). */
|
|
1262
|
+
supportsSessionLoad() {
|
|
1263
|
+
return acpRuntimeSupportsSessionLoad(this.runtimeCapabilities);
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
function buildApprovalRequest(args) {
|
|
1267
|
+
const toolCall = asRecord3(args.params.toolCall) ?? {};
|
|
1268
|
+
const title = typeof toolCall.title === "string" && toolCall.title.trim() ? toolCall.title.trim() : "ACP tool call";
|
|
1269
|
+
const toolCallId = readString2(toolCall, "toolCallId") ?? readString2(toolCall, "tool_call_id");
|
|
1270
|
+
const requestId = args.id == null ? toolCallId ?? `acp:${args.now()}` : String(args.id);
|
|
1271
|
+
const acpKind = typeof toolCall.kind === "string" ? toolCall.kind : void 0;
|
|
1272
|
+
const approval = {
|
|
1273
|
+
id: requestId,
|
|
1274
|
+
method: "session/request_permission",
|
|
1275
|
+
kind: approvalKindFor(acpKind),
|
|
1276
|
+
params: args.params
|
|
1277
|
+
};
|
|
1278
|
+
approval.summary = acpKind ? `${acpKind}: ${title}` : title;
|
|
1279
|
+
return approval;
|
|
1280
|
+
}
|
|
1281
|
+
function approvalKindFor(acpKind) {
|
|
1282
|
+
switch (acpKind) {
|
|
1283
|
+
case "execute":
|
|
1284
|
+
case "exec":
|
|
1285
|
+
case "shell":
|
|
1286
|
+
return "exec";
|
|
1287
|
+
case "edit":
|
|
1288
|
+
case "write":
|
|
1289
|
+
return "patch";
|
|
1290
|
+
case "read":
|
|
1291
|
+
case "search":
|
|
1292
|
+
case "fetch":
|
|
1293
|
+
return "tool";
|
|
1294
|
+
default:
|
|
1295
|
+
return "other";
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function readPermissionOptions(value) {
|
|
1299
|
+
if (!Array.isArray(value)) {
|
|
1300
|
+
return [];
|
|
1301
|
+
}
|
|
1302
|
+
return value.flatMap((option) => {
|
|
1303
|
+
const record = asRecord3(option);
|
|
1304
|
+
const optionId = readString2(record, "optionId");
|
|
1305
|
+
if (!record || !optionId) {
|
|
1306
|
+
return [];
|
|
1307
|
+
}
|
|
1308
|
+
const normalized = { optionId };
|
|
1309
|
+
const name = readString2(record, "name");
|
|
1310
|
+
if (name !== void 0) normalized.name = name;
|
|
1311
|
+
const kind = readString2(record, "kind");
|
|
1312
|
+
if (kind !== void 0) normalized.kind = kind;
|
|
1313
|
+
return [normalized];
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
function permissionOutcomeFromDecision(decision, options) {
|
|
1317
|
+
const token = permissionDecisionToken(decision);
|
|
1318
|
+
const optionId = selectPermissionOptionId(token, options);
|
|
1319
|
+
return optionId ? { outcome: { outcome: "selected", optionId } } : cancelledPermissionOutcome();
|
|
1320
|
+
}
|
|
1321
|
+
function cancelledPermissionOutcome() {
|
|
1322
|
+
return { outcome: { outcome: "cancelled" } };
|
|
1323
|
+
}
|
|
1324
|
+
function selectPermissionOptionId(decision, options) {
|
|
1325
|
+
const normalized = decision.toLowerCase();
|
|
1326
|
+
const exact = options.find(
|
|
1327
|
+
(option) => [option.optionId, option.name, option.kind].filter((value) => typeof value === "string").some((value) => value.toLowerCase() === normalized)
|
|
1328
|
+
);
|
|
1329
|
+
if (exact) {
|
|
1330
|
+
return exact.optionId;
|
|
1331
|
+
}
|
|
1332
|
+
if (normalized === "approve" || normalized === "accept" || normalized === "allow") {
|
|
1333
|
+
return (options.find((option) => option.kind === "allow_once") ?? options.find((option) => option.kind === "allow_always") ?? options.find((option) => option.name?.toLowerCase().includes("allow")))?.optionId;
|
|
1334
|
+
}
|
|
1335
|
+
if (normalized === "reject" || normalized === "decline" || normalized === "deny") {
|
|
1336
|
+
return (options.find((option) => option.kind === "reject_once") ?? options.find((option) => option.name?.toLowerCase().includes("reject")))?.optionId;
|
|
1337
|
+
}
|
|
1338
|
+
return void 0;
|
|
1339
|
+
}
|
|
1340
|
+
function textPrompt(text) {
|
|
1341
|
+
return [{ type: "text", text }];
|
|
1342
|
+
}
|
|
1343
|
+
function mimeTypeForImagePath(imagePath) {
|
|
1344
|
+
switch (extname(imagePath).toLowerCase()) {
|
|
1345
|
+
case ".jpg":
|
|
1346
|
+
case ".jpeg":
|
|
1347
|
+
return "image/jpeg";
|
|
1348
|
+
case ".gif":
|
|
1349
|
+
return "image/gif";
|
|
1350
|
+
case ".webp":
|
|
1351
|
+
return "image/webp";
|
|
1352
|
+
case ".bmp":
|
|
1353
|
+
return "image/bmp";
|
|
1354
|
+
case ".svg":
|
|
1355
|
+
return "image/svg+xml";
|
|
1356
|
+
case ".png":
|
|
1357
|
+
default:
|
|
1358
|
+
return "image/png";
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function errorMessage(error) {
|
|
1362
|
+
if (error instanceof Error && error.message.trim()) {
|
|
1363
|
+
return error.message;
|
|
1364
|
+
}
|
|
1365
|
+
const message = String(error).trim();
|
|
1366
|
+
return message || "Turn failed.";
|
|
1367
|
+
}
|
|
1368
|
+
function readString2(record, key) {
|
|
1369
|
+
const value = record?.[key];
|
|
1370
|
+
return typeof value === "string" ? value : void 0;
|
|
1371
|
+
}
|
|
1372
|
+
function asRecord3(value) {
|
|
1373
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// src/strategies/strategy-types.ts
|
|
1377
|
+
function buildAcpBackendId(registryId) {
|
|
1378
|
+
return `acp:${registryId}`;
|
|
1379
|
+
}
|
|
1380
|
+
var DEFAULT_QUIRKS = {
|
|
1381
|
+
surfaceThoughts: true,
|
|
1382
|
+
titleFrom: "topic-update"
|
|
1383
|
+
};
|
|
1384
|
+
function defaultQuirks(overrides = {}) {
|
|
1385
|
+
return { ...DEFAULT_QUIRKS, ...overrides };
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// src/strategies/gemini.ts
|
|
1389
|
+
var geminiStrategy = {
|
|
1390
|
+
id: "gemini",
|
|
1391
|
+
backendId: buildAcpBackendId("gemini"),
|
|
1392
|
+
displayName: "Gemini CLI",
|
|
1393
|
+
authors: ["Google"],
|
|
1394
|
+
discoveryProbe: {
|
|
1395
|
+
command: "gemini",
|
|
1396
|
+
versionArgs: ["--version"],
|
|
1397
|
+
helpArgs: ["--help"],
|
|
1398
|
+
helpMatches: /(^|\s)--acp(\s|,|$)/
|
|
1399
|
+
},
|
|
1400
|
+
spawn: {
|
|
1401
|
+
command: "gemini",
|
|
1402
|
+
args: ["--acp"],
|
|
1403
|
+
// Gemini refuses to operate without workspace trust; matching PwrAgnt's
|
|
1404
|
+
// launch-descriptor normalization, append --skip-trust + set the env flag.
|
|
1405
|
+
ensureArgs: ["--skip-trust"],
|
|
1406
|
+
env: { GEMINI_CLI_TRUST_WORKSPACE: "true" }
|
|
1407
|
+
},
|
|
1408
|
+
quirks: defaultQuirks({ surfaceThoughts: true, titleFrom: "topic-update" })
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
// src/strategies/grok.ts
|
|
1412
|
+
import { homedir } from "os";
|
|
1413
|
+
import path from "path";
|
|
1414
|
+
var grokStrategy = {
|
|
1415
|
+
id: "grok",
|
|
1416
|
+
backendId: buildAcpBackendId("grok"),
|
|
1417
|
+
displayName: "Grok",
|
|
1418
|
+
authors: ["xAI"],
|
|
1419
|
+
discoveryProbe: {
|
|
1420
|
+
command: "grok",
|
|
1421
|
+
versionArgs: ["--version"],
|
|
1422
|
+
helpArgs: ["agent", "stdio", "--help"],
|
|
1423
|
+
helpMatches: /Run the agent over stdio/i,
|
|
1424
|
+
fallbackCommands: [
|
|
1425
|
+
path.join(homedir(), ".grok", "bin", "grok"),
|
|
1426
|
+
"/opt/homebrew/bin/grok",
|
|
1427
|
+
"/usr/local/bin/grok"
|
|
1428
|
+
]
|
|
1429
|
+
},
|
|
1430
|
+
spawn: {
|
|
1431
|
+
command: "grok",
|
|
1432
|
+
args: ["agent", "stdio"]
|
|
1433
|
+
},
|
|
1434
|
+
// Grok auto-generates the thread title via its vendor notification
|
|
1435
|
+
// `_x.ai/session_notification` carrying `session_summary_generated`.
|
|
1436
|
+
quirks: defaultQuirks({
|
|
1437
|
+
surfaceThoughts: true,
|
|
1438
|
+
titleFrom: "session-summary",
|
|
1439
|
+
vendorNotificationMethods: ["_x.ai/session_notification"]
|
|
1440
|
+
})
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
// src/strategies/kimi.ts
|
|
1444
|
+
var kimiStrategy = {
|
|
1445
|
+
id: "kimi",
|
|
1446
|
+
backendId: buildAcpBackendId("kimi"),
|
|
1447
|
+
displayName: "Kimi Code CLI",
|
|
1448
|
+
authors: ["Moonshot AI"],
|
|
1449
|
+
discoveryProbe: {
|
|
1450
|
+
command: "kimi",
|
|
1451
|
+
versionArgs: ["--version"],
|
|
1452
|
+
helpArgs: ["acp", "--help"],
|
|
1453
|
+
helpMatches: /\bACP server\b/i
|
|
1454
|
+
},
|
|
1455
|
+
spawn: {
|
|
1456
|
+
command: "kimi",
|
|
1457
|
+
args: ["acp"]
|
|
1458
|
+
},
|
|
1459
|
+
quirks: defaultQuirks({ surfaceThoughts: true, titleFrom: "topic-update" })
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
// src/strategies/qwen.ts
|
|
1463
|
+
import { homedir as homedir2 } from "os";
|
|
1464
|
+
import path2 from "path";
|
|
1465
|
+
var qwenStrategy = {
|
|
1466
|
+
id: "qwen",
|
|
1467
|
+
backendId: buildAcpBackendId("qwen"),
|
|
1468
|
+
displayName: "Qwen Code",
|
|
1469
|
+
authors: ["Qwen Team"],
|
|
1470
|
+
license: "Apache-2.0",
|
|
1471
|
+
repositoryUrl: "https://github.com/QwenLM/qwen-code",
|
|
1472
|
+
discoveryProbe: {
|
|
1473
|
+
command: "qwen",
|
|
1474
|
+
versionArgs: ["--version"],
|
|
1475
|
+
helpArgs: ["--help"],
|
|
1476
|
+
helpMatches: /(^|\s)--acp(\s|,|$)/,
|
|
1477
|
+
fallbackCommands: [
|
|
1478
|
+
path2.join(homedir2(), ".qwen", "bin", "qwen"),
|
|
1479
|
+
"/opt/homebrew/bin/qwen",
|
|
1480
|
+
"/usr/local/bin/qwen"
|
|
1481
|
+
]
|
|
1482
|
+
},
|
|
1483
|
+
spawn: {
|
|
1484
|
+
command: "qwen",
|
|
1485
|
+
args: ["--acp"]
|
|
1486
|
+
},
|
|
1487
|
+
// Qwen's thought chunks are noisy internal scaffolding — suppress them.
|
|
1488
|
+
quirks: defaultQuirks({ surfaceThoughts: false, titleFrom: "topic-update" })
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
// src/strategies/index.ts
|
|
1492
|
+
var BUILT_IN_ACP_STRATEGIES = [
|
|
1493
|
+
geminiStrategy,
|
|
1494
|
+
kimiStrategy,
|
|
1495
|
+
grokStrategy,
|
|
1496
|
+
qwenStrategy
|
|
1497
|
+
];
|
|
1498
|
+
function buildStrategyTable(strategies = BUILT_IN_ACP_STRATEGIES) {
|
|
1499
|
+
const table = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (const strategy of strategies) {
|
|
1501
|
+
table.set(strategy.id, strategy);
|
|
1502
|
+
}
|
|
1503
|
+
return table;
|
|
1504
|
+
}
|
|
1505
|
+
function strategyById(id, table = buildStrategyTable()) {
|
|
1506
|
+
return table.get(id);
|
|
1507
|
+
}
|
|
1508
|
+
function strategyByBackendId(backendId, strategies = BUILT_IN_ACP_STRATEGIES) {
|
|
1509
|
+
return strategies.find((strategy) => strategy.backendId === backendId);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// src/discovery/acp-local-discovery.ts
|
|
1513
|
+
import { execFile as execFileCallback } from "child_process";
|
|
1514
|
+
import { promisify } from "util";
|
|
1515
|
+
var execFile = promisify(execFileCallback);
|
|
1516
|
+
async function discoverLocalAcpAgents(options = {}) {
|
|
1517
|
+
const strategies = options.strategies ?? BUILT_IN_ACP_STRATEGIES;
|
|
1518
|
+
const probe = options.probe ?? defaultProbe;
|
|
1519
|
+
const now = options.now ?? Date.now;
|
|
1520
|
+
const discovered = await Promise.all(
|
|
1521
|
+
strategies.map(
|
|
1522
|
+
(strategy) => discoverStrategy(strategy, probe, now, options.overrides?.[strategy.id])
|
|
1523
|
+
)
|
|
1524
|
+
);
|
|
1525
|
+
return discovered.filter((agent) => agent !== void 0);
|
|
1526
|
+
}
|
|
1527
|
+
async function discoverStrategy(strategy, probe, now, override) {
|
|
1528
|
+
const candidates = candidateCommands(strategy, override);
|
|
1529
|
+
for (const command of candidates) {
|
|
1530
|
+
const [versionResult, helpResult] = await Promise.all([
|
|
1531
|
+
runProbe(probe, command, strategy.discoveryProbe.versionArgs),
|
|
1532
|
+
runProbe(probe, command, strategy.discoveryProbe.helpArgs)
|
|
1533
|
+
]);
|
|
1534
|
+
if (!versionResult || !helpResult) {
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
if (!strategy.discoveryProbe.helpMatches.test(resultText(helpResult))) {
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
const version = parseCliVersion(resultText(versionResult));
|
|
1541
|
+
const agent = {
|
|
1542
|
+
strategyId: strategy.id,
|
|
1543
|
+
backendId: strategy.backendId,
|
|
1544
|
+
name: strategy.displayName,
|
|
1545
|
+
command,
|
|
1546
|
+
args: ensureArgs(strategy.spawn.args, strategy.spawn.ensureArgs),
|
|
1547
|
+
env: strategy.spawn.env ?? {},
|
|
1548
|
+
discoveredAt: now()
|
|
1549
|
+
};
|
|
1550
|
+
if (version !== void 0) agent.version = version;
|
|
1551
|
+
return agent;
|
|
1552
|
+
}
|
|
1553
|
+
return void 0;
|
|
1554
|
+
}
|
|
1555
|
+
function candidateCommands(strategy, override) {
|
|
1556
|
+
const candidates = [];
|
|
1557
|
+
if (override && override.trim()) {
|
|
1558
|
+
candidates.push(override.trim());
|
|
1559
|
+
}
|
|
1560
|
+
candidates.push(strategy.discoveryProbe.command);
|
|
1561
|
+
for (const fallback of strategy.discoveryProbe.fallbackCommands ?? []) {
|
|
1562
|
+
candidates.push(fallback);
|
|
1563
|
+
}
|
|
1564
|
+
return [...new Set(candidates)];
|
|
1565
|
+
}
|
|
1566
|
+
function ensureArgs(args, ensure) {
|
|
1567
|
+
if (!ensure || ensure.length === 0) {
|
|
1568
|
+
return args;
|
|
1569
|
+
}
|
|
1570
|
+
const result = [...args];
|
|
1571
|
+
for (const arg of ensure) {
|
|
1572
|
+
if (!result.includes(arg)) result.push(arg);
|
|
1573
|
+
}
|
|
1574
|
+
return result;
|
|
1575
|
+
}
|
|
1576
|
+
async function defaultProbe(command, args) {
|
|
1577
|
+
return await execFile(command, args, { timeout: 5e3, maxBuffer: 1024 * 1024 });
|
|
1578
|
+
}
|
|
1579
|
+
async function runProbe(probe, command, args) {
|
|
1580
|
+
try {
|
|
1581
|
+
return await probe(command, args);
|
|
1582
|
+
} catch {
|
|
1583
|
+
return void 0;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
function resultText(result) {
|
|
1587
|
+
return [result.stdout, result.stderr].flatMap((value) => value === void 0 ? [] : [value]).map((value) => Buffer.isBuffer(value) ? value.toString("utf8") : value).join("\n");
|
|
1588
|
+
}
|
|
1589
|
+
function parseCliVersion(output) {
|
|
1590
|
+
const trimmed = output.trim();
|
|
1591
|
+
if (!trimmed) {
|
|
1592
|
+
return void 0;
|
|
1593
|
+
}
|
|
1594
|
+
return trimmed.match(/\d+\.\d+\.\d+(?:[-+][\w.-]+)?/)?.[0] ?? trimmed;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// src/discovery/acp-agent-allowlist.ts
|
|
1598
|
+
var BANNED_ACP_REGISTRY_IDS = /* @__PURE__ */ new Set(["codex-acp"]);
|
|
1599
|
+
var DEFAULT_ACP_AGENT_ALLOWLIST = [
|
|
1600
|
+
{ id: "local-grok-cli", registryId: "grok", distributionKinds: ["local"] },
|
|
1601
|
+
{ id: "local-qwen-code-cli", registryId: "qwen", distributionKinds: ["local"] }
|
|
1602
|
+
];
|
|
1603
|
+
var AcpAgentAllowlist = class {
|
|
1604
|
+
constructor(rules) {
|
|
1605
|
+
this.rules = rules;
|
|
1606
|
+
}
|
|
1607
|
+
rules;
|
|
1608
|
+
evaluate(agent) {
|
|
1609
|
+
if (isBannedAcpRegistryId(agent.id)) {
|
|
1610
|
+
return { allowed: false, reason: "banned" };
|
|
1611
|
+
}
|
|
1612
|
+
const matchingRules = this.rules.filter((rule) => rule.registryId === agent.id);
|
|
1613
|
+
if (matchingRules.length === 0) {
|
|
1614
|
+
return { allowed: false, reason: "not-allowlisted" };
|
|
1615
|
+
}
|
|
1616
|
+
for (const rule of matchingRules) {
|
|
1617
|
+
const denial = evaluateRule(rule, agent);
|
|
1618
|
+
if (!denial) {
|
|
1619
|
+
return {
|
|
1620
|
+
allowed: true,
|
|
1621
|
+
ruleId: rule.id,
|
|
1622
|
+
unverifiedBinaryAllowed: rule.allowUnverifiedBinary === true
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
return { allowed: false, reason: "allowlist-rule-mismatch" };
|
|
1627
|
+
}
|
|
1628
|
+
evaluateDistribution(agent, distribution) {
|
|
1629
|
+
if (isBannedAcpRegistryId(agent.id)) {
|
|
1630
|
+
return { allowed: false, reason: "banned" };
|
|
1631
|
+
}
|
|
1632
|
+
const matchingRules = this.rules.filter((rule) => rule.registryId === agent.id);
|
|
1633
|
+
if (matchingRules.length === 0) {
|
|
1634
|
+
return { allowed: false, reason: "not-allowlisted" };
|
|
1635
|
+
}
|
|
1636
|
+
for (const rule of matchingRules) {
|
|
1637
|
+
const denial = evaluateDistributionRule(rule, agent, distribution);
|
|
1638
|
+
if (!denial) {
|
|
1639
|
+
return {
|
|
1640
|
+
allowed: true,
|
|
1641
|
+
ruleId: rule.id,
|
|
1642
|
+
unverifiedBinaryAllowed: rule.allowUnverifiedBinary === true
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
return { allowed: false, reason: "allowlist-rule-mismatch" };
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
var defaultAcpAgentAllowlist = new AcpAgentAllowlist(
|
|
1650
|
+
DEFAULT_ACP_AGENT_ALLOWLIST
|
|
1651
|
+
);
|
|
1652
|
+
function isBannedAcpRegistryId(registryId) {
|
|
1653
|
+
return BANNED_ACP_REGISTRY_IDS.has(registryId);
|
|
1654
|
+
}
|
|
1655
|
+
function evaluateRule(rule, agent) {
|
|
1656
|
+
if (rule.versions && (!agent.version || !rule.versions.includes(agent.version))) {
|
|
1657
|
+
return "version-not-allowed";
|
|
1658
|
+
}
|
|
1659
|
+
if (isGplFamilyLicense(agent.license) && !rule.allowGplFamilyLicense) {
|
|
1660
|
+
return "license-not-allowed";
|
|
1661
|
+
}
|
|
1662
|
+
let distributionDeniedBySource = false;
|
|
1663
|
+
for (const distribution of agent.distributions) {
|
|
1664
|
+
const denial = evaluateDistributionRule(rule, agent, distribution, {
|
|
1665
|
+
skipAgentChecks: true
|
|
1666
|
+
});
|
|
1667
|
+
if (!denial) {
|
|
1668
|
+
return void 0;
|
|
1669
|
+
}
|
|
1670
|
+
if (denial === "distribution-source-not-allowed") {
|
|
1671
|
+
distributionDeniedBySource = true;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
return distributionDeniedBySource ? "distribution-source-not-allowed" : "distribution-not-allowed";
|
|
1675
|
+
}
|
|
1676
|
+
function evaluateDistributionRule(rule, agent, distribution, options = {}) {
|
|
1677
|
+
if (!options.skipAgentChecks && rule.versions && (!agent.version || !rule.versions.includes(agent.version))) {
|
|
1678
|
+
return "version-not-allowed";
|
|
1679
|
+
}
|
|
1680
|
+
if (!options.skipAgentChecks && isGplFamilyLicense(agent.license) && !rule.allowGplFamilyLicense) {
|
|
1681
|
+
return "license-not-allowed";
|
|
1682
|
+
}
|
|
1683
|
+
if (!distributionAllowedByKind(rule, distribution)) {
|
|
1684
|
+
return "distribution-not-allowed";
|
|
1685
|
+
}
|
|
1686
|
+
if (!distributionSourceAllowed(rule, distribution)) {
|
|
1687
|
+
return "distribution-source-not-allowed";
|
|
1688
|
+
}
|
|
1689
|
+
return void 0;
|
|
1690
|
+
}
|
|
1691
|
+
function distributionAllowedByKind(rule, distribution) {
|
|
1692
|
+
return !rule.distributionKinds || rule.distributionKinds.includes(distribution.kind);
|
|
1693
|
+
}
|
|
1694
|
+
function distributionSourceAllowed(rule, distribution) {
|
|
1695
|
+
if (distribution.kind === "npx" || distribution.kind === "uvx") {
|
|
1696
|
+
return !rule.allowedPackageNames || rule.allowedPackageNames.includes(distribution.packageName);
|
|
1697
|
+
}
|
|
1698
|
+
if (distribution.kind !== "binary") {
|
|
1699
|
+
return false;
|
|
1700
|
+
}
|
|
1701
|
+
if (!rule.allowedArchiveHosts) {
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1704
|
+
try {
|
|
1705
|
+
return rule.allowedArchiveHosts.includes(new URL(distribution.archiveUrl).host);
|
|
1706
|
+
} catch {
|
|
1707
|
+
return false;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
function isGplFamilyLicense(license) {
|
|
1711
|
+
return /\b(?:GPL|AGPL|LGPL)\b/i.test(license ?? "");
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/discovery/acp-registry-types.ts
|
|
1715
|
+
var ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
|
|
1716
|
+
|
|
1717
|
+
// src/discovery/acp-registry-service.ts
|
|
1718
|
+
var AcpRegistryService = class {
|
|
1719
|
+
allowlist;
|
|
1720
|
+
fetcher;
|
|
1721
|
+
now;
|
|
1722
|
+
registryUrl;
|
|
1723
|
+
constructor(options = {}) {
|
|
1724
|
+
this.allowlist = options.allowlist ?? defaultAcpAgentAllowlist;
|
|
1725
|
+
this.fetcher = options.fetch ?? (async (input, init) => {
|
|
1726
|
+
const response = await globalThis.fetch(input, init);
|
|
1727
|
+
return {
|
|
1728
|
+
ok: response.ok,
|
|
1729
|
+
status: response.status,
|
|
1730
|
+
statusText: response.statusText,
|
|
1731
|
+
json: () => response.json()
|
|
1732
|
+
};
|
|
1733
|
+
});
|
|
1734
|
+
this.now = options.now ?? Date.now;
|
|
1735
|
+
this.registryUrl = options.registryUrl ?? ACP_REGISTRY_URL;
|
|
1736
|
+
}
|
|
1737
|
+
async fetchRegistry() {
|
|
1738
|
+
const response = await this.fetcher(this.registryUrl, {
|
|
1739
|
+
headers: { accept: "application/json" }
|
|
1740
|
+
});
|
|
1741
|
+
if (!response.ok) {
|
|
1742
|
+
throw new Error(
|
|
1743
|
+
`ACP registry request failed: ${response.status} ${response.statusText}`
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
const raw = await response.json();
|
|
1747
|
+
return { fetchedAt: this.now(), agents: normalizeRegistry(raw), raw };
|
|
1748
|
+
}
|
|
1749
|
+
applyAllowlist(snapshot) {
|
|
1750
|
+
return snapshot.agents.map((agent) => {
|
|
1751
|
+
const allowlist = this.allowlist.evaluate(agent);
|
|
1752
|
+
const distributionPolicies = agent.distributions.map(
|
|
1753
|
+
(distribution) => this.evaluateDistribution(agent, distribution)
|
|
1754
|
+
);
|
|
1755
|
+
const installablePolicy = distributionPolicies.find((policy) => policy.installable);
|
|
1756
|
+
const firstBlockedAllowedPolicy = distributionPolicies.find(
|
|
1757
|
+
(policy) => policy.allowlist.allowed
|
|
1758
|
+
);
|
|
1759
|
+
const installable = Boolean(installablePolicy);
|
|
1760
|
+
const result = {
|
|
1761
|
+
...agent,
|
|
1762
|
+
allowlist,
|
|
1763
|
+
installable,
|
|
1764
|
+
verificationStatus: installablePolicy?.verificationStatus ?? firstBlockedAllowedPolicy?.verificationStatus ?? "not-applicable"
|
|
1765
|
+
};
|
|
1766
|
+
const unavailableReason = installable ? void 0 : firstBlockedAllowedPolicy?.unavailableReason ?? (allowlist.allowed ? "distribution-not-installable" : allowlist.reason);
|
|
1767
|
+
if (unavailableReason !== void 0) result.unavailableReason = unavailableReason;
|
|
1768
|
+
return result;
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
evaluateDistribution(agent, distribution) {
|
|
1772
|
+
const allowlist = this.allowlist.evaluateDistribution(agent, distribution);
|
|
1773
|
+
return evaluateAcpDistributionPolicy(distribution, allowlist);
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
function evaluateAcpDistributionPolicy(distribution, allowlist) {
|
|
1777
|
+
if (!allowlist.allowed) {
|
|
1778
|
+
return {
|
|
1779
|
+
allowlist,
|
|
1780
|
+
installable: false,
|
|
1781
|
+
verificationStatus: "not-applicable",
|
|
1782
|
+
unavailableReason: allowlist.reason
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
if (distribution.kind !== "binary") {
|
|
1786
|
+
return { allowlist, installable: true, verificationStatus: "not-applicable" };
|
|
1787
|
+
}
|
|
1788
|
+
if (distribution.checksum) {
|
|
1789
|
+
return { allowlist, installable: true, verificationStatus: "verified" };
|
|
1790
|
+
}
|
|
1791
|
+
return {
|
|
1792
|
+
allowlist,
|
|
1793
|
+
installable: allowlist.unverifiedBinaryAllowed,
|
|
1794
|
+
verificationStatus: allowlist.unverifiedBinaryAllowed ? "unverified-allowed" : "unverified-blocked",
|
|
1795
|
+
...allowlist.unverifiedBinaryAllowed ? {} : { unavailableReason: "binary-integrity-metadata-missing" }
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
function normalizeRegistry(raw) {
|
|
1799
|
+
const record = asRecord4(raw);
|
|
1800
|
+
const rawAgents = Array.isArray(record?.agents) ? record.agents : [];
|
|
1801
|
+
return rawAgents.flatMap((item) => {
|
|
1802
|
+
const agent = normalizeAgent(item);
|
|
1803
|
+
return agent ? [agent] : [];
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
function normalizeAgent(raw) {
|
|
1807
|
+
const record = asRecord4(raw);
|
|
1808
|
+
if (!record || typeof record.id !== "string" || typeof record.name !== "string") {
|
|
1809
|
+
return void 0;
|
|
1810
|
+
}
|
|
1811
|
+
const distributions = normalizeDistributions(record.distribution);
|
|
1812
|
+
if (distributions.length === 0) {
|
|
1813
|
+
return void 0;
|
|
1814
|
+
}
|
|
1815
|
+
const agent = {
|
|
1816
|
+
id: record.id,
|
|
1817
|
+
backendId: buildAcpBackendId(record.id),
|
|
1818
|
+
name: record.name,
|
|
1819
|
+
authors: Array.isArray(record.authors) ? record.authors.filter((author) => typeof author === "string") : [],
|
|
1820
|
+
distributions,
|
|
1821
|
+
distributionKinds: [...new Set(distributions.map((d) => d.kind))],
|
|
1822
|
+
auth: normalizeAuth(record.auth),
|
|
1823
|
+
raw
|
|
1824
|
+
};
|
|
1825
|
+
const version = stringValue(record.version);
|
|
1826
|
+
if (version !== void 0) agent.version = version;
|
|
1827
|
+
const description = stringValue(record.description);
|
|
1828
|
+
if (description !== void 0) agent.description = description;
|
|
1829
|
+
const license = stringValue(record.license);
|
|
1830
|
+
if (license !== void 0) agent.license = license;
|
|
1831
|
+
const repositoryUrl = stringValue(record.repository);
|
|
1832
|
+
if (repositoryUrl !== void 0) agent.repositoryUrl = repositoryUrl;
|
|
1833
|
+
const websiteUrl = stringValue(record.website);
|
|
1834
|
+
if (websiteUrl !== void 0) agent.websiteUrl = websiteUrl;
|
|
1835
|
+
const iconUrl = stringValue(record.icon);
|
|
1836
|
+
if (iconUrl !== void 0) agent.iconUrl = iconUrl;
|
|
1837
|
+
return agent;
|
|
1838
|
+
}
|
|
1839
|
+
function normalizeDistributions(raw) {
|
|
1840
|
+
const record = asRecord4(raw);
|
|
1841
|
+
if (!record) {
|
|
1842
|
+
return [];
|
|
1843
|
+
}
|
|
1844
|
+
const distributions = [];
|
|
1845
|
+
const npx = normalizePackageDistribution("npx", record.npx);
|
|
1846
|
+
if (npx) distributions.push(npx);
|
|
1847
|
+
const uvx = normalizePackageDistribution("uvx", record.uvx);
|
|
1848
|
+
if (uvx) distributions.push(uvx);
|
|
1849
|
+
const binaryRecord = asRecord4(record.binary);
|
|
1850
|
+
if (binaryRecord) {
|
|
1851
|
+
for (const [platform, value] of Object.entries(binaryRecord)) {
|
|
1852
|
+
const binary = normalizeBinaryDistribution(platform, value);
|
|
1853
|
+
if (binary) distributions.push(binary);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return distributions;
|
|
1857
|
+
}
|
|
1858
|
+
function normalizePackageDistribution(kind, raw) {
|
|
1859
|
+
const record = asRecord4(raw);
|
|
1860
|
+
if (!record || typeof record.package !== "string") {
|
|
1861
|
+
return void 0;
|
|
1862
|
+
}
|
|
1863
|
+
return {
|
|
1864
|
+
kind,
|
|
1865
|
+
packageName: record.package,
|
|
1866
|
+
args: stringArray(record.args),
|
|
1867
|
+
env: stringRecord(record.env)
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
function normalizeBinaryDistribution(platform, raw) {
|
|
1871
|
+
const record = asRecord4(raw);
|
|
1872
|
+
if (!record || typeof record.archive !== "string" || typeof record.cmd !== "string") {
|
|
1873
|
+
return void 0;
|
|
1874
|
+
}
|
|
1875
|
+
const distribution = {
|
|
1876
|
+
kind: "binary",
|
|
1877
|
+
platform,
|
|
1878
|
+
archiveUrl: record.archive,
|
|
1879
|
+
command: record.cmd,
|
|
1880
|
+
args: stringArray(record.args),
|
|
1881
|
+
env: stringRecord(record.env)
|
|
1882
|
+
};
|
|
1883
|
+
const checksum = stringValue(record.checksum) ?? stringValue(record.sha256);
|
|
1884
|
+
if (checksum !== void 0) distribution.checksum = checksum;
|
|
1885
|
+
const signatureUrl = stringValue(record.signature) ?? stringValue(record.signatureUrl);
|
|
1886
|
+
if (signatureUrl !== void 0) distribution.signatureUrl = signatureUrl;
|
|
1887
|
+
return distribution;
|
|
1888
|
+
}
|
|
1889
|
+
function normalizeAuth(raw) {
|
|
1890
|
+
if (!raw) {
|
|
1891
|
+
return { required: false, methods: [] };
|
|
1892
|
+
}
|
|
1893
|
+
const record = asRecord4(raw);
|
|
1894
|
+
if (!record) {
|
|
1895
|
+
return { required: true, methods: ["unknown"], raw };
|
|
1896
|
+
}
|
|
1897
|
+
const methods = stringArray(record.methods).map(
|
|
1898
|
+
(method) => method === "agent-managed" || method === "terminal" ? method : "unknown"
|
|
1899
|
+
);
|
|
1900
|
+
return {
|
|
1901
|
+
required: record.required === false ? false : true,
|
|
1902
|
+
methods: methods.length > 0 ? [...new Set(methods)] : ["unknown"],
|
|
1903
|
+
raw
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
function asRecord4(value) {
|
|
1907
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1908
|
+
}
|
|
1909
|
+
function stringValue(value) {
|
|
1910
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
1911
|
+
}
|
|
1912
|
+
function stringArray(value) {
|
|
1913
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1914
|
+
}
|
|
1915
|
+
function stringRecord(value) {
|
|
1916
|
+
const record = asRecord4(value);
|
|
1917
|
+
if (!record) {
|
|
1918
|
+
return {};
|
|
1919
|
+
}
|
|
1920
|
+
return Object.fromEntries(
|
|
1921
|
+
Object.entries(record).filter(
|
|
1922
|
+
(entry) => typeof entry[1] === "string"
|
|
1923
|
+
)
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
export {
|
|
1927
|
+
ACP_REGISTRY_URL,
|
|
1928
|
+
AcpAgentAllowlist,
|
|
1929
|
+
AcpAgentClient,
|
|
1930
|
+
AcpRegistryService,
|
|
1931
|
+
AcpSessionNormalizer,
|
|
1932
|
+
AcpStdioJsonRpcTransport,
|
|
1933
|
+
BANNED_ACP_REGISTRY_IDS,
|
|
1934
|
+
BUILT_IN_ACP_STRATEGIES,
|
|
1935
|
+
DEFAULT_ACP_AGENT_ALLOWLIST,
|
|
1936
|
+
acpRuntimeSupportsSessionLoad,
|
|
1937
|
+
acpSessionRuntimeStateFromCapabilities,
|
|
1938
|
+
acpSessionRuntimeStateFromUpdate,
|
|
1939
|
+
asRecord2 as asRecord,
|
|
1940
|
+
buildAcpBackendId,
|
|
1941
|
+
buildStrategyTable,
|
|
1942
|
+
defaultAcpAgentAllowlist,
|
|
1943
|
+
defaultQuirks,
|
|
1944
|
+
discoverLocalAcpAgents,
|
|
1945
|
+
evaluateAcpDistributionPolicy,
|
|
1946
|
+
geminiStrategy,
|
|
1947
|
+
grokStrategy,
|
|
1948
|
+
isBannedAcpRegistryId,
|
|
1949
|
+
kimiStrategy,
|
|
1950
|
+
mergeAcpRuntimeState,
|
|
1951
|
+
modeLabelFor,
|
|
1952
|
+
normalizeAcpRuntimeCapabilities,
|
|
1953
|
+
normalizeRegistry,
|
|
1954
|
+
qwenStrategy,
|
|
1955
|
+
readAcpContentText,
|
|
1956
|
+
readBoolean,
|
|
1957
|
+
readContentText,
|
|
1958
|
+
readFirstLocationPath,
|
|
1959
|
+
readFirstString,
|
|
1960
|
+
readKind,
|
|
1961
|
+
readNonEmptyString,
|
|
1962
|
+
readNumber,
|
|
1963
|
+
readPromptText,
|
|
1964
|
+
readString,
|
|
1965
|
+
readToolCallId,
|
|
1966
|
+
readToolContentText,
|
|
1967
|
+
readToolOutput,
|
|
1968
|
+
readUpdateText,
|
|
1969
|
+
strategyByBackendId,
|
|
1970
|
+
strategyById,
|
|
1971
|
+
toolCallFromUpdate
|
|
1972
|
+
};
|
|
1973
|
+
//# sourceMappingURL=index.js.map
|