acpx 0.12.1 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -375
- package/dist/{cli-pKoIEH5O.js → cli-_7Sei6dD.js} +5 -4
- package/dist/cli-_7Sei6dD.js.map +1 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +72 -19
- package/dist/cli.js.map +1 -1
- package/dist/{client-CFo_9MIo.d.ts → client-CxNllqui.d.ts} +17 -3
- package/dist/client-CxNllqui.d.ts.map +1 -0
- package/dist/{flags-CjeFX5ki.js → flags-Ds6FzmHG.js} +36 -9
- package/dist/flags-Ds6FzmHG.js.map +1 -0
- package/dist/{flows-Cym6H3vg.js → flows-fzH0GKYF.js} +14 -4
- package/dist/flows-fzH0GKYF.js.map +1 -0
- package/dist/flows.d.ts +6 -2
- package/dist/flows.d.ts.map +1 -1
- package/dist/flows.js +1 -1
- package/dist/{live-checkpoint-DQp4JSHD.js → live-checkpoint-BSIrfgVo.js} +560 -196
- package/dist/live-checkpoint-BSIrfgVo.js.map +1 -0
- package/dist/{output-fNqTAsFQ.js → output-BZQE0gI1.js} +74 -11
- package/dist/output-BZQE0gI1.js.map +1 -0
- package/dist/runtime.d.ts +106 -19
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +631 -158
- package/dist/runtime.js.map +1 -1
- package/dist/{session-options-C4uYkKDy.d.ts → session-options-DwRDODlr.d.ts} +28 -3
- package/dist/session-options-DwRDODlr.d.ts.map +1 -0
- package/package.json +19 -19
- package/skills/acpx/SKILL.md +5 -2
- package/dist/cli-pKoIEH5O.js.map +0 -1
- package/dist/client-CFo_9MIo.d.ts.map +0 -1
- package/dist/flags-CjeFX5ki.js.map +0 -1
- package/dist/flows-Cym6H3vg.js.map +0 -1
- package/dist/live-checkpoint-DQp4JSHD.js.map +0 -1
- package/dist/output-fNqTAsFQ.js.map +0 -1
- package/dist/session-options-C4uYkKDy.d.ts.map +0 -1
package/dist/runtime.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { A as REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE,
|
|
1
|
+
import { A as REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, D as applyLifecycleSnapshotToRecord, E as applyConversation, F as modelStateFromConfigOptions, Ft as normalizeAgentName, Ht as normalizeOutputError, I as normalizeAgentCommandInput, It as resolveAgentArgv, Lt as resolveAgentCommand, M as RequestedModelUnsupportedError, Mt as withTimeout, Nt as DEFAULT_AGENT_NAME, O as reconcileAgentSessionId, Ot as textPrompt, P as isRequestedModelUnsupportedError, Pt as listBuiltInAgents, Rt as resolveCanonicalAgentName, S as advertisedModelState, T as sessionOptionsFromRecord, Ut as extractAcpError, Wt as isAcpResourceNotFoundError, _ as createSessionConversation, a as applyRequestedModelIfAdvertised, b as recordSessionUpdate, c as setCurrentModelId, d as setDesiredModelId, dt as serializeSessionRecordForDisk, et as createAtomicWriteTempPath, f as syncAdvertisedModelState, g as cloneSessionConversation, h as cloneSessionAcpxState, i as connectAndLoadSession, j as REQUESTED_MODEL_UNSUPPORTED_REASONS, k as AcpClient, l as setDesiredConfigOption, m as applyConfigOptionsToState, mt as defaultSessionEventLog, n as runPromptTurn, o as currentModelIdFromSetModelResponse, p as applyConfigOptionsToRecord, r as withConnectedSession, s as clearDesiredConfigOption, t as LiveSessionCheckpoint, tt as assertPersistedKeyPolicy, u as setDesiredModeId, ut as parseSessionRecord, v as recordClientOperation, w as persistSessionOptions, x as trimConversationForRuntime, y as recordPromptSubmission } from "./live-checkpoint-BSIrfgVo.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
6
|
//#region src/runtime/public/errors.ts
|
|
6
7
|
var AcpRuntimeError = class extends Error {
|
|
7
8
|
code;
|
|
@@ -107,7 +108,46 @@ function planStatusText(payload) {
|
|
|
107
108
|
const content = asTrimmedString((Array.isArray(payload.entries) ? payload.entries : []).find((entry) => isRecord(entry))?.content);
|
|
108
109
|
return content ? `plan: ${content}` : null;
|
|
109
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Documented allowlist for text_delta.meta origin fields.
|
|
113
|
+
* Only these keys may be copied from ACP update `_meta`.
|
|
114
|
+
* Unknown keys (including secret-like producer-controlled names) are dropped.
|
|
115
|
+
*/
|
|
116
|
+
const ORIGIN_META_KEYS = [
|
|
117
|
+
"origin",
|
|
118
|
+
"kind",
|
|
119
|
+
"source"
|
|
120
|
+
];
|
|
121
|
+
/**
|
|
122
|
+
* Preserve a fail-closed subset of ACP update origin fields for text_delta
|
|
123
|
+
* consumers. Empty or unusable values are omitted rather than inferred.
|
|
124
|
+
*/
|
|
125
|
+
function extractTextDeltaOrigin(payload) {
|
|
126
|
+
const messageId = asOptionalString(payload.messageId);
|
|
127
|
+
const meta = sanitizeOriginMeta(payload._meta);
|
|
128
|
+
return {
|
|
129
|
+
...messageId ? { messageId } : {},
|
|
130
|
+
...meta ? { meta } : {}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Copy only allowlisted string origin keys from wire `_meta`.
|
|
135
|
+
* Nested objects, arrays, non-strings, and unknown keys are dropped.
|
|
136
|
+
*/
|
|
137
|
+
function sanitizeOriginMeta(value) {
|
|
138
|
+
if (!isRecord(value)) return;
|
|
139
|
+
const out = {};
|
|
140
|
+
for (const key of ORIGIN_META_KEYS) {
|
|
141
|
+
const entry = value[key];
|
|
142
|
+
if (typeof entry !== "string") continue;
|
|
143
|
+
const trimmed = entry.trim();
|
|
144
|
+
if (!trimmed) continue;
|
|
145
|
+
out[key] = trimmed;
|
|
146
|
+
}
|
|
147
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
148
|
+
}
|
|
110
149
|
function resolveTextChunk(params) {
|
|
150
|
+
const origin = extractTextDeltaOrigin(params.payload);
|
|
111
151
|
const contentRaw = params.payload.content;
|
|
112
152
|
if (isRecord(contentRaw)) {
|
|
113
153
|
const contentType = asTrimmedString(contentRaw.type);
|
|
@@ -117,7 +157,8 @@ function resolveTextChunk(params) {
|
|
|
117
157
|
type: "text_delta",
|
|
118
158
|
text,
|
|
119
159
|
stream: params.stream,
|
|
120
|
-
tag: params.tag
|
|
160
|
+
tag: params.tag,
|
|
161
|
+
...origin
|
|
121
162
|
};
|
|
122
163
|
}
|
|
123
164
|
const text = asString(params.payload.text);
|
|
@@ -126,7 +167,8 @@ function resolveTextChunk(params) {
|
|
|
126
167
|
type: "text_delta",
|
|
127
168
|
text,
|
|
128
169
|
stream: params.stream,
|
|
129
|
-
tag: params.tag
|
|
170
|
+
tag: params.tag,
|
|
171
|
+
...origin
|
|
130
172
|
};
|
|
131
173
|
}
|
|
132
174
|
function createTextDeltaEvent(params) {
|
|
@@ -461,9 +503,15 @@ function shouldReuseExistingRecord(record, params) {
|
|
|
461
503
|
if (record.acpx?.reset_on_next_ensure === true) return false;
|
|
462
504
|
if (path.resolve(record.cwd) !== path.resolve(params.cwd)) return false;
|
|
463
505
|
if (record.agentCommand !== params.agentCommand) return false;
|
|
506
|
+
if (!sameArgv(record.agentArgv, params.agentArgv)) return false;
|
|
464
507
|
if (params.resumeSessionId && record.acpSessionId !== params.resumeSessionId) return false;
|
|
465
508
|
return true;
|
|
466
509
|
}
|
|
510
|
+
function sameArgv(left, right) {
|
|
511
|
+
const leftValues = left ?? [];
|
|
512
|
+
const rightValues = right ?? [];
|
|
513
|
+
return leftValues.length === rightValues.length && leftValues.every((value, index) => value === rightValues[index]);
|
|
514
|
+
}
|
|
467
515
|
//#endregion
|
|
468
516
|
//#region src/runtime/engine/manager.ts
|
|
469
517
|
function createDeferred() {
|
|
@@ -478,6 +526,22 @@ function createDeferred() {
|
|
|
478
526
|
reject
|
|
479
527
|
};
|
|
480
528
|
}
|
|
529
|
+
async function settleAttempt(run) {
|
|
530
|
+
try {
|
|
531
|
+
return {
|
|
532
|
+
ok: true,
|
|
533
|
+
value: await run()
|
|
534
|
+
};
|
|
535
|
+
} catch (error) {
|
|
536
|
+
return {
|
|
537
|
+
ok: false,
|
|
538
|
+
error
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function firstFailedAttempt(attempts) {
|
|
543
|
+
return attempts.find((attempt) => !attempt.ok);
|
|
544
|
+
}
|
|
481
545
|
var AsyncEventQueue = class {
|
|
482
546
|
items = [];
|
|
483
547
|
waits = [];
|
|
@@ -561,6 +625,7 @@ function createInitialRecord(params) {
|
|
|
561
625
|
acpSessionId: params.sessionId,
|
|
562
626
|
agentSessionId: params.agentSessionId,
|
|
563
627
|
agentCommand: params.agentCommand,
|
|
628
|
+
agentArgv: params.agentArgv,
|
|
564
629
|
cwd: params.cwd,
|
|
565
630
|
name: params.sessionName,
|
|
566
631
|
createdAt: now,
|
|
@@ -737,7 +802,10 @@ var AcpRuntimeManager = class {
|
|
|
737
802
|
options;
|
|
738
803
|
deps;
|
|
739
804
|
activeControllers = /* @__PURE__ */ new Map();
|
|
740
|
-
|
|
805
|
+
retainedSessionOwners = /* @__PURE__ */ new Map();
|
|
806
|
+
pendingOneShotRecordIds = /* @__PURE__ */ new Map();
|
|
807
|
+
ensureSessionLocks = /* @__PURE__ */ new Map();
|
|
808
|
+
runtimeOperationLocks = /* @__PURE__ */ new Map();
|
|
741
809
|
closingActiveRecords = /* @__PURE__ */ new Set();
|
|
742
810
|
constructor(options, deps = {}) {
|
|
743
811
|
this.options = options;
|
|
@@ -746,22 +814,123 @@ var AcpRuntimeManager = class {
|
|
|
746
814
|
createClient(options) {
|
|
747
815
|
return this.deps.clientFactory?.(options) ?? new AcpClient(options);
|
|
748
816
|
}
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
817
|
+
createSessionOwner(input) {
|
|
818
|
+
const owner = {
|
|
819
|
+
...input,
|
|
820
|
+
bufferSessionUpdates: false,
|
|
821
|
+
pendingSessionUpdates: []
|
|
822
|
+
};
|
|
823
|
+
input.client.setEventHandlers({
|
|
824
|
+
onSessionUpdate: (notification) => this.routeOwnedSessionUpdate(owner, notification),
|
|
825
|
+
onClientOperation: (operation) => this.routeOwnedClientOperation(owner, operation)
|
|
826
|
+
});
|
|
827
|
+
return owner;
|
|
828
|
+
}
|
|
829
|
+
routeOwnedSessionUpdate(owner, notification) {
|
|
830
|
+
const active = owner.activeTurn;
|
|
831
|
+
if (active) {
|
|
832
|
+
const { task, turn } = active;
|
|
833
|
+
if (turn.connectionUpdates) {
|
|
834
|
+
turn.connectionUpdates.push(notification);
|
|
835
|
+
this.emitRuntimeTurnEvent(task, {
|
|
836
|
+
jsonrpc: "2.0",
|
|
837
|
+
method: "session/update",
|
|
838
|
+
params: notification
|
|
839
|
+
});
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
|
|
843
|
+
trimConversationForRuntime(turn.conversation);
|
|
844
|
+
turn.liveCheckpoint.request();
|
|
845
|
+
this.emitRuntimeTurnEvent(task, {
|
|
846
|
+
jsonrpc: "2.0",
|
|
847
|
+
method: "session/update",
|
|
848
|
+
params: notification
|
|
849
|
+
});
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (owner.bufferSessionUpdates) {
|
|
853
|
+
owner.pendingSessionUpdates.push(notification);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const projection = owner.projection;
|
|
857
|
+
if (!projection) {
|
|
858
|
+
owner.pendingSessionUpdates.push(notification);
|
|
755
859
|
return;
|
|
756
860
|
}
|
|
757
|
-
|
|
758
|
-
|
|
861
|
+
projection.acpxState = recordSessionUpdate(projection.conversation, projection.acpxState, notification);
|
|
862
|
+
trimConversationForRuntime(projection.conversation);
|
|
863
|
+
projection.checkpoint.request();
|
|
864
|
+
}
|
|
865
|
+
routeOwnedClientOperation(owner, operation) {
|
|
866
|
+
const active = owner.activeTurn;
|
|
867
|
+
if (!active) return;
|
|
868
|
+
const { task, turn } = active;
|
|
869
|
+
turn.acpxState = recordClientOperation(turn.conversation, turn.acpxState, operation);
|
|
870
|
+
trimConversationForRuntime(turn.conversation);
|
|
871
|
+
turn.liveCheckpoint.request();
|
|
872
|
+
this.emitRuntimeTurnEvent(task, {
|
|
873
|
+
type: "client_operation",
|
|
874
|
+
...operation
|
|
875
|
+
});
|
|
759
876
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
877
|
+
attachIdleProjection(owner, record, conversation = cloneSessionConversation(record), acpxState = cloneSessionAcpxState(record.acpx)) {
|
|
878
|
+
let projection;
|
|
879
|
+
projection = {
|
|
880
|
+
record,
|
|
881
|
+
conversation,
|
|
882
|
+
acpxState,
|
|
883
|
+
checkpoint: new LiveSessionCheckpoint({ save: async () => {
|
|
884
|
+
record.lastUsedAt = isoNow();
|
|
885
|
+
record.acpx = projection.acpxState;
|
|
886
|
+
applyConversation(record, projection.conversation);
|
|
887
|
+
applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
|
|
888
|
+
await this.refreshClosedState(record);
|
|
889
|
+
await this.options.sessionStore.save(record);
|
|
890
|
+
} })
|
|
891
|
+
};
|
|
892
|
+
owner.projection = projection;
|
|
893
|
+
owner.activeTurn = void 0;
|
|
894
|
+
this.drainPendingSessionUpdates(owner);
|
|
895
|
+
}
|
|
896
|
+
drainPendingSessionUpdates(owner) {
|
|
897
|
+
for (const notification of owner.pendingSessionUpdates.splice(0)) this.routeOwnedSessionUpdate(owner, notification);
|
|
898
|
+
}
|
|
899
|
+
async flushSessionOwner(owner) {
|
|
900
|
+
await owner.client.waitForSessionUpdatesIdle?.().catch(() => {});
|
|
901
|
+
await owner.projection?.checkpoint.flush();
|
|
902
|
+
}
|
|
903
|
+
removeRetainedSessionOwner(owner) {
|
|
904
|
+
if (owner.recordId && this.retainedSessionOwners.get(owner.recordId) === owner) this.retainedSessionOwners.delete(owner.recordId);
|
|
905
|
+
if (owner.mode === "oneshot" && owner.recordId && this.pendingOneShotRecordIds.get(owner.sessionKey) === owner.recordId) this.pendingOneShotRecordIds.delete(owner.sessionKey);
|
|
906
|
+
}
|
|
907
|
+
async readRetainedSessionOwner(record, options) {
|
|
908
|
+
const owner = this.retainedSessionOwners.get(record.acpxRecordId);
|
|
909
|
+
if (!owner) return;
|
|
910
|
+
await this.flushSessionOwner(owner);
|
|
911
|
+
const projectedRecord = owner.projection?.record;
|
|
912
|
+
if (projectedRecord && projectedRecord !== record) Object.assign(record, structuredClone(projectedRecord));
|
|
913
|
+
if (!owner.client.hasReusableSession(record.acpSessionId)) {
|
|
914
|
+
this.removeRetainedSessionOwner(owner);
|
|
915
|
+
await this.stopSessionOwner(owner);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (options.consume) this.removeRetainedSessionOwner(owner);
|
|
919
|
+
return owner;
|
|
920
|
+
}
|
|
921
|
+
async closeRetainedSessionOwner(recordId) {
|
|
922
|
+
const owner = this.retainedSessionOwners.get(recordId);
|
|
923
|
+
if (!owner) return;
|
|
924
|
+
this.removeRetainedSessionOwner(owner);
|
|
925
|
+
await this.stopSessionOwner(owner);
|
|
926
|
+
}
|
|
927
|
+
async stopSessionOwner(owner) {
|
|
928
|
+
await this.flushSessionOwner(owner).catch(() => {});
|
|
929
|
+
await owner.client.close().catch(() => {});
|
|
930
|
+
await owner.projection?.checkpoint.flush().catch(() => {});
|
|
931
|
+
try {
|
|
932
|
+
owner.client.clearEventHandlers();
|
|
933
|
+
} catch {}
|
|
765
934
|
}
|
|
766
935
|
async refreshClosedState(record) {
|
|
767
936
|
if (!this.closingActiveRecords.has(record.acpxRecordId)) return record.closed === true;
|
|
@@ -774,32 +943,43 @@ var AcpRuntimeManager = class {
|
|
|
774
943
|
};
|
|
775
944
|
return true;
|
|
776
945
|
}
|
|
777
|
-
async
|
|
778
|
-
const { record,
|
|
779
|
-
if (
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
946
|
+
async retainPersistentSessionOwnerAfterTurn(input) {
|
|
947
|
+
const { record, owner, conversation, acpxState } = input;
|
|
948
|
+
if (!this.canRetainPersistentSessionOwner(owner, record)) {
|
|
949
|
+
owner.activeTurn = void 0;
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
this.attachIdleProjection(owner, record, conversation, acpxState);
|
|
953
|
+
const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
|
|
954
|
+
this.retainedSessionOwners.set(record.acpxRecordId, owner);
|
|
955
|
+
if (previousOwner && previousOwner !== owner) {
|
|
956
|
+
this.removeRetainedSessionOwner(previousOwner);
|
|
957
|
+
await this.stopSessionOwner(previousOwner);
|
|
958
|
+
}
|
|
783
959
|
return true;
|
|
784
960
|
}
|
|
961
|
+
canRetainPersistentSessionOwner(owner, record) {
|
|
962
|
+
return owner.mode === "persistent" && !record.closed && !(owner.client.hasUnresolvedPrompt?.() ?? false) && owner.client.hasReusableSession(record.acpSessionId);
|
|
963
|
+
}
|
|
785
964
|
async withRuntimeControlSession(record, sessionMode, run) {
|
|
786
|
-
const
|
|
787
|
-
if (
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
965
|
+
const owner = await this.readRetainedSessionOwner(record, { consume: false });
|
|
966
|
+
if (owner) {
|
|
967
|
+
const ownedRecord = owner.projection?.record ?? record;
|
|
968
|
+
owner.bufferSessionUpdates = true;
|
|
969
|
+
try {
|
|
970
|
+
const value = await run({
|
|
971
|
+
client: owner.client,
|
|
972
|
+
sessionId: ownedRecord.acpSessionId,
|
|
973
|
+
record: ownedRecord
|
|
974
|
+
});
|
|
975
|
+
this.refreshOwnedRecordLifecycle(owner, ownedRecord);
|
|
976
|
+
return {
|
|
977
|
+
value,
|
|
978
|
+
record: ownedRecord
|
|
979
|
+
};
|
|
980
|
+
} finally {
|
|
981
|
+
await this.finishBufferedOwnerControl(owner, ownedRecord);
|
|
982
|
+
}
|
|
803
983
|
}
|
|
804
984
|
const result = await withConnectedSession({
|
|
805
985
|
sessionRecordId: record.acpxRecordId,
|
|
@@ -809,7 +989,9 @@ var AcpRuntimeManager = class {
|
|
|
809
989
|
mcpServers: [...this.options.mcpServers ?? []],
|
|
810
990
|
permissionMode: this.options.permissionMode,
|
|
811
991
|
nonInteractivePermissions: this.options.nonInteractivePermissions,
|
|
992
|
+
permissionPolicy: this.options.permissionPolicy,
|
|
812
993
|
onPermissionRequest: this.options.onPermissionRequest,
|
|
994
|
+
elicitationModes: this.options.elicitationModes,
|
|
813
995
|
verbose: this.options.verbose,
|
|
814
996
|
timeoutMs: this.options.timeoutMs,
|
|
815
997
|
resumePolicy: resumePolicyForSessionMode(sessionMode),
|
|
@@ -820,32 +1002,117 @@ var AcpRuntimeManager = class {
|
|
|
820
1002
|
record: result.record
|
|
821
1003
|
};
|
|
822
1004
|
}
|
|
1005
|
+
refreshOwnedRecordLifecycle(owner, record) {
|
|
1006
|
+
record.lastUsedAt = isoNow();
|
|
1007
|
+
record.closed = false;
|
|
1008
|
+
record.closedAt = void 0;
|
|
1009
|
+
record.protocolVersion = owner.client.initializeResult?.protocolVersion;
|
|
1010
|
+
record.agentCapabilities = owner.client.initializeResult?.agentCapabilities;
|
|
1011
|
+
applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
|
|
1012
|
+
}
|
|
1013
|
+
async finishBufferedOwnerControl(owner, record) {
|
|
1014
|
+
const projection = owner.projection;
|
|
1015
|
+
if (projection) projection.acpxState = cloneSessionAcpxState(record.acpx);
|
|
1016
|
+
owner.bufferSessionUpdates = false;
|
|
1017
|
+
this.drainPendingSessionUpdates(owner);
|
|
1018
|
+
if (projection) {
|
|
1019
|
+
record.acpx = projection.acpxState;
|
|
1020
|
+
applyConversation(record, projection.conversation);
|
|
1021
|
+
await projection.checkpoint.flush();
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
823
1024
|
async ensureSession(input) {
|
|
1025
|
+
return await this.withEnsureSessionLock(input, async () => this.ensureSessionWithOwnership(input));
|
|
1026
|
+
}
|
|
1027
|
+
async withEnsureSessionLock(input, run) {
|
|
1028
|
+
const key = `${input.mode}\0${input.sessionKey}`;
|
|
1029
|
+
return await this.withManagerLock(this.ensureSessionLocks, key, run);
|
|
1030
|
+
}
|
|
1031
|
+
async withManagerLock(locks, key, run) {
|
|
1032
|
+
const previous = locks.get(key) ?? Promise.resolve();
|
|
1033
|
+
let release;
|
|
1034
|
+
const gate = new Promise((resolve) => {
|
|
1035
|
+
release = resolve;
|
|
1036
|
+
});
|
|
1037
|
+
const tail = previous.then(() => gate);
|
|
1038
|
+
locks.set(key, tail);
|
|
1039
|
+
await previous;
|
|
1040
|
+
try {
|
|
1041
|
+
return await run();
|
|
1042
|
+
} finally {
|
|
1043
|
+
release();
|
|
1044
|
+
if (locks.get(key) === tail) locks.delete(key);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
async ensureSessionWithOwnership(input) {
|
|
824
1048
|
const cwd = path.resolve(input.cwd?.trim() || this.options.cwd);
|
|
825
|
-
const agentCommand = this.options.agentRegistry.resolve(input.agent);
|
|
826
|
-
const
|
|
827
|
-
if (input.mode === "persistent" && existing && shouldReuseExistingRecord(existing, {
|
|
1049
|
+
const { agentCommand, agentArgv } = normalizeAgentCommandInput(this.options.agentRegistry.resolve(input.agent));
|
|
1050
|
+
const agent = {
|
|
828
1051
|
cwd,
|
|
829
1052
|
agentCommand,
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1053
|
+
agentArgv
|
|
1054
|
+
};
|
|
1055
|
+
const existing = await this.loadExistingRuntimeSession(input);
|
|
1056
|
+
if (existing && this.canReuseRuntimeSession(input, agent, existing)) return await this.reuseRuntimeSession(existing.record);
|
|
1057
|
+
await this.closeConflictingPersistentSession(input, existing?.owner);
|
|
1058
|
+
return await this.createOwnedRuntimeSession(input, agent);
|
|
1059
|
+
}
|
|
1060
|
+
async loadExistingRuntimeSession(input) {
|
|
1061
|
+
const existingRecordId = input.mode === "persistent" ? input.sessionKey : this.pendingOneShotRecordIds.get(input.sessionKey);
|
|
1062
|
+
if (!existingRecordId) return;
|
|
1063
|
+
let record = await this.options.sessionStore.load(existingRecordId);
|
|
1064
|
+
if (!record) return;
|
|
1065
|
+
const owner = this.retainedSessionOwners.get(record.acpxRecordId);
|
|
1066
|
+
if (owner) {
|
|
1067
|
+
await this.flushSessionOwner(owner);
|
|
1068
|
+
record = owner.projection?.record ?? record;
|
|
837
1069
|
}
|
|
1070
|
+
return {
|
|
1071
|
+
record,
|
|
1072
|
+
owner
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
canReuseRuntimeSession(input, agent, existing) {
|
|
1076
|
+
if (!shouldReuseExistingRecord(existing.record, {
|
|
1077
|
+
cwd: agent.cwd,
|
|
1078
|
+
agentCommand: agent.agentCommand,
|
|
1079
|
+
agentArgv: agent.agentArgv,
|
|
1080
|
+
resumeSessionId: input.resumeSessionId
|
|
1081
|
+
})) return false;
|
|
1082
|
+
if (input.mode === "persistent") return true;
|
|
1083
|
+
return Boolean(existing.owner && isDeepStrictEqual(sessionOptionsFromRecord(existing.record), input.sessionOptions));
|
|
1084
|
+
}
|
|
1085
|
+
async reuseRuntimeSession(record) {
|
|
1086
|
+
record.closed = false;
|
|
1087
|
+
record.closedAt = void 0;
|
|
1088
|
+
this.closingActiveRecords.delete(record.acpxRecordId);
|
|
1089
|
+
await this.options.sessionStore.save(record);
|
|
1090
|
+
return record;
|
|
1091
|
+
}
|
|
1092
|
+
async closeConflictingPersistentSession(input, owner) {
|
|
1093
|
+
if (input.mode === "persistent" && owner?.recordId) await this.closeRetainedSessionOwner(owner.recordId);
|
|
1094
|
+
}
|
|
1095
|
+
async createOwnedRuntimeSession(input, agent) {
|
|
1096
|
+
const { cwd, agentCommand, agentArgv } = agent;
|
|
838
1097
|
const client = this.createClient({
|
|
839
1098
|
agentCommand,
|
|
1099
|
+
agentArgv,
|
|
840
1100
|
cwd,
|
|
841
1101
|
mcpServers: [...this.options.mcpServers ?? []],
|
|
842
1102
|
permissionMode: this.options.permissionMode,
|
|
843
1103
|
nonInteractivePermissions: this.options.nonInteractivePermissions,
|
|
1104
|
+
permissionPolicy: this.options.permissionPolicy,
|
|
844
1105
|
onPermissionRequest: this.options.onPermissionRequest,
|
|
1106
|
+
elicitationModes: this.options.elicitationModes,
|
|
845
1107
|
verbose: this.options.verbose,
|
|
846
1108
|
sessionOptions: input.sessionOptions
|
|
847
1109
|
});
|
|
848
|
-
|
|
1110
|
+
const owner = this.createSessionOwner({
|
|
1111
|
+
client,
|
|
1112
|
+
sessionKey: input.sessionKey,
|
|
1113
|
+
mode: input.mode
|
|
1114
|
+
});
|
|
1115
|
+
let retained = false;
|
|
849
1116
|
try {
|
|
850
1117
|
await client.start();
|
|
851
1118
|
const session = await createOrLoadRuntimeSession(client, input.resumeSessionId, cwd);
|
|
@@ -853,22 +1120,29 @@ var AcpRuntimeManager = class {
|
|
|
853
1120
|
input,
|
|
854
1121
|
client,
|
|
855
1122
|
agentCommand,
|
|
1123
|
+
agentArgv,
|
|
856
1124
|
cwd,
|
|
857
1125
|
session
|
|
858
1126
|
});
|
|
859
|
-
|
|
1127
|
+
this.attachIdleProjection(owner, record);
|
|
1128
|
+
await this.retainInitializedSessionOwner(owner, record);
|
|
1129
|
+
retained = true;
|
|
860
1130
|
return record;
|
|
861
1131
|
} finally {
|
|
862
|
-
if (!
|
|
1132
|
+
if (!retained) {
|
|
1133
|
+
client.clearEventHandlers();
|
|
1134
|
+
await client.close();
|
|
1135
|
+
}
|
|
863
1136
|
}
|
|
864
1137
|
}
|
|
865
1138
|
async createAndSaveRuntimeRecord(params) {
|
|
866
|
-
const { input, client, agentCommand, cwd, session } = params;
|
|
1139
|
+
const { input, client, agentCommand, agentArgv, cwd, session } = params;
|
|
867
1140
|
const record = createInitialRecord({
|
|
868
1141
|
recordId: createRecordId(input.sessionKey, input.mode),
|
|
869
1142
|
sessionName: input.sessionKey,
|
|
870
1143
|
sessionId: session.sessionId,
|
|
871
1144
|
agentCommand,
|
|
1145
|
+
agentArgv,
|
|
872
1146
|
cwd,
|
|
873
1147
|
agentSessionId: session.agentSessionId
|
|
874
1148
|
});
|
|
@@ -892,17 +1166,28 @@ var AcpRuntimeManager = class {
|
|
|
892
1166
|
await this.options.sessionStore.save(record);
|
|
893
1167
|
return record;
|
|
894
1168
|
}
|
|
895
|
-
async
|
|
896
|
-
|
|
897
|
-
const
|
|
898
|
-
this.
|
|
899
|
-
|
|
900
|
-
|
|
1169
|
+
async retainInitializedSessionOwner(owner, record) {
|
|
1170
|
+
owner.recordId = record.acpxRecordId;
|
|
1171
|
+
const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
|
|
1172
|
+
this.retainedSessionOwners.set(record.acpxRecordId, owner);
|
|
1173
|
+
if (owner.mode === "oneshot") this.pendingOneShotRecordIds.set(owner.sessionKey, record.acpxRecordId);
|
|
1174
|
+
if (previousOwner && previousOwner !== owner) {
|
|
1175
|
+
this.removeRetainedSessionOwner(previousOwner);
|
|
1176
|
+
await this.stopSessionOwner(previousOwner);
|
|
1177
|
+
}
|
|
901
1178
|
}
|
|
902
1179
|
startTurn(input) {
|
|
903
|
-
|
|
1180
|
+
let promptInput;
|
|
1181
|
+
try {
|
|
1182
|
+
promptInput = toPromptInput(input.text, input.attachments);
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
this.closeRetainedOneShotHandle(input.handle).catch(() => {});
|
|
1185
|
+
throw error;
|
|
1186
|
+
}
|
|
904
1187
|
const queue = new AsyncEventQueue();
|
|
905
1188
|
const result = createDeferred();
|
|
1189
|
+
const promptStarted = createDeferred();
|
|
1190
|
+
promptStarted.promise.catch(() => {});
|
|
906
1191
|
const sessionReady = createDeferred();
|
|
907
1192
|
sessionReady.promise.catch(() => {});
|
|
908
1193
|
let resultSettled = false;
|
|
@@ -934,13 +1219,17 @@ var AcpRuntimeManager = class {
|
|
|
934
1219
|
};
|
|
935
1220
|
if (input.signal) {
|
|
936
1221
|
if (input.signal.aborted) {
|
|
1222
|
+
promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
|
|
937
1223
|
closeStream();
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1224
|
+
this.closeRetainedOneShotHandle(input.handle).catch(() => {}).then(() => {
|
|
1225
|
+
settleResult({
|
|
1226
|
+
status: "cancelled",
|
|
1227
|
+
stopReason: "cancelled"
|
|
1228
|
+
});
|
|
941
1229
|
});
|
|
942
1230
|
return {
|
|
943
1231
|
requestId: input.requestId,
|
|
1232
|
+
promptStarted: promptStarted.promise,
|
|
944
1233
|
events: queue.iterate(),
|
|
945
1234
|
result: result.promise,
|
|
946
1235
|
cancel: async () => {},
|
|
@@ -953,6 +1242,7 @@ var AcpRuntimeManager = class {
|
|
|
953
1242
|
input,
|
|
954
1243
|
promptInput,
|
|
955
1244
|
queue,
|
|
1245
|
+
promptStarted,
|
|
956
1246
|
sessionReady,
|
|
957
1247
|
state,
|
|
958
1248
|
settleResult,
|
|
@@ -960,6 +1250,7 @@ var AcpRuntimeManager = class {
|
|
|
960
1250
|
});
|
|
961
1251
|
return {
|
|
962
1252
|
requestId: input.requestId,
|
|
1253
|
+
promptStarted: promptStarted.promise,
|
|
963
1254
|
events: queue.iterate(),
|
|
964
1255
|
result: result.promise,
|
|
965
1256
|
cancel: async () => {
|
|
@@ -970,70 +1261,172 @@ var AcpRuntimeManager = class {
|
|
|
970
1261
|
}
|
|
971
1262
|
};
|
|
972
1263
|
}
|
|
1264
|
+
async closeRetainedOneShotHandle(handle) {
|
|
1265
|
+
const recordId = handle.acpxRecordId ?? handle.sessionKey;
|
|
1266
|
+
if (this.retainedSessionOwners.get(recordId)?.mode === "oneshot") await this.closeRetainedSessionOwner(recordId);
|
|
1267
|
+
}
|
|
973
1268
|
async runRuntimeTurnTask(task) {
|
|
974
1269
|
let turn;
|
|
1270
|
+
let terminalResult;
|
|
975
1271
|
try {
|
|
976
1272
|
turn = await this.prepareRuntimeTurn(task);
|
|
977
1273
|
const { sessionId, resumed, loadError } = await this.connectRuntimeTurn(task, turn);
|
|
978
1274
|
await this.resolveRuntimeTurnReady(task, turn, resumed, loadError);
|
|
979
|
-
if (this.cancelRuntimeTurnBeforePrompt(task))
|
|
980
|
-
|
|
981
|
-
|
|
1275
|
+
if (this.cancelRuntimeTurnBeforePrompt(task)) terminalResult = {
|
|
1276
|
+
status: "cancelled",
|
|
1277
|
+
stopReason: "cancelled"
|
|
1278
|
+
};
|
|
1279
|
+
else {
|
|
1280
|
+
await this.applyPendingRuntimeTurnCancel(task, turn);
|
|
1281
|
+
const response = await this.runRuntimePrompt(task, turn, sessionId);
|
|
1282
|
+
await this.saveCompletedRuntimeTurn(turn, response.stopReason);
|
|
1283
|
+
terminalResult = {
|
|
1284
|
+
status: response.stopReason === "cancelled" ? "cancelled" : "completed",
|
|
1285
|
+
...response.stopReason ? { stopReason: response.stopReason } : {}
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
terminalResult = this.failRuntimeTurn(task, error);
|
|
1290
|
+
}
|
|
1291
|
+
try {
|
|
1292
|
+
await this.finalizeRuntimeTurn(task, turn);
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
terminalResult = this.failRuntimeTurn(task, error);
|
|
1295
|
+
}
|
|
1296
|
+
task.settleResult(terminalResult);
|
|
1297
|
+
}
|
|
1298
|
+
async runRuntimePrompt(task, turn, sessionId) {
|
|
1299
|
+
try {
|
|
1300
|
+
return await runPromptTurn({
|
|
982
1301
|
client: turn.client,
|
|
983
1302
|
sessionId,
|
|
984
1303
|
prompt: task.promptInput,
|
|
985
1304
|
timeoutMs: task.input.timeoutMs ?? this.options.timeoutMs,
|
|
986
1305
|
conversation: turn.conversation,
|
|
987
|
-
promptMessageId: turn.promptMessageId
|
|
1306
|
+
promptMessageId: turn.promptMessageId,
|
|
1307
|
+
onPromptRequestStarted: () => task.promptStarted.resolve(),
|
|
1308
|
+
onElicitation: task.input.onElicitation
|
|
988
1309
|
});
|
|
989
|
-
await this.saveCompletedRuntimeTurn(turn, response.stopReason);
|
|
990
|
-
task.settleResult({
|
|
991
|
-
status: response.stopReason === "cancelled" ? "cancelled" : "completed",
|
|
992
|
-
...response.stopReason ? { stopReason: response.stopReason } : {}
|
|
993
|
-
});
|
|
994
|
-
} catch (error) {
|
|
995
|
-
this.failRuntimeTurn(task, error);
|
|
996
1310
|
} finally {
|
|
997
|
-
|
|
1311
|
+
turn.client.endPromptElicitation?.(sessionId);
|
|
998
1312
|
}
|
|
999
1313
|
}
|
|
1000
1314
|
async prepareRuntimeTurn(task) {
|
|
1001
|
-
const
|
|
1315
|
+
const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
|
|
1316
|
+
return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.prepareRuntimeTurnWithOwnership(task));
|
|
1317
|
+
}
|
|
1318
|
+
async prepareRuntimeTurnWithOwnership(task) {
|
|
1319
|
+
const { record, retainedOwner, conversation, acpxState, promptMessageId } = await this.prepareRuntimeTurnState(task);
|
|
1320
|
+
try {
|
|
1321
|
+
const client = retainedOwner?.client ?? this.createTurnClient(record);
|
|
1322
|
+
const owner = this.resolveRuntimeTurnOwner(task, record, client, retainedOwner);
|
|
1323
|
+
const turn = this.createRunningRuntimeTurn({
|
|
1324
|
+
record,
|
|
1325
|
+
conversation,
|
|
1326
|
+
acpxState,
|
|
1327
|
+
client,
|
|
1328
|
+
owner,
|
|
1329
|
+
pendingClient: retainedOwner?.client,
|
|
1330
|
+
promptMessageId
|
|
1331
|
+
});
|
|
1332
|
+
this.activateRuntimeTurn(task, turn);
|
|
1333
|
+
return turn;
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
this.restoreBufferedSessionOwner(retainedOwner);
|
|
1336
|
+
throw error;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
async prepareRuntimeTurnState(task) {
|
|
1340
|
+
const { record, retainedOwner } = await this.acquireRuntimeTurnState(task);
|
|
1002
1341
|
const conversation = cloneSessionConversation(record);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1342
|
+
const acpxState = cloneSessionAcpxState(record.acpx);
|
|
1343
|
+
try {
|
|
1344
|
+
const promptStartedAt = isoNow();
|
|
1345
|
+
const promptMessageId = recordPromptSubmission(conversation, task.promptInput, promptStartedAt);
|
|
1346
|
+
trimConversationForRuntime(conversation);
|
|
1347
|
+
record.lastPromptAt = promptStartedAt;
|
|
1348
|
+
record.lastUsedAt = promptStartedAt;
|
|
1349
|
+
record.acpx = acpxState;
|
|
1350
|
+
applyConversation(record, conversation);
|
|
1351
|
+
await this.options.sessionStore.save(record);
|
|
1352
|
+
return {
|
|
1353
|
+
record,
|
|
1354
|
+
retainedOwner,
|
|
1355
|
+
conversation,
|
|
1356
|
+
acpxState,
|
|
1357
|
+
promptMessageId
|
|
1358
|
+
};
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
this.restoreBufferedSessionOwner(retainedOwner);
|
|
1361
|
+
throw error;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
async acquireRuntimeTurnState(task) {
|
|
1365
|
+
const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
|
|
1366
|
+
let record = await this.requireRecord(recordId);
|
|
1367
|
+
const retainedOwner = await this.readRetainedSessionOwner(record, { consume: false });
|
|
1368
|
+
if (!retainedOwner) return { record };
|
|
1369
|
+
const projection = retainedOwner.projection;
|
|
1370
|
+
if (projection) record = structuredClone(projection.record);
|
|
1371
|
+
retainedOwner.bufferSessionUpdates = true;
|
|
1372
|
+
return {
|
|
1373
|
+
record,
|
|
1374
|
+
retainedOwner
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
restoreBufferedSessionOwner(owner) {
|
|
1378
|
+
if (!owner) return;
|
|
1379
|
+
owner.bufferSessionUpdates = false;
|
|
1380
|
+
this.drainPendingSessionUpdates(owner);
|
|
1381
|
+
}
|
|
1382
|
+
resolveRuntimeTurnOwner(task, record, client, retainedOwner) {
|
|
1383
|
+
return retainedOwner ?? this.createSessionOwner({
|
|
1384
|
+
client,
|
|
1385
|
+
sessionKey: record.name ?? task.input.handle.sessionKey,
|
|
1386
|
+
mode: task.input.sessionMode
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
createRunningRuntimeTurn(input) {
|
|
1390
|
+
const { record, conversation, acpxState, client, owner, pendingClient, promptMessageId } = input;
|
|
1014
1391
|
const turn = {
|
|
1015
1392
|
record,
|
|
1016
1393
|
conversation,
|
|
1017
1394
|
acpxState,
|
|
1018
1395
|
liveCheckpoint: this.createRuntimeTurnCheckpoint(record, conversation, () => turn.acpxState),
|
|
1019
1396
|
client,
|
|
1397
|
+
owner,
|
|
1020
1398
|
pendingClient,
|
|
1399
|
+
connectionUpdates: pendingClient ? void 0 : [],
|
|
1021
1400
|
promptMessageId,
|
|
1022
1401
|
activeSessionId: record.acpSessionId
|
|
1023
1402
|
};
|
|
1403
|
+
return turn;
|
|
1404
|
+
}
|
|
1405
|
+
activateRuntimeTurn(task, turn) {
|
|
1406
|
+
const { owner, record } = turn;
|
|
1407
|
+
this.removeRetainedSessionOwner(owner);
|
|
1408
|
+
owner.recordId = record.acpxRecordId;
|
|
1024
1409
|
task.state.activeController = this.buildRuntimeTurnController(task, turn);
|
|
1025
1410
|
this.activeControllers.set(record.acpxRecordId, task.state.activeController);
|
|
1026
|
-
|
|
1027
|
-
|
|
1411
|
+
owner.projection = void 0;
|
|
1412
|
+
owner.activeTurn = {
|
|
1413
|
+
task,
|
|
1414
|
+
turn
|
|
1415
|
+
};
|
|
1416
|
+
owner.bufferSessionUpdates = false;
|
|
1417
|
+
this.drainPendingSessionUpdates(owner);
|
|
1028
1418
|
}
|
|
1029
1419
|
createTurnClient(record) {
|
|
1030
1420
|
return this.createClient({
|
|
1031
1421
|
agentCommand: record.agentCommand,
|
|
1422
|
+
agentArgv: record.agentArgv,
|
|
1032
1423
|
cwd: record.cwd,
|
|
1033
1424
|
mcpServers: [...this.options.mcpServers ?? []],
|
|
1034
1425
|
permissionMode: this.options.permissionMode,
|
|
1035
1426
|
nonInteractivePermissions: this.options.nonInteractivePermissions,
|
|
1427
|
+
permissionPolicy: this.options.permissionPolicy,
|
|
1036
1428
|
onPermissionRequest: this.options.onPermissionRequest,
|
|
1429
|
+
elicitationModes: this.options.elicitationModes,
|
|
1037
1430
|
verbose: this.options.verbose,
|
|
1038
1431
|
sessionOptions: sessionOptionsFromRecord(record)
|
|
1039
1432
|
});
|
|
@@ -1106,29 +1499,6 @@ var AcpRuntimeManager = class {
|
|
|
1106
1499
|
applyConfigOptionResponseToTurn(turn, response);
|
|
1107
1500
|
applyDesiredConfigOptionToTurn(turn, configId, value);
|
|
1108
1501
|
}
|
|
1109
|
-
installRuntimeTurnEventHandlers(task, turn) {
|
|
1110
|
-
turn.client.setEventHandlers({
|
|
1111
|
-
onSessionUpdate: (notification) => {
|
|
1112
|
-
turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
|
|
1113
|
-
trimConversationForRuntime(turn.conversation);
|
|
1114
|
-
turn.liveCheckpoint.request();
|
|
1115
|
-
this.emitRuntimeTurnEvent(task, {
|
|
1116
|
-
jsonrpc: "2.0",
|
|
1117
|
-
method: "session/update",
|
|
1118
|
-
params: notification
|
|
1119
|
-
});
|
|
1120
|
-
},
|
|
1121
|
-
onClientOperation: (operation) => {
|
|
1122
|
-
turn.acpxState = recordClientOperation(turn.conversation, turn.acpxState, operation);
|
|
1123
|
-
trimConversationForRuntime(turn.conversation);
|
|
1124
|
-
turn.liveCheckpoint.request();
|
|
1125
|
-
this.emitRuntimeTurnEvent(task, {
|
|
1126
|
-
type: "client_operation",
|
|
1127
|
-
...operation
|
|
1128
|
-
});
|
|
1129
|
-
}
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
1502
|
emitRuntimeTurnEvent(task, payload) {
|
|
1133
1503
|
const parsed = parsePromptEventLine(JSON.stringify(payload));
|
|
1134
1504
|
if (!parsed) return;
|
|
@@ -1140,9 +1510,17 @@ var AcpRuntimeManager = class {
|
|
|
1140
1510
|
resumed: false,
|
|
1141
1511
|
loadError: void 0
|
|
1142
1512
|
} : await this.connectRuntimeTurnClient(task, turn);
|
|
1143
|
-
|
|
1513
|
+
this.drainRuntimeConnectionUpdates(turn);
|
|
1144
1514
|
return loaded;
|
|
1145
1515
|
}
|
|
1516
|
+
drainRuntimeConnectionUpdates(turn) {
|
|
1517
|
+
if (!turn.connectionUpdates) return;
|
|
1518
|
+
turn.acpxState = cloneSessionAcpxState(turn.record.acpx);
|
|
1519
|
+
for (const notification of turn.connectionUpdates) turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
|
|
1520
|
+
turn.connectionUpdates = void 0;
|
|
1521
|
+
trimConversationForRuntime(turn.conversation);
|
|
1522
|
+
turn.liveCheckpoint.request();
|
|
1523
|
+
}
|
|
1146
1524
|
async connectRuntimeTurnClient(task, turn) {
|
|
1147
1525
|
return await connectAndLoadSession({
|
|
1148
1526
|
client: turn.client,
|
|
@@ -1183,10 +1561,7 @@ var AcpRuntimeManager = class {
|
|
|
1183
1561
|
cancelRuntimeTurnBeforePrompt(task) {
|
|
1184
1562
|
if (!task.state.pendingCancel && !task.input.signal?.aborted) return false;
|
|
1185
1563
|
task.state.pendingCancel = false;
|
|
1186
|
-
task.
|
|
1187
|
-
status: "cancelled",
|
|
1188
|
-
stopReason: "cancelled"
|
|
1189
|
-
});
|
|
1564
|
+
task.promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
|
|
1190
1565
|
return true;
|
|
1191
1566
|
}
|
|
1192
1567
|
async applyPendingRuntimeTurnCancel(task, turn) {
|
|
@@ -1206,9 +1581,10 @@ var AcpRuntimeManager = class {
|
|
|
1206
1581
|
await this.options.sessionStore.save(turn.record);
|
|
1207
1582
|
}
|
|
1208
1583
|
failRuntimeTurn(task, error) {
|
|
1584
|
+
task.promptStarted.reject(error);
|
|
1209
1585
|
task.sessionReady.reject(error);
|
|
1210
1586
|
const normalized = normalizeOutputError(error, { origin: "runtime" });
|
|
1211
|
-
|
|
1587
|
+
return {
|
|
1212
1588
|
status: "failed",
|
|
1213
1589
|
error: {
|
|
1214
1590
|
message: normalized.message,
|
|
@@ -1216,13 +1592,36 @@ var AcpRuntimeManager = class {
|
|
|
1216
1592
|
...normalized.detailCode ? { detailCode: normalized.detailCode } : {},
|
|
1217
1593
|
...normalized.retryable !== void 0 ? { retryable: normalized.retryable } : {}
|
|
1218
1594
|
}
|
|
1219
|
-
}
|
|
1595
|
+
};
|
|
1220
1596
|
}
|
|
1221
1597
|
async finalizeRuntimeTurn(task, turn) {
|
|
1222
1598
|
task.state.turnActive = false;
|
|
1223
|
-
task.input.signal?.removeEventListener("abort", task.abortHandler);
|
|
1224
|
-
turn
|
|
1225
|
-
|
|
1599
|
+
const abortHandlerAttempt = await settleAttempt(() => task.input.signal?.removeEventListener("abort", task.abortHandler));
|
|
1600
|
+
const recordAttempt = await settleAttempt(async () => turn ? await this.finalizeRuntimeTurnRecord(turn) : false);
|
|
1601
|
+
let failure = firstFailedAttempt([abortHandlerAttempt, recordAttempt]);
|
|
1602
|
+
let pooled = recordAttempt.ok ? recordAttempt.value : false;
|
|
1603
|
+
if (failure) {
|
|
1604
|
+
this.discardRetainedRuntimeTurnOwner(turn);
|
|
1605
|
+
pooled = false;
|
|
1606
|
+
}
|
|
1607
|
+
const closeAttempt = await settleAttempt(async () => this.closeRuntimeTurnClient(turn, pooled));
|
|
1608
|
+
this.cleanupRuntimeTurn(task, turn);
|
|
1609
|
+
failure ??= firstFailedAttempt([closeAttempt]);
|
|
1610
|
+
if (failure) throw failure.error;
|
|
1611
|
+
}
|
|
1612
|
+
discardRetainedRuntimeTurnOwner(turn) {
|
|
1613
|
+
if (turn) {
|
|
1614
|
+
this.removeRetainedSessionOwner(turn.owner);
|
|
1615
|
+
turn.owner.activeTurn = void 0;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
async closeRuntimeTurnClient(turn, pooled) {
|
|
1619
|
+
if (!turn || pooled) return;
|
|
1620
|
+
turn.owner.activeTurn = void 0;
|
|
1621
|
+
const failure = firstFailedAttempt([await settleAttempt(() => turn.client.clearEventHandlers()), await settleAttempt(async () => turn.client.close())]);
|
|
1622
|
+
if (failure) throw failure.error;
|
|
1623
|
+
}
|
|
1624
|
+
cleanupRuntimeTurn(task, turn) {
|
|
1226
1625
|
if (turn) {
|
|
1227
1626
|
this.activeControllers.delete(turn.record.acpxRecordId);
|
|
1228
1627
|
this.closingActiveRecords.delete(turn.record.acpxRecordId);
|
|
@@ -1230,17 +1629,20 @@ var AcpRuntimeManager = class {
|
|
|
1230
1629
|
task.queue.close();
|
|
1231
1630
|
}
|
|
1232
1631
|
async finalizeRuntimeTurnRecord(turn) {
|
|
1632
|
+
this.drainRuntimeConnectionUpdates(turn);
|
|
1233
1633
|
applyLifecycleSnapshotToRecord(turn.record, turn.client.getAgentLifecycleSnapshot());
|
|
1234
1634
|
turn.record.acpx = turn.acpxState;
|
|
1235
1635
|
applyConversation(turn.record, turn.conversation);
|
|
1236
1636
|
turn.record.lastUsedAt = isoNow();
|
|
1237
|
-
await turn.liveCheckpoint.flush()
|
|
1637
|
+
await turn.liveCheckpoint.flush();
|
|
1238
1638
|
const closed = await this.refreshClosedState(turn.record);
|
|
1239
|
-
await this.options.sessionStore.save(turn.record)
|
|
1639
|
+
await this.options.sessionStore.save(turn.record);
|
|
1240
1640
|
if (closed) return false;
|
|
1241
|
-
return await this.
|
|
1641
|
+
return await this.retainPersistentSessionOwnerAfterTurn({
|
|
1242
1642
|
record: turn.record,
|
|
1243
|
-
|
|
1643
|
+
owner: turn.owner,
|
|
1644
|
+
conversation: turn.conversation,
|
|
1645
|
+
acpxState: turn.acpxState
|
|
1244
1646
|
});
|
|
1245
1647
|
}
|
|
1246
1648
|
async *runTurn(input) {
|
|
@@ -1249,7 +1651,10 @@ var AcpRuntimeManager = class {
|
|
|
1249
1651
|
yield legacyTerminalEventFromTurnResult(await turn.result);
|
|
1250
1652
|
}
|
|
1251
1653
|
async getStatus(handle) {
|
|
1252
|
-
const
|
|
1654
|
+
const recordId = handle.acpxRecordId ?? handle.sessionKey;
|
|
1655
|
+
const owner = this.retainedSessionOwners.get(recordId);
|
|
1656
|
+
if (owner) await this.flushSessionOwner(owner);
|
|
1657
|
+
const record = await this.requireRecord(recordId);
|
|
1253
1658
|
return {
|
|
1254
1659
|
summary: statusSummary(record),
|
|
1255
1660
|
acpxRecordId: record.acpxRecordId,
|
|
@@ -1267,6 +1672,10 @@ var AcpRuntimeManager = class {
|
|
|
1267
1672
|
};
|
|
1268
1673
|
}
|
|
1269
1674
|
async setMode(handle, mode, sessionMode = "persistent") {
|
|
1675
|
+
const recordId = handle.acpxRecordId ?? handle.sessionKey;
|
|
1676
|
+
await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setModeWithOwnership(handle, mode, sessionMode));
|
|
1677
|
+
}
|
|
1678
|
+
async setModeWithOwnership(handle, mode, sessionMode) {
|
|
1270
1679
|
const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
|
|
1271
1680
|
const controller = this.activeControllers.get(record.acpxRecordId);
|
|
1272
1681
|
let targetRecord = record;
|
|
@@ -1278,6 +1687,10 @@ var AcpRuntimeManager = class {
|
|
|
1278
1687
|
await this.options.sessionStore.save(targetRecord);
|
|
1279
1688
|
}
|
|
1280
1689
|
async setConfigOption(handle, key, value, sessionMode = "persistent") {
|
|
1690
|
+
const recordId = handle.acpxRecordId ?? handle.sessionKey;
|
|
1691
|
+
await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setConfigOptionWithOwnership(handle, key, value, sessionMode));
|
|
1692
|
+
}
|
|
1693
|
+
async setConfigOptionWithOwnership(handle, key, value, sessionMode) {
|
|
1281
1694
|
const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
|
|
1282
1695
|
const controller = this.activeControllers.get(record.acpxRecordId);
|
|
1283
1696
|
if (controller) {
|
|
@@ -1298,42 +1711,80 @@ var AcpRuntimeManager = class {
|
|
|
1298
1711
|
await this.activeControllers.get(handle.acpxRecordId ?? handle.sessionKey)?.requestCancelActivePrompt();
|
|
1299
1712
|
}
|
|
1300
1713
|
async close(handle, options = {}) {
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1714
|
+
const recordId = handle.acpxRecordId ?? handle.sessionKey;
|
|
1715
|
+
const record = await this.resolveRuntimeRecordForClose(recordId);
|
|
1716
|
+
this.markActiveRuntimeRecordClosing(record);
|
|
1303
1717
|
await this.cancel(handle);
|
|
1304
|
-
|
|
1718
|
+
await this.closeRuntimeRecordOwnership(record, options.discardPersistentState === true);
|
|
1719
|
+
record.closed = true;
|
|
1720
|
+
record.closedAt = isoNow();
|
|
1721
|
+
await this.options.sessionStore.save(record);
|
|
1722
|
+
}
|
|
1723
|
+
async resolveRuntimeRecordForClose(recordId) {
|
|
1724
|
+
const retainedOwner = this.retainedSessionOwners.get(recordId);
|
|
1725
|
+
if (retainedOwner) await this.flushSessionOwner(retainedOwner);
|
|
1726
|
+
return retainedOwner?.projection?.record ?? await this.requireRecord(recordId);
|
|
1727
|
+
}
|
|
1728
|
+
markActiveRuntimeRecordClosing(record) {
|
|
1729
|
+
if (this.activeControllers.has(record.acpxRecordId)) this.closingActiveRecords.add(record.acpxRecordId);
|
|
1730
|
+
}
|
|
1731
|
+
async closeRuntimeRecordOwnership(record, discardPersistentState) {
|
|
1732
|
+
if (discardPersistentState) {
|
|
1305
1733
|
await this.closeBackendSession(record);
|
|
1306
1734
|
record.acpx = {
|
|
1307
1735
|
...record.acpx,
|
|
1308
1736
|
reset_on_next_ensure: true
|
|
1309
1737
|
};
|
|
1310
|
-
} else await this.
|
|
1311
|
-
record.closed = true;
|
|
1312
|
-
record.closedAt = isoNow();
|
|
1313
|
-
await this.options.sessionStore.save(record);
|
|
1738
|
+
} else await this.closeRetainedSessionOwner(record.acpxRecordId);
|
|
1314
1739
|
}
|
|
1315
1740
|
async closeBackendSession(record) {
|
|
1316
|
-
const
|
|
1317
|
-
|
|
1741
|
+
const connection = await this.acquireBackendCloseConnection(record);
|
|
1742
|
+
try {
|
|
1743
|
+
await this.requestBackendSessionClose(record, connection);
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
this.handleBackendSessionCloseError(record, error);
|
|
1746
|
+
} finally {
|
|
1747
|
+
await this.finalizeBackendCloseConnection(connection);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
async finalizeBackendCloseConnection(connection) {
|
|
1751
|
+
const failure = firstFailedAttempt([
|
|
1752
|
+
await settleAttempt(async () => {
|
|
1753
|
+
if (connection.owner) await this.flushSessionOwner(connection.owner);
|
|
1754
|
+
}),
|
|
1755
|
+
await settleAttempt(() => connection.owner?.client.clearEventHandlers()),
|
|
1756
|
+
await settleAttempt(async () => connection.client.close())
|
|
1757
|
+
]);
|
|
1758
|
+
if (failure) throw failure.error;
|
|
1759
|
+
}
|
|
1760
|
+
async acquireBackendCloseConnection(record) {
|
|
1761
|
+
const owner = await this.readRetainedSessionOwner(record, { consume: true });
|
|
1762
|
+
if (owner) return {
|
|
1763
|
+
client: owner.client,
|
|
1764
|
+
owner
|
|
1765
|
+
};
|
|
1766
|
+
return { client: this.createClient({
|
|
1318
1767
|
agentCommand: record.agentCommand,
|
|
1768
|
+
agentArgv: record.agentArgv,
|
|
1319
1769
|
cwd: record.cwd,
|
|
1320
1770
|
mcpServers: [...this.options.mcpServers ?? []],
|
|
1321
1771
|
permissionMode: this.options.permissionMode,
|
|
1322
1772
|
nonInteractivePermissions: this.options.nonInteractivePermissions,
|
|
1773
|
+
permissionPolicy: this.options.permissionPolicy,
|
|
1323
1774
|
onPermissionRequest: this.options.onPermissionRequest,
|
|
1775
|
+
elicitationModes: this.options.elicitationModes,
|
|
1324
1776
|
verbose: this.options.verbose
|
|
1325
|
-
});
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
}
|
|
1777
|
+
}) };
|
|
1778
|
+
}
|
|
1779
|
+
async requestBackendSessionClose(record, connection) {
|
|
1780
|
+
if (!connection.owner) await withTimeout(connection.client.start(), this.options.timeoutMs);
|
|
1781
|
+
if (!connection.client.supportsCloseSession()) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`);
|
|
1782
|
+
await withTimeout(connection.client.closeSession(record.acpSessionId), this.options.timeoutMs);
|
|
1783
|
+
}
|
|
1784
|
+
handleBackendSessionCloseError(record, error) {
|
|
1785
|
+
if (isAcpResourceNotFoundError(error)) return;
|
|
1786
|
+
if (isUnsupportedSessionCloseError(error)) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`, { cause: error });
|
|
1787
|
+
throw error;
|
|
1337
1788
|
}
|
|
1338
1789
|
async requireRecord(sessionId) {
|
|
1339
1790
|
const record = await this.options.sessionStore.load(sessionId);
|
|
@@ -1382,7 +1833,7 @@ var FileSessionStore = class {
|
|
|
1382
1833
|
const persisted = serializeSessionRecordForDisk(record);
|
|
1383
1834
|
assertPersistedKeyPolicy(persisted);
|
|
1384
1835
|
const file = this.filePath(record.acpxRecordId);
|
|
1385
|
-
const tempFile =
|
|
1836
|
+
const tempFile = createAtomicWriteTempPath(file);
|
|
1386
1837
|
const payload = JSON.stringify(persisted, null, 2);
|
|
1387
1838
|
await fs.writeFile(tempFile, `${payload}\n`, "utf8");
|
|
1388
1839
|
await fs.rename(tempFile, file);
|
|
@@ -1464,7 +1915,7 @@ function normalizeRuntimeDetails(details) {
|
|
|
1464
1915
|
}
|
|
1465
1916
|
async function probeRuntime(options, deps = {}) {
|
|
1466
1917
|
const agentName = options.probeAgent?.trim() || "codex";
|
|
1467
|
-
const agentCommand = options.agentRegistry.resolve(agentName);
|
|
1918
|
+
const agentCommand = normalizeAgentCommandInput(options.agentRegistry.resolve(agentName));
|
|
1468
1919
|
const client = createProbeClient(options, agentCommand, deps);
|
|
1469
1920
|
try {
|
|
1470
1921
|
await client.start();
|
|
@@ -1473,7 +1924,7 @@ async function probeRuntime(options, deps = {}) {
|
|
|
1473
1924
|
message: "embedded ACP runtime ready",
|
|
1474
1925
|
details: [
|
|
1475
1926
|
`agent=${agentName}`,
|
|
1476
|
-
`command=${agentCommand}`,
|
|
1927
|
+
`command=${agentCommand.agentCommand}`,
|
|
1477
1928
|
`cwd=${options.cwd}`,
|
|
1478
1929
|
...client.initializeResult?.protocolVersion ? [`protocolVersion=${client.initializeResult.protocolVersion}`] : []
|
|
1479
1930
|
]
|
|
@@ -1484,7 +1935,7 @@ async function probeRuntime(options, deps = {}) {
|
|
|
1484
1935
|
message: "embedded ACP runtime probe failed",
|
|
1485
1936
|
details: [
|
|
1486
1937
|
`agent=${agentName}`,
|
|
1487
|
-
`command=${agentCommand}`,
|
|
1938
|
+
`command=${agentCommand.agentCommand}`,
|
|
1488
1939
|
`cwd=${options.cwd}`,
|
|
1489
1940
|
formatRuntimeDetail(error)
|
|
1490
1941
|
]
|
|
@@ -1495,11 +1946,12 @@ async function probeRuntime(options, deps = {}) {
|
|
|
1495
1946
|
}
|
|
1496
1947
|
function createProbeClient(options, agentCommand, deps) {
|
|
1497
1948
|
const clientOptions = {
|
|
1498
|
-
agentCommand,
|
|
1949
|
+
...agentCommand,
|
|
1499
1950
|
cwd: options.cwd,
|
|
1500
1951
|
mcpServers: [...options.mcpServers ?? []],
|
|
1501
1952
|
permissionMode: options.permissionMode,
|
|
1502
1953
|
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1954
|
+
permissionPolicy: options.permissionPolicy,
|
|
1503
1955
|
verbose: options.verbose
|
|
1504
1956
|
};
|
|
1505
1957
|
return deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
|
|
@@ -1513,15 +1965,31 @@ const ACPX_CAPABILITIES = { controls: [
|
|
|
1513
1965
|
"session/status"
|
|
1514
1966
|
] };
|
|
1515
1967
|
function createAgentRegistry(params) {
|
|
1968
|
+
const overrides = normalizeRegistryOverrides(params?.overrides);
|
|
1516
1969
|
return {
|
|
1517
1970
|
resolve(agentName) {
|
|
1518
|
-
|
|
1971
|
+
const normalizedAgentName = normalizeAgentName(agentName);
|
|
1972
|
+
return overrides[normalizedAgentName] ?? overrides[resolveCanonicalAgentName(agentName)] ?? resolveAgentArgv(agentName) ?? resolveAgentCommand(agentName);
|
|
1519
1973
|
},
|
|
1520
1974
|
list() {
|
|
1521
|
-
return listBuiltInAgents(
|
|
1975
|
+
return listBuiltInAgents(overrides);
|
|
1522
1976
|
}
|
|
1523
1977
|
};
|
|
1524
1978
|
}
|
|
1979
|
+
function normalizeRegistryOverrides(values) {
|
|
1980
|
+
const normalized = {};
|
|
1981
|
+
for (const [name, value] of Object.entries(values ?? {})) {
|
|
1982
|
+
const normalizedName = normalizeAgentName(name);
|
|
1983
|
+
if (!normalizedName) continue;
|
|
1984
|
+
const normalizedValue = normalizeRegistryOverride(value);
|
|
1985
|
+
if (normalizedValue) normalized[normalizedName] = normalizedValue;
|
|
1986
|
+
}
|
|
1987
|
+
return normalized;
|
|
1988
|
+
}
|
|
1989
|
+
function normalizeRegistryOverride(value) {
|
|
1990
|
+
if (typeof value === "string") return value.trim() || void 0;
|
|
1991
|
+
return value.length > 0 && value[0]?.length ? [...value] : void 0;
|
|
1992
|
+
}
|
|
1525
1993
|
var AcpxRuntime = class {
|
|
1526
1994
|
options;
|
|
1527
1995
|
testOptions;
|
|
@@ -1592,10 +2060,14 @@ var AcpxRuntime = class {
|
|
|
1592
2060
|
sessionMode: state.mode,
|
|
1593
2061
|
requestId: input.requestId,
|
|
1594
2062
|
timeoutMs: input.timeoutMs,
|
|
1595
|
-
signal: input.signal
|
|
2063
|
+
signal: input.signal,
|
|
2064
|
+
onElicitation: input.onElicitation
|
|
1596
2065
|
}));
|
|
1597
2066
|
return {
|
|
1598
2067
|
requestId: input.requestId,
|
|
2068
|
+
get promptStarted() {
|
|
2069
|
+
return turnPromise.then((turn) => turn.promptStarted);
|
|
2070
|
+
},
|
|
1599
2071
|
events: { async *[Symbol.asyncIterator]() {
|
|
1600
2072
|
yield* (await turnPromise).events;
|
|
1601
2073
|
} },
|
|
@@ -1620,7 +2092,8 @@ var AcpxRuntime = class {
|
|
|
1620
2092
|
sessionMode: state.mode,
|
|
1621
2093
|
requestId: input.requestId,
|
|
1622
2094
|
timeoutMs: input.timeoutMs,
|
|
1623
|
-
signal: input.signal
|
|
2095
|
+
signal: input.signal,
|
|
2096
|
+
onElicitation: input.onElicitation
|
|
1624
2097
|
});
|
|
1625
2098
|
}
|
|
1626
2099
|
async getCapabilities(input) {
|