acpx 0.7.0 → 0.9.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 (50) hide show
  1. package/README.md +14 -6
  2. package/dist/{cli-T-Z-9x6a.js → cli-Bf3yjqzE.js} +35 -10
  3. package/dist/cli-Bf3yjqzE.js.map +1 -0
  4. package/dist/cli.d.ts +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +724 -241
  7. package/dist/cli.js.map +1 -1
  8. package/dist/{client-COPilhO_.d.ts → client-BssohYqM.d.ts} +35 -4
  9. package/dist/client-BssohYqM.d.ts.map +1 -0
  10. package/dist/flags-C-rwARqg.js +260 -0
  11. package/dist/flags-C-rwARqg.js.map +1 -0
  12. package/dist/{flows-CF8w1rPI.js → flows-WLs26_5Y.js} +405 -337
  13. package/dist/flows-WLs26_5Y.js.map +1 -0
  14. package/dist/flows.d.ts +23 -2
  15. package/dist/flows.d.ts.map +1 -1
  16. package/dist/flows.js +1 -1
  17. package/dist/{prompt-turn-CVPMWdj1.js → live-checkpoint-D5d-K9s1.js} +2487 -609
  18. package/dist/live-checkpoint-D5d-K9s1.js.map +1 -0
  19. package/dist/output-DPg20dvn.js +4146 -0
  20. package/dist/output-DPg20dvn.js.map +1 -0
  21. package/dist/runtime.d.ts +56 -4
  22. package/dist/runtime.d.ts.map +1 -1
  23. package/dist/runtime.js +676 -393
  24. package/dist/runtime.js.map +1 -1
  25. package/dist/{types-CVBeQyi3.d.ts → session-options-CFudjdkU.d.ts} +62 -3
  26. package/dist/session-options-CFudjdkU.d.ts.map +1 -0
  27. package/package.json +30 -25
  28. package/skills/acpx/SKILL.md +211 -13
  29. package/dist/cli-T-Z-9x6a.js.map +0 -1
  30. package/dist/client-COPilhO_.d.ts.map +0 -1
  31. package/dist/flags-Dj-IXgo9.js +0 -163
  32. package/dist/flags-Dj-IXgo9.js.map +0 -1
  33. package/dist/flows-CF8w1rPI.js.map +0 -1
  34. package/dist/ipc-ABXlXzGP.js +0 -1290
  35. package/dist/ipc-ABXlXzGP.js.map +0 -1
  36. package/dist/jsonrpc-DSxh2w5R.js +0 -68
  37. package/dist/jsonrpc-DSxh2w5R.js.map +0 -1
  38. package/dist/output-DmHvT8vm.js +0 -807
  39. package/dist/output-DmHvT8vm.js.map +0 -1
  40. package/dist/perf-metrics-C2pXfxvR.js +0 -598
  41. package/dist/perf-metrics-C2pXfxvR.js.map +0 -1
  42. package/dist/prompt-turn-CVPMWdj1.js.map +0 -1
  43. package/dist/render-N5YwotCy.js +0 -172
  44. package/dist/render-N5YwotCy.js.map +0 -1
  45. package/dist/rolldown-runtime-CiIaOW0V.js +0 -13
  46. package/dist/session-CDaQe6BH.js +0 -1538
  47. package/dist/session-CDaQe6BH.js.map +0 -1
  48. package/dist/session-options-pCbHn_n7.d.ts +0 -13
  49. package/dist/session-options-pCbHn_n7.d.ts.map +0 -1
  50. package/dist/types-CVBeQyi3.d.ts.map +0 -1
package/dist/runtime.js CHANGED
@@ -1,5 +1,4 @@
1
- import { C as isAcpResourceNotFoundError, S as extractAcpError, g as textPrompt, x as normalizeOutputError } from "./perf-metrics-C2pXfxvR.js";
2
- import { $ as resolveAgentCommand, C as AcpClient, G as serializeSessionRecordForDisk, L as parseSessionRecord, S as trimConversationForRuntime, W as assertPersistedKeyPolicy, X as DEFAULT_AGENT_NAME, Y as withTimeout, Z as listBuiltInAgents, _ as cloneSessionConversation, a as connectAndLoadSession, b as recordPromptSubmission, c as reconcileAgentSessionId, d as setDesiredConfigOption, f as setDesiredModeId, g as cloneSessionAcpxState, h as applyConfigOptionsToRecord, n as withConnectedSession, o as applyConversation, s as applyLifecycleSnapshotToRecord, t as runPromptTurn, v as createSessionConversation, x as recordSessionUpdate, y as recordClientOperation, z as defaultSessionEventLog } from "./prompt-turn-CVPMWdj1.js";
1
+ import { C as applyConversation, Dt as isAcpResourceNotFoundError, E as AcpClient, Et as extractAcpError, K as defaultSessionEventLog, T as reconcileAgentSessionId, Tt as normalizeOutputError, W as parseSessionRecord, Z as assertPersistedKeyPolicy, _ 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, ot as serializeSessionRecordForDisk, 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-D5d-K9s1.js";
3
2
  import path from "node:path";
4
3
  import fs from "node:fs/promises";
5
4
  import { randomUUID } from "node:crypto";
@@ -37,6 +36,7 @@ function deriveAgentFromSessionKey(sessionKey, fallbackAgent) {
37
36
  }
38
37
  //#endregion
39
38
  //#region src/runtime/public/events.ts
39
+ const TOOL_OUTPUT_SUMMARY_MAX_CHARS = 500;
40
40
  function safeParseJsonObject(line) {
41
41
  try {
42
42
  const parsed = JSON.parse(line);
@@ -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;
@@ -169,20 +177,108 @@ function summarizeToolInput(rawInput) {
169
177
  "search"
170
178
  ]);
171
179
  }
180
+ function truncateToolSummary(value) {
181
+ if (value.length <= TOOL_OUTPUT_SUMMARY_MAX_CHARS) return value;
182
+ return `${value.slice(0, TOOL_OUTPUT_SUMMARY_MAX_CHARS - 1)}…`;
183
+ }
184
+ function readToolContentText(value) {
185
+ const record = isRecord(value) ? value : void 0;
186
+ if (!record) return;
187
+ if (record.type === "content") return readToolContentText(record.content);
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) => {
195
+ const resource = isRecord(record.resource) ? record.resource : void 0;
196
+ return asString(resource?.text) || asOptionalString(resource?.uri);
197
+ },
198
+ diff: (record) => `diff ${asOptionalString(record.path) || "file"}`,
199
+ terminal: (record) => {
200
+ const terminalId = asOptionalString(record.terminalId) || asOptionalString(record.id);
201
+ return terminalId ? `[terminal] ${terminalId}` : "[terminal]";
202
+ }
203
+ };
204
+ function toolContentTextReader(type) {
205
+ return Object.hasOwn(TOOL_CONTENT_TEXT_READERS, type) ? TOOL_CONTENT_TEXT_READERS[type] : void 0;
206
+ }
207
+ function summarizeToolContent(content) {
208
+ if (!Array.isArray(content)) return;
209
+ const fragments = content.map((entry) => readToolContentText(entry)?.trim()).filter((entry) => Boolean(entry));
210
+ if (fragments.length === 0) return;
211
+ return truncateToolSummary([...new Set(fragments)].join("\n"));
212
+ }
213
+ function summarizeToolOutput(rawOutput) {
214
+ if (rawOutput == null) return;
215
+ if (isScalarToolOutput(rawOutput)) return truncateToolSummary(String(rawOutput));
216
+ const record = isRecord(rawOutput) ? rawOutput : void 0;
217
+ if (!record) return;
218
+ return truncateToolSummary(readFirstString(record, [
219
+ "text",
220
+ "message",
221
+ "error",
222
+ "stdout",
223
+ "stderr",
224
+ "content"
225
+ ]) ?? "") || void 0;
226
+ }
227
+ function isScalarToolOutput(value) {
228
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
229
+ }
230
+ function shouldForwardArray(value) {
231
+ return Array.isArray(value);
232
+ }
233
+ function readToolKind(value) {
234
+ const kind = asOptionalString(value);
235
+ return kind && TOOL_KINDS.has(kind) ? kind : void 0;
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
+ ]);
172
248
  function createToolCallEvent(params) {
173
249
  const title = asTrimmedString(params.payload.title) || "tool call";
174
250
  const status = asTrimmedString(params.payload.status);
175
251
  const inputSummary = summarizeToolInput(params.payload.rawInput);
252
+ const outputSummary = summarizeToolContent(params.payload.content) ?? summarizeToolOutput(params.payload.rawOutput);
176
253
  const toolCallId = asOptionalString(params.payload.toolCallId);
254
+ const kind = readToolKind(params.payload.kind);
177
255
  const summaryText = status ? `${title} (${status})` : title;
178
- return {
256
+ const detailSummary = params.tag === "tool_call_update" ? outputSummary ?? inputSummary : inputSummary ?? outputSummary;
257
+ const event = {
179
258
  type: "tool_call",
180
- text: inputSummary ? `${summaryText}: ${inputSummary}` : summaryText,
259
+ text: detailSummary ? `${summaryText}: ${detailSummary}` : summaryText,
181
260
  tag: params.tag,
182
- ...toolCallId ? { toolCallId } : {},
183
- ...status ? { status } : {},
184
261
  title
185
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;
186
282
  }
187
283
  function parsePromptEventLine(line) {
188
284
  const trimmed = line.trim();
@@ -196,88 +292,94 @@ function parsePromptEventLine(line) {
196
292
  const type = structured.type;
197
293
  const payload = structured.payload;
198
294
  const tag = structured.tag;
199
- switch (type) {
200
- case "text": return createTextDeltaEvent({
201
- content: asString(payload.content),
202
- stream: "output",
203
- tag
204
- });
205
- case "thought": return createTextDeltaEvent({
206
- content: asString(payload.content),
207
- stream: "thought",
208
- tag
209
- });
210
- case "tool_call": return createToolCallEvent({
211
- payload,
212
- tag: tag ?? "tool_call"
213
- });
214
- case "tool_call_update": return createToolCallEvent({
215
- payload,
216
- tag: tag ?? "tool_call_update"
217
- });
218
- case "agent_message_chunk": return resolveTextChunk({
219
- payload,
220
- stream: "output",
221
- tag: "agent_message_chunk"
222
- });
223
- case "agent_thought_chunk": return resolveTextChunk({
224
- payload,
225
- stream: "thought",
226
- tag: "agent_thought_chunk"
227
- });
228
- case "usage_update": {
229
- const used = asOptionalFiniteNumber(payload.used);
230
- const size = asOptionalFiniteNumber(payload.size);
231
- return {
232
- type: "status",
233
- text: used != null && size != null ? `usage updated: ${used}/${size}` : "usage updated",
234
- tag: "usage_update",
235
- ...used != null ? { used } : {},
236
- ...size != null ? { size } : {}
237
- };
238
- }
239
- case "available_commands_update":
240
- case "current_mode_update":
241
- case "config_option_update":
242
- case "session_info_update":
243
- case "plan": {
244
- const text = resolveStatusTextForTag({
245
- tag: type,
246
- payload
247
- });
248
- if (!text) return null;
249
- return {
250
- type: "status",
251
- text,
252
- tag: type
253
- };
254
- }
255
- case "client_operation": {
256
- const text = [
257
- asTrimmedString(payload.method) || "operation",
258
- asTrimmedString(payload.status),
259
- asTrimmedString(payload.summary)
260
- ].filter(Boolean).join(" ");
261
- if (!text) return null;
262
- return {
263
- type: "status",
264
- text,
265
- ...tag ? { tag } : {}
266
- };
267
- }
268
- case "update": {
269
- const update = asTrimmedString(payload.update);
270
- if (!update) return null;
271
- return {
272
- type: "status",
273
- text: update,
274
- ...tag ? { tag } : {}
275
- };
276
- }
277
- case "done":
278
- case "error": return null;
279
- default: return null;
280
- }
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;
281
383
  }
282
384
  //#endregion
283
385
  //#region src/runtime/engine/reuse-policy.ts
@@ -357,12 +459,23 @@ function toPromptInput(text, attachments) {
357
459
  text
358
460
  });
359
461
  for (const attachment of attachments) {
360
- if (!attachment.mediaType.startsWith("image/")) throw new AcpRuntimeError("ACP_TURN_FAILED", `Unsupported ACP runtime attachment media type: ${attachment.mediaType}`);
361
- blocks.push({
362
- type: "image",
363
- mimeType: attachment.mediaType,
364
- data: attachment.data
365
- });
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}`);
366
479
  }
367
480
  return blocks.length > 0 ? blocks : textPrompt(text);
368
481
  }
@@ -415,7 +528,60 @@ function statusSummary(record) {
415
528
  record.closed ? "closed" : "open"
416
529
  ].filter(Boolean).join(" ");
417
530
  }
531
+ function buildModelsField(record) {
532
+ const available = record.acpx?.available_models;
533
+ const currentModelId = record.acpx?.current_model_id;
534
+ if (!available || available.length === 0) return currentModelId === void 0 ? {} : { models: {
535
+ currentModelId,
536
+ availableModelIds: []
537
+ } };
538
+ return { models: {
539
+ ...currentModelId !== void 0 ? { currentModelId } : {},
540
+ availableModelIds: [...available]
541
+ } };
542
+ }
543
+ function advertisedConfigOptionIds(record) {
544
+ const configOptions = record.acpx?.config_options;
545
+ if (!configOptions) return;
546
+ return new Set(configOptions.map((option) => option.id).filter((id) => typeof id === "string" && id.trim().length > 0));
547
+ }
548
+ function resolveSupportedConfigOptionId(record, configId) {
549
+ const advertisedIds = advertisedConfigOptionIds(record);
550
+ if (!advertisedIds) return configId;
551
+ if (advertisedIds.has(configId)) return configId;
552
+ if (configId === "thinking" && advertisedIds.has("effort")) return "effort";
553
+ const supported = [...advertisedIds].toSorted();
554
+ const supportedText = supported.length > 0 ? supported.join(", ") : "none";
555
+ throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `ACP session ${record.acpxRecordId} does not advertise config option '${configId}'. Supported config options: ${supportedText}.`);
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
+ }
418
582
  var AcpRuntimeManager = class {
583
+ options;
584
+ deps;
419
585
  activeControllers = /* @__PURE__ */ new Map();
420
586
  pendingPersistentClients = /* @__PURE__ */ new Map();
421
587
  closingActiveRecords = /* @__PURE__ */ new Set();
@@ -489,6 +655,7 @@ var AcpRuntimeManager = class {
489
655
  mcpServers: [...this.options.mcpServers ?? []],
490
656
  permissionMode: this.options.permissionMode,
491
657
  nonInteractivePermissions: this.options.nonInteractivePermissions,
658
+ onPermissionRequest: this.options.onPermissionRequest,
492
659
  verbose: this.options.verbose,
493
660
  timeoutMs: this.options.timeoutMs,
494
661
  resumePolicy: resumePolicyForSessionMode(sessionMode),
@@ -520,50 +687,63 @@ var AcpRuntimeManager = class {
520
687
  mcpServers: [...this.options.mcpServers ?? []],
521
688
  permissionMode: this.options.permissionMode,
522
689
  nonInteractivePermissions: this.options.nonInteractivePermissions,
523
- verbose: this.options.verbose
690
+ onPermissionRequest: this.options.onPermissionRequest,
691
+ verbose: this.options.verbose,
692
+ sessionOptions: input.sessionOptions
524
693
  });
525
694
  let keepClientOpen = false;
526
695
  try {
527
696
  await client.start();
528
- let sessionId;
529
- let agentSessionId;
530
- let sessionResult;
531
- if (input.resumeSessionId) {
532
- const loaded = await client.loadSession(input.resumeSessionId, cwd);
533
- sessionId = input.resumeSessionId;
534
- agentSessionId = loaded.agentSessionId;
535
- sessionResult = loaded;
536
- } else {
537
- const created = await client.createSession(cwd);
538
- sessionId = created.sessionId;
539
- agentSessionId = created.agentSessionId;
540
- sessionResult = created;
541
- }
542
- const record = createInitialRecord({
543
- recordId: createRecordId(input.sessionKey, input.mode),
544
- sessionName: input.sessionKey,
545
- sessionId,
697
+ const session = await createOrLoadRuntimeSession(client, input.resumeSessionId, cwd);
698
+ const record = await this.createAndSaveRuntimeRecord({
699
+ input,
700
+ client,
546
701
  agentCommand,
547
702
  cwd,
548
- agentSessionId
703
+ session
549
704
  });
550
- this.closingActiveRecords.delete(record.acpxRecordId);
551
- record.protocolVersion = client.initializeResult?.protocolVersion;
552
- record.agentCapabilities = client.initializeResult?.agentCapabilities;
553
- applyConfigOptionsToRecord(record, sessionResult);
554
- applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
555
- await this.options.sessionStore.save(record);
556
- if (input.mode === "persistent") {
557
- const previousClient = this.pendingPersistentClients.get(record.acpxRecordId);
558
- this.pendingPersistentClients.set(record.acpxRecordId, client);
559
- keepClientOpen = true;
560
- await previousClient?.close().catch(() => {});
561
- }
705
+ keepClientOpen = await this.keepPersistentClient(input.mode, record.acpxRecordId, client);
562
706
  return record;
563
707
  } finally {
564
708
  if (!keepClientOpen) await client.close();
565
709
  }
566
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
+ }
567
747
  startTurn(input) {
568
748
  const promptInput = toPromptInput(input.text, input.attachments);
569
749
  const queue = new AsyncEventQueue();
@@ -571,10 +751,12 @@ var AcpRuntimeManager = class {
571
751
  const sessionReady = createDeferred();
572
752
  sessionReady.promise.catch(() => {});
573
753
  let resultSettled = false;
574
- let pendingCancel = false;
575
- let turnActive = true;
754
+ const state = {
755
+ pendingCancel: false,
756
+ turnActive: true,
757
+ activeController: null
758
+ };
576
759
  let streamClosed = false;
577
- let activeController = null;
578
760
  const settleResult = (next) => {
579
761
  if (resultSettled) return;
580
762
  resultSettled = true;
@@ -587,9 +769,9 @@ var AcpRuntimeManager = class {
587
769
  queue.close();
588
770
  };
589
771
  const requestCancel = async () => {
590
- if (activeController) return await activeController.requestCancelActivePrompt();
591
- if (!turnActive) return false;
592
- pendingCancel = true;
772
+ if (state.activeController) return await state.activeController.requestCancelActivePrompt();
773
+ if (!state.turnActive) return false;
774
+ state.pendingCancel = true;
593
775
  return true;
594
776
  };
595
777
  const abortHandler = () => {
@@ -612,207 +794,15 @@ var AcpRuntimeManager = class {
612
794
  }
613
795
  input.signal.addEventListener("abort", abortHandler, { once: true });
614
796
  }
615
- (async () => {
616
- let record = null;
617
- let conversation = null;
618
- let acpxState;
619
- let client = null;
620
- try {
621
- record = await this.requireRecord(input.handle.acpxRecordId ?? input.handle.sessionKey);
622
- conversation = cloneSessionConversation(record);
623
- acpxState = cloneSessionAcpxState(record.acpx);
624
- const promptStartedAt = isoNow();
625
- const promptMessageId = recordPromptSubmission(conversation, promptInput, promptStartedAt);
626
- trimConversationForRuntime(conversation);
627
- record.lastPromptAt = promptStartedAt;
628
- record.lastUsedAt = promptStartedAt;
629
- record.acpx = acpxState;
630
- applyConversation(record, conversation);
631
- await this.options.sessionStore.save(record);
632
- const pendingClient = await this.readPendingPersistentClient(record, { consume: true });
633
- client = pendingClient ?? this.createClient({
634
- agentCommand: record.agentCommand,
635
- cwd: record.cwd,
636
- mcpServers: [...this.options.mcpServers ?? []],
637
- permissionMode: this.options.permissionMode,
638
- nonInteractivePermissions: this.options.nonInteractivePermissions,
639
- verbose: this.options.verbose
640
- });
641
- const runtimeClient = client;
642
- const runtimeConversation = conversation;
643
- const runtimeRecord = record;
644
- let activeSessionId = record.acpSessionId;
645
- const applyPendingCancel = async () => {
646
- if (!pendingCancel || !runtimeClient.hasActivePrompt()) return false;
647
- const cancelled = await runtimeClient.requestCancelActivePrompt();
648
- if (cancelled) pendingCancel = false;
649
- return cancelled;
650
- };
651
- activeController = {
652
- hasActivePrompt: () => runtimeClient.hasActivePrompt(),
653
- requestCancelActivePrompt: async () => {
654
- if (runtimeClient.hasActivePrompt()) return await runtimeClient.requestCancelActivePrompt();
655
- if (!turnActive) return false;
656
- pendingCancel = true;
657
- return true;
658
- },
659
- setSessionMode: async (modeId) => {
660
- if (!runtimeClient.hasActivePrompt()) await sessionReady.promise;
661
- await runtimeClient.setSessionMode(activeSessionId, modeId);
662
- const nextState = cloneSessionAcpxState(acpxState) ?? {};
663
- nextState.desired_mode_id = modeId;
664
- acpxState = nextState;
665
- },
666
- setSessionModel: async (modelId) => {
667
- if (!runtimeClient.hasActivePrompt()) await sessionReady.promise;
668
- await runtimeClient.setSessionModel(activeSessionId, modelId);
669
- },
670
- setSessionConfigOption: async (configId, value) => {
671
- if (!runtimeClient.hasActivePrompt()) await sessionReady.promise;
672
- const response = await runtimeClient.setSessionConfigOption(activeSessionId, configId, value);
673
- if (response?.configOptions) {
674
- const nextState = cloneSessionAcpxState(acpxState) ?? {};
675
- nextState.config_options = structuredClone(response.configOptions);
676
- acpxState = nextState;
677
- }
678
- if (configId === "mode") {
679
- const nextState = cloneSessionAcpxState(acpxState) ?? {};
680
- nextState.desired_mode_id = value;
681
- acpxState = nextState;
682
- } else if (configId !== "model") {
683
- const nextState = cloneSessionAcpxState(acpxState) ?? {};
684
- nextState.desired_config_options = {
685
- ...nextState.desired_config_options,
686
- [configId]: value
687
- };
688
- acpxState = nextState;
689
- }
690
- return response;
691
- }
692
- };
693
- const emitParsed = (payload) => {
694
- if (streamClosed) return;
695
- const parsed = parsePromptEventLine(JSON.stringify(payload));
696
- if (!parsed) return;
697
- queue.push(parsed);
698
- };
699
- this.activeControllers.set(runtimeRecord.acpxRecordId, activeController);
700
- runtimeClient.setEventHandlers({
701
- onSessionUpdate: (notification) => {
702
- acpxState = recordSessionUpdate(runtimeConversation, acpxState, notification);
703
- trimConversationForRuntime(runtimeConversation);
704
- emitParsed({
705
- jsonrpc: "2.0",
706
- method: "session/update",
707
- params: notification
708
- });
709
- },
710
- onClientOperation: (operation) => {
711
- acpxState = recordClientOperation(runtimeConversation, acpxState, operation);
712
- trimConversationForRuntime(runtimeConversation);
713
- emitParsed({
714
- type: "client_operation",
715
- ...operation
716
- });
717
- }
718
- });
719
- const { sessionId, resumed, loadError } = pendingClient ? {
720
- sessionId: record.acpSessionId,
721
- resumed: false,
722
- loadError: void 0
723
- } : await connectAndLoadSession({
724
- client: runtimeClient,
725
- record: runtimeRecord,
726
- resumePolicy: resumePolicyForSessionMode(input.sessionMode),
727
- timeoutMs: this.options.timeoutMs,
728
- activeController,
729
- onClientAvailable: (controller) => {
730
- activeController = controller;
731
- this.activeControllers.set(runtimeRecord.acpxRecordId, controller);
732
- },
733
- onConnectedRecord: (connectedRecord) => {
734
- connectedRecord.lastPromptAt = isoNow();
735
- },
736
- onSessionIdResolved: (sessionIdValue) => {
737
- activeSessionId = sessionIdValue;
738
- }
739
- });
740
- sessionReady.resolve();
741
- runtimeRecord.lastRequestId = input.requestId;
742
- runtimeRecord.lastPromptAt = isoNow();
743
- runtimeRecord.closed = false;
744
- runtimeRecord.closedAt = void 0;
745
- runtimeRecord.lastUsedAt = isoNow();
746
- if (resumed || loadError) emitParsed({
747
- type: "status",
748
- text: loadError ? `load fallback: ${loadError}` : "session resumed"
749
- });
750
- if (pendingCancel || input.signal?.aborted) {
751
- pendingCancel = false;
752
- settleResult({
753
- status: "cancelled",
754
- stopReason: "cancelled"
755
- });
756
- return;
757
- }
758
- await applyPendingCancel();
759
- const response = await runPromptTurn({
760
- client: runtimeClient,
761
- sessionId,
762
- prompt: promptInput,
763
- timeoutMs: input.timeoutMs ?? this.options.timeoutMs,
764
- conversation: runtimeConversation,
765
- promptMessageId
766
- });
767
- runtimeRecord.acpSessionId = activeSessionId;
768
- reconcileAgentSessionId(runtimeRecord, runtimeRecord.agentSessionId);
769
- runtimeRecord.protocolVersion = runtimeClient.initializeResult?.protocolVersion;
770
- runtimeRecord.agentCapabilities = runtimeClient.initializeResult?.agentCapabilities;
771
- runtimeRecord.acpx = acpxState;
772
- applyConversation(runtimeRecord, runtimeConversation);
773
- applyLifecycleSnapshotToRecord(runtimeRecord, runtimeClient.getAgentLifecycleSnapshot());
774
- await this.options.sessionStore.save(runtimeRecord);
775
- settleResult({
776
- status: response.stopReason === "cancelled" ? "cancelled" : "completed",
777
- ...response.stopReason ? { stopReason: response.stopReason } : {}
778
- });
779
- } catch (error) {
780
- sessionReady.reject(error);
781
- const normalized = normalizeOutputError(error, { origin: "runtime" });
782
- settleResult({
783
- status: "failed",
784
- error: {
785
- message: normalized.message,
786
- ...normalized.code ? { code: normalized.code } : {},
787
- ...normalized.detailCode ? { detailCode: normalized.detailCode } : {},
788
- ...normalized.retryable !== void 0 ? { retryable: normalized.retryable } : {}
789
- }
790
- });
791
- } finally {
792
- turnActive = false;
793
- if (input.signal) input.signal.removeEventListener("abort", abortHandler);
794
- client?.clearEventHandlers();
795
- let pooled = false;
796
- if (record && conversation) {
797
- applyLifecycleSnapshotToRecord(record, client?.getAgentLifecycleSnapshot() ?? { running: false });
798
- record.acpx = acpxState;
799
- applyConversation(record, conversation);
800
- record.lastUsedAt = isoNow();
801
- const closed = await this.refreshClosedState(record);
802
- await this.options.sessionStore.save(record).catch(() => {});
803
- if (!closed && client) pooled = await this.retainPersistentClientAfterTurn({
804
- record,
805
- client
806
- });
807
- }
808
- if (!pooled) await client?.close().catch(() => {});
809
- if (record) {
810
- this.activeControllers.delete(record.acpxRecordId);
811
- this.closingActiveRecords.delete(record.acpxRecordId);
812
- }
813
- queue.close();
814
- }
815
- })();
797
+ this.runRuntimeTurnTask({
798
+ input,
799
+ promptInput,
800
+ queue,
801
+ sessionReady,
802
+ state,
803
+ settleResult,
804
+ abortHandler
805
+ });
816
806
  return {
817
807
  requestId: input.requestId,
818
808
  events: queue.iterate(),
@@ -825,6 +815,284 @@ var AcpRuntimeManager = class {
825
815
  }
826
816
  };
827
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
+ }
828
1096
  async *runTurn(input) {
829
1097
  const turn = this.startTurn(input);
830
1098
  yield* turn.events;
@@ -837,6 +1105,7 @@ var AcpRuntimeManager = class {
837
1105
  acpxRecordId: record.acpxRecordId,
838
1106
  backendSessionId: record.acpSessionId,
839
1107
  agentSessionId: record.agentSessionId,
1108
+ ...buildModelsField(record),
840
1109
  details: {
841
1110
  cwd: record.cwd,
842
1111
  lastUsedAt: record.lastUsedAt,
@@ -859,18 +1128,21 @@ var AcpRuntimeManager = class {
859
1128
  async setConfigOption(handle, key, value, sessionMode = "persistent") {
860
1129
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
861
1130
  const controller = this.activeControllers.get(record.acpxRecordId);
862
- let targetRecord = record;
863
1131
  if (controller) {
864
- const response = await controller.setSessionConfigOption(key, value);
865
- applyConfigOptionsToRecord(targetRecord, response);
866
- } else targetRecord = (await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
867
- applyConfigOptionsToRecord(connectedRecord, await client.setSessionConfigOption(sessionId, key, value));
868
- if (key === "mode") setDesiredModeId(connectedRecord, value);
869
- else setDesiredConfigOption(connectedRecord, key, value);
870
- })).record;
871
- if (key === "mode") setDesiredModeId(targetRecord, value);
872
- else setDesiredConfigOption(targetRecord, key, value);
873
- await this.options.sessionStore.save(targetRecord);
1132
+ const { configId, response } = await controller.setResolvedSessionConfigOption(key, value);
1133
+ applyConfigOptionsToRecord(record, response);
1134
+ if (configId === "mode") setDesiredModeId(record, value);
1135
+ else setDesiredConfigOption(record, configId, value);
1136
+ await this.options.sessionStore.save(record);
1137
+ return;
1138
+ }
1139
+ const result = await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
1140
+ const configId = resolveSupportedConfigOptionId(connectedRecord, key);
1141
+ applyConfigOptionsToRecord(connectedRecord, await client.setSessionConfigOption(sessionId, configId, value));
1142
+ if (configId === "mode") setDesiredModeId(connectedRecord, value);
1143
+ else setDesiredConfigOption(connectedRecord, configId, value);
1144
+ });
1145
+ await this.options.sessionStore.save(result.record);
874
1146
  }
875
1147
  async cancel(handle) {
876
1148
  await this.activeControllers.get(handle.acpxRecordId ?? handle.sessionKey)?.requestCancelActivePrompt();
@@ -898,6 +1170,7 @@ var AcpRuntimeManager = class {
898
1170
  mcpServers: [...this.options.mcpServers ?? []],
899
1171
  permissionMode: this.options.permissionMode,
900
1172
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1173
+ onPermissionRequest: this.options.onPermissionRequest,
901
1174
  verbose: this.options.verbose
902
1175
  });
903
1176
  try {
@@ -924,6 +1197,7 @@ function safeSessionId(sessionId) {
924
1197
  return encodeURIComponent(sessionId);
925
1198
  }
926
1199
  var FileSessionStore = class {
1200
+ stateDir;
927
1201
  constructor(stateDir) {
928
1202
  this.stateDir = stateDir;
929
1203
  }
@@ -999,21 +1273,30 @@ function writeHandleState(handle, state) {
999
1273
  }
1000
1274
  //#endregion
1001
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
+ }
1002
1293
  function formatRuntimeDetail(value) {
1003
1294
  if (value instanceof Error) return value.message || value.name;
1004
1295
  if (typeof value === "string") return value;
1005
- if (value == null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") return String(value);
1006
- if (typeof value === "function") return value.name ? `[Function ${value.name}]` : "[Function]";
1007
- const seen = /* @__PURE__ */ new WeakSet();
1296
+ if (isPrimitiveDetail(value)) return String(value);
1297
+ if (typeof value === "function") return formatFunctionDetail(value);
1008
1298
  try {
1009
- return JSON.stringify(value, (_key, nested) => {
1010
- if (nested instanceof Error) return nested.message || nested.name;
1011
- if (nested && typeof nested === "object") {
1012
- if (seen.has(nested)) return "[Circular]";
1013
- seen.add(nested);
1014
- }
1015
- return nested;
1016
- }) ?? "undefined";
1299
+ return serializeRuntimeDetail(value);
1017
1300
  } catch {
1018
1301
  return "unserializable object";
1019
1302
  }
@@ -1024,21 +1307,7 @@ function normalizeRuntimeDetails(details) {
1024
1307
  async function probeRuntime(options, deps = {}) {
1025
1308
  const agentName = options.probeAgent?.trim() || "codex";
1026
1309
  const agentCommand = options.agentRegistry.resolve(agentName);
1027
- const client = deps.clientFactory?.({
1028
- agentCommand,
1029
- cwd: options.cwd,
1030
- mcpServers: [...options.mcpServers ?? []],
1031
- permissionMode: options.permissionMode,
1032
- nonInteractivePermissions: options.nonInteractivePermissions,
1033
- verbose: options.verbose
1034
- }) ?? new AcpClient({
1035
- agentCommand,
1036
- cwd: options.cwd,
1037
- mcpServers: [...options.mcpServers ?? []],
1038
- permissionMode: options.permissionMode,
1039
- nonInteractivePermissions: options.nonInteractivePermissions,
1040
- verbose: options.verbose
1041
- });
1310
+ const client = createProbeClient(options, agentCommand, deps);
1042
1311
  try {
1043
1312
  await client.start();
1044
1313
  return {
@@ -1066,6 +1335,17 @@ async function probeRuntime(options, deps = {}) {
1066
1335
  await client.close().catch(() => {});
1067
1336
  }
1068
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
+ }
1069
1349
  //#endregion
1070
1350
  //#region src/runtime.ts
1071
1351
  const ACPX_BACKEND_ID = "acpx";
@@ -1085,6 +1365,8 @@ function createAgentRegistry(params) {
1085
1365
  };
1086
1366
  }
1087
1367
  var AcpxRuntime = class {
1368
+ options;
1369
+ testOptions;
1088
1370
  healthy = false;
1089
1371
  manager = null;
1090
1372
  managerPromise = null;
@@ -1119,7 +1401,8 @@ var AcpxRuntime = class {
1119
1401
  agent,
1120
1402
  mode: input.mode,
1121
1403
  cwd: input.cwd ?? this.options.cwd,
1122
- resumeSessionId: input.resumeSessionId
1404
+ resumeSessionId: input.resumeSessionId,
1405
+ sessionOptions: input.sessionOptions
1123
1406
  });
1124
1407
  const handle = {
1125
1408
  sessionKey: input.sessionKey,