@engineeros/connector 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # EngineerOS Connector
2
2
 
3
+ The connector uses an authenticated EngineerOS WebSocket for pairing, workspace identity, run lifecycle, and evidence. Coding work can run through any ACP v1-compatible agent over stdio. Codex CLI remains available as the default built-in adapter.
4
+
5
+ ## Pair an ACP coding agent
6
+
7
+ Run the command from the repository the agent should work in:
8
+
9
+ ```powershell
10
+ npx --yes @engineeros/connector@latest pair PAIRING-CODE --url http://localhost:8000 --workspace . --onboard --agent-name "My ACP agent" --agent-command my-agent --agent-args '["--acp"]'
11
+ ```
12
+
13
+ `--agent-command` must start an ACP v1 agent over NDJSON stdio. `--agent-args` is a JSON array so arguments are passed without invoking a shell. Omit both options to use the installed Codex CLI adapter.
14
+
3
15
  Connect a local Codex CLI workspace to EngineerOS through an outbound WebSocket.
4
16
 
5
17
  This package is only a connector. EngineerOS owns every prompt, Goal instruction, and
@@ -11,13 +11,18 @@ import {
11
11
  } from "../src/config.mjs";
12
12
  import {
13
13
  assessmentProgressMessage,
14
+ applyAcceptedChange,
14
15
  executeAssignment,
15
16
  executeConnectedPrompt,
16
17
  executeWorkspaceAssessment,
17
- inspectCodexCli,
18
+ inspectCodingAgent,
18
19
  stopProcess,
19
20
  workspaceSnapshot,
20
21
  } from "../src/runner.mjs";
22
+ import {
23
+ describeWebSocketError,
24
+ startConnectionWatchdog,
25
+ } from "../src/connection.mjs";
21
26
 
22
27
  const { command, positional, flags } = parseArgs(process.argv.slice(2));
23
28
 
@@ -46,6 +51,10 @@ if (command === "pair") {
46
51
  workspace: path.resolve(flags.workspace || process.cwd()),
47
52
  onboard: flags.onboard === true,
48
53
  onboarding_pending: flags.onboard === true,
54
+ agent_protocol: flags["agent-command"] ? "acp" : "codex",
55
+ agent_command: flags["agent-command"] || null,
56
+ agent_args: parseAgentArgs(flags["agent-args"]),
57
+ agent_name: flags["agent-name"] || null,
49
58
  name:
50
59
  flags.name ||
51
60
  `${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
@@ -55,7 +64,9 @@ if (command === "pair") {
55
64
  pairing_code: pairingCode,
56
65
  name: config.name,
57
66
  capabilities: {
58
- codex_cli: true,
67
+ agent_protocols: flags["agent-command"] ? ["acp"] : ["codex"],
68
+ coding_agent: true,
69
+ codex_cli: !flags["agent-command"],
59
70
  platform: process.platform,
60
71
  workspace_name: path.basename(config.workspace),
61
72
  },
@@ -75,15 +86,16 @@ if (command === "pair") {
75
86
  fail("Use `engineeros-connector pair`, `start`, or `status`.");
76
87
  }
77
88
 
78
- let codexCli;
89
+ let codingAgent;
79
90
  try {
80
- codexCli = await inspectCodexCli(config.workspace);
91
+ codingAgent = await inspectCodingAgent(config, config.workspace);
81
92
  } catch (error) {
82
93
  fail(error instanceof Error ? error.message : String(error));
83
94
  }
84
- console.log(`Using ${codexCli.version}.`);
95
+ console.log(`Using ${codingAgent.name} through ${codingAgent.protocol} (${codingAgent.version}).`);
85
96
  if (firstMessage.type === "pair") {
86
- firstMessage.capabilities.codex_cli_version = codexCli.version;
97
+ firstMessage.capabilities.agent_name = codingAgent.name;
98
+ firstMessage.capabilities.agent_version = codingAgent.version;
87
99
  }
88
100
 
89
101
  let stopped = false;
@@ -93,12 +105,18 @@ const assessments = [];
93
105
  const prompts = [];
94
106
  let socket;
95
107
  let pingTimer;
108
+ let reconnectTimer;
109
+ let clearConnectionWatchdog;
96
110
  let reconnectDelay = 1_000;
97
111
  let connectionRejected = false;
112
+ let lastConnectionError;
98
113
  let snapshotInFlight = false;
99
114
 
100
115
  process.on("SIGINT", async () => {
101
116
  stopped = true;
117
+ clearConnectionWatchdog?.();
118
+ clearTimeout(reconnectTimer);
119
+ clearInterval(pingTimer);
102
120
  await stopProcess(active?.child);
103
121
  socket?.close();
104
122
  process.exit(0);
@@ -109,13 +127,22 @@ await connect();
109
127
  async function connect() {
110
128
  console.log(`Connecting ${config.name} to ${config.server_url}`);
111
129
  connectionRejected = false;
130
+ lastConnectionError = undefined;
112
131
  socket = new WebSocket(config.server_url);
132
+ clearConnectionWatchdog = startConnectionWatchdog(socket, config.server_url, {
133
+ onTimeout: (message) => {
134
+ lastConnectionError = message;
135
+ console.error(message);
136
+ scheduleReconnect();
137
+ },
138
+ });
113
139
  socket.addEventListener("open", () =>
114
140
  socket.send(JSON.stringify(firstMessage)),
115
141
  );
116
142
  socket.addEventListener("message", async (event) => {
117
143
  const message = JSON.parse(String(event.data));
118
144
  if (message.type === "paired") {
145
+ clearConnectionWatchdog?.();
119
146
  config = {
120
147
  ...config,
121
148
  connector_id: message.connector.id,
@@ -134,6 +161,7 @@ async function connect() {
134
161
  return;
135
162
  }
136
163
  if (message.type === "authenticated") {
164
+ clearConnectionWatchdog?.();
137
165
  console.log("Connected and waiting for EngineerOS runs.");
138
166
  reconnectDelay = 1_000;
139
167
  startPings();
@@ -212,6 +240,7 @@ async function connect() {
212
240
  }
213
241
  });
214
242
  socket.addEventListener("close", () => {
243
+ clearConnectionWatchdog?.();
215
244
  clearInterval(pingTimer);
216
245
  if (active?.kind === "prompt") {
217
246
  active.cancelled = true;
@@ -225,13 +254,27 @@ async function connect() {
225
254
  return;
226
255
  }
227
256
  if (stopped) return;
257
+ scheduleReconnect();
258
+ });
259
+ socket.addEventListener("error", (event) => {
260
+ lastConnectionError = describeWebSocketError(event);
228
261
  console.error(
229
- `Connection lost. Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
262
+ `WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
230
263
  );
231
- setTimeout(connect, reconnectDelay);
232
- reconnectDelay = Math.min(30_000, reconnectDelay * 2);
233
264
  });
234
- socket.addEventListener("error", () => {});
265
+ }
266
+
267
+ function scheduleReconnect() {
268
+ if (stopped || connectionRejected || reconnectTimer) return;
269
+ const detail = lastConnectionError ? ` Last error: ${lastConnectionError}` : "";
270
+ console.error(
271
+ `Connection unavailable.${detail} Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
272
+ );
273
+ reconnectTimer = setTimeout(() => {
274
+ reconnectTimer = undefined;
275
+ void connect();
276
+ }, reconnectDelay);
277
+ reconnectDelay = Math.min(30_000, reconnectDelay * 2);
235
278
  }
236
279
 
237
280
  async function submitWorkspaceSnapshot() {
@@ -318,7 +361,7 @@ function pump() {
318
361
  JSON.stringify({
319
362
  type: "run.claim",
320
363
  run_id: runId,
321
- runner_name: "Codex CLI",
364
+ runner_name: codingAgent.name,
322
365
  metadata: {
323
366
  hostname: os.hostname(),
324
367
  workspace_name: path.basename(config.workspace),
@@ -330,7 +373,7 @@ function pump() {
330
373
  async function executePrompt(assignment) {
331
374
  const promptId = assignment.prompt_id;
332
375
  console.log(
333
- `Answering ${assignment.purpose || "project"} prompt with Codex CLI.`,
376
+ `Answering ${assignment.purpose || "project"} prompt with ${codingAgent.name}.`,
334
377
  );
335
378
  try {
336
379
  const result = await executeConnectedPrompt(assignment, config, {
@@ -368,7 +411,7 @@ async function executePrompt(assignment) {
368
411
 
369
412
  async function executeAssessment(assignment) {
370
413
  const assessmentId = assignment.assessment_id;
371
- console.log(`Assessing workspace with Codex CLI (${assessmentId}).`);
414
+ console.log(`Assessing workspace with ${codingAgent.name} (${assessmentId}).`);
372
415
  let progress = 10;
373
416
  const reportedMilestones = new Set();
374
417
  const reportProgress = (message, { milestone = true } = {}) => {
@@ -420,7 +463,7 @@ async function executeAssessment(assignment) {
420
463
  `EngineerOS rejected the assessment (${response.status}): ${await response.text()}`,
421
464
  );
422
465
  }
423
- console.log("Codex workspace assessment is current in EngineerOS.");
466
+ console.log(`${codingAgent.name} workspace assessment is current in EngineerOS.`);
424
467
  } catch (error) {
425
468
  if (socket.readyState === WebSocket.OPEN) {
426
469
  socket.send(
@@ -441,7 +484,7 @@ async function executeAssessment(assignment) {
441
484
 
442
485
  async function execute(assignment) {
443
486
  const runId = assignment.run_id;
444
- console.log(`Running Goal ${runId} with Codex CLI.`);
487
+ console.log(`Running Goal ${runId} with ${codingAgent.name}.`);
445
488
  let progress = 10;
446
489
  const heartbeat = setInterval(() => {
447
490
  if (socket.readyState === WebSocket.OPEN && active?.runId === runId) {
@@ -451,7 +494,7 @@ async function execute(assignment) {
451
494
  type: "run.progress",
452
495
  run_id: runId,
453
496
  progress_percent: progress,
454
- message: "Codex is working",
497
+ message: `${codingAgent.name} is working`,
455
498
  }),
456
499
  );
457
500
  }
@@ -463,7 +506,7 @@ async function execute(assignment) {
463
506
  },
464
507
  onEvent: (event) => {
465
508
  const message =
466
- event.message || event.item?.text || event.type || "Codex is working";
509
+ event.message || event.item?.text || event.type || `${codingAgent.name} is working`;
467
510
  if (socket.readyState === WebSocket.OPEN) {
468
511
  socket.send(
469
512
  JSON.stringify({
@@ -492,7 +535,17 @@ async function execute(assignment) {
492
535
  throw new Error(
493
536
  `EngineerOS rejected the result (${response.status}): ${await response.text()}`,
494
537
  );
495
- console.log(`Run submitted. Review workspace: ${result.run_workspace}`);
538
+ const integration = await applyAcceptedChange(
539
+ config.workspace,
540
+ assignment.base_revision,
541
+ result.head_revision,
542
+ );
543
+ if (integration.applied) {
544
+ console.log(`Run accepted and applied to the connected repository at ${integration.revision}.`);
545
+ } else {
546
+ console.warn(`Run accepted but not applied locally. ${integration.reason}`);
547
+ console.warn(`The verified run workspace remains at ${result.run_workspace}.`);
548
+ }
496
549
  } catch (error) {
497
550
  if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
498
551
  socket.send(
@@ -525,6 +578,19 @@ function parseArgs(args) {
525
578
  return { command, positional, flags };
526
579
  }
527
580
 
581
+ function parseAgentArgs(value) {
582
+ if (!value) return [];
583
+ try {
584
+ const parsed = JSON.parse(value);
585
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
586
+ throw new Error();
587
+ }
588
+ return parsed;
589
+ } catch {
590
+ fail('--agent-args must be a JSON array, for example: --agent-args "[\\"acp\\"]"');
591
+ }
592
+ }
593
+
528
594
  function fail(message) {
529
595
  console.error(message);
530
596
  process.exit(1);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.5.0",
4
- "description": "Connect a local Codex CLI workspace to EngineerOS over an outbound WebSocket.",
3
+ "version": "0.6.1",
4
+ "description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "license": "UNLICENSED",
@@ -15,8 +15,8 @@
15
15
  },
16
16
  "scripts": {
17
17
  "start": "node ./bin/engineeros-connector.mjs",
18
- "test": "node --test",
19
- "type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/runner.mjs"
18
+ "test": "node --test --test-concurrency=1",
19
+ "type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/connection.mjs && node --check ./src/runner.mjs"
20
20
  },
21
21
  "engines": {
22
22
  "node": ">=22"
@@ -31,5 +31,8 @@
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
34
+ },
35
+ "dependencies": {
36
+ "@agentclientprotocol/sdk": "1.3.0"
34
37
  }
35
38
  }
@@ -0,0 +1,105 @@
1
+ import { spawn } from "node:child_process";
2
+ import { Readable, Writable } from "node:stream";
3
+ import * as acp from "@agentclientprotocol/sdk";
4
+
5
+ function permissionOutcome(options = []) {
6
+ const selected =
7
+ options.find((option) => option.kind === "allow_once") ??
8
+ options.find((option) => option.kind === "allow_always");
9
+ return selected
10
+ ? { outcome: { outcome: "selected", optionId: selected.optionId } }
11
+ : { outcome: { outcome: "cancelled" } };
12
+ }
13
+
14
+ export function launchAcpAgent(workspace, prompt, config, callbacks = {}) {
15
+ if (!config.agent_command) {
16
+ throw new Error(
17
+ "No ACP coding agent is configured. Pair again with --agent-command and optional --agent-args JSON.",
18
+ );
19
+ }
20
+ const child = spawn(
21
+ config.agent_command,
22
+ Array.isArray(config.agent_args) ? config.agent_args : [],
23
+ {
24
+ cwd: workspace,
25
+ env: process.env,
26
+ shell: false,
27
+ stdio: ["pipe", "pipe", "pipe"],
28
+ },
29
+ );
30
+ let stderr = "";
31
+ child.stderr.setEncoding("utf8");
32
+ child.stderr.on("data", (chunk) => {
33
+ stderr += chunk;
34
+ callbacks.onEvent?.({
35
+ type: "agent.stderr",
36
+ message: String(chunk).trim().slice(0, 500),
37
+ });
38
+ });
39
+
40
+ const stream = acp.ndJsonStream(
41
+ Writable.toWeb(child.stdin),
42
+ Readable.toWeb(child.stdout),
43
+ );
44
+ let finalMessage = "";
45
+ const completed = acp
46
+ .client({ name: "EngineerOS" })
47
+ .onRequest(acp.methods.client.session.requestPermission, (ctx) =>
48
+ permissionOutcome(ctx.params.options),
49
+ )
50
+ .connectWith(stream, async (ctx) => {
51
+ const initialized = await ctx.request(acp.methods.agent.initialize, {
52
+ protocolVersion: acp.PROTOCOL_VERSION,
53
+ clientCapabilities: {},
54
+ });
55
+ callbacks.onEvent?.({
56
+ type: "agent.connected",
57
+ message: `ACP agent connected with protocol ${initialized.protocolVersion}`,
58
+ });
59
+ return ctx.buildSession(workspace).withSession(async (session) => {
60
+ child.engineerOsCancel = () =>
61
+ ctx.notify(acp.methods.agent.session.cancel, {
62
+ sessionId: session.sessionId,
63
+ });
64
+ void session.prompt(prompt);
65
+ for (;;) {
66
+ const message = await session.nextUpdate();
67
+ if (message.kind === "stop") {
68
+ if (message.stopReason === "error") {
69
+ throw new Error("The ACP agent ended the task with an error.");
70
+ }
71
+ return { finalMessage, model: "acp-agent" };
72
+ }
73
+ const update = message.update;
74
+ if (
75
+ update.sessionUpdate === "agent_message_chunk" &&
76
+ update.content?.type === "text"
77
+ ) {
78
+ finalMessage += update.content.text;
79
+ }
80
+ callbacks.onEvent?.({
81
+ type: `acp.${update.sessionUpdate}`,
82
+ message:
83
+ update.title ??
84
+ update.content?.text ??
85
+ update.status ??
86
+ update.sessionUpdate,
87
+ });
88
+ }
89
+ });
90
+ })
91
+ .catch((error) => {
92
+ const detail = stderr.trim().slice(-1_000);
93
+ throw new Error(
94
+ `ACP agent failed: ${error instanceof Error ? error.message : String(error)}${detail ? ` ${detail}` : ""}`,
95
+ );
96
+ })
97
+ .finally(() => {
98
+ child.kill("SIGKILL");
99
+ child.stdin.destroy();
100
+ child.stdout.destroy();
101
+ child.stderr.destroy();
102
+ child.unref();
103
+ });
104
+ return { child, completed };
105
+ }
@@ -0,0 +1,26 @@
1
+ export function describeWebSocketError(event) {
2
+ return (
3
+ event?.error?.message ||
4
+ event?.message ||
5
+ "The WebSocket connection failed without providing an error detail."
6
+ );
7
+ }
8
+
9
+ export function startConnectionWatchdog(
10
+ socket,
11
+ url,
12
+ { timeoutMs = 15_000, onTimeout = console.error } = {},
13
+ ) {
14
+ const timer = setTimeout(() => {
15
+ onTimeout(
16
+ `EngineerOS did not complete the WebSocket handshake at ${url} within ${Math.round(timeoutMs / 1_000)} seconds. Check that the backend is running and the URL is reachable from this machine.`,
17
+ );
18
+ try {
19
+ socket.close();
20
+ } catch {
21
+ // Reconnect scheduling is owned by the caller.
22
+ }
23
+ }, timeoutMs);
24
+
25
+ return () => clearTimeout(timer);
26
+ }
package/src/runner.mjs CHANGED
@@ -11,6 +11,7 @@ import os from "node:os";
11
11
  import path from "node:path";
12
12
  import { promisify } from "node:util";
13
13
  import { deflateRaw } from "node:zlib";
14
+ import { launchAcpAgent } from "./acp-client.mjs";
14
15
 
15
16
  const deflate = promisify(deflateRaw);
16
17
  const MAX_ARCHIVE_BYTES = 25 * 1024 * 1024;
@@ -94,10 +95,11 @@ export async function executeAssignment(assignment, config, callbacks) {
94
95
  assignment.run_id,
95
96
  assignment.base_revision,
96
97
  );
97
- const controller = launchCodexProcess(
98
+ const controller = launchAgentProcess(
98
99
  runWorkspace,
99
100
  execution.prompt,
100
101
  execution.sandboxMode,
102
+ config,
101
103
  callbacks,
102
104
  );
103
105
  callbacks.onProcess?.(controller.child);
@@ -110,10 +112,11 @@ export async function executeAssignment(assignment, config, callbacks) {
110
112
  assignment.base_revision,
111
113
  assignment.run_id,
112
114
  );
113
- const verifier = launchCodexProcess(
115
+ const verifier = launchAgentProcess(
114
116
  runWorkspace,
115
117
  verificationPrompt(assignment, committed.revision, committed.changedFiles),
116
118
  "read-only",
119
+ config,
117
120
  callbacks,
118
121
  );
119
122
  callbacks.onProcess?.(verifier.child);
@@ -135,6 +138,50 @@ export async function executeAssignment(assignment, config, callbacks) {
135
138
  };
136
139
  }
137
140
 
141
+ export async function applyAcceptedChange(workspace, baseRevision, headRevision) {
142
+ const current = await run("git", ["rev-parse", "HEAD"], workspace, {
143
+ allowFailure: true,
144
+ });
145
+ if (current.code !== 0) {
146
+ return { applied: false, reason: "The connected workspace is not a Git repository." };
147
+ }
148
+ const currentHead = current.stdout.trim();
149
+ const alreadyApplied = await run(
150
+ "git",
151
+ ["merge-base", "--is-ancestor", headRevision, currentHead],
152
+ workspace,
153
+ { allowFailure: true },
154
+ );
155
+ if (alreadyApplied.code === 0) return { applied: true, revision: currentHead };
156
+ const status = await run("git", ["status", "--porcelain"], workspace);
157
+ if (status.stdout.trim()) {
158
+ return {
159
+ applied: false,
160
+ reason: `The connected workspace has uncommitted changes. Apply accepted commit ${headRevision} after preserving them.`,
161
+ };
162
+ }
163
+ if (currentHead !== baseRevision) {
164
+ return {
165
+ applied: false,
166
+ reason: `The connected branch moved from frozen base ${baseRevision} to ${currentHead}. Apply accepted commit ${headRevision} with git cherry-pick.`,
167
+ };
168
+ }
169
+ const applied = await run("git", ["cherry-pick", headRevision], workspace, {
170
+ allowFailure: true,
171
+ });
172
+ if (applied.code !== 0) {
173
+ await run("git", ["cherry-pick", "--abort"], workspace, {
174
+ allowFailure: true,
175
+ });
176
+ return {
177
+ applied: false,
178
+ reason: `The accepted commit ${headRevision} could not be applied cleanly. Apply it manually with git cherry-pick.`,
179
+ };
180
+ }
181
+ const integrated = await run("git", ["rev-parse", "HEAD"], workspace);
182
+ return { applied: true, revision: integrated.stdout.trim() };
183
+ }
184
+
138
185
  async function commitRunChange(workspace, baseRevision, runId) {
139
186
  await run("git", ["add", "-A"], workspace);
140
187
  await run(
@@ -254,10 +301,11 @@ export async function executeWorkspaceAssessment(
254
301
  changeImpact.markdown,
255
302
  )
256
303
  : execution.prompt;
257
- const controller = launchCodexProcess(
304
+ const controller = launchAgentProcess(
258
305
  config.workspace,
259
306
  prompt,
260
307
  execution.sandboxMode,
308
+ config,
261
309
  callbacks,
262
310
  );
263
311
  callbacks.onProcess?.(controller.child);
@@ -375,10 +423,11 @@ function boundedText(value, maximum) {
375
423
 
376
424
  export async function executeConnectedPrompt(assignment, config, callbacks) {
377
425
  const execution = connectorExecution(assignment);
378
- const controller = launchCodexProcess(
426
+ const controller = launchAgentProcess(
379
427
  config.workspace,
380
428
  execution.prompt,
381
429
  execution.sandboxMode,
430
+ config,
382
431
  callbacks,
383
432
  );
384
433
  callbacks.onProcess?.(controller.child);
@@ -387,7 +436,22 @@ export async function executeConnectedPrompt(assignment, config, callbacks) {
387
436
  if (!content) {
388
437
  throw new Error("Codex completed without returning a response.");
389
438
  }
390
- return { content, model: "codex-cli" };
439
+ return { content, model: completed.model ?? config.agent_protocol ?? "coding-agent" };
440
+ }
441
+
442
+ export async function inspectCodingAgent(config, workspace = process.cwd()) {
443
+ if (config.agent_protocol === "acp") {
444
+ if (!config.agent_command) {
445
+ throw new Error("ACP requires --agent-command when pairing the connector.");
446
+ }
447
+ return {
448
+ protocol: "acp",
449
+ name: config.agent_name || path.basename(config.agent_command),
450
+ version: "ACP v1",
451
+ };
452
+ }
453
+ const codex = await inspectCodexCli(workspace);
454
+ return { protocol: "codex", name: "Codex CLI", version: codex.version };
391
455
  }
392
456
 
393
457
  export async function inspectCodexCli(workspace = process.cwd()) {
@@ -481,6 +545,7 @@ export function connectorExecution(assignment) {
481
545
 
482
546
  export async function stopProcess(child) {
483
547
  if (!child || child.exitCode !== null) return;
548
+ await child.engineerOsCancel?.();
484
549
  if (process.platform === "win32") {
485
550
  await run(
486
551
  "taskkill",
@@ -658,6 +723,13 @@ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
658
723
  return { child, completed };
659
724
  }
660
725
 
726
+ function launchAgentProcess(workspace, prompt, sandbox, config, callbacks) {
727
+ if (config.agent_protocol === "acp") {
728
+ return launchAcpAgent(workspace, prompt, config, callbacks);
729
+ }
730
+ return launchCodexProcess(workspace, prompt, sandbox, callbacks);
731
+ }
732
+
661
733
  function runCodexCommand(command, args, cwd) {
662
734
  return new Promise((resolve, reject) => {
663
735
  const child = spawn(command, args, {