acpx 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{cli-idpWyCOs.js → cli-DZjj5kij.js} +8 -12
  2. package/dist/cli-DZjj5kij.js.map +1 -0
  3. package/dist/cli.d.ts +2 -2
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +1017 -1009
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client-S8QicFBT.d.ts +119 -0
  8. package/dist/client-S8QicFBT.d.ts.map +1 -0
  9. package/dist/{flags-CCcX9fZj.js → flags-W3vEVSiS.js} +5 -55
  10. package/dist/flags-W3vEVSiS.js.map +1 -0
  11. package/dist/{flows-BL1tSvZT.js → flows-CLQYpPh7.js} +471 -281
  12. package/dist/flows-CLQYpPh7.js.map +1 -0
  13. package/dist/flows.d.ts +5 -9
  14. package/dist/flows.d.ts.map +1 -1
  15. package/dist/flows.js +1 -1
  16. package/dist/{queue-ipc-CE8_QGX3.js → ipc-BM335WFg.js} +12 -571
  17. package/dist/ipc-BM335WFg.js.map +1 -0
  18. package/dist/{acp-jsonrpc-BbBgC5gO.js → jsonrpc-DSxh2w5R.js} +2 -2
  19. package/dist/jsonrpc-DSxh2w5R.js.map +1 -0
  20. package/dist/{output-Du3m6oPQ.js → output-C4QhjpM6.js} +6 -6
  21. package/dist/output-C4QhjpM6.js.map +1 -0
  22. package/dist/perf-metrics-D0um6IR6.js +588 -0
  23. package/dist/perf-metrics-D0um6IR6.js.map +1 -0
  24. package/dist/{session-RO_LZUnv.js → prompt-turn-CbSSNHjk.js} +2568 -3667
  25. package/dist/prompt-turn-CbSSNHjk.js.map +1 -0
  26. package/dist/{output-render-Bz58qaQn.js → render-Br-kVPK_.js} +7 -6
  27. package/dist/render-Br-kVPK_.js.map +1 -0
  28. package/dist/runtime.d.ts +267 -0
  29. package/dist/runtime.d.ts.map +1 -0
  30. package/dist/runtime.js +983 -0
  31. package/dist/runtime.js.map +1 -0
  32. package/dist/session-BiBN0BvM.js +1488 -0
  33. package/dist/session-BiBN0BvM.js.map +1 -0
  34. package/dist/{types-CeRKmEQ1.d.ts → types-Cgigsj1X.d.ts} +40 -3
  35. package/dist/types-Cgigsj1X.d.ts.map +1 -0
  36. package/package.json +5 -3
  37. package/dist/acp-jsonrpc-BbBgC5gO.js.map +0 -1
  38. package/dist/cli-idpWyCOs.js.map +0 -1
  39. package/dist/flags-CCcX9fZj.js.map +0 -1
  40. package/dist/flows-BL1tSvZT.js.map +0 -1
  41. package/dist/output-Du3m6oPQ.js.map +0 -1
  42. package/dist/output-render-Bz58qaQn.js.map +0 -1
  43. package/dist/queue-ipc-CE8_QGX3.js.map +0 -1
  44. package/dist/session-RO_LZUnv.js.map +0 -1
  45. package/dist/types-CeRKmEQ1.d.ts.map +0 -1
@@ -0,0 +1,983 @@
1
+ import { g as textPrompt, x as normalizeOutputError } from "./perf-metrics-D0um6IR6.js";
2
+ import { B as serializeSessionRecordForDisk, G as DEFAULT_AGENT_NAME, J as resolveAgentCommand, K as listBuiltInAgents, M as parseSessionRecord, P as defaultSessionEventLog, _ as recordSessionUpdate, a as applyConversation, f as cloneSessionAcpxState, g as recordPromptSubmission, h as recordClientOperation, i as connectAndLoadSession, l as setDesiredModeId, m as createSessionConversation, n as withConnectedSession, o as applyLifecycleSnapshotToRecord, p as cloneSessionConversation, s as reconcileAgentSessionId, t as runPromptTurn, v as trimConversationForRuntime, y as AcpClient, z as assertPersistedKeyPolicy } from "./prompt-turn-CbSSNHjk.js";
3
+ import path from "node:path";
4
+ import fs from "node:fs/promises";
5
+ import { randomUUID } from "node:crypto";
6
+ //#region src/runtime/public/errors.ts
7
+ var AcpRuntimeError = class extends Error {
8
+ code;
9
+ cause;
10
+ constructor(code, message, options) {
11
+ super(message);
12
+ this.name = "AcpRuntimeError";
13
+ this.code = code;
14
+ this.cause = options?.cause;
15
+ }
16
+ };
17
+ function isAcpRuntimeError(value) {
18
+ return value instanceof AcpRuntimeError;
19
+ }
20
+ //#endregion
21
+ //#region src/runtime/public/shared.ts
22
+ function isRecord(value) {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+ function asTrimmedString(value) {
26
+ return typeof value === "string" ? value.trim() : "";
27
+ }
28
+ function asString(value) {
29
+ return typeof value === "string" ? value : void 0;
30
+ }
31
+ function asOptionalString(value) {
32
+ return asTrimmedString(value) || void 0;
33
+ }
34
+ function asOptionalBoolean(value) {
35
+ return typeof value === "boolean" ? value : void 0;
36
+ }
37
+ function deriveAgentFromSessionKey(sessionKey, fallbackAgent) {
38
+ const match = sessionKey.match(/^agent:([^:]+):/i);
39
+ return (match?.[1] ? asTrimmedString(match[1]) : "") || fallbackAgent;
40
+ }
41
+ //#endregion
42
+ //#region src/runtime/public/events.ts
43
+ function safeParseJsonObject(line) {
44
+ try {
45
+ const parsed = JSON.parse(line);
46
+ return isRecord(parsed) ? parsed : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ function asOptionalFiniteNumber(value) {
52
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
53
+ }
54
+ function resolveStructuredPromptPayload(parsed) {
55
+ if (asTrimmedString(parsed.method) === "session/update") {
56
+ const params = parsed.params;
57
+ if (isRecord(params) && isRecord(params.update)) {
58
+ const update = params.update;
59
+ const tag = asOptionalString(update.sessionUpdate);
60
+ return {
61
+ type: tag ?? "",
62
+ payload: update,
63
+ ...tag ? { tag } : {}
64
+ };
65
+ }
66
+ }
67
+ const sessionUpdate = asOptionalString(parsed.sessionUpdate);
68
+ if (sessionUpdate) return {
69
+ type: sessionUpdate,
70
+ payload: parsed,
71
+ tag: sessionUpdate
72
+ };
73
+ const type = asTrimmedString(parsed.type);
74
+ const tag = asOptionalString(parsed.tag);
75
+ return {
76
+ type,
77
+ payload: parsed,
78
+ ...tag ? { tag } : {}
79
+ };
80
+ }
81
+ function resolveStatusTextForTag(params) {
82
+ const { tag, payload } = params;
83
+ if (tag === "available_commands_update") {
84
+ const commands = Array.isArray(payload.availableCommands) ? payload.availableCommands : [];
85
+ return commands.length > 0 ? `available commands updated (${commands.length})` : "available commands updated";
86
+ }
87
+ if (tag === "current_mode_update") {
88
+ const mode = asTrimmedString(payload.currentModeId) || asTrimmedString(payload.modeId) || asTrimmedString(payload.mode);
89
+ return mode ? `mode updated: ${mode}` : "mode updated";
90
+ }
91
+ if (tag === "config_option_update") {
92
+ const id = asTrimmedString(payload.id) || asTrimmedString(payload.configOptionId);
93
+ const value = asTrimmedString(payload.currentValue) || asTrimmedString(payload.value) || asTrimmedString(payload.optionValue);
94
+ if (id && value) return `config updated: ${id}=${value}`;
95
+ if (id) return `config updated: ${id}`;
96
+ return "config updated";
97
+ }
98
+ if (tag === "session_info_update") return asTrimmedString(payload.summary) || asTrimmedString(payload.message) || "session updated";
99
+ if (tag === "plan") {
100
+ const content = asTrimmedString((Array.isArray(payload.entries) ? payload.entries : []).find((entry) => isRecord(entry))?.content);
101
+ return content ? `plan: ${content}` : null;
102
+ }
103
+ return null;
104
+ }
105
+ function resolveTextChunk(params) {
106
+ const contentRaw = params.payload.content;
107
+ if (isRecord(contentRaw)) {
108
+ const contentType = asTrimmedString(contentRaw.type);
109
+ if (contentType && contentType !== "text") return null;
110
+ const text = asString(contentRaw.text);
111
+ if (text && text.length > 0) return {
112
+ type: "text_delta",
113
+ text,
114
+ stream: params.stream,
115
+ tag: params.tag
116
+ };
117
+ }
118
+ const text = asString(params.payload.text);
119
+ if (!text || text.length === 0) return null;
120
+ return {
121
+ type: "text_delta",
122
+ text,
123
+ stream: params.stream,
124
+ tag: params.tag
125
+ };
126
+ }
127
+ function createTextDeltaEvent(params) {
128
+ if (params.content == null || params.content.length === 0) return null;
129
+ return {
130
+ type: "text_delta",
131
+ text: params.content,
132
+ stream: params.stream,
133
+ ...params.tag ? { tag: params.tag } : {}
134
+ };
135
+ }
136
+ function createToolCallEvent(params) {
137
+ const title = asTrimmedString(params.payload.title) || "tool call";
138
+ const status = asTrimmedString(params.payload.status);
139
+ const toolCallId = asOptionalString(params.payload.toolCallId);
140
+ return {
141
+ type: "tool_call",
142
+ text: status ? `${title} (${status})` : title,
143
+ tag: params.tag,
144
+ ...toolCallId ? { toolCallId } : {},
145
+ ...status ? { status } : {},
146
+ title
147
+ };
148
+ }
149
+ function parsePromptEventLine(line) {
150
+ const trimmed = line.trim();
151
+ if (!trimmed) return null;
152
+ const parsed = safeParseJsonObject(trimmed);
153
+ if (!parsed) return {
154
+ type: "status",
155
+ text: trimmed
156
+ };
157
+ const structured = resolveStructuredPromptPayload(parsed);
158
+ const type = structured.type;
159
+ const payload = structured.payload;
160
+ const tag = structured.tag;
161
+ switch (type) {
162
+ case "text": return createTextDeltaEvent({
163
+ content: asString(payload.content),
164
+ stream: "output",
165
+ tag
166
+ });
167
+ case "thought": return createTextDeltaEvent({
168
+ content: asString(payload.content),
169
+ stream: "thought",
170
+ tag
171
+ });
172
+ case "tool_call": return createToolCallEvent({
173
+ payload,
174
+ tag: tag ?? "tool_call"
175
+ });
176
+ case "tool_call_update": return createToolCallEvent({
177
+ payload,
178
+ tag: tag ?? "tool_call_update"
179
+ });
180
+ case "agent_message_chunk": return resolveTextChunk({
181
+ payload,
182
+ stream: "output",
183
+ tag: "agent_message_chunk"
184
+ });
185
+ case "agent_thought_chunk": return resolveTextChunk({
186
+ payload,
187
+ stream: "thought",
188
+ tag: "agent_thought_chunk"
189
+ });
190
+ case "usage_update": {
191
+ const used = asOptionalFiniteNumber(payload.used);
192
+ const size = asOptionalFiniteNumber(payload.size);
193
+ return {
194
+ type: "status",
195
+ text: used != null && size != null ? `usage updated: ${used}/${size}` : "usage updated",
196
+ tag: "usage_update",
197
+ ...used != null ? { used } : {},
198
+ ...size != null ? { size } : {}
199
+ };
200
+ }
201
+ case "available_commands_update":
202
+ case "current_mode_update":
203
+ case "config_option_update":
204
+ case "session_info_update":
205
+ case "plan": {
206
+ const text = resolveStatusTextForTag({
207
+ tag: type,
208
+ payload
209
+ });
210
+ if (!text) return null;
211
+ return {
212
+ type: "status",
213
+ text,
214
+ tag: type
215
+ };
216
+ }
217
+ case "client_operation": {
218
+ const text = [
219
+ asTrimmedString(payload.method) || "operation",
220
+ asTrimmedString(payload.status),
221
+ asTrimmedString(payload.summary)
222
+ ].filter(Boolean).join(" ");
223
+ if (!text) return null;
224
+ return {
225
+ type: "status",
226
+ text,
227
+ ...tag ? { tag } : {}
228
+ };
229
+ }
230
+ case "update": {
231
+ const update = asTrimmedString(payload.update);
232
+ if (!update) return null;
233
+ return {
234
+ type: "status",
235
+ text: update,
236
+ ...tag ? { tag } : {}
237
+ };
238
+ }
239
+ case "done": return {
240
+ type: "done",
241
+ stopReason: asOptionalString(payload.stopReason)
242
+ };
243
+ case "error": return {
244
+ type: "error",
245
+ message: asTrimmedString(payload.message) || "acpx runtime error",
246
+ code: asOptionalString(payload.code),
247
+ retryable: asOptionalBoolean(payload.retryable)
248
+ };
249
+ default: return null;
250
+ }
251
+ }
252
+ //#endregion
253
+ //#region src/runtime/engine/reuse-policy.ts
254
+ function shouldReuseExistingRecord(record, params) {
255
+ if (path.resolve(record.cwd) !== path.resolve(params.cwd)) return false;
256
+ if (record.agentCommand !== params.agentCommand) return false;
257
+ if (params.resumeSessionId && record.acpSessionId !== params.resumeSessionId) return false;
258
+ return true;
259
+ }
260
+ //#endregion
261
+ //#region src/runtime/engine/manager.ts
262
+ function createDeferred() {
263
+ let resolve;
264
+ let reject;
265
+ return {
266
+ promise: new Promise((res, rej) => {
267
+ resolve = res;
268
+ reject = rej;
269
+ }),
270
+ resolve,
271
+ reject
272
+ };
273
+ }
274
+ var AsyncEventQueue = class {
275
+ items = [];
276
+ waits = [];
277
+ closed = false;
278
+ push(item) {
279
+ if (this.closed) return;
280
+ const waiter = this.waits.shift();
281
+ if (waiter) {
282
+ waiter.resolve(item);
283
+ return;
284
+ }
285
+ this.items.push(item);
286
+ }
287
+ close() {
288
+ if (this.closed) return;
289
+ this.closed = true;
290
+ for (const waiter of this.waits.splice(0)) waiter.resolve(null);
291
+ }
292
+ async next() {
293
+ if (this.items.length > 0) return this.items.shift() ?? null;
294
+ if (this.closed) return null;
295
+ const waiter = createDeferred();
296
+ this.waits.push(waiter);
297
+ return await waiter.promise;
298
+ }
299
+ async *iterate() {
300
+ while (true) {
301
+ const next = await this.next();
302
+ if (!next) return;
303
+ yield next;
304
+ }
305
+ }
306
+ };
307
+ function isoNow() {
308
+ return (/* @__PURE__ */ new Date()).toISOString();
309
+ }
310
+ function toPromptInput(text, attachments) {
311
+ if (!attachments || attachments.length === 0) return text;
312
+ const blocks = [];
313
+ if (text) blocks.push({
314
+ type: "text",
315
+ text
316
+ });
317
+ for (const attachment of attachments) {
318
+ if (!attachment.mediaType.startsWith("image/")) throw new AcpRuntimeError("ACP_TURN_FAILED", `Unsupported ACP runtime attachment media type: ${attachment.mediaType}`);
319
+ blocks.push({
320
+ type: "image",
321
+ mimeType: attachment.mediaType,
322
+ data: attachment.data
323
+ });
324
+ }
325
+ return blocks.length > 0 ? blocks : textPrompt(text);
326
+ }
327
+ function createInitialRecord(params) {
328
+ const now = isoNow();
329
+ return {
330
+ schema: "acpx.session.v1",
331
+ acpxRecordId: params.recordId,
332
+ acpSessionId: params.sessionId,
333
+ agentSessionId: params.agentSessionId,
334
+ agentCommand: params.agentCommand,
335
+ cwd: params.cwd,
336
+ name: params.sessionName,
337
+ createdAt: now,
338
+ lastUsedAt: now,
339
+ lastSeq: 0,
340
+ eventLog: defaultSessionEventLog(params.recordId),
341
+ closed: false,
342
+ closedAt: void 0,
343
+ ...createSessionConversation(now),
344
+ acpx: {}
345
+ };
346
+ }
347
+ function createRecordId(sessionKey, mode) {
348
+ if (mode === "persistent") return sessionKey;
349
+ return `${sessionKey}:oneshot:${randomUUID()}`;
350
+ }
351
+ function resumePolicyForSessionMode(mode) {
352
+ return mode === "persistent" ? "same-session-only" : "allow-new";
353
+ }
354
+ function statusSummary(record) {
355
+ return [
356
+ `session=${record.acpxRecordId}`,
357
+ `backendSessionId=${record.acpSessionId}`,
358
+ record.agentSessionId ? `agentSessionId=${record.agentSessionId}` : null,
359
+ record.pid != null ? `pid=${record.pid}` : null,
360
+ record.closed ? "closed" : "open"
361
+ ].filter(Boolean).join(" ");
362
+ }
363
+ var AcpRuntimeManager = class {
364
+ activeControllers = /* @__PURE__ */ new Map();
365
+ pendingPersistentClients = /* @__PURE__ */ new Map();
366
+ constructor(options, deps = {}) {
367
+ this.options = options;
368
+ this.deps = deps;
369
+ }
370
+ createClient(options) {
371
+ return this.deps.clientFactory?.(options) ?? new AcpClient(options);
372
+ }
373
+ async ensureSession(input) {
374
+ const cwd = path.resolve(input.cwd?.trim() || this.options.cwd);
375
+ const agentCommand = this.options.agentRegistry.resolve(input.agent);
376
+ const existing = await this.options.sessionStore.load(input.sessionKey);
377
+ if (input.mode === "persistent" && existing && shouldReuseExistingRecord(existing, {
378
+ cwd,
379
+ agentCommand,
380
+ resumeSessionId: input.resumeSessionId
381
+ })) {
382
+ existing.closed = false;
383
+ existing.closedAt = void 0;
384
+ await this.options.sessionStore.save(existing);
385
+ return existing;
386
+ }
387
+ const client = this.createClient({
388
+ agentCommand,
389
+ cwd,
390
+ mcpServers: [...this.options.mcpServers ?? []],
391
+ permissionMode: this.options.permissionMode,
392
+ nonInteractivePermissions: this.options.nonInteractivePermissions,
393
+ verbose: this.options.verbose
394
+ });
395
+ let keepClientOpen = false;
396
+ try {
397
+ await client.start();
398
+ let sessionId;
399
+ let agentSessionId;
400
+ if (input.resumeSessionId) {
401
+ const loaded = await client.loadSession(input.resumeSessionId, cwd);
402
+ sessionId = input.resumeSessionId;
403
+ agentSessionId = loaded.agentSessionId;
404
+ } else {
405
+ const created = await client.createSession(cwd);
406
+ sessionId = created.sessionId;
407
+ agentSessionId = created.agentSessionId;
408
+ }
409
+ const record = createInitialRecord({
410
+ recordId: createRecordId(input.sessionKey, input.mode),
411
+ sessionName: input.sessionKey,
412
+ sessionId,
413
+ agentCommand,
414
+ cwd,
415
+ agentSessionId
416
+ });
417
+ record.protocolVersion = client.initializeResult?.protocolVersion;
418
+ record.agentCapabilities = client.initializeResult?.agentCapabilities;
419
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
420
+ await this.options.sessionStore.save(record);
421
+ if (input.mode === "persistent") {
422
+ const previousClient = this.pendingPersistentClients.get(record.acpxRecordId);
423
+ this.pendingPersistentClients.set(record.acpxRecordId, client);
424
+ keepClientOpen = true;
425
+ await previousClient?.close().catch(() => {});
426
+ }
427
+ return record;
428
+ } finally {
429
+ if (!keepClientOpen) await client.close();
430
+ }
431
+ }
432
+ async *runTurn(input) {
433
+ const record = await this.requireRecord(input.handle.acpxRecordId ?? input.handle.sessionKey);
434
+ const conversation = cloneSessionConversation(record);
435
+ let acpxState = cloneSessionAcpxState(record.acpx);
436
+ const promptInput = toPromptInput(input.text, input.attachments);
437
+ const promptMessageId = recordPromptSubmission(conversation, promptInput, isoNow());
438
+ trimConversationForRuntime(conversation);
439
+ const queue = new AsyncEventQueue();
440
+ let pendingClient = this.pendingPersistentClients.get(record.acpxRecordId);
441
+ if (pendingClient) {
442
+ this.pendingPersistentClients.delete(record.acpxRecordId);
443
+ if (!pendingClient.hasReusableSession(record.acpSessionId)) {
444
+ await pendingClient.close().catch(() => {});
445
+ pendingClient = void 0;
446
+ }
447
+ }
448
+ const client = pendingClient ?? this.createClient({
449
+ agentCommand: record.agentCommand,
450
+ cwd: record.cwd,
451
+ mcpServers: [...this.options.mcpServers ?? []],
452
+ permissionMode: this.options.permissionMode,
453
+ nonInteractivePermissions: this.options.nonInteractivePermissions,
454
+ verbose: this.options.verbose
455
+ });
456
+ let activeSessionId = record.acpSessionId;
457
+ let sawDone = false;
458
+ let pendingCancel = false;
459
+ let turnActive = true;
460
+ const sessionReady = createDeferred();
461
+ sessionReady.promise.catch(() => {});
462
+ const applyPendingCancel = async () => {
463
+ if (!pendingCancel || !client.hasActivePrompt()) return false;
464
+ const cancelled = await client.requestCancelActivePrompt();
465
+ if (cancelled) pendingCancel = false;
466
+ return cancelled;
467
+ };
468
+ const activeController = {
469
+ hasActivePrompt: () => client.hasActivePrompt(),
470
+ requestCancelActivePrompt: async () => {
471
+ if (client.hasActivePrompt()) return await client.requestCancelActivePrompt();
472
+ if (!turnActive) return false;
473
+ pendingCancel = true;
474
+ return true;
475
+ },
476
+ setSessionMode: async (modeId) => {
477
+ if (!client.hasActivePrompt()) await sessionReady.promise;
478
+ await client.setSessionMode(activeSessionId, modeId);
479
+ },
480
+ setSessionModel: async (modelId) => {
481
+ if (!client.hasActivePrompt()) await sessionReady.promise;
482
+ await client.setSessionModel(activeSessionId, modelId);
483
+ },
484
+ setSessionConfigOption: async (configId, value) => {
485
+ if (!client.hasActivePrompt()) await sessionReady.promise;
486
+ return await client.setSessionConfigOption(activeSessionId, configId, value);
487
+ }
488
+ };
489
+ const emitParsed = (payload) => {
490
+ const parsed = parsePromptEventLine(JSON.stringify(payload));
491
+ if (!parsed) return;
492
+ if (parsed.type === "done") sawDone = true;
493
+ queue.push(parsed);
494
+ };
495
+ const abortHandler = () => {
496
+ activeController.requestCancelActivePrompt();
497
+ };
498
+ if (input.signal) {
499
+ if (input.signal.aborted) {
500
+ queue.close();
501
+ return;
502
+ }
503
+ input.signal.addEventListener("abort", abortHandler, { once: true });
504
+ }
505
+ this.activeControllers.set(record.acpxRecordId, activeController);
506
+ (async () => {
507
+ try {
508
+ client.setEventHandlers({
509
+ onSessionUpdate: (notification) => {
510
+ acpxState = recordSessionUpdate(conversation, acpxState, notification);
511
+ trimConversationForRuntime(conversation);
512
+ emitParsed({
513
+ jsonrpc: "2.0",
514
+ method: "session/update",
515
+ params: notification
516
+ });
517
+ },
518
+ onClientOperation: (operation) => {
519
+ acpxState = recordClientOperation(conversation, acpxState, operation);
520
+ trimConversationForRuntime(conversation);
521
+ emitParsed({
522
+ type: "client_operation",
523
+ ...operation
524
+ });
525
+ }
526
+ });
527
+ const { sessionId, resumed, loadError } = pendingClient ? {
528
+ sessionId: record.acpSessionId,
529
+ resumed: false,
530
+ loadError: void 0
531
+ } : await connectAndLoadSession({
532
+ client,
533
+ record,
534
+ resumePolicy: resumePolicyForSessionMode(input.sessionMode),
535
+ timeoutMs: this.options.timeoutMs,
536
+ activeController,
537
+ onClientAvailable: (controller) => {
538
+ this.activeControllers.set(record.acpxRecordId, controller);
539
+ },
540
+ onConnectedRecord: (connectedRecord) => {
541
+ connectedRecord.lastPromptAt = isoNow();
542
+ },
543
+ onSessionIdResolved: (sessionIdValue) => {
544
+ activeSessionId = sessionIdValue;
545
+ }
546
+ });
547
+ sessionReady.resolve();
548
+ record.lastRequestId = input.requestId;
549
+ record.lastPromptAt = isoNow();
550
+ record.closed = false;
551
+ record.closedAt = void 0;
552
+ record.lastUsedAt = isoNow();
553
+ if (resumed || loadError) emitParsed({
554
+ type: "status",
555
+ text: loadError ? `load fallback: ${loadError}` : "session resumed"
556
+ });
557
+ if (pendingCancel || input.signal?.aborted) {
558
+ pendingCancel = false;
559
+ if (!sawDone) queue.push({
560
+ type: "done",
561
+ stopReason: "cancelled"
562
+ });
563
+ return;
564
+ }
565
+ await applyPendingCancel();
566
+ const response = await runPromptTurn({
567
+ client,
568
+ sessionId,
569
+ prompt: promptInput,
570
+ timeoutMs: input.timeoutMs ?? this.options.timeoutMs,
571
+ conversation,
572
+ promptMessageId
573
+ });
574
+ record.acpSessionId = activeSessionId;
575
+ reconcileAgentSessionId(record, record.agentSessionId);
576
+ record.protocolVersion = client.initializeResult?.protocolVersion;
577
+ record.agentCapabilities = client.initializeResult?.agentCapabilities;
578
+ record.acpx = acpxState;
579
+ applyConversation(record, conversation);
580
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
581
+ await this.options.sessionStore.save(record);
582
+ if (!sawDone) queue.push({
583
+ type: "done",
584
+ stopReason: response.stopReason
585
+ });
586
+ } catch (error) {
587
+ sessionReady.reject(error);
588
+ const normalized = normalizeOutputError(error, { origin: "runtime" });
589
+ queue.push({
590
+ type: "error",
591
+ message: normalized.message,
592
+ code: normalized.code,
593
+ retryable: normalized.retryable
594
+ });
595
+ } finally {
596
+ turnActive = false;
597
+ if (input.signal) input.signal.removeEventListener("abort", abortHandler);
598
+ this.activeControllers.delete(record.acpxRecordId);
599
+ client.clearEventHandlers();
600
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
601
+ record.acpx = acpxState;
602
+ applyConversation(record, conversation);
603
+ record.lastUsedAt = isoNow();
604
+ await this.options.sessionStore.save(record).catch(() => {});
605
+ await client.close().catch(() => {});
606
+ queue.close();
607
+ }
608
+ })();
609
+ yield* queue.iterate();
610
+ }
611
+ async getStatus(handle) {
612
+ const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
613
+ return {
614
+ summary: statusSummary(record),
615
+ acpxRecordId: record.acpxRecordId,
616
+ backendSessionId: record.acpSessionId,
617
+ agentSessionId: record.agentSessionId,
618
+ details: {
619
+ cwd: record.cwd,
620
+ lastUsedAt: record.lastUsedAt,
621
+ closed: record.closed === true
622
+ }
623
+ };
624
+ }
625
+ async setMode(handle, mode, sessionMode = "persistent") {
626
+ const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
627
+ const controller = this.activeControllers.get(record.acpxRecordId);
628
+ let targetRecord = record;
629
+ if (controller) await controller.setSessionMode(mode);
630
+ else targetRecord = (await withConnectedSession({
631
+ sessionRecordId: record.acpxRecordId,
632
+ loadRecord: async (sessionRecordId) => await this.requireRecord(sessionRecordId),
633
+ saveRecord: async (connectedRecord) => await this.options.sessionStore.save(connectedRecord),
634
+ createClient: (options) => this.createClient(options),
635
+ mcpServers: [...this.options.mcpServers ?? []],
636
+ permissionMode: this.options.permissionMode,
637
+ nonInteractivePermissions: this.options.nonInteractivePermissions,
638
+ verbose: this.options.verbose,
639
+ timeoutMs: this.options.timeoutMs,
640
+ resumePolicy: resumePolicyForSessionMode(sessionMode),
641
+ run: async ({ client, sessionId }) => {
642
+ await client.setSessionMode(sessionId, mode);
643
+ }
644
+ })).record;
645
+ setDesiredModeId(targetRecord, mode);
646
+ await this.options.sessionStore.save(targetRecord);
647
+ }
648
+ async setConfigOption(handle, key, value, sessionMode = "persistent") {
649
+ const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
650
+ const controller = this.activeControllers.get(record.acpxRecordId);
651
+ let targetRecord = record;
652
+ if (controller) await controller.setSessionConfigOption(key, value);
653
+ else targetRecord = (await withConnectedSession({
654
+ sessionRecordId: record.acpxRecordId,
655
+ loadRecord: async (sessionRecordId) => await this.requireRecord(sessionRecordId),
656
+ saveRecord: async (connectedRecord) => await this.options.sessionStore.save(connectedRecord),
657
+ createClient: (options) => this.createClient(options),
658
+ mcpServers: [...this.options.mcpServers ?? []],
659
+ permissionMode: this.options.permissionMode,
660
+ nonInteractivePermissions: this.options.nonInteractivePermissions,
661
+ verbose: this.options.verbose,
662
+ timeoutMs: this.options.timeoutMs,
663
+ resumePolicy: resumePolicyForSessionMode(sessionMode),
664
+ run: async ({ client, sessionId, record: connectedRecord }) => {
665
+ await client.setSessionConfigOption(sessionId, key, value);
666
+ if (key === "mode") setDesiredModeId(connectedRecord, value);
667
+ }
668
+ })).record;
669
+ if (key === "mode") setDesiredModeId(targetRecord, value);
670
+ await this.options.sessionStore.save(targetRecord);
671
+ }
672
+ async cancel(handle) {
673
+ await this.activeControllers.get(handle.acpxRecordId ?? handle.sessionKey)?.requestCancelActivePrompt();
674
+ }
675
+ async close(handle) {
676
+ const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
677
+ await this.cancel(handle);
678
+ record.closed = true;
679
+ record.closedAt = isoNow();
680
+ await this.options.sessionStore.save(record);
681
+ }
682
+ async requireRecord(sessionId) {
683
+ const record = await this.options.sessionStore.load(sessionId);
684
+ if (!record) throw new Error(`ACP session not found: ${sessionId}`);
685
+ return record;
686
+ }
687
+ };
688
+ //#endregion
689
+ //#region src/runtime/public/file-session-store.ts
690
+ function safeSessionId(sessionId) {
691
+ return encodeURIComponent(sessionId);
692
+ }
693
+ var FileSessionStore = class {
694
+ constructor(stateDir) {
695
+ this.stateDir = stateDir;
696
+ }
697
+ get sessionDir() {
698
+ return path.join(this.stateDir, "sessions");
699
+ }
700
+ filePath(sessionId) {
701
+ return path.join(this.sessionDir, `${safeSessionId(sessionId)}.json`);
702
+ }
703
+ async ensureDir() {
704
+ await fs.mkdir(this.sessionDir, { recursive: true });
705
+ }
706
+ async load(sessionId) {
707
+ await this.ensureDir();
708
+ try {
709
+ const payload = await fs.readFile(this.filePath(sessionId), "utf8");
710
+ return parseSessionRecord(JSON.parse(payload)) ?? void 0;
711
+ } catch (error) {
712
+ if (error.code === "ENOENT") return;
713
+ throw error;
714
+ }
715
+ }
716
+ async save(record) {
717
+ await this.ensureDir();
718
+ const persisted = serializeSessionRecordForDisk(record);
719
+ assertPersistedKeyPolicy(persisted);
720
+ const file = this.filePath(record.acpxRecordId);
721
+ const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
722
+ const payload = JSON.stringify(persisted, null, 2);
723
+ await fs.writeFile(tempFile, `${payload}\n`, "utf8");
724
+ await fs.rename(tempFile, file);
725
+ }
726
+ };
727
+ function createFileSessionStore(options) {
728
+ return new FileSessionStore(path.resolve(options.stateDir));
729
+ }
730
+ //#endregion
731
+ //#region src/runtime/public/handle-state.ts
732
+ const ACPX_RUNTIME_HANDLE_PREFIX = "acpx:v2:";
733
+ function encodeAcpxRuntimeHandleState(state) {
734
+ return `${ACPX_RUNTIME_HANDLE_PREFIX}${Buffer.from(JSON.stringify(state), "utf8").toString("base64url")}`;
735
+ }
736
+ function decodeAcpxRuntimeHandleState(runtimeSessionName) {
737
+ const trimmed = runtimeSessionName.trim();
738
+ if (!trimmed.startsWith(ACPX_RUNTIME_HANDLE_PREFIX)) return null;
739
+ try {
740
+ const raw = Buffer.from(trimmed.slice(8), "base64url").toString("utf8");
741
+ const parsed = JSON.parse(raw);
742
+ const name = asOptionalString(parsed.name);
743
+ const agent = asOptionalString(parsed.agent);
744
+ const cwd = asOptionalString(parsed.cwd);
745
+ const mode = asOptionalString(parsed.mode);
746
+ if (!name || !agent || !cwd || mode !== "persistent" && mode !== "oneshot") return null;
747
+ return {
748
+ name,
749
+ agent,
750
+ cwd,
751
+ mode,
752
+ acpxRecordId: asOptionalString(parsed.acpxRecordId),
753
+ backendSessionId: asOptionalString(parsed.backendSessionId),
754
+ agentSessionId: asOptionalString(parsed.agentSessionId)
755
+ };
756
+ } catch {
757
+ return null;
758
+ }
759
+ }
760
+ function writeHandleState(handle, state) {
761
+ handle.runtimeSessionName = encodeAcpxRuntimeHandleState(state);
762
+ handle.cwd = state.cwd;
763
+ handle.acpxRecordId = state.acpxRecordId;
764
+ handle.backendSessionId = state.backendSessionId;
765
+ handle.agentSessionId = state.agentSessionId;
766
+ }
767
+ //#endregion
768
+ //#region src/runtime/public/probe.ts
769
+ async function probeRuntime(options, deps = {}) {
770
+ const agentName = options.probeAgent?.trim() || "codex";
771
+ const agentCommand = options.agentRegistry.resolve(agentName);
772
+ const client = deps.clientFactory?.({
773
+ agentCommand,
774
+ cwd: options.cwd,
775
+ mcpServers: [...options.mcpServers ?? []],
776
+ permissionMode: options.permissionMode,
777
+ nonInteractivePermissions: options.nonInteractivePermissions,
778
+ verbose: options.verbose
779
+ }) ?? new AcpClient({
780
+ agentCommand,
781
+ cwd: options.cwd,
782
+ mcpServers: [...options.mcpServers ?? []],
783
+ permissionMode: options.permissionMode,
784
+ nonInteractivePermissions: options.nonInteractivePermissions,
785
+ verbose: options.verbose
786
+ });
787
+ try {
788
+ await client.start();
789
+ return {
790
+ ok: true,
791
+ message: "embedded ACP runtime ready",
792
+ details: [
793
+ `agent=${agentName}`,
794
+ `command=${agentCommand}`,
795
+ `cwd=${options.cwd}`,
796
+ ...client.initializeResult?.protocolVersion ? [`protocolVersion=${client.initializeResult.protocolVersion}`] : []
797
+ ]
798
+ };
799
+ } catch (error) {
800
+ return {
801
+ ok: false,
802
+ message: "embedded ACP runtime probe failed",
803
+ details: [
804
+ `agent=${agentName}`,
805
+ `command=${agentCommand}`,
806
+ `cwd=${options.cwd}`,
807
+ error instanceof Error ? error.message : String(error)
808
+ ]
809
+ };
810
+ } finally {
811
+ await client.close().catch(() => {});
812
+ }
813
+ }
814
+ //#endregion
815
+ //#region src/runtime.ts
816
+ const ACPX_BACKEND_ID = "acpx";
817
+ const ACPX_CAPABILITIES = { controls: [
818
+ "session/set_mode",
819
+ "session/set_config_option",
820
+ "session/status"
821
+ ] };
822
+ function createAgentRegistry(params) {
823
+ return {
824
+ resolve(agentName) {
825
+ return resolveAgentCommand(agentName, params?.overrides);
826
+ },
827
+ list() {
828
+ return listBuiltInAgents(params?.overrides);
829
+ }
830
+ };
831
+ }
832
+ var AcpxRuntime = class {
833
+ healthy = false;
834
+ manager = null;
835
+ managerPromise = null;
836
+ constructor(options, testOptions) {
837
+ this.options = options;
838
+ this.testOptions = testOptions;
839
+ }
840
+ isHealthy() {
841
+ return this.healthy;
842
+ }
843
+ async probeAvailability() {
844
+ this.healthy = (await this.runProbe()).ok;
845
+ }
846
+ async doctor() {
847
+ const report = await this.runProbe();
848
+ this.healthy = report.ok;
849
+ return {
850
+ ok: report.ok,
851
+ code: report.ok ? void 0 : "ACP_BACKEND_UNAVAILABLE",
852
+ message: report.message,
853
+ details: report.details
854
+ };
855
+ }
856
+ async ensureSession(input) {
857
+ const sessionName = input.sessionKey.trim();
858
+ if (!sessionName) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
859
+ const agent = input.agent.trim();
860
+ if (!agent) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP agent id is required.");
861
+ const record = await (await this.getManager()).ensureSession({
862
+ sessionKey: sessionName,
863
+ agent,
864
+ mode: input.mode,
865
+ cwd: input.cwd ?? this.options.cwd,
866
+ resumeSessionId: input.resumeSessionId
867
+ });
868
+ const handle = {
869
+ sessionKey: input.sessionKey,
870
+ backend: ACPX_BACKEND_ID,
871
+ runtimeSessionName: "",
872
+ cwd: record.cwd,
873
+ acpxRecordId: record.acpxRecordId,
874
+ backendSessionId: record.acpSessionId,
875
+ agentSessionId: record.agentSessionId
876
+ };
877
+ writeHandleState(handle, {
878
+ name: sessionName,
879
+ agent,
880
+ cwd: record.cwd,
881
+ mode: input.mode,
882
+ acpxRecordId: record.acpxRecordId,
883
+ backendSessionId: record.acpSessionId,
884
+ agentSessionId: record.agentSessionId
885
+ });
886
+ return handle;
887
+ }
888
+ async *runTurn(input) {
889
+ const state = this.resolveHandleState(input.handle);
890
+ yield* (await this.getManager()).runTurn({
891
+ handle: {
892
+ ...input.handle,
893
+ acpxRecordId: state.acpxRecordId ?? input.handle.acpxRecordId ?? input.handle.sessionKey
894
+ },
895
+ text: input.text,
896
+ attachments: input.attachments,
897
+ mode: input.mode,
898
+ sessionMode: state.mode,
899
+ requestId: input.requestId,
900
+ timeoutMs: input.timeoutMs,
901
+ signal: input.signal
902
+ });
903
+ }
904
+ getCapabilities() {
905
+ return ACPX_CAPABILITIES;
906
+ }
907
+ async getStatus(input) {
908
+ const state = this.resolveHandleState(input.handle);
909
+ return await (await this.getManager()).getStatus({
910
+ ...input.handle,
911
+ acpxRecordId: state.acpxRecordId ?? input.handle.acpxRecordId ?? input.handle.sessionKey
912
+ });
913
+ }
914
+ async setMode(input) {
915
+ const state = this.resolveHandleState(input.handle);
916
+ await (await this.getManager()).setMode({
917
+ ...input.handle,
918
+ acpxRecordId: state.acpxRecordId ?? input.handle.acpxRecordId ?? input.handle.sessionKey
919
+ }, input.mode, state.mode);
920
+ }
921
+ async setConfigOption(input) {
922
+ const state = this.resolveHandleState(input.handle);
923
+ await (await this.getManager()).setConfigOption({
924
+ ...input.handle,
925
+ acpxRecordId: state.acpxRecordId ?? input.handle.acpxRecordId ?? input.handle.sessionKey
926
+ }, input.key, input.value, state.mode);
927
+ }
928
+ async cancel(input) {
929
+ const state = this.resolveHandleState(input.handle);
930
+ await (await this.getManager()).cancel({
931
+ ...input.handle,
932
+ acpxRecordId: state.acpxRecordId ?? input.handle.acpxRecordId ?? input.handle.sessionKey
933
+ });
934
+ }
935
+ async close(input) {
936
+ const state = this.resolveHandleState(input.handle);
937
+ await (await this.getManager()).close({
938
+ ...input.handle,
939
+ acpxRecordId: state.acpxRecordId ?? input.handle.acpxRecordId ?? input.handle.sessionKey
940
+ });
941
+ }
942
+ async getManager() {
943
+ if (this.manager) return this.manager;
944
+ if (!this.managerPromise) this.managerPromise = Promise.resolve(this.testOptions?.managerFactory?.(this.options) ?? new AcpRuntimeManager(this.options)).then((manager) => {
945
+ this.manager = manager;
946
+ return manager;
947
+ });
948
+ return await this.managerPromise;
949
+ }
950
+ async runProbe() {
951
+ return await (this.testOptions?.probeRunner?.(this.options) ?? probeRuntime(this.options));
952
+ }
953
+ resolveHandleState(handle) {
954
+ const decoded = decodeAcpxRuntimeHandleState(handle.runtimeSessionName);
955
+ if (decoded) return {
956
+ ...decoded,
957
+ acpxRecordId: decoded.acpxRecordId ?? handle.acpxRecordId,
958
+ backendSessionId: decoded.backendSessionId ?? handle.backendSessionId,
959
+ agentSessionId: decoded.agentSessionId ?? handle.agentSessionId
960
+ };
961
+ const runtimeSessionName = handle.runtimeSessionName.trim();
962
+ if (!runtimeSessionName) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "Invalid embedded ACP runtime handle: runtimeSessionName is missing.");
963
+ return {
964
+ name: runtimeSessionName,
965
+ agent: deriveAgentFromSessionKey(handle.sessionKey, DEFAULT_AGENT_NAME),
966
+ cwd: handle.cwd ?? this.options.cwd,
967
+ mode: "persistent",
968
+ acpxRecordId: handle.acpxRecordId,
969
+ backendSessionId: handle.backendSessionId,
970
+ agentSessionId: handle.agentSessionId
971
+ };
972
+ }
973
+ };
974
+ function createAcpRuntime(options) {
975
+ return new AcpxRuntime(options);
976
+ }
977
+ function createRuntimeStore(options) {
978
+ return createFileSessionStore(options);
979
+ }
980
+ //#endregion
981
+ export { ACPX_BACKEND_ID, AcpRuntimeError, AcpxRuntime, DEFAULT_AGENT_NAME, createAcpRuntime, createAgentRegistry, createFileSessionStore, createRuntimeStore, decodeAcpxRuntimeHandleState, encodeAcpxRuntimeHandleState, isAcpRuntimeError };
982
+
983
+ //# sourceMappingURL=runtime.js.map