@peanut996/acp-router 0.8.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.
@@ -0,0 +1,718 @@
1
+ import { spawn } from "node:child_process";
2
+ import { SERVER_NAME, SERVER_VERSION, ACP_MODE_MAP, ACP_STARTUP_DELAY_MS, AGENT_ERROR_PATTERNS, AGENT_ERROR_KEY_PATTERN } from "./constants.js";
3
+ import { safeEnv, sleep, preview, isPlainObject, uniqueStrings, buildAcpProcessClosedEvent } from "./utils.js";
4
+ import { appendJsonl } from "./storage.js";
5
+ import { resolveAcpLaunchTarget, collectWorktreeState } from "./agents.js";
6
+ class AcpStdioClient {
7
+ command;
8
+ args;
9
+ cwd;
10
+ timeoutMs;
11
+ env;
12
+ permissionProfile;
13
+ onEvent;
14
+ onProcessStart;
15
+ nextId;
16
+ pending;
17
+ stdoutBuffer;
18
+ logEvents;
19
+ child;
20
+ startError;
21
+ constructor({ command, args, cwd, timeoutMs, env, permissionProfile, onEvent, onProcessStart }) {
22
+ this.command = command;
23
+ this.args = args;
24
+ this.cwd = cwd;
25
+ this.timeoutMs = timeoutMs;
26
+ this.env = env ?? safeEnv();
27
+ this.permissionProfile = permissionProfile ?? "bypassPermissions";
28
+ this.onEvent = onEvent ?? (() => { });
29
+ this.onProcessStart = onProcessStart;
30
+ this.nextId = 1;
31
+ this.pending = new Map();
32
+ this.stdoutBuffer = "";
33
+ this.logEvents = [];
34
+ this.child = null;
35
+ this.startError = null;
36
+ }
37
+ async start() {
38
+ const currentDepth = Number.parseInt(process.env.ACP_ROUTER_DEPTH ?? "0", 10) || 0;
39
+ const childEnv = {
40
+ ...this.env,
41
+ ACP_ROUTER_DEPTH: String(currentDepth + 1)
42
+ };
43
+ this.child = spawn(this.command, this.args, {
44
+ cwd: this.cwd,
45
+ stdio: ["pipe", "pipe", "pipe"],
46
+ env: childEnv
47
+ });
48
+ this.child.stdout.setEncoding("utf8");
49
+ this.child.stderr.setEncoding("utf8");
50
+ if (typeof this.onProcessStart === "function") {
51
+ await Promise.resolve(this.onProcessStart(this.child)).catch((error) => {
52
+ this.logEvents.push({
53
+ type: "process_record_error",
54
+ timestamp: new Date().toISOString(),
55
+ message: `Failed to record ACP process pid: ${error.message}`
56
+ });
57
+ });
58
+ }
59
+ this.child.stdout.on("data", (chunk) => this.handleStdout(chunk));
60
+ this.child.stderr.on("data", (chunk) => this.handleStderr(chunk));
61
+ this.child.on("error", (error) => {
62
+ this.startError = error;
63
+ this.rejectPending(error);
64
+ });
65
+ this.child.on("exit", (code, signal) => this.rejectPending(new Error(`ACP process exited with code=${code} signal=${signal}`)));
66
+ await sleep(ACP_STARTUP_DELAY_MS);
67
+ if (this.startError)
68
+ throw this.startError;
69
+ }
70
+ request(method, params) {
71
+ const id = this.nextId++;
72
+ const payload = { jsonrpc: "2.0", id, method, params };
73
+ return new Promise((resolve, reject) => {
74
+ const timer = setTimeout(() => {
75
+ this.pending.delete(id);
76
+ const stderrTail = this.logEvents
77
+ .filter((e) => e.type === "acp_stderr")
78
+ .slice(-5)
79
+ .map((e) => e.message)
80
+ .join("\n");
81
+ const error = new Error(stderrTail
82
+ ? `ACP request timed out: ${method}. Recent stderr:\n${stderrTail}`
83
+ : `ACP request timed out: ${method} (no stderr output).`);
84
+ error.code = "timeout";
85
+ reject(error);
86
+ }, this.timeoutMs);
87
+ this.pending.set(id, { method, resolve, reject, timer });
88
+ this.write(payload);
89
+ });
90
+ }
91
+ respond(id, result) {
92
+ this.write({ jsonrpc: "2.0", id, result });
93
+ }
94
+ respondError(id, code, message) {
95
+ this.write({ jsonrpc: "2.0", id, error: { code, message } });
96
+ }
97
+ write(payload) {
98
+ if (!this.child || !this.child.stdin.writable) {
99
+ throw new Error("ACP process is not writable.");
100
+ }
101
+ this.child.stdin.write(`${JSON.stringify(payload)}\n`);
102
+ }
103
+ handleStdout(chunk) {
104
+ this.stdoutBuffer += chunk;
105
+ while (true) {
106
+ const newlineIndex = this.stdoutBuffer.indexOf("\n");
107
+ if (newlineIndex === -1)
108
+ return;
109
+ const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
110
+ this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
111
+ if (!line)
112
+ continue;
113
+ this.handleMessageLine(line);
114
+ }
115
+ }
116
+ handleMessageLine(line) {
117
+ let message;
118
+ try {
119
+ message = JSON.parse(line);
120
+ }
121
+ catch {
122
+ this.logEvents.push({
123
+ type: "acp_stdout_parse_error",
124
+ timestamp: new Date().toISOString(),
125
+ message: preview(line, 300)
126
+ });
127
+ return;
128
+ }
129
+ if (Object.prototype.hasOwnProperty.call(message, "id") && (message.result || message.error)) {
130
+ const pending = this.pending.get(message.id);
131
+ if (!pending)
132
+ return;
133
+ clearTimeout(pending.timer);
134
+ this.pending.delete(message.id);
135
+ if (message.error) {
136
+ pending.reject(new Error(`${pending.method} failed: ${message.error.message ?? JSON.stringify(message.error)}`));
137
+ }
138
+ else {
139
+ pending.resolve(message.result);
140
+ }
141
+ return;
142
+ }
143
+ if (message.method && Object.prototype.hasOwnProperty.call(message, "id")) {
144
+ this.handleClientRequest(message);
145
+ return;
146
+ }
147
+ if (message.method) {
148
+ this.handleNotification(message);
149
+ }
150
+ }
151
+ handleClientRequest(message) {
152
+ if (message.method === "session/request_permission") {
153
+ const outcome = this.resolvePermissionOutcome(message.params);
154
+ this.logEvents.push({
155
+ type: outcome === "approved" ? "acp_permission_approved" : "acp_permission_cancelled",
156
+ timestamp: new Date().toISOString(),
157
+ message: outcome === "approved"
158
+ ? `Agent Router approved an ACP permission request (${this.permissionProfile}).`
159
+ : "Agent Router cancelled an ACP permission request.",
160
+ params: message.params
161
+ });
162
+ this.respond(message.id, { outcome });
163
+ return;
164
+ }
165
+ this.respondError(message.id, -32601, `Unsupported client method: ${message.method}`);
166
+ }
167
+ resolvePermissionOutcome(params) {
168
+ switch (this.permissionProfile) {
169
+ case "bypassPermissions":
170
+ return "approved";
171
+ case "acceptEdits": {
172
+ const perms = params?.permissions ?? [];
173
+ const hasNonFilePermission = perms.some((p) => p.type !== "file_edit" && p.type !== "write");
174
+ if (hasNonFilePermission)
175
+ return "cancelled";
176
+ return "approved";
177
+ }
178
+ case "plan":
179
+ return "cancelled";
180
+ default:
181
+ return "cancelled";
182
+ }
183
+ }
184
+ handleNotification(message) {
185
+ const event = normalizeAcpNotification(message);
186
+ this.onEvent(event);
187
+ }
188
+ handleStderr(chunk) {
189
+ for (const line of chunk.split(/\r?\n/).filter(Boolean)) {
190
+ const event = {
191
+ type: "acp_stderr",
192
+ timestamp: new Date().toISOString(),
193
+ message: preview(line, 500)
194
+ };
195
+ if (typeof this.onEvent === "function") {
196
+ try {
197
+ this.onEvent(event);
198
+ }
199
+ catch { }
200
+ }
201
+ }
202
+ }
203
+ drainLogEvents() {
204
+ const events = this.logEvents;
205
+ this.logEvents = [];
206
+ return events;
207
+ }
208
+ rejectPending(error) {
209
+ for (const pending of this.pending.values()) {
210
+ clearTimeout(pending.timer);
211
+ pending.reject(error);
212
+ }
213
+ this.pending.clear();
214
+ }
215
+ dispose() {
216
+ for (const pending of this.pending.values())
217
+ clearTimeout(pending.timer);
218
+ this.pending.clear();
219
+ if (this.child && !this.child.killed) {
220
+ this.child.kill("SIGTERM");
221
+ const timer = setTimeout(() => {
222
+ if (this.child && !this.child.killed) {
223
+ this.child.kill("SIGKILL");
224
+ }
225
+ }, 1000);
226
+ timer.unref?.();
227
+ }
228
+ }
229
+ }
230
+ function normalizeAcpNotification(message) {
231
+ if (message.method === "session/update") {
232
+ const update = message.params?.update ?? {};
233
+ const event = {
234
+ type: `acp_${update.sessionUpdate ?? "session_update"}`,
235
+ timestamp: new Date().toISOString(),
236
+ message: describeSessionUpdate(update),
237
+ params: message.params
238
+ };
239
+ return event;
240
+ }
241
+ return {
242
+ type: `acp_${message.method.replaceAll("/", "_")}`,
243
+ timestamp: new Date().toISOString(),
244
+ message: message.method,
245
+ params: message.params
246
+ };
247
+ }
248
+ function describeSessionUpdate(update) {
249
+ if (update.sessionUpdate === "agent_message_chunk") {
250
+ return preview(update.content?.text ?? "", 300);
251
+ }
252
+ if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") {
253
+ return preview(update.title ?? update.status ?? update.toolCallId ?? "tool call update", 300);
254
+ }
255
+ if (update.sessionUpdate === "plan") {
256
+ return "Agent plan update.";
257
+ }
258
+ if (update.sessionUpdate === "available_commands_update") {
259
+ return `Available commands updated: ${(update.availableCommands ?? []).length}`;
260
+ }
261
+ return update.sessionUpdate ?? "session update";
262
+ }
263
+ function summarizeInitializeResult(result) {
264
+ return {
265
+ protocolVersion: result.protocolVersion,
266
+ agentInfo: result.agentInfo ?? null,
267
+ agentCapabilities: {
268
+ loadSession: Boolean(result.agentCapabilities?.loadSession),
269
+ sessionCapabilities: Object.keys(result.agentCapabilities?.sessionCapabilities ?? {})
270
+ },
271
+ authMethods: (result.authMethods ?? []).map((method) => ({ id: method.id, name: method.name }))
272
+ };
273
+ }
274
+ function summarizeAcpConfigOptions(configOptions) {
275
+ if (!Array.isArray(configOptions))
276
+ return [];
277
+ return configOptions
278
+ .map((option) => {
279
+ const id = option.id ?? option.configId ?? null;
280
+ const title = option.title ?? option.name ?? option.label ?? id;
281
+ const category = option.category ?? null;
282
+ const description = option.description ? preview(option.description, 300) : null;
283
+ const choices = summarizeConfigChoices(option.options ?? option.values ?? option.choices);
284
+ return {
285
+ id,
286
+ title,
287
+ category,
288
+ type: option.type ?? option.input?.type ?? (choices.length > 0 ? "select" : null),
289
+ description,
290
+ currentValue: summarizeConfigValue(option.value ?? option.currentValue ?? option.defaultValue),
291
+ options: choices
292
+ };
293
+ })
294
+ .filter((option) => option.id || option.category || option.options.length > 0);
295
+ }
296
+ function summarizeConfigChoices(choices) {
297
+ if (!Array.isArray(choices))
298
+ return [];
299
+ return choices.map((choice) => {
300
+ if (typeof choice === "string")
301
+ return { value: choice, label: choice, description: null };
302
+ if (!isPlainObject(choice))
303
+ return null;
304
+ const value = choice.value ?? choice.id ?? choice.name ?? choice.label ?? choice.title;
305
+ const label = choice.label ?? choice.title ?? choice.name ?? choice.value ?? choice.id;
306
+ if (typeof value !== "string" || !value)
307
+ return null;
308
+ return {
309
+ value,
310
+ label: typeof label === "string" && label ? label : value,
311
+ description: choice.description ? preview(choice.description, 300) : null
312
+ };
313
+ }).filter((choice) => choice !== null);
314
+ }
315
+ function summarizeConfigValue(value) {
316
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
317
+ return value;
318
+ return null;
319
+ }
320
+ function extractModelOptions(configOptions) {
321
+ return configOptions
322
+ .filter((option) => (option.category === "model"
323
+ || /model/i.test(option.id ?? "")
324
+ || /model/i.test(option.title ?? "")))
325
+ .flatMap((option) => option.options.map((choice) => ({
326
+ configId: option.id,
327
+ value: choice.value,
328
+ label: choice.label,
329
+ description: choice.description
330
+ })));
331
+ }
332
+ function buildDispatchPrompt(prompt) {
333
+ return [
334
+ prompt,
335
+ "",
336
+ "When you finish, report:",
337
+ "- changed files",
338
+ "- validation commands and results",
339
+ "- risks or incomplete work"
340
+ ].join("\n");
341
+ }
342
+ function extractAgentText(events) {
343
+ const chunks = events
344
+ .filter((event) => event.type === "acp_agent_message_chunk")
345
+ .map((event) => event.params?.update?.content?.text)
346
+ .filter(Boolean);
347
+ return chunks.join("").trim();
348
+ }
349
+ function extractAgentErrors(events) {
350
+ const candidates = [];
351
+ for (const event of events) {
352
+ candidates.push(event.message);
353
+ candidates.push(event.errorMessage);
354
+ candidates.push(event.params?.update?.content?.text);
355
+ candidates.push(event.params?.update?.error?.message);
356
+ candidates.push(event.params?.update?.message);
357
+ candidates.push(event.params?.error?.message);
358
+ candidates.push(event.result?.error?.message);
359
+ candidates.push(event.payload?.error?.message);
360
+ candidates.push(...collectDiagnosticStrings(event.params));
361
+ candidates.push(...collectDiagnosticStrings(event.payload));
362
+ }
363
+ return uniqueStrings(candidates
364
+ .filter((value) => typeof value === "string")
365
+ .map((value) => preview(value.trim().replace(/\s+/g, " "), 500))
366
+ .filter(Boolean)
367
+ .filter((value) => AGENT_ERROR_PATTERNS.some((pattern) => pattern.test(value)))).slice(0, 10);
368
+ }
369
+ function collectDiagnosticStrings(value, depth = 0) {
370
+ if (depth > 5 || value == null)
371
+ return [];
372
+ if (typeof value === "string")
373
+ return [value];
374
+ if (Array.isArray(value))
375
+ return value.flatMap((item) => collectDiagnosticStrings(item, depth + 1));
376
+ if (typeof value !== "object")
377
+ return [];
378
+ const values = [];
379
+ for (const [key, child] of Object.entries(value)) {
380
+ if (AGENT_ERROR_KEY_PATTERN.test(key)
381
+ && (typeof child === "string" || typeof child === "number" || typeof child === "boolean")) {
382
+ values.push(`${key}: ${child}`);
383
+ }
384
+ values.push(...collectDiagnosticStrings(child, depth + 1));
385
+ }
386
+ return values;
387
+ }
388
+ function buildFailureReason(adapterLabel, error, agentErrors) {
389
+ if (agentErrors.length > 0) {
390
+ const suffix = error.code === "timeout" ? " (request timed out after agent error)" : "";
391
+ return `${adapterLabel} failed: ${agentErrors.join("; ")}${suffix}`;
392
+ }
393
+ return `${adapterLabel} failed: ${error.message}`;
394
+ }
395
+ function diffChangedFiles(beforeState, afterState) {
396
+ const before = new Set(Array.isArray(beforeState?.preExistingChangedFiles) ? beforeState.preExistingChangedFiles : []);
397
+ const afterFiles = afterState?.preExistingChangedFiles;
398
+ const after = Array.isArray(afterFiles) ? afterFiles : [];
399
+ const introduced = after.filter((file) => !before.has(file));
400
+ return introduced.length > 0 ? introduced : after;
401
+ }
402
+ async function runAcpStdioJob({ args, job, session, selectedAgent, timeoutSec, agentEnv, controller }) {
403
+ const acpSpec = selectedAgent.acp;
404
+ const launchTarget = resolveAcpLaunchTarget(acpSpec, selectedAgent, args.worktree);
405
+ if (!launchTarget)
406
+ throw new Error(`No ACP adapter is available for ${selectedAgent.id}.`);
407
+ const adapterLabel = acpSpec?.label ?? `${selectedAgent.displayName} ACP`;
408
+ const adapterStatus = acpSpec?.adapterStatus ?? `${selectedAgent.id}_acp`;
409
+ const permissionProfile = job.permissionProfile ?? "bypassPermissions";
410
+ const events = [];
411
+ const startedAt = Date.now();
412
+ let providerSessionId = session.providerSessionId ?? null;
413
+ let agentConfigOptions = [];
414
+ let availableModels = [];
415
+ let writeChain = Promise.resolve();
416
+ const streamEvent = (event) => {
417
+ events.push(event);
418
+ writeChain = writeChain.then(() => appendJsonl(job.logPath, [{
419
+ ...event,
420
+ jobId: job.jobId,
421
+ sessionId: job.sessionId,
422
+ agentId: selectedAgent.id
423
+ }]).catch(() => { }));
424
+ };
425
+ const client = new AcpStdioClient({
426
+ command: launchTarget.command,
427
+ args: launchTarget.args,
428
+ cwd: args.worktree,
429
+ timeoutMs: timeoutSec * 1000,
430
+ env: agentEnv,
431
+ permissionProfile,
432
+ onEvent: streamEvent,
433
+ onProcessStart: (child) => controller?.recordProcess({
434
+ pid: child.pid,
435
+ kind: "acp_stdio",
436
+ command: launchTarget.processLabel,
437
+ startedAt: new Date().toISOString()
438
+ })
439
+ });
440
+ if (controller) {
441
+ controller.cancelProcess = () => {
442
+ client.dispose();
443
+ return true;
444
+ };
445
+ }
446
+ try {
447
+ await client.start();
448
+ const initialize = await client.request("initialize", {
449
+ protocolVersion: 1,
450
+ clientCapabilities: {},
451
+ clientInfo: {
452
+ name: SERVER_NAME,
453
+ title: "Agent Router",
454
+ version: SERVER_VERSION
455
+ }
456
+ });
457
+ streamEvent({
458
+ type: "acp_initialize",
459
+ timestamp: new Date().toISOString(),
460
+ message: `${adapterLabel} initialized.`,
461
+ result: summarizeInitializeResult(initialize)
462
+ });
463
+ const sessionResult = providerSessionId
464
+ ? await client.request("session/resume", {
465
+ sessionId: providerSessionId,
466
+ cwd: args.worktree,
467
+ mcpServers: []
468
+ })
469
+ : await client.request("session/new", {
470
+ cwd: args.worktree,
471
+ mcpServers: []
472
+ });
473
+ providerSessionId = providerSessionId ?? sessionResult.sessionId;
474
+ agentConfigOptions = summarizeAcpConfigOptions(sessionResult.configOptions);
475
+ availableModels = extractModelOptions(agentConfigOptions);
476
+ streamEvent({
477
+ type: session.providerSessionId ? "acp_session_resumed" : "acp_session_created",
478
+ timestamp: new Date().toISOString(),
479
+ message: `${adapterLabel} session ready: ${providerSessionId}`,
480
+ providerSessionId
481
+ });
482
+ if (agentConfigOptions.length > 0) {
483
+ streamEvent({
484
+ type: "acp_config_options",
485
+ timestamp: new Date().toISOString(),
486
+ message: `${adapterLabel} exposed ${agentConfigOptions.length} config option(s), including ${availableModels.length} model option(s).`,
487
+ configOptions: agentConfigOptions,
488
+ availableModels
489
+ });
490
+ }
491
+ const modeOption = agentConfigOptions.find((o) => o.category === "mode" || o.id === "mode");
492
+ const targetMode = ACP_MODE_MAP[selectedAgent.id]?.[permissionProfile];
493
+ if (modeOption && targetMode) {
494
+ const modeValueExists = modeOption.options.some((o) => o.value === targetMode);
495
+ if (modeValueExists) {
496
+ const setConfigResult = await client.request("session/set_config_option", {
497
+ sessionId: providerSessionId,
498
+ configId: modeOption.id ?? "mode",
499
+ value: targetMode
500
+ });
501
+ if (Array.isArray(setConfigResult?.configOptions)) {
502
+ agentConfigOptions = summarizeAcpConfigOptions(setConfigResult.configOptions);
503
+ availableModels = extractModelOptions(agentConfigOptions);
504
+ }
505
+ streamEvent({
506
+ type: "acp_mode_set",
507
+ timestamp: new Date().toISOString(),
508
+ message: `Set ${selectedAgent.id} mode to ${targetMode} (permissionProfile=${permissionProfile}).`,
509
+ permissionProfile,
510
+ mode: targetMode
511
+ });
512
+ }
513
+ else {
514
+ streamEvent({
515
+ type: "acp_mode_set_skipped",
516
+ timestamp: new Date().toISOString(),
517
+ message: `${adapterLabel} mode option does not include value "${targetMode}" for permissionProfile=${permissionProfile}; skipping mode setting.`,
518
+ permissionProfile,
519
+ attemptedMode: targetMode,
520
+ availableModeValues: modeOption.options.map((o) => o.value)
521
+ });
522
+ }
523
+ }
524
+ if (args.model) {
525
+ const modelOption = agentConfigOptions.find((o) => o.category === "model" || /model/i.test(o.id ?? ""));
526
+ if (modelOption) {
527
+ const modelValueExists = modelOption.options.some((o) => o.value === args.model);
528
+ if (modelValueExists) {
529
+ const setModelResult = await client.request("session/set_config_option", {
530
+ sessionId: providerSessionId,
531
+ configId: modelOption.id ?? "model",
532
+ value: args.model
533
+ });
534
+ if (Array.isArray(setModelResult?.configOptions)) {
535
+ agentConfigOptions = summarizeAcpConfigOptions(setModelResult.configOptions);
536
+ availableModels = extractModelOptions(agentConfigOptions);
537
+ }
538
+ streamEvent({
539
+ type: "acp_model_set",
540
+ timestamp: new Date().toISOString(),
541
+ message: `Set ${selectedAgent.id} model to ${args.model}.`,
542
+ model: args.model
543
+ });
544
+ }
545
+ else {
546
+ streamEvent({
547
+ type: "acp_model_set_skipped",
548
+ timestamp: new Date().toISOString(),
549
+ message: `${adapterLabel} model option does not include value "${args.model}"; skipping model setting.`,
550
+ attemptedModel: args.model,
551
+ availableModelValues: modelOption.options.map((o) => o.value)
552
+ });
553
+ }
554
+ }
555
+ else {
556
+ streamEvent({
557
+ type: "acp_model_set_skipped",
558
+ timestamp: new Date().toISOString(),
559
+ message: `${adapterLabel} does not expose a model config option; skipping model setting.`,
560
+ attemptedModel: args.model
561
+ });
562
+ }
563
+ }
564
+ const promptResult = await client.request("session/prompt", {
565
+ sessionId: providerSessionId,
566
+ prompt: [
567
+ {
568
+ type: "text",
569
+ text: buildDispatchPrompt(args.prompt)
570
+ }
571
+ ]
572
+ });
573
+ const completedAt = new Date().toISOString();
574
+ const stopReason = promptResult.stopReason ?? null;
575
+ for (const logEvent of client.drainLogEvents())
576
+ streamEvent(logEvent);
577
+ streamEvent({
578
+ type: "acp_prompt_completed",
579
+ timestamp: completedAt,
580
+ message: `${adapterLabel} prompt completed with stopReason=${stopReason ?? "unknown"}.`,
581
+ stopReason
582
+ });
583
+ streamEvent(buildAcpProcessClosedEvent(startedAt));
584
+ await writeChain;
585
+ const afterState = args.collectDiff === false
586
+ ? { skipped: true, reason: "collectDiff disabled" }
587
+ : await collectWorktreeState(args.worktree);
588
+ const changedFiles = diffChangedFiles(job.worktreeState, afterState);
589
+ const agentText = extractAgentText(events);
590
+ const planViolations = (permissionProfile === "plan" && changedFiles.length > 0)
591
+ ? [`plan_mode_violation: Agent modified ${changedFiles.length} file(s) despite permissionProfile=plan. The ACP adapter did not enforce read-only mode.`]
592
+ : [];
593
+ const planRisks = planViolations.length > 0
594
+ ? planViolations
595
+ : (stopReason && stopReason !== "end_turn" ? [`${adapterLabel} stopped with ${stopReason}.`] : []);
596
+ return {
597
+ events: [...events],
598
+ sessionPatch: {
599
+ providerSessionId,
600
+ agentConfigOptions,
601
+ availableModels,
602
+ status: "idle",
603
+ canContinue: true
604
+ },
605
+ jobPatch: {
606
+ status: "completed",
607
+ endedAt: completedAt,
608
+ adapterStatus,
609
+ providerSessionId,
610
+ stopReason,
611
+ failureReason: null,
612
+ agentErrors: [],
613
+ agentConfigOptions,
614
+ availableModels,
615
+ changedFiles,
616
+ worktreeState: {
617
+ before: job.worktreeState,
618
+ after: afterState
619
+ },
620
+ resultSummary: agentText || `${adapterLabel} completed with stopReason=${stopReason ?? "unknown"}.`,
621
+ validation: [],
622
+ risks: planRisks
623
+ }
624
+ };
625
+ }
626
+ catch (error) {
627
+ const failedAt = new Date().toISOString();
628
+ for (const logEvent of client.drainLogEvents())
629
+ streamEvent(logEvent);
630
+ const collectedEvents = [...events];
631
+ const agentErrors = extractAgentErrors(collectedEvents);
632
+ const cancelled = controller?.cancelRequested === true;
633
+ const failureReason = cancelled
634
+ ? (controller.cancelReason || `${adapterLabel} cancelled by Agent Router caller.`)
635
+ : buildFailureReason(adapterLabel, error, agentErrors);
636
+ streamEvent({
637
+ type: cancelled ? "acp_cancelled" : "acp_error",
638
+ timestamp: failedAt,
639
+ message: failureReason,
640
+ errorMessage: error.message,
641
+ agentErrors
642
+ });
643
+ streamEvent(buildAcpProcessClosedEvent(startedAt));
644
+ await writeChain;
645
+ const afterState = args.collectDiff === false
646
+ ? { skipped: true, reason: "collectDiff disabled" }
647
+ : await collectWorktreeState(args.worktree);
648
+ return {
649
+ events: [...events],
650
+ sessionPatch: {
651
+ providerSessionId,
652
+ agentConfigOptions,
653
+ availableModels,
654
+ status: "idle",
655
+ canContinue: Boolean(providerSessionId)
656
+ },
657
+ jobPatch: {
658
+ status: cancelled ? "cancelled" : error.code === "timeout" ? "timed_out" : "failed",
659
+ endedAt: failedAt,
660
+ adapterStatus,
661
+ providerSessionId,
662
+ failureReason,
663
+ agentErrors,
664
+ agentConfigOptions,
665
+ availableModels,
666
+ changedFiles: diffChangedFiles(job.worktreeState, afterState),
667
+ worktreeState: {
668
+ before: job.worktreeState,
669
+ after: afterState
670
+ },
671
+ resultSummary: failureReason,
672
+ validation: [],
673
+ risks: cancelled ? [] : ["Inspect the job log before re-running the agent."]
674
+ }
675
+ };
676
+ }
677
+ finally {
678
+ client.dispose();
679
+ }
680
+ }
681
+ async function probeAgentModels({ selectedAgent, worktree, env, timeoutMs }) {
682
+ const cwd = worktree ?? process.cwd();
683
+ const launchTarget = resolveAcpLaunchTarget(selectedAgent.acp, selectedAgent, cwd);
684
+ if (!launchTarget)
685
+ throw new Error(`No ACP adapter is available for ${selectedAgent.id}.`);
686
+ const client = new AcpStdioClient({
687
+ command: launchTarget.command,
688
+ args: launchTarget.args,
689
+ cwd,
690
+ timeoutMs: timeoutMs ?? 10000,
691
+ env,
692
+ onEvent: () => { }
693
+ });
694
+ try {
695
+ await client.start();
696
+ await client.request("initialize", {
697
+ protocolVersion: 1,
698
+ clientCapabilities: {},
699
+ clientInfo: { name: SERVER_NAME, title: "Agent Router", version: SERVER_VERSION }
700
+ });
701
+ const sessionResult = await client.request("session/new", {
702
+ cwd,
703
+ mcpServers: []
704
+ });
705
+ const configOptions = summarizeAcpConfigOptions(sessionResult.configOptions);
706
+ const models = extractModelOptions(configOptions);
707
+ return {
708
+ agentId: selectedAgent.id,
709
+ models,
710
+ configOptions
711
+ };
712
+ }
713
+ finally {
714
+ client.dispose();
715
+ }
716
+ }
717
+ export { AcpStdioClient, normalizeAcpNotification, describeSessionUpdate, summarizeInitializeResult, summarizeAcpConfigOptions, summarizeConfigChoices, summarizeConfigValue, extractModelOptions, buildDispatchPrompt, extractAgentText, extractAgentErrors, collectDiagnosticStrings, buildFailureReason, diffChangedFiles, runAcpStdioJob, probeAgentModels };
718
+ //# sourceMappingURL=acp-client.js.map