acpx 0.8.0 → 0.10.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.
Files changed (33) hide show
  1. package/README.md +8 -4
  2. package/dist/{cli-BGYGVo3b.js → cli-8dP_TqBp.js} +4 -4
  3. package/dist/{cli-BGYGVo3b.js.map → cli-8dP_TqBp.js.map} +1 -1
  4. package/dist/cli.d.ts +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +930 -252
  7. package/dist/cli.js.map +1 -1
  8. package/dist/{client-FzXPdgP7.d.ts → client-C4iJBO0j.d.ts} +30 -3
  9. package/dist/client-C4iJBO0j.d.ts.map +1 -0
  10. package/dist/{flags-D706STfk.js → flags--2oX_ubW.js} +96 -39
  11. package/dist/flags--2oX_ubW.js.map +1 -0
  12. package/dist/{flows-hcjHmU7P.js → flows-e4umXVbY.js} +401 -336
  13. package/dist/flows-e4umXVbY.js.map +1 -0
  14. package/dist/flows.d.ts +21 -1
  15. package/dist/flows.d.ts.map +1 -1
  16. package/dist/flows.js +1 -1
  17. package/dist/{live-checkpoint-B9ctAuqV.js → live-checkpoint-CuFft_Nd.js} +1614 -931
  18. package/dist/live-checkpoint-CuFft_Nd.js.map +1 -0
  19. package/dist/{output-BL9XRWzS.js → output-Di77Yugq.js} +1153 -719
  20. package/dist/output-Di77Yugq.js.map +1 -0
  21. package/dist/runtime.d.ts +30 -2
  22. package/dist/runtime.d.ts.map +1 -1
  23. package/dist/runtime.js +579 -425
  24. package/dist/runtime.js.map +1 -1
  25. package/dist/{session-options-BJyG6zEH.d.ts → session-options-Bh1bIqQ2.d.ts} +14 -1
  26. package/dist/{session-options-BJyG6zEH.d.ts.map → session-options-Bh1bIqQ2.d.ts.map} +1 -1
  27. package/package.json +21 -12
  28. package/skills/acpx/SKILL.md +22 -5
  29. package/dist/client-FzXPdgP7.d.ts.map +0 -1
  30. package/dist/flags-D706STfk.js.map +0 -1
  31. package/dist/flows-hcjHmU7P.js.map +0 -1
  32. package/dist/live-checkpoint-B9ctAuqV.js.map +0 -1
  33. package/dist/output-BL9XRWzS.js.map +0 -1
package/dist/runtime.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Dt as isAcpResourceNotFoundError, E as AcpClient, Et as extractAcpError, K as defaultSessionEventLog, S as trimConversationForRuntime, Tt as normalizeOutputError, W as parseSessionRecord, Z as assertPersistedKeyPolicy, _ as cloneSessionConversation, _t as withTimeout, a as applyConversation, b as recordPromptSubmission, c as applyRequestedModelIfAdvertised, d as setDesiredConfigOption, f as setDesiredModeId, g as cloneSessionAcpxState, h as applyConfigOptionsToRecord, i as connectAndLoadSession, m as syncAdvertisedModelState, n as runPromptTurn, o as applyLifecycleSnapshotToRecord, ot as serializeSessionRecordForDisk, pt as textPrompt, r as withConnectedSession, s as reconcileAgentSessionId, t as LiveSessionCheckpoint, u as setCurrentModelId, v as createSessionConversation, vt as DEFAULT_AGENT_NAME, w as persistSessionOptions, x as recordSessionUpdate, xt as resolveAgentCommand, y as recordClientOperation, yt as listBuiltInAgents } from "./live-checkpoint-B9ctAuqV.js";
1
+ import { $ as defaultSessionEventLog, B as assertPersistedKeyPolicy, C as applyConversation, Dt as isAcpResourceNotFoundError, E as AcpClient, Et as extractAcpError, T as reconcileAgentSessionId, Tt as normalizeOutputError, X as serializeSessionRecordForDisk, Y as parseSessionRecord, _ as recordPromptSubmission, _t as withTimeout, a as applyRequestedModelIfAdvertised, c as setDesiredConfigOption, d as syncAdvertisedModelState, f as applyConfigOptionsToRecord, g as recordClientOperation, h as createSessionConversation, i as connectAndLoadSession, l as setDesiredModeId, m as cloneSessionConversation, n as runPromptTurn, p as cloneSessionAcpxState, pt as textPrompt, r as withConnectedSession, s as setCurrentModelId, t as LiveSessionCheckpoint, v as recordSessionUpdate, vt as DEFAULT_AGENT_NAME, w as applyLifecycleSnapshotToRecord, x as persistSessionOptions, xt as resolveAgentCommand, y as trimConversationForRuntime, yt as listBuiltInAgents } from "./live-checkpoint-CuFft_Nd.js";
2
2
  import path from "node:path";
3
3
  import fs from "node:fs/promises";
4
4
  import { randomUUID } from "node:crypto";
@@ -76,28 +76,36 @@ function resolveStructuredPromptPayload(parsed) {
76
76
  };
77
77
  }
78
78
  function resolveStatusTextForTag(params) {
79
- const { tag, payload } = params;
80
- if (tag === "available_commands_update") {
81
- const commands = Array.isArray(payload.availableCommands) ? payload.availableCommands : [];
82
- return commands.length > 0 ? `available commands updated (${commands.length})` : "available commands updated";
83
- }
84
- if (tag === "current_mode_update") {
85
- const mode = asTrimmedString(payload.currentModeId) || asTrimmedString(payload.modeId) || asTrimmedString(payload.mode);
86
- return mode ? `mode updated: ${mode}` : "mode updated";
87
- }
88
- if (tag === "config_option_update") {
89
- const id = asTrimmedString(payload.id) || asTrimmedString(payload.configOptionId);
90
- const value = asTrimmedString(payload.currentValue) || asTrimmedString(payload.value) || asTrimmedString(payload.optionValue);
91
- if (id && value) return `config updated: ${id}=${value}`;
92
- if (id) return `config updated: ${id}`;
93
- return "config updated";
94
- }
95
- if (tag === "session_info_update") return asTrimmedString(payload.summary) || asTrimmedString(payload.message) || "session updated";
96
- if (tag === "plan") {
97
- const content = asTrimmedString((Array.isArray(payload.entries) ? payload.entries : []).find((entry) => isRecord(entry))?.content);
98
- return content ? `plan: ${content}` : null;
99
- }
100
- return null;
79
+ const resolver = STATUS_TEXT_RESOLVERS[params.tag];
80
+ return resolver ? resolver(params.payload) : null;
81
+ }
82
+ const STATUS_TEXT_RESOLVERS = {
83
+ available_commands_update: availableCommandsStatusText,
84
+ current_mode_update: currentModeStatusText,
85
+ config_option_update: configOptionStatusText,
86
+ session_info_update: sessionInfoStatusText,
87
+ plan: planStatusText
88
+ };
89
+ function availableCommandsStatusText(payload) {
90
+ const commands = Array.isArray(payload.availableCommands) ? payload.availableCommands : [];
91
+ return commands.length > 0 ? `available commands updated (${commands.length})` : "available commands updated";
92
+ }
93
+ function currentModeStatusText(payload) {
94
+ const mode = asTrimmedString(payload.currentModeId) || asTrimmedString(payload.modeId) || asTrimmedString(payload.mode);
95
+ return mode ? `mode updated: ${mode}` : "mode updated";
96
+ }
97
+ function configOptionStatusText(payload) {
98
+ const id = asTrimmedString(payload.id) || asTrimmedString(payload.configOptionId);
99
+ const value = asTrimmedString(payload.currentValue) || asTrimmedString(payload.value) || asTrimmedString(payload.optionValue);
100
+ if (id && value) return `config updated: ${id}=${value}`;
101
+ return id ? `config updated: ${id}` : "config updated";
102
+ }
103
+ function sessionInfoStatusText(payload) {
104
+ return asTrimmedString(payload.summary) || asTrimmedString(payload.message) || "session updated";
105
+ }
106
+ function planStatusText(payload) {
107
+ const content = asTrimmedString((Array.isArray(payload.entries) ? payload.entries : []).find((entry) => isRecord(entry))?.content);
108
+ return content ? `plan: ${content}` : null;
101
109
  }
102
110
  function resolveTextChunk(params) {
103
111
  const contentRaw = params.payload.content;
@@ -177,17 +185,24 @@ function readToolContentText(value) {
177
185
  const record = isRecord(value) ? value : void 0;
178
186
  if (!record) return;
179
187
  if (record.type === "content") return readToolContentText(record.content);
180
- if (record.type === "text") return asString(record.text);
181
- if (record.type === "resource_link") return asOptionalString(record.title) || asOptionalString(record.name) || asOptionalString(record.uri);
182
- if (record.type === "resource") {
188
+ return toolContentTextReader(String(record.type))?.(record);
189
+ }
190
+ const TOOL_CONTENT_TEXT_READERS = {
191
+ text: (record) => asString(record.text),
192
+ audio: (record) => `[audio] ${asOptionalString(record.mimeType) || "audio"}`,
193
+ resource_link: (record) => asOptionalString(record.title) || asOptionalString(record.name) || asOptionalString(record.uri),
194
+ resource: (record) => {
183
195
  const resource = isRecord(record.resource) ? record.resource : void 0;
184
196
  return asString(resource?.text) || asOptionalString(resource?.uri);
185
- }
186
- if (record.type === "diff") return `diff ${asOptionalString(record.path) || "file"}`;
187
- if (record.type === "terminal") {
197
+ },
198
+ diff: (record) => `diff ${asOptionalString(record.path) || "file"}`,
199
+ terminal: (record) => {
188
200
  const terminalId = asOptionalString(record.terminalId) || asOptionalString(record.id);
189
201
  return terminalId ? `[terminal] ${terminalId}` : "[terminal]";
190
202
  }
203
+ };
204
+ function toolContentTextReader(type) {
205
+ return Object.hasOwn(TOOL_CONTENT_TEXT_READERS, type) ? TOOL_CONTENT_TEXT_READERS[type] : void 0;
191
206
  }
192
207
  function summarizeToolContent(content) {
193
208
  if (!Array.isArray(content)) return;
@@ -197,7 +212,7 @@ function summarizeToolContent(content) {
197
212
  }
198
213
  function summarizeToolOutput(rawOutput) {
199
214
  if (rawOutput == null) return;
200
- if (typeof rawOutput === "string" || typeof rawOutput === "number" || typeof rawOutput === "boolean") return truncateToolSummary(String(rawOutput));
215
+ if (isScalarToolOutput(rawOutput)) return truncateToolSummary(String(rawOutput));
201
216
  const record = isRecord(rawOutput) ? rawOutput : void 0;
202
217
  if (!record) return;
203
218
  return truncateToolSummary(readFirstString(record, [
@@ -209,13 +224,27 @@ function summarizeToolOutput(rawOutput) {
209
224
  "content"
210
225
  ]) ?? "") || void 0;
211
226
  }
227
+ function isScalarToolOutput(value) {
228
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
229
+ }
212
230
  function shouldForwardArray(value) {
213
231
  return Array.isArray(value);
214
232
  }
215
233
  function readToolKind(value) {
216
234
  const kind = asOptionalString(value);
217
- if (kind === "read" || kind === "edit" || kind === "delete" || kind === "move" || kind === "search" || kind === "execute" || kind === "fetch" || kind === "think" || kind === "other") return kind;
235
+ return kind && TOOL_KINDS.has(kind) ? kind : void 0;
218
236
  }
237
+ const TOOL_KINDS = new Set([
238
+ "read",
239
+ "edit",
240
+ "delete",
241
+ "move",
242
+ "search",
243
+ "execute",
244
+ "fetch",
245
+ "think",
246
+ "other"
247
+ ]);
219
248
  function createToolCallEvent(params) {
220
249
  const title = asTrimmedString(params.payload.title) || "tool call";
221
250
  const status = asTrimmedString(params.payload.status);
@@ -225,19 +254,31 @@ function createToolCallEvent(params) {
225
254
  const kind = readToolKind(params.payload.kind);
226
255
  const summaryText = status ? `${title} (${status})` : title;
227
256
  const detailSummary = params.tag === "tool_call_update" ? outputSummary ?? inputSummary : inputSummary ?? outputSummary;
228
- return {
257
+ const event = {
229
258
  type: "tool_call",
230
259
  text: detailSummary ? `${summaryText}: ${detailSummary}` : summaryText,
231
260
  tag: params.tag,
232
- ...toolCallId ? { toolCallId } : {},
233
- ...status ? { status } : {},
234
- ...kind ? { kind } : {},
235
- ...shouldForwardArray(params.payload.locations) ? { locations: params.payload.locations } : {},
236
- ...Object.prototype.hasOwnProperty.call(params.payload, "rawInput") ? { rawInput: params.payload.rawInput } : {},
237
- ...Object.prototype.hasOwnProperty.call(params.payload, "rawOutput") ? { rawOutput: params.payload.rawOutput } : {},
238
- ...shouldForwardArray(params.payload.content) ? { content: params.payload.content } : {},
239
261
  title
240
262
  };
263
+ assignToolCallEventMetadata(event, params.payload, {
264
+ toolCallId,
265
+ status,
266
+ kind
267
+ });
268
+ return event;
269
+ }
270
+ function assignToolCallEventMetadata(event, payload, values) {
271
+ if (event.type !== "tool_call") return;
272
+ if (values.toolCallId) event.toolCallId = values.toolCallId;
273
+ if (values.status) event.status = values.status;
274
+ if (values.kind) event.kind = values.kind;
275
+ assignForwardedToolPayload(event, payload);
276
+ }
277
+ function assignForwardedToolPayload(event, payload) {
278
+ if (shouldForwardArray(payload.locations)) event.locations = payload.locations;
279
+ if (Object.prototype.hasOwnProperty.call(payload, "rawInput")) event.rawInput = payload.rawInput;
280
+ if (Object.prototype.hasOwnProperty.call(payload, "rawOutput")) event.rawOutput = payload.rawOutput;
281
+ if (shouldForwardArray(payload.content)) event.content = payload.content;
241
282
  }
242
283
  function parsePromptEventLine(line) {
243
284
  const trimmed = line.trim();
@@ -251,88 +292,94 @@ function parsePromptEventLine(line) {
251
292
  const type = structured.type;
252
293
  const payload = structured.payload;
253
294
  const tag = structured.tag;
254
- switch (type) {
255
- case "text": return createTextDeltaEvent({
256
- content: asString(payload.content),
257
- stream: "output",
258
- tag
259
- });
260
- case "thought": return createTextDeltaEvent({
261
- content: asString(payload.content),
262
- stream: "thought",
263
- tag
264
- });
265
- case "tool_call": return createToolCallEvent({
266
- payload,
267
- tag: tag ?? "tool_call"
268
- });
269
- case "tool_call_update": return createToolCallEvent({
270
- payload,
271
- tag: tag ?? "tool_call_update"
272
- });
273
- case "agent_message_chunk": return resolveTextChunk({
274
- payload,
275
- stream: "output",
276
- tag: "agent_message_chunk"
277
- });
278
- case "agent_thought_chunk": return resolveTextChunk({
279
- payload,
280
- stream: "thought",
281
- tag: "agent_thought_chunk"
282
- });
283
- case "usage_update": {
284
- const used = asOptionalFiniteNumber(payload.used);
285
- const size = asOptionalFiniteNumber(payload.size);
286
- return {
287
- type: "status",
288
- text: used != null && size != null ? `usage updated: ${used}/${size}` : "usage updated",
289
- tag: "usage_update",
290
- ...used != null ? { used } : {},
291
- ...size != null ? { size } : {}
292
- };
293
- }
294
- case "available_commands_update":
295
- case "current_mode_update":
296
- case "config_option_update":
297
- case "session_info_update":
298
- case "plan": {
299
- const text = resolveStatusTextForTag({
300
- tag: type,
301
- payload
302
- });
303
- if (!text) return null;
304
- return {
305
- type: "status",
306
- text,
307
- tag: type
308
- };
309
- }
310
- case "client_operation": {
311
- const text = [
312
- asTrimmedString(payload.method) || "operation",
313
- asTrimmedString(payload.status),
314
- asTrimmedString(payload.summary)
315
- ].filter(Boolean).join(" ");
316
- if (!text) return null;
317
- return {
318
- type: "status",
319
- text,
320
- ...tag ? { tag } : {}
321
- };
322
- }
323
- case "update": {
324
- const update = asTrimmedString(payload.update);
325
- if (!update) return null;
326
- return {
327
- type: "status",
328
- text: update,
329
- ...tag ? { tag } : {}
330
- };
331
- }
332
- case "done":
333
- case "error": return null;
334
- default: return null;
335
- }
295
+ const parser = promptEventParser(type);
296
+ return parser ? parser(payload, tag) : null;
297
+ }
298
+ const PROMPT_EVENT_PARSERS = {
299
+ text: (payload, tag) => createTextDeltaEvent({
300
+ content: asString(payload.content),
301
+ stream: "output",
302
+ tag
303
+ }),
304
+ thought: (payload, tag) => createTextDeltaEvent({
305
+ content: asString(payload.content),
306
+ stream: "thought",
307
+ tag
308
+ }),
309
+ tool_call: (payload, tag) => createToolCallEvent({
310
+ payload,
311
+ tag: tag ?? "tool_call"
312
+ }),
313
+ tool_call_update: (payload, tag) => createToolCallEvent({
314
+ payload,
315
+ tag: tag ?? "tool_call_update"
316
+ }),
317
+ agent_message_chunk: (payload) => resolveTextChunk({
318
+ payload,
319
+ stream: "output",
320
+ tag: "agent_message_chunk"
321
+ }),
322
+ agent_thought_chunk: (payload) => resolveTextChunk({
323
+ payload,
324
+ stream: "thought",
325
+ tag: "agent_thought_chunk"
326
+ }),
327
+ usage_update: usageUpdateEvent,
328
+ available_commands_update: (payload) => statusUpdateEvent("available_commands_update", payload),
329
+ current_mode_update: (payload) => statusUpdateEvent("current_mode_update", payload),
330
+ config_option_update: (payload) => statusUpdateEvent("config_option_update", payload),
331
+ session_info_update: (payload) => statusUpdateEvent("session_info_update", payload),
332
+ plan: (payload) => statusUpdateEvent("plan", payload),
333
+ client_operation: clientOperationEvent,
334
+ update: updateStatusEvent,
335
+ done: () => null,
336
+ error: () => null
337
+ };
338
+ function promptEventParser(type) {
339
+ return Object.hasOwn(PROMPT_EVENT_PARSERS, type) ? PROMPT_EVENT_PARSERS[type] : void 0;
340
+ }
341
+ function usageUpdateEvent(payload) {
342
+ const used = asOptionalFiniteNumber(payload.used);
343
+ const size = asOptionalFiniteNumber(payload.size);
344
+ return {
345
+ type: "status",
346
+ text: used != null && size != null ? `usage updated: ${used}/${size}` : "usage updated",
347
+ tag: "usage_update",
348
+ ...used != null ? { used } : {},
349
+ ...size != null ? { size } : {}
350
+ };
351
+ }
352
+ function statusUpdateEvent(tag, payload) {
353
+ const text = resolveStatusTextForTag({
354
+ tag,
355
+ payload
356
+ });
357
+ if (!text) return null;
358
+ return {
359
+ type: "status",
360
+ text,
361
+ tag
362
+ };
363
+ }
364
+ function clientOperationEvent(payload, tag) {
365
+ const text = [
366
+ asTrimmedString(payload.method) || "operation",
367
+ asTrimmedString(payload.status),
368
+ asTrimmedString(payload.summary)
369
+ ].filter(Boolean).join(" ");
370
+ return text ? {
371
+ type: "status",
372
+ text,
373
+ ...tag ? { tag } : {}
374
+ } : null;
375
+ }
376
+ function updateStatusEvent(payload, tag) {
377
+ const update = asTrimmedString(payload.update);
378
+ return update ? {
379
+ type: "status",
380
+ text: update,
381
+ ...tag ? { tag } : {}
382
+ } : null;
336
383
  }
337
384
  //#endregion
338
385
  //#region src/runtime/engine/reuse-policy.ts
@@ -412,12 +459,23 @@ function toPromptInput(text, attachments) {
412
459
  text
413
460
  });
414
461
  for (const attachment of attachments) {
415
- if (!attachment.mediaType.startsWith("image/")) throw new AcpRuntimeError("ACP_TURN_FAILED", `Unsupported ACP runtime attachment media type: ${attachment.mediaType}`);
416
- blocks.push({
417
- type: "image",
418
- mimeType: attachment.mediaType,
419
- data: attachment.data
420
- });
462
+ if (attachment.mediaType.startsWith("image/")) {
463
+ blocks.push({
464
+ type: "image",
465
+ mimeType: attachment.mediaType,
466
+ data: attachment.data
467
+ });
468
+ continue;
469
+ }
470
+ if (attachment.mediaType.startsWith("audio/")) {
471
+ blocks.push({
472
+ type: "audio",
473
+ mimeType: attachment.mediaType,
474
+ data: attachment.data
475
+ });
476
+ continue;
477
+ }
478
+ throw new AcpRuntimeError("ACP_TURN_FAILED", `Unsupported ACP runtime attachment media type: ${attachment.mediaType}`);
421
479
  }
422
480
  return blocks.length > 0 ? blocks : textPrompt(text);
423
481
  }
@@ -496,6 +554,31 @@ function resolveSupportedConfigOptionId(record, configId) {
496
554
  const supportedText = supported.length > 0 ? supported.join(", ") : "none";
497
555
  throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `ACP session ${record.acpxRecordId} does not advertise config option '${configId}'. Supported config options: ${supportedText}.`);
498
556
  }
557
+ async function createOrLoadRuntimeSession(client, resumeSessionId, cwd) {
558
+ if (resumeSessionId) {
559
+ if (client.supportsResumeSession()) {
560
+ const resumed = await client.resumeSession(resumeSessionId, cwd);
561
+ return {
562
+ sessionId: resumeSessionId,
563
+ agentSessionId: resumed.agentSessionId,
564
+ sessionResult: resumed
565
+ };
566
+ }
567
+ if (!client.supportsLoadSession()) throw new Error(`Agent does not support session/resume or session/load; cannot resume session ${resumeSessionId}`);
568
+ const loaded = await client.loadSession(resumeSessionId, cwd);
569
+ return {
570
+ sessionId: resumeSessionId,
571
+ agentSessionId: loaded.agentSessionId,
572
+ sessionResult: loaded
573
+ };
574
+ }
575
+ const created = await client.createSession(cwd);
576
+ return {
577
+ sessionId: created.sessionId,
578
+ agentSessionId: created.agentSessionId,
579
+ sessionResult: created
580
+ };
581
+ }
499
582
  var AcpRuntimeManager = class {
500
583
  options;
501
584
  deps;
@@ -611,56 +694,56 @@ var AcpRuntimeManager = class {
611
694
  let keepClientOpen = false;
612
695
  try {
613
696
  await client.start();
614
- let sessionId;
615
- let agentSessionId;
616
- let sessionResult;
617
- if (input.resumeSessionId) {
618
- const loaded = await client.loadSession(input.resumeSessionId, cwd);
619
- sessionId = input.resumeSessionId;
620
- agentSessionId = loaded.agentSessionId;
621
- sessionResult = loaded;
622
- } else {
623
- const created = await client.createSession(cwd);
624
- sessionId = created.sessionId;
625
- agentSessionId = created.agentSessionId;
626
- sessionResult = created;
627
- }
628
- const record = createInitialRecord({
629
- recordId: createRecordId(input.sessionKey, input.mode),
630
- sessionName: input.sessionKey,
631
- sessionId,
632
- agentCommand,
633
- cwd,
634
- agentSessionId
635
- });
636
- this.closingActiveRecords.delete(record.acpxRecordId);
637
- record.protocolVersion = client.initializeResult?.protocolVersion;
638
- record.agentCapabilities = client.initializeResult?.agentCapabilities;
639
- applyConfigOptionsToRecord(record, sessionResult);
640
- const requestedModelApplied = await applyRequestedModelIfAdvertised({
697
+ const session = await createOrLoadRuntimeSession(client, input.resumeSessionId, cwd);
698
+ const record = await this.createAndSaveRuntimeRecord({
699
+ input,
641
700
  client,
642
- sessionId,
643
- requestedModel: input.sessionOptions?.model,
644
- models: sessionResult.models,
645
701
  agentCommand,
646
- timeoutMs: this.options.timeoutMs
702
+ cwd,
703
+ session
647
704
  });
648
- syncAdvertisedModelState(record, sessionResult.models);
649
- if (requestedModelApplied) setCurrentModelId(record, input.sessionOptions?.model);
650
- applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
651
- persistSessionOptions(record, input.sessionOptions);
652
- await this.options.sessionStore.save(record);
653
- if (input.mode === "persistent") {
654
- const previousClient = this.pendingPersistentClients.get(record.acpxRecordId);
655
- this.pendingPersistentClients.set(record.acpxRecordId, client);
656
- keepClientOpen = true;
657
- await previousClient?.close().catch(() => {});
658
- }
705
+ keepClientOpen = await this.keepPersistentClient(input.mode, record.acpxRecordId, client);
659
706
  return record;
660
707
  } finally {
661
708
  if (!keepClientOpen) await client.close();
662
709
  }
663
710
  }
711
+ async createAndSaveRuntimeRecord(params) {
712
+ const { input, client, agentCommand, cwd, session } = params;
713
+ const record = createInitialRecord({
714
+ recordId: createRecordId(input.sessionKey, input.mode),
715
+ sessionName: input.sessionKey,
716
+ sessionId: session.sessionId,
717
+ agentCommand,
718
+ cwd,
719
+ agentSessionId: session.agentSessionId
720
+ });
721
+ this.closingActiveRecords.delete(record.acpxRecordId);
722
+ record.protocolVersion = client.initializeResult?.protocolVersion;
723
+ record.agentCapabilities = client.initializeResult?.agentCapabilities;
724
+ applyConfigOptionsToRecord(record, session.sessionResult);
725
+ const requestedModelApplied = await applyRequestedModelIfAdvertised({
726
+ client,
727
+ sessionId: session.sessionId,
728
+ requestedModel: input.sessionOptions?.model,
729
+ models: session.sessionResult.models,
730
+ agentCommand,
731
+ timeoutMs: this.options.timeoutMs
732
+ });
733
+ syncAdvertisedModelState(record, session.sessionResult.models);
734
+ if (requestedModelApplied) setCurrentModelId(record, input.sessionOptions?.model);
735
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
736
+ persistSessionOptions(record, input.sessionOptions);
737
+ await this.options.sessionStore.save(record);
738
+ return record;
739
+ }
740
+ async keepPersistentClient(mode, recordId, client) {
741
+ if (mode !== "persistent") return false;
742
+ const previousClient = this.pendingPersistentClients.get(recordId);
743
+ this.pendingPersistentClients.set(recordId, client);
744
+ await previousClient?.close().catch(() => {});
745
+ return true;
746
+ }
664
747
  startTurn(input) {
665
748
  const promptInput = toPromptInput(input.text, input.attachments);
666
749
  const queue = new AsyncEventQueue();
@@ -668,10 +751,12 @@ var AcpRuntimeManager = class {
668
751
  const sessionReady = createDeferred();
669
752
  sessionReady.promise.catch(() => {});
670
753
  let resultSettled = false;
671
- let pendingCancel = false;
672
- let turnActive = true;
754
+ const state = {
755
+ pendingCancel: false,
756
+ turnActive: true,
757
+ activeController: null
758
+ };
673
759
  let streamClosed = false;
674
- let activeController = null;
675
760
  const settleResult = (next) => {
676
761
  if (resultSettled) return;
677
762
  resultSettled = true;
@@ -684,9 +769,9 @@ var AcpRuntimeManager = class {
684
769
  queue.close();
685
770
  };
686
771
  const requestCancel = async () => {
687
- if (activeController) return await activeController.requestCancelActivePrompt();
688
- if (!turnActive) return false;
689
- pendingCancel = true;
772
+ if (state.activeController) return await state.activeController.requestCancelActivePrompt();
773
+ if (!state.turnActive) return false;
774
+ state.pendingCancel = true;
690
775
  return true;
691
776
  };
692
777
  const abortHandler = () => {
@@ -709,230 +794,15 @@ var AcpRuntimeManager = class {
709
794
  }
710
795
  input.signal.addEventListener("abort", abortHandler, { once: true });
711
796
  }
712
- (async () => {
713
- let record = null;
714
- let conversation = null;
715
- let acpxState;
716
- let liveCheckpoint;
717
- let client = null;
718
- try {
719
- record = await this.requireRecord(input.handle.acpxRecordId ?? input.handle.sessionKey);
720
- conversation = cloneSessionConversation(record);
721
- acpxState = cloneSessionAcpxState(record.acpx);
722
- const promptStartedAt = isoNow();
723
- const promptMessageId = recordPromptSubmission(conversation, promptInput, promptStartedAt);
724
- trimConversationForRuntime(conversation);
725
- record.lastPromptAt = promptStartedAt;
726
- record.lastUsedAt = promptStartedAt;
727
- record.acpx = acpxState;
728
- applyConversation(record, conversation);
729
- await this.options.sessionStore.save(record);
730
- const pendingClient = await this.readPendingPersistentClient(record, { consume: true });
731
- client = pendingClient ?? this.createClient({
732
- agentCommand: record.agentCommand,
733
- cwd: record.cwd,
734
- mcpServers: [...this.options.mcpServers ?? []],
735
- permissionMode: this.options.permissionMode,
736
- nonInteractivePermissions: this.options.nonInteractivePermissions,
737
- onPermissionRequest: this.options.onPermissionRequest,
738
- verbose: this.options.verbose
739
- });
740
- const runtimeClient = client;
741
- const runtimeConversation = conversation;
742
- const runtimeRecord = record;
743
- liveCheckpoint = new LiveSessionCheckpoint({ save: async () => {
744
- runtimeRecord.lastUsedAt = isoNow();
745
- runtimeRecord.acpx = acpxState;
746
- applyConversation(runtimeRecord, runtimeConversation);
747
- await this.refreshClosedState(runtimeRecord);
748
- await this.options.sessionStore.save(runtimeRecord);
749
- } });
750
- let activeSessionId = record.acpSessionId;
751
- const applyPendingCancel = async () => {
752
- if (!pendingCancel || !runtimeClient.hasActivePrompt()) return false;
753
- const cancelled = await runtimeClient.requestCancelActivePrompt();
754
- if (cancelled) pendingCancel = false;
755
- return cancelled;
756
- };
757
- activeController = {
758
- hasActivePrompt: () => runtimeClient.hasActivePrompt(),
759
- requestCancelActivePrompt: async () => {
760
- if (runtimeClient.hasActivePrompt()) return await runtimeClient.requestCancelActivePrompt();
761
- if (!turnActive) return false;
762
- pendingCancel = true;
763
- return true;
764
- },
765
- setSessionMode: async (modeId) => {
766
- if (!runtimeClient.hasActivePrompt()) await sessionReady.promise;
767
- await runtimeClient.setSessionMode(activeSessionId, modeId);
768
- const nextState = cloneSessionAcpxState(acpxState) ?? {};
769
- nextState.desired_mode_id = modeId;
770
- acpxState = nextState;
771
- },
772
- setSessionModel: async (modelId) => {
773
- if (!runtimeClient.hasActivePrompt()) await sessionReady.promise;
774
- await runtimeClient.setSessionModel(activeSessionId, modelId);
775
- },
776
- setSessionConfigOption: async (configId, value) => {
777
- return (await activeController.setResolvedSessionConfigOption(configId, value)).response;
778
- },
779
- setResolvedSessionConfigOption: async (configId, value) => {
780
- if (!runtimeClient.hasActivePrompt()) await sessionReady.promise;
781
- const resolvedConfigId = resolveSupportedConfigOptionId({
782
- ...runtimeRecord,
783
- acpx: acpxState ?? void 0
784
- }, configId);
785
- const response = await runtimeClient.setSessionConfigOption(activeSessionId, resolvedConfigId, value);
786
- if (response?.configOptions) {
787
- const nextState = cloneSessionAcpxState(acpxState) ?? {};
788
- nextState.config_options = structuredClone(response.configOptions);
789
- acpxState = nextState;
790
- }
791
- if (resolvedConfigId === "mode") {
792
- const nextState = cloneSessionAcpxState(acpxState) ?? {};
793
- nextState.desired_mode_id = value;
794
- acpxState = nextState;
795
- } else if (resolvedConfigId !== "model") {
796
- const nextState = cloneSessionAcpxState(acpxState) ?? {};
797
- nextState.desired_config_options = {
798
- ...nextState.desired_config_options,
799
- [resolvedConfigId]: value
800
- };
801
- acpxState = nextState;
802
- }
803
- return {
804
- configId: resolvedConfigId,
805
- response
806
- };
807
- }
808
- };
809
- const emitParsed = (payload) => {
810
- if (streamClosed) return;
811
- const parsed = parsePromptEventLine(JSON.stringify(payload));
812
- if (!parsed) return;
813
- queue.push(parsed);
814
- };
815
- this.activeControllers.set(runtimeRecord.acpxRecordId, activeController);
816
- runtimeClient.setEventHandlers({
817
- onSessionUpdate: (notification) => {
818
- acpxState = recordSessionUpdate(runtimeConversation, acpxState, notification);
819
- trimConversationForRuntime(runtimeConversation);
820
- liveCheckpoint?.request();
821
- emitParsed({
822
- jsonrpc: "2.0",
823
- method: "session/update",
824
- params: notification
825
- });
826
- },
827
- onClientOperation: (operation) => {
828
- acpxState = recordClientOperation(runtimeConversation, acpxState, operation);
829
- trimConversationForRuntime(runtimeConversation);
830
- liveCheckpoint?.request();
831
- emitParsed({
832
- type: "client_operation",
833
- ...operation
834
- });
835
- }
836
- });
837
- const { sessionId, resumed, loadError } = pendingClient ? {
838
- sessionId: record.acpSessionId,
839
- resumed: false,
840
- loadError: void 0
841
- } : await connectAndLoadSession({
842
- client: runtimeClient,
843
- record: runtimeRecord,
844
- resumePolicy: resumePolicyForSessionMode(input.sessionMode),
845
- timeoutMs: this.options.timeoutMs,
846
- activeController,
847
- onClientAvailable: () => {
848
- if (activeController) this.activeControllers.set(runtimeRecord.acpxRecordId, activeController);
849
- },
850
- onConnectedRecord: (connectedRecord) => {
851
- connectedRecord.lastPromptAt = isoNow();
852
- },
853
- onSessionIdResolved: (sessionIdValue) => {
854
- activeSessionId = sessionIdValue;
855
- }
856
- });
857
- acpxState = cloneSessionAcpxState(runtimeRecord.acpx);
858
- sessionReady.resolve();
859
- runtimeRecord.lastRequestId = input.requestId;
860
- runtimeRecord.lastPromptAt = isoNow();
861
- runtimeRecord.closed = false;
862
- runtimeRecord.closedAt = void 0;
863
- runtimeRecord.lastUsedAt = isoNow();
864
- await liveCheckpoint.checkpoint();
865
- if (resumed || loadError) emitParsed({
866
- type: "status",
867
- text: loadError ? `load fallback: ${loadError}` : "session resumed"
868
- });
869
- if (pendingCancel || input.signal?.aborted) {
870
- pendingCancel = false;
871
- settleResult({
872
- status: "cancelled",
873
- stopReason: "cancelled"
874
- });
875
- return;
876
- }
877
- await applyPendingCancel();
878
- const response = await runPromptTurn({
879
- client: runtimeClient,
880
- sessionId,
881
- prompt: promptInput,
882
- timeoutMs: input.timeoutMs ?? this.options.timeoutMs,
883
- conversation: runtimeConversation,
884
- promptMessageId
885
- });
886
- runtimeRecord.acpSessionId = activeSessionId;
887
- reconcileAgentSessionId(runtimeRecord, runtimeRecord.agentSessionId);
888
- runtimeRecord.protocolVersion = runtimeClient.initializeResult?.protocolVersion;
889
- runtimeRecord.agentCapabilities = runtimeClient.initializeResult?.agentCapabilities;
890
- runtimeRecord.acpx = acpxState;
891
- applyConversation(runtimeRecord, runtimeConversation);
892
- applyLifecycleSnapshotToRecord(runtimeRecord, runtimeClient.getAgentLifecycleSnapshot());
893
- await this.options.sessionStore.save(runtimeRecord);
894
- settleResult({
895
- status: response.stopReason === "cancelled" ? "cancelled" : "completed",
896
- ...response.stopReason ? { stopReason: response.stopReason } : {}
897
- });
898
- } catch (error) {
899
- sessionReady.reject(error);
900
- const normalized = normalizeOutputError(error, { origin: "runtime" });
901
- settleResult({
902
- status: "failed",
903
- error: {
904
- message: normalized.message,
905
- ...normalized.code ? { code: normalized.code } : {},
906
- ...normalized.detailCode ? { detailCode: normalized.detailCode } : {},
907
- ...normalized.retryable !== void 0 ? { retryable: normalized.retryable } : {}
908
- }
909
- });
910
- } finally {
911
- turnActive = false;
912
- if (input.signal) input.signal.removeEventListener("abort", abortHandler);
913
- client?.clearEventHandlers();
914
- let pooled = false;
915
- if (record && conversation) {
916
- applyLifecycleSnapshotToRecord(record, client?.getAgentLifecycleSnapshot() ?? { running: false });
917
- record.acpx = acpxState;
918
- applyConversation(record, conversation);
919
- record.lastUsedAt = isoNow();
920
- await liveCheckpoint?.flush().catch(() => {});
921
- const closed = await this.refreshClosedState(record);
922
- await this.options.sessionStore.save(record).catch(() => {});
923
- if (!closed && client) pooled = await this.retainPersistentClientAfterTurn({
924
- record,
925
- client
926
- });
927
- }
928
- if (!pooled) await client?.close().catch(() => {});
929
- if (record) {
930
- this.activeControllers.delete(record.acpxRecordId);
931
- this.closingActiveRecords.delete(record.acpxRecordId);
932
- }
933
- queue.close();
934
- }
935
- })();
797
+ this.runRuntimeTurnTask({
798
+ input,
799
+ promptInput,
800
+ queue,
801
+ sessionReady,
802
+ state,
803
+ settleResult,
804
+ abortHandler
805
+ });
936
806
  return {
937
807
  requestId: input.requestId,
938
808
  events: queue.iterate(),
@@ -945,6 +815,284 @@ var AcpRuntimeManager = class {
945
815
  }
946
816
  };
947
817
  }
818
+ async runRuntimeTurnTask(task) {
819
+ let turn;
820
+ try {
821
+ turn = await this.prepareRuntimeTurn(task);
822
+ const { sessionId, resumed, loadError } = await this.connectRuntimeTurn(task, turn);
823
+ await this.resolveRuntimeTurnReady(task, turn, resumed, loadError);
824
+ if (this.cancelRuntimeTurnBeforePrompt(task)) return;
825
+ await this.applyPendingRuntimeTurnCancel(task, turn);
826
+ const response = await runPromptTurn({
827
+ client: turn.client,
828
+ sessionId,
829
+ prompt: task.promptInput,
830
+ timeoutMs: task.input.timeoutMs ?? this.options.timeoutMs,
831
+ conversation: turn.conversation,
832
+ promptMessageId: turn.promptMessageId
833
+ });
834
+ await this.saveCompletedRuntimeTurn(turn, response.stopReason);
835
+ task.settleResult({
836
+ status: response.stopReason === "cancelled" ? "cancelled" : "completed",
837
+ ...response.stopReason ? { stopReason: response.stopReason } : {}
838
+ });
839
+ } catch (error) {
840
+ this.failRuntimeTurn(task, error);
841
+ } finally {
842
+ await this.finalizeRuntimeTurn(task, turn);
843
+ }
844
+ }
845
+ async prepareRuntimeTurn(task) {
846
+ const record = await this.requireRecord(task.input.handle.acpxRecordId ?? task.input.handle.sessionKey);
847
+ const conversation = cloneSessionConversation(record);
848
+ let acpxState = cloneSessionAcpxState(record.acpx);
849
+ const promptStartedAt = isoNow();
850
+ const promptMessageId = recordPromptSubmission(conversation, task.promptInput, promptStartedAt);
851
+ trimConversationForRuntime(conversation);
852
+ record.lastPromptAt = promptStartedAt;
853
+ record.lastUsedAt = promptStartedAt;
854
+ record.acpx = acpxState;
855
+ applyConversation(record, conversation);
856
+ await this.options.sessionStore.save(record);
857
+ const pendingClient = await this.readPendingPersistentClient(record, { consume: true });
858
+ const client = pendingClient ?? this.createTurnClient(record);
859
+ const turn = {
860
+ record,
861
+ conversation,
862
+ acpxState,
863
+ liveCheckpoint: this.createRuntimeTurnCheckpoint(record, conversation, () => turn.acpxState),
864
+ client,
865
+ pendingClient,
866
+ promptMessageId,
867
+ activeSessionId: record.acpSessionId
868
+ };
869
+ task.state.activeController = this.buildRuntimeTurnController(task, turn);
870
+ this.activeControllers.set(record.acpxRecordId, task.state.activeController);
871
+ this.installRuntimeTurnEventHandlers(task, turn);
872
+ return turn;
873
+ }
874
+ createTurnClient(record) {
875
+ return this.createClient({
876
+ agentCommand: record.agentCommand,
877
+ cwd: record.cwd,
878
+ mcpServers: [...this.options.mcpServers ?? []],
879
+ permissionMode: this.options.permissionMode,
880
+ nonInteractivePermissions: this.options.nonInteractivePermissions,
881
+ onPermissionRequest: this.options.onPermissionRequest,
882
+ verbose: this.options.verbose
883
+ });
884
+ }
885
+ createRuntimeTurnCheckpoint(record, conversation, readAcpxState) {
886
+ return new LiveSessionCheckpoint({ save: async () => {
887
+ record.lastUsedAt = isoNow();
888
+ record.acpx = readAcpxState();
889
+ applyConversation(record, conversation);
890
+ await this.refreshClosedState(record);
891
+ await this.options.sessionStore.save(record);
892
+ } });
893
+ }
894
+ buildRuntimeTurnController(task, turn) {
895
+ return {
896
+ hasActivePrompt: () => turn.client.hasActivePrompt(),
897
+ requestCancelActivePrompt: async () => await this.requestRuntimeTurnCancel(task, turn),
898
+ setSessionMode: async (modeId) => {
899
+ await this.waitForRuntimeControlSession(task, turn);
900
+ await turn.client.setSessionMode(turn.activeSessionId, modeId);
901
+ const nextState = cloneSessionAcpxState(turn.acpxState) ?? {};
902
+ nextState.desired_mode_id = modeId;
903
+ turn.acpxState = nextState;
904
+ },
905
+ setSessionModel: async (modelId) => {
906
+ await this.waitForRuntimeControlSession(task, turn);
907
+ await turn.client.setSessionModel(turn.activeSessionId, modelId);
908
+ },
909
+ setSessionConfigOption: async (configId, value) => {
910
+ return (await task.state.activeController.setResolvedSessionConfigOption(configId, value)).response;
911
+ },
912
+ setResolvedSessionConfigOption: async (configId, value) => await this.setRuntimeResolvedSessionConfigOption(task, turn, configId, value)
913
+ };
914
+ }
915
+ async waitForRuntimeControlSession(task, turn) {
916
+ if (turn.client.hasActivePrompt()) return;
917
+ await task.sessionReady.promise;
918
+ }
919
+ async requestRuntimeTurnCancel(task, turn) {
920
+ if (turn.client.hasActivePrompt()) return await turn.client.requestCancelActivePrompt();
921
+ if (!task.state.turnActive) return false;
922
+ task.state.pendingCancel = true;
923
+ return true;
924
+ }
925
+ async setRuntimeResolvedSessionConfigOption(task, turn, configId, value) {
926
+ await this.waitForRuntimeControlSession(task, turn);
927
+ const resolvedConfigId = resolveSupportedConfigOptionId({
928
+ ...turn.record,
929
+ acpx: turn.acpxState ?? void 0
930
+ }, configId);
931
+ const response = await turn.client.setSessionConfigOption(turn.activeSessionId, resolvedConfigId, value);
932
+ this.applyRuntimeConfigOptionState(turn, resolvedConfigId, value, response);
933
+ return {
934
+ configId: resolvedConfigId,
935
+ response
936
+ };
937
+ }
938
+ applyRuntimeConfigOptionState(turn, configId, value, response) {
939
+ if (response?.configOptions) {
940
+ const nextState = cloneSessionAcpxState(turn.acpxState) ?? {};
941
+ nextState.config_options = structuredClone(response.configOptions);
942
+ turn.acpxState = nextState;
943
+ }
944
+ if (configId === "mode") {
945
+ const nextState = cloneSessionAcpxState(turn.acpxState) ?? {};
946
+ nextState.desired_mode_id = value;
947
+ turn.acpxState = nextState;
948
+ return;
949
+ }
950
+ if (configId !== "model") {
951
+ const nextState = cloneSessionAcpxState(turn.acpxState) ?? {};
952
+ nextState.desired_config_options = {
953
+ ...nextState.desired_config_options,
954
+ [configId]: value
955
+ };
956
+ turn.acpxState = nextState;
957
+ }
958
+ }
959
+ installRuntimeTurnEventHandlers(task, turn) {
960
+ turn.client.setEventHandlers({
961
+ onSessionUpdate: (notification) => {
962
+ turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
963
+ trimConversationForRuntime(turn.conversation);
964
+ turn.liveCheckpoint.request();
965
+ this.emitRuntimeTurnEvent(task, {
966
+ jsonrpc: "2.0",
967
+ method: "session/update",
968
+ params: notification
969
+ });
970
+ },
971
+ onClientOperation: (operation) => {
972
+ turn.acpxState = recordClientOperation(turn.conversation, turn.acpxState, operation);
973
+ trimConversationForRuntime(turn.conversation);
974
+ turn.liveCheckpoint.request();
975
+ this.emitRuntimeTurnEvent(task, {
976
+ type: "client_operation",
977
+ ...operation
978
+ });
979
+ }
980
+ });
981
+ }
982
+ emitRuntimeTurnEvent(task, payload) {
983
+ const parsed = parsePromptEventLine(JSON.stringify(payload));
984
+ if (!parsed) return;
985
+ task.queue.push(parsed);
986
+ }
987
+ async connectRuntimeTurn(task, turn) {
988
+ const loaded = turn.pendingClient ? {
989
+ sessionId: turn.record.acpSessionId,
990
+ resumed: false,
991
+ loadError: void 0
992
+ } : await this.connectRuntimeTurnClient(task, turn);
993
+ turn.acpxState = cloneSessionAcpxState(turn.record.acpx);
994
+ return loaded;
995
+ }
996
+ async connectRuntimeTurnClient(task, turn) {
997
+ return await connectAndLoadSession({
998
+ client: turn.client,
999
+ record: turn.record,
1000
+ resumePolicy: resumePolicyForSessionMode(task.input.sessionMode),
1001
+ timeoutMs: this.options.timeoutMs,
1002
+ activeController: task.state.activeController,
1003
+ onClientAvailable: () => this.publishRuntimeTurnController(task, turn),
1004
+ onConnectedRecord: (connectedRecord) => {
1005
+ connectedRecord.lastPromptAt = isoNow();
1006
+ },
1007
+ onSessionIdResolved: (sessionIdValue) => {
1008
+ turn.activeSessionId = sessionIdValue;
1009
+ }
1010
+ });
1011
+ }
1012
+ publishRuntimeTurnController(task, turn) {
1013
+ const controller = task.state.activeController;
1014
+ if (controller) this.activeControllers.set(turn.record.acpxRecordId, controller);
1015
+ }
1016
+ async resolveRuntimeTurnReady(task, turn, resumed, loadError) {
1017
+ task.sessionReady.resolve();
1018
+ turn.record.lastRequestId = task.input.requestId;
1019
+ turn.record.lastPromptAt = isoNow();
1020
+ turn.record.closed = false;
1021
+ turn.record.closedAt = void 0;
1022
+ turn.record.lastUsedAt = isoNow();
1023
+ await turn.liveCheckpoint.checkpoint();
1024
+ this.emitRuntimeTurnLoadStatus(task, resumed, loadError);
1025
+ }
1026
+ emitRuntimeTurnLoadStatus(task, resumed, loadError) {
1027
+ if (!resumed && !loadError) return;
1028
+ this.emitRuntimeTurnEvent(task, {
1029
+ type: "status",
1030
+ text: loadError ? `session reconnect fallback: ${loadError}` : "session resumed"
1031
+ });
1032
+ }
1033
+ cancelRuntimeTurnBeforePrompt(task) {
1034
+ if (!task.state.pendingCancel && !task.input.signal?.aborted) return false;
1035
+ task.state.pendingCancel = false;
1036
+ task.settleResult({
1037
+ status: "cancelled",
1038
+ stopReason: "cancelled"
1039
+ });
1040
+ return true;
1041
+ }
1042
+ async applyPendingRuntimeTurnCancel(task, turn) {
1043
+ if (!task.state.pendingCancel || !turn.client.hasActivePrompt()) return false;
1044
+ const cancelled = await turn.client.requestCancelActivePrompt();
1045
+ if (cancelled) task.state.pendingCancel = false;
1046
+ return cancelled;
1047
+ }
1048
+ async saveCompletedRuntimeTurn(turn, _stopReason) {
1049
+ turn.record.acpSessionId = turn.activeSessionId;
1050
+ reconcileAgentSessionId(turn.record, turn.record.agentSessionId);
1051
+ turn.record.protocolVersion = turn.client.initializeResult?.protocolVersion;
1052
+ turn.record.agentCapabilities = turn.client.initializeResult?.agentCapabilities;
1053
+ turn.record.acpx = turn.acpxState;
1054
+ applyConversation(turn.record, turn.conversation);
1055
+ applyLifecycleSnapshotToRecord(turn.record, turn.client.getAgentLifecycleSnapshot());
1056
+ await this.options.sessionStore.save(turn.record);
1057
+ }
1058
+ failRuntimeTurn(task, error) {
1059
+ task.sessionReady.reject(error);
1060
+ const normalized = normalizeOutputError(error, { origin: "runtime" });
1061
+ task.settleResult({
1062
+ status: "failed",
1063
+ error: {
1064
+ message: normalized.message,
1065
+ ...normalized.code ? { code: normalized.code } : {},
1066
+ ...normalized.detailCode ? { detailCode: normalized.detailCode } : {},
1067
+ ...normalized.retryable !== void 0 ? { retryable: normalized.retryable } : {}
1068
+ }
1069
+ });
1070
+ }
1071
+ async finalizeRuntimeTurn(task, turn) {
1072
+ task.state.turnActive = false;
1073
+ task.input.signal?.removeEventListener("abort", task.abortHandler);
1074
+ turn?.client.clearEventHandlers();
1075
+ if (!(turn ? await this.finalizeRuntimeTurnRecord(turn) : false)) await turn?.client.close().catch(() => {});
1076
+ if (turn) {
1077
+ this.activeControllers.delete(turn.record.acpxRecordId);
1078
+ this.closingActiveRecords.delete(turn.record.acpxRecordId);
1079
+ }
1080
+ task.queue.close();
1081
+ }
1082
+ async finalizeRuntimeTurnRecord(turn) {
1083
+ applyLifecycleSnapshotToRecord(turn.record, turn.client.getAgentLifecycleSnapshot());
1084
+ turn.record.acpx = turn.acpxState;
1085
+ applyConversation(turn.record, turn.conversation);
1086
+ turn.record.lastUsedAt = isoNow();
1087
+ await turn.liveCheckpoint.flush().catch(() => {});
1088
+ const closed = await this.refreshClosedState(turn.record);
1089
+ await this.options.sessionStore.save(turn.record).catch(() => {});
1090
+ if (closed) return false;
1091
+ return await this.retainPersistentClientAfterTurn({
1092
+ record: turn.record,
1093
+ client: turn.client
1094
+ });
1095
+ }
948
1096
  async *runTurn(input) {
949
1097
  const turn = this.startTurn(input);
950
1098
  yield* turn.events;
@@ -1125,21 +1273,30 @@ function writeHandleState(handle, state) {
1125
1273
  }
1126
1274
  //#endregion
1127
1275
  //#region src/runtime/public/probe.ts
1276
+ function isPrimitiveDetail(value) {
1277
+ return value == null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol";
1278
+ }
1279
+ function formatFunctionDetail(value) {
1280
+ return value.name ? `[Function ${value.name}]` : "[Function]";
1281
+ }
1282
+ function serializeRuntimeDetail(value) {
1283
+ const seen = /* @__PURE__ */ new WeakSet();
1284
+ return JSON.stringify(value, (_key, nested) => {
1285
+ if (nested instanceof Error) return nested.message || nested.name;
1286
+ if (nested && typeof nested === "object") {
1287
+ if (seen.has(nested)) return "[Circular]";
1288
+ seen.add(nested);
1289
+ }
1290
+ return nested;
1291
+ }) ?? "undefined";
1292
+ }
1128
1293
  function formatRuntimeDetail(value) {
1129
1294
  if (value instanceof Error) return value.message || value.name;
1130
1295
  if (typeof value === "string") return value;
1131
- if (value == null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") return String(value);
1132
- if (typeof value === "function") return value.name ? `[Function ${value.name}]` : "[Function]";
1133
- const seen = /* @__PURE__ */ new WeakSet();
1296
+ if (isPrimitiveDetail(value)) return String(value);
1297
+ if (typeof value === "function") return formatFunctionDetail(value);
1134
1298
  try {
1135
- return JSON.stringify(value, (_key, nested) => {
1136
- if (nested instanceof Error) return nested.message || nested.name;
1137
- if (nested && typeof nested === "object") {
1138
- if (seen.has(nested)) return "[Circular]";
1139
- seen.add(nested);
1140
- }
1141
- return nested;
1142
- }) ?? "undefined";
1299
+ return serializeRuntimeDetail(value);
1143
1300
  } catch {
1144
1301
  return "unserializable object";
1145
1302
  }
@@ -1150,21 +1307,7 @@ function normalizeRuntimeDetails(details) {
1150
1307
  async function probeRuntime(options, deps = {}) {
1151
1308
  const agentName = options.probeAgent?.trim() || "codex";
1152
1309
  const agentCommand = options.agentRegistry.resolve(agentName);
1153
- const client = deps.clientFactory?.({
1154
- agentCommand,
1155
- cwd: options.cwd,
1156
- mcpServers: [...options.mcpServers ?? []],
1157
- permissionMode: options.permissionMode,
1158
- nonInteractivePermissions: options.nonInteractivePermissions,
1159
- verbose: options.verbose
1160
- }) ?? new AcpClient({
1161
- agentCommand,
1162
- cwd: options.cwd,
1163
- mcpServers: [...options.mcpServers ?? []],
1164
- permissionMode: options.permissionMode,
1165
- nonInteractivePermissions: options.nonInteractivePermissions,
1166
- verbose: options.verbose
1167
- });
1310
+ const client = createProbeClient(options, agentCommand, deps);
1168
1311
  try {
1169
1312
  await client.start();
1170
1313
  return {
@@ -1192,6 +1335,17 @@ async function probeRuntime(options, deps = {}) {
1192
1335
  await client.close().catch(() => {});
1193
1336
  }
1194
1337
  }
1338
+ function createProbeClient(options, agentCommand, deps) {
1339
+ const clientOptions = {
1340
+ agentCommand,
1341
+ cwd: options.cwd,
1342
+ mcpServers: [...options.mcpServers ?? []],
1343
+ permissionMode: options.permissionMode,
1344
+ nonInteractivePermissions: options.nonInteractivePermissions,
1345
+ verbose: options.verbose
1346
+ };
1347
+ return deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
1348
+ }
1195
1349
  //#endregion
1196
1350
  //#region src/runtime.ts
1197
1351
  const ACPX_BACKEND_ID = "acpx";