acpx 0.13.0 → 0.13.2
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 -382
- package/dist/{cli-D52eGvxU.js → cli-DJT3MRRI.js} +6 -5
- package/dist/{cli-D52eGvxU.js.map → cli-DJT3MRRI.js.map} +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +20 -13
- package/dist/cli.js.map +1 -1
- package/dist/{client-BeKvumq9.d.ts → client-CxNllqui.d.ts} +15 -3
- package/dist/client-CxNllqui.d.ts.map +1 -0
- package/dist/{flags---9OR06b.js → flags-CM2rAdBo.js} +3 -3
- package/dist/{flags---9OR06b.js.map → flags-CM2rAdBo.js.map} +1 -1
- package/dist/{flows-CYfjwbn2.js → flows-BiRKgCnW.js} +4 -4
- package/dist/{flows-CYfjwbn2.js.map → flows-BiRKgCnW.js.map} +1 -1
- package/dist/flows.d.ts +2 -2
- package/dist/flows.js +1 -1
- package/dist/{live-checkpoint-CBecfnSH.js → live-checkpoint-Gw2oGjhe.js} +445 -252
- package/dist/live-checkpoint-Gw2oGjhe.js.map +1 -0
- package/dist/{output-CzN8vLdE.js → output-DiPPprGk.js} +40 -84
- package/dist/output-DiPPprGk.js.map +1 -0
- package/dist/runtime.d.ts +107 -23
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +600 -202
- package/dist/runtime.js.map +1 -1
- package/dist/{session-options-CTdZvr68.d.ts → session-options-DwRDODlr.d.ts} +25 -3
- package/dist/session-options-DwRDODlr.d.ts.map +1 -0
- package/package.json +15 -15
- package/skills/acpx/SKILL.md +5 -1
- package/dist/client-BeKvumq9.d.ts.map +0 -1
- package/dist/live-checkpoint-CBecfnSH.js.map +0 -1
- package/dist/output-CzN8vLdE.js.map +0 -1
- package/dist/session-options-CTdZvr68.d.ts.map +0 -1
package/dist/runtime.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as RequestedModelUnsupportedError, At as DEFAULT_AGENT_NAME, Bt as extractAcpError, C as sessionOptionsFromRecord, D as AcpClient, E as reconcileAgentSessionId, Ft as resolveCanonicalAgentName, M as modelStateFromConfigOptions, Mt as normalizeAgentName, N as normalizeAgentCommandInput, Nt as resolveAgentArgv, O as REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, Pt as resolveAgentCommand, Q as assertPersistedKeyPolicy, S as persistSessionOptions, T as applyLifecycleSnapshotToRecord, Tt as textPrompt, Vt as isAcpResourceNotFoundError, Z as createAtomicWriteTempPath, _ as recordPromptSubmission, a as applyConfigOptionSelection, b as advertisedModelState, c as applyRequestedModelIfAdvertised, ct as serializeSessionRecordForDisk, d as setDesiredModeId, dt as defaultSessionEventLog, f as syncAdvertisedModelState, g as recordClientOperation, h as createSessionConversation, i as connectAndLoadSession, j as isRequestedModelUnsupportedError, jt as listBuiltInAgents, k as REQUESTED_MODEL_UNSUPPORTED_REASONS, kt as withTimeout, l as currentModelIdFromSetModelResponse, m as cloneSessionConversation, n as runPromptTurn, o as applyConfigOptionsToRecord, p as cloneSessionAcpxState, r as withConnectedSession, s as applyModelSelection, st as parseSessionRecord, t as LiveSessionCheckpoint, u as setCurrentModelId, v as recordSessionUpdate, w as applyConversation, y as trimConversationForRuntime, zt as normalizeOutputError } from "./live-checkpoint-Gw2oGjhe.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) {
|
|
@@ -179,7 +221,7 @@ function summarizeToolInput(rawInput) {
|
|
|
179
221
|
}
|
|
180
222
|
function truncateToolSummary(value) {
|
|
181
223
|
if (value.length <= TOOL_OUTPUT_SUMMARY_MAX_CHARS) return value;
|
|
182
|
-
return `${value.slice(0,
|
|
224
|
+
return `${value.slice(0, 499)}…`;
|
|
183
225
|
}
|
|
184
226
|
function readToolContentText(value) {
|
|
185
227
|
const record = isRecord(value) ? value : void 0;
|
|
@@ -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 = [];
|
|
@@ -685,30 +749,6 @@ function resolveSupportedConfigOptionId(record, configId) {
|
|
|
685
749
|
const supportedText = supported.length > 0 ? supported.join(", ") : "none";
|
|
686
750
|
throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `ACP session ${record.acpxRecordId} does not advertise config option '${configId}'. Supported config options: ${supportedText}.`);
|
|
687
751
|
}
|
|
688
|
-
function applyConfigOptionResponseToTurn(turn, response) {
|
|
689
|
-
if (!response?.configOptions) return;
|
|
690
|
-
turn.acpxState = applyConfigOptionsToState(turn.acpxState, response.configOptions);
|
|
691
|
-
}
|
|
692
|
-
function applyDesiredConfigOptionToTurn(turn, configId, value) {
|
|
693
|
-
const nextState = cloneSessionAcpxState(turn.acpxState) ?? {};
|
|
694
|
-
if (configId === modelStateFromConfigOptions(nextState.config_options)?.configId) {
|
|
695
|
-
nextState.session_options = {
|
|
696
|
-
...nextState.session_options,
|
|
697
|
-
model: value
|
|
698
|
-
};
|
|
699
|
-
clearDesiredConfigOption(nextState, configId);
|
|
700
|
-
} else if (configId === "mode") nextState.desired_mode_id = value;
|
|
701
|
-
else nextState.desired_config_options = {
|
|
702
|
-
...nextState.desired_config_options,
|
|
703
|
-
[configId]: value
|
|
704
|
-
};
|
|
705
|
-
turn.acpxState = nextState;
|
|
706
|
-
}
|
|
707
|
-
function applyDesiredConfigOptionToRecord(record, configId, value) {
|
|
708
|
-
if (configId === modelStateFromConfigOptions(record.acpx?.config_options)?.configId) setDesiredModelId(record, value, configId);
|
|
709
|
-
else if (configId === "mode") setDesiredModeId(record, value);
|
|
710
|
-
else setDesiredConfigOption(record, configId, value);
|
|
711
|
-
}
|
|
712
752
|
async function createOrLoadRuntimeSession(client, resumeSessionId, cwd) {
|
|
713
753
|
if (resumeSessionId) {
|
|
714
754
|
if (client.supportsResumeSession()) {
|
|
@@ -738,7 +778,10 @@ var AcpRuntimeManager = class {
|
|
|
738
778
|
options;
|
|
739
779
|
deps;
|
|
740
780
|
activeControllers = /* @__PURE__ */ new Map();
|
|
741
|
-
|
|
781
|
+
retainedSessionOwners = /* @__PURE__ */ new Map();
|
|
782
|
+
pendingOneShotRecordIds = /* @__PURE__ */ new Map();
|
|
783
|
+
ensureSessionLocks = /* @__PURE__ */ new Map();
|
|
784
|
+
runtimeOperationLocks = /* @__PURE__ */ new Map();
|
|
742
785
|
closingActiveRecords = /* @__PURE__ */ new Set();
|
|
743
786
|
constructor(options, deps = {}) {
|
|
744
787
|
this.options = options;
|
|
@@ -747,22 +790,116 @@ var AcpRuntimeManager = class {
|
|
|
747
790
|
createClient(options) {
|
|
748
791
|
return this.deps.clientFactory?.(options) ?? new AcpClient(options);
|
|
749
792
|
}
|
|
750
|
-
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
793
|
+
createSessionOwner(input) {
|
|
794
|
+
const owner = {
|
|
795
|
+
...input,
|
|
796
|
+
bufferSessionUpdates: false,
|
|
797
|
+
pendingSessionUpdates: []
|
|
798
|
+
};
|
|
799
|
+
input.client.setEventHandlers({
|
|
800
|
+
onSessionUpdate: (notification) => this.routeOwnedSessionUpdate(owner, notification),
|
|
801
|
+
onClientOperation: (operation) => this.routeOwnedClientOperation(owner, operation)
|
|
802
|
+
});
|
|
803
|
+
return owner;
|
|
804
|
+
}
|
|
805
|
+
routeOwnedSessionUpdate(owner, notification) {
|
|
806
|
+
const active = owner.activeTurn;
|
|
807
|
+
if (active) {
|
|
808
|
+
const { task, turn } = active;
|
|
809
|
+
if (turn.connected) {
|
|
810
|
+
turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
|
|
811
|
+
turn.liveCheckpoint.request();
|
|
812
|
+
} else turn.record.acpx = recordSessionUpdate(turn.conversation, turn.record.acpx, notification);
|
|
813
|
+
trimConversationForRuntime(turn.conversation);
|
|
814
|
+
this.emitRuntimeTurnEvent(task, {
|
|
815
|
+
jsonrpc: "2.0",
|
|
816
|
+
method: "session/update",
|
|
817
|
+
params: notification
|
|
818
|
+
});
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (owner.bufferSessionUpdates) {
|
|
822
|
+
owner.pendingSessionUpdates.push(notification);
|
|
756
823
|
return;
|
|
757
824
|
}
|
|
758
|
-
|
|
759
|
-
|
|
825
|
+
const projection = owner.projection;
|
|
826
|
+
if (!projection) {
|
|
827
|
+
owner.pendingSessionUpdates.push(notification);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
projection.record.acpx = recordSessionUpdate(projection.conversation, projection.record.acpx, notification);
|
|
831
|
+
trimConversationForRuntime(projection.conversation);
|
|
832
|
+
projection.checkpoint.request();
|
|
833
|
+
}
|
|
834
|
+
routeOwnedClientOperation(owner, operation) {
|
|
835
|
+
const active = owner.activeTurn;
|
|
836
|
+
if (!active) return;
|
|
837
|
+
const { task, turn } = active;
|
|
838
|
+
if (turn.connected) {
|
|
839
|
+
turn.acpxState = recordClientOperation(turn.conversation, turn.acpxState, operation);
|
|
840
|
+
turn.liveCheckpoint.request();
|
|
841
|
+
} else turn.record.acpx = recordClientOperation(turn.conversation, turn.record.acpx, operation);
|
|
842
|
+
trimConversationForRuntime(turn.conversation);
|
|
843
|
+
this.emitRuntimeTurnEvent(task, {
|
|
844
|
+
type: "client_operation",
|
|
845
|
+
...operation
|
|
846
|
+
});
|
|
760
847
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
848
|
+
attachIdleProjection(owner, record, conversation = cloneSessionConversation(record), acpxState = record.acpx) {
|
|
849
|
+
record.acpx = acpxState;
|
|
850
|
+
owner.projection = {
|
|
851
|
+
record,
|
|
852
|
+
conversation,
|
|
853
|
+
checkpoint: new LiveSessionCheckpoint({ save: async () => {
|
|
854
|
+
if (!owner.recordId) return;
|
|
855
|
+
record.lastUsedAt = isoNow();
|
|
856
|
+
applyConversation(record, conversation);
|
|
857
|
+
applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
|
|
858
|
+
await this.refreshClosedState(record);
|
|
859
|
+
await this.options.sessionStore.save(record);
|
|
860
|
+
} })
|
|
861
|
+
};
|
|
862
|
+
owner.activeTurn = void 0;
|
|
863
|
+
this.drainPendingSessionUpdates(owner);
|
|
864
|
+
}
|
|
865
|
+
drainPendingSessionUpdates(owner) {
|
|
866
|
+
for (const notification of owner.pendingSessionUpdates.splice(0)) this.routeOwnedSessionUpdate(owner, notification);
|
|
867
|
+
}
|
|
868
|
+
async flushSessionOwner(owner) {
|
|
869
|
+
await owner.client.waitForSessionUpdatesIdle?.().catch(() => {});
|
|
870
|
+
await owner.projection?.checkpoint.flush();
|
|
871
|
+
}
|
|
872
|
+
removeRetainedSessionOwner(owner) {
|
|
873
|
+
if (owner.recordId && this.retainedSessionOwners.get(owner.recordId) === owner) this.retainedSessionOwners.delete(owner.recordId);
|
|
874
|
+
if (owner.mode === "oneshot" && owner.recordId && this.pendingOneShotRecordIds.get(owner.sessionKey) === owner.recordId) this.pendingOneShotRecordIds.delete(owner.sessionKey);
|
|
875
|
+
}
|
|
876
|
+
async readRetainedSessionOwner(record, options) {
|
|
877
|
+
const owner = this.retainedSessionOwners.get(record.acpxRecordId);
|
|
878
|
+
if (!owner) return;
|
|
879
|
+
await this.flushSessionOwner(owner);
|
|
880
|
+
const projectedRecord = owner.projection?.record;
|
|
881
|
+
if (projectedRecord && projectedRecord !== record) Object.assign(record, structuredClone(projectedRecord));
|
|
882
|
+
if (!owner.client.hasReusableSession(record.acpSessionId)) {
|
|
883
|
+
this.removeRetainedSessionOwner(owner);
|
|
884
|
+
await this.stopSessionOwner(owner);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (options.consume) this.removeRetainedSessionOwner(owner);
|
|
888
|
+
return owner;
|
|
889
|
+
}
|
|
890
|
+
async closeRetainedSessionOwner(recordId) {
|
|
891
|
+
const owner = this.retainedSessionOwners.get(recordId);
|
|
892
|
+
if (!owner) return;
|
|
893
|
+
this.removeRetainedSessionOwner(owner);
|
|
894
|
+
await this.stopSessionOwner(owner);
|
|
895
|
+
}
|
|
896
|
+
async stopSessionOwner(owner) {
|
|
897
|
+
await this.flushSessionOwner(owner).catch(() => {});
|
|
898
|
+
await owner.client.close().catch(() => {});
|
|
899
|
+
await owner.projection?.checkpoint.flush().catch(() => {});
|
|
900
|
+
try {
|
|
901
|
+
owner.client.clearEventHandlers();
|
|
902
|
+
} catch {}
|
|
766
903
|
}
|
|
767
904
|
async refreshClosedState(record) {
|
|
768
905
|
if (!this.closingActiveRecords.has(record.acpxRecordId)) return record.closed === true;
|
|
@@ -775,32 +912,42 @@ var AcpRuntimeManager = class {
|
|
|
775
912
|
};
|
|
776
913
|
return true;
|
|
777
914
|
}
|
|
778
|
-
async
|
|
779
|
-
const { record,
|
|
780
|
-
if (
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
915
|
+
async retainPersistentSessionOwnerAfterTurn(input) {
|
|
916
|
+
const { record, owner, conversation, acpxState } = input;
|
|
917
|
+
if (!this.canRetainPersistentSessionOwner(owner, record)) {
|
|
918
|
+
owner.activeTurn = void 0;
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
this.attachIdleProjection(owner, record, conversation, acpxState);
|
|
922
|
+
const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
|
|
923
|
+
this.retainedSessionOwners.set(record.acpxRecordId, owner);
|
|
924
|
+
if (previousOwner && previousOwner !== owner) {
|
|
925
|
+
this.removeRetainedSessionOwner(previousOwner);
|
|
926
|
+
await this.stopSessionOwner(previousOwner);
|
|
927
|
+
}
|
|
784
928
|
return true;
|
|
785
929
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
930
|
+
canRetainPersistentSessionOwner(owner, record) {
|
|
931
|
+
return owner.mode === "persistent" && !record.closed && !(owner.client.hasUnresolvedPrompt?.() ?? false) && owner.client.hasReusableSession(record.acpSessionId);
|
|
932
|
+
}
|
|
933
|
+
async withRuntimeControlSession(record, sessionMode, run, replacingConfigOption) {
|
|
934
|
+
const owner = await this.readRetainedSessionOwner(record, { consume: false });
|
|
935
|
+
if (owner) {
|
|
936
|
+
const ownedRecord = owner.projection?.record ?? record;
|
|
937
|
+
try {
|
|
938
|
+
const value = await run({
|
|
939
|
+
client: owner.client,
|
|
940
|
+
sessionId: ownedRecord.acpSessionId,
|
|
941
|
+
record: ownedRecord
|
|
942
|
+
});
|
|
943
|
+
this.refreshOwnedRecordLifecycle(owner, ownedRecord);
|
|
944
|
+
return {
|
|
945
|
+
value,
|
|
946
|
+
record: ownedRecord
|
|
947
|
+
};
|
|
948
|
+
} finally {
|
|
949
|
+
await this.flushSessionOwner(owner);
|
|
950
|
+
}
|
|
804
951
|
}
|
|
805
952
|
const result = await withConnectedSession({
|
|
806
953
|
sessionRecordId: record.acpxRecordId,
|
|
@@ -810,10 +957,13 @@ var AcpRuntimeManager = class {
|
|
|
810
957
|
mcpServers: [...this.options.mcpServers ?? []],
|
|
811
958
|
permissionMode: this.options.permissionMode,
|
|
812
959
|
nonInteractivePermissions: this.options.nonInteractivePermissions,
|
|
960
|
+
permissionPolicy: this.options.permissionPolicy,
|
|
813
961
|
onPermissionRequest: this.options.onPermissionRequest,
|
|
962
|
+
elicitationModes: this.options.elicitationModes,
|
|
814
963
|
verbose: this.options.verbose,
|
|
815
964
|
timeoutMs: this.options.timeoutMs,
|
|
816
965
|
resumePolicy: resumePolicyForSessionMode(sessionMode),
|
|
966
|
+
replacingConfigOption,
|
|
817
967
|
run
|
|
818
968
|
});
|
|
819
969
|
return {
|
|
@@ -821,21 +971,87 @@ var AcpRuntimeManager = class {
|
|
|
821
971
|
record: result.record
|
|
822
972
|
};
|
|
823
973
|
}
|
|
974
|
+
refreshOwnedRecordLifecycle(owner, record) {
|
|
975
|
+
record.lastUsedAt = isoNow();
|
|
976
|
+
record.closed = false;
|
|
977
|
+
record.closedAt = void 0;
|
|
978
|
+
record.protocolVersion = owner.client.initializeResult?.protocolVersion;
|
|
979
|
+
record.agentCapabilities = owner.client.initializeResult?.agentCapabilities;
|
|
980
|
+
applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
|
|
981
|
+
}
|
|
824
982
|
async ensureSession(input) {
|
|
983
|
+
return await this.withEnsureSessionLock(input, async () => this.ensureSessionWithOwnership(input));
|
|
984
|
+
}
|
|
985
|
+
async withEnsureSessionLock(input, run) {
|
|
986
|
+
const key = `${input.mode}\0${input.sessionKey}`;
|
|
987
|
+
return await this.withManagerLock(this.ensureSessionLocks, key, run);
|
|
988
|
+
}
|
|
989
|
+
async withManagerLock(locks, key, run) {
|
|
990
|
+
const previous = locks.get(key) ?? Promise.resolve();
|
|
991
|
+
let release;
|
|
992
|
+
const gate = new Promise((resolve) => {
|
|
993
|
+
release = resolve;
|
|
994
|
+
});
|
|
995
|
+
const tail = previous.then(() => gate);
|
|
996
|
+
locks.set(key, tail);
|
|
997
|
+
await previous;
|
|
998
|
+
try {
|
|
999
|
+
return await run();
|
|
1000
|
+
} finally {
|
|
1001
|
+
release();
|
|
1002
|
+
if (locks.get(key) === tail) locks.delete(key);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
async ensureSessionWithOwnership(input) {
|
|
825
1006
|
const cwd = path.resolve(input.cwd?.trim() || this.options.cwd);
|
|
826
1007
|
const { agentCommand, agentArgv } = normalizeAgentCommandInput(this.options.agentRegistry.resolve(input.agent));
|
|
827
|
-
const
|
|
828
|
-
if (input.mode === "persistent" && existing && shouldReuseExistingRecord(existing, {
|
|
1008
|
+
const agent = {
|
|
829
1009
|
cwd,
|
|
830
1010
|
agentCommand,
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1011
|
+
agentArgv
|
|
1012
|
+
};
|
|
1013
|
+
const existing = await this.loadExistingRuntimeSession(input);
|
|
1014
|
+
if (existing && this.canReuseRuntimeSession(input, agent, existing)) return await this.reuseRuntimeSession(existing.record);
|
|
1015
|
+
await this.closeConflictingPersistentSession(input, existing?.owner);
|
|
1016
|
+
return await this.createOwnedRuntimeSession(input, agent);
|
|
1017
|
+
}
|
|
1018
|
+
async loadExistingRuntimeSession(input) {
|
|
1019
|
+
const existingRecordId = input.mode === "persistent" ? input.sessionKey : this.pendingOneShotRecordIds.get(input.sessionKey);
|
|
1020
|
+
if (!existingRecordId) return;
|
|
1021
|
+
let record = await this.options.sessionStore.load(existingRecordId);
|
|
1022
|
+
if (!record) return;
|
|
1023
|
+
const owner = this.retainedSessionOwners.get(record.acpxRecordId);
|
|
1024
|
+
if (owner) {
|
|
1025
|
+
await this.flushSessionOwner(owner);
|
|
1026
|
+
record = owner.projection?.record ?? record;
|
|
838
1027
|
}
|
|
1028
|
+
return {
|
|
1029
|
+
record,
|
|
1030
|
+
owner
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
canReuseRuntimeSession(input, agent, existing) {
|
|
1034
|
+
if (!shouldReuseExistingRecord(existing.record, {
|
|
1035
|
+
cwd: agent.cwd,
|
|
1036
|
+
agentCommand: agent.agentCommand,
|
|
1037
|
+
agentArgv: agent.agentArgv,
|
|
1038
|
+
resumeSessionId: input.resumeSessionId
|
|
1039
|
+
})) return false;
|
|
1040
|
+
if (input.mode === "persistent") return true;
|
|
1041
|
+
return Boolean(existing.owner && isDeepStrictEqual(sessionOptionsFromRecord(existing.record), input.sessionOptions));
|
|
1042
|
+
}
|
|
1043
|
+
async reuseRuntimeSession(record) {
|
|
1044
|
+
record.closed = false;
|
|
1045
|
+
record.closedAt = void 0;
|
|
1046
|
+
this.closingActiveRecords.delete(record.acpxRecordId);
|
|
1047
|
+
await this.options.sessionStore.save(record);
|
|
1048
|
+
return record;
|
|
1049
|
+
}
|
|
1050
|
+
async closeConflictingPersistentSession(input, owner) {
|
|
1051
|
+
if (input.mode === "persistent" && owner?.recordId) await this.closeRetainedSessionOwner(owner.recordId);
|
|
1052
|
+
}
|
|
1053
|
+
async createOwnedRuntimeSession(input, agent) {
|
|
1054
|
+
const { cwd, agentCommand, agentArgv } = agent;
|
|
839
1055
|
const client = this.createClient({
|
|
840
1056
|
agentCommand,
|
|
841
1057
|
agentArgv,
|
|
@@ -843,30 +1059,43 @@ var AcpRuntimeManager = class {
|
|
|
843
1059
|
mcpServers: [...this.options.mcpServers ?? []],
|
|
844
1060
|
permissionMode: this.options.permissionMode,
|
|
845
1061
|
nonInteractivePermissions: this.options.nonInteractivePermissions,
|
|
1062
|
+
permissionPolicy: this.options.permissionPolicy,
|
|
846
1063
|
onPermissionRequest: this.options.onPermissionRequest,
|
|
1064
|
+
elicitationModes: this.options.elicitationModes,
|
|
847
1065
|
verbose: this.options.verbose,
|
|
848
1066
|
sessionOptions: input.sessionOptions
|
|
849
1067
|
});
|
|
850
|
-
|
|
1068
|
+
const owner = this.createSessionOwner({
|
|
1069
|
+
client,
|
|
1070
|
+
sessionKey: input.sessionKey,
|
|
1071
|
+
mode: input.mode
|
|
1072
|
+
});
|
|
1073
|
+
let retained = false;
|
|
851
1074
|
try {
|
|
852
1075
|
await client.start();
|
|
853
1076
|
const session = await createOrLoadRuntimeSession(client, input.resumeSessionId, cwd);
|
|
854
|
-
const record = await this.
|
|
1077
|
+
const record = await this.prepareInitialRuntimeRecord({
|
|
855
1078
|
input,
|
|
856
1079
|
client,
|
|
1080
|
+
owner,
|
|
857
1081
|
agentCommand,
|
|
858
1082
|
agentArgv,
|
|
859
1083
|
cwd,
|
|
860
1084
|
session
|
|
861
1085
|
});
|
|
862
|
-
|
|
1086
|
+
await this.retainInitializedSessionOwner(owner, record);
|
|
1087
|
+
retained = true;
|
|
863
1088
|
return record;
|
|
864
1089
|
} finally {
|
|
865
|
-
if (!
|
|
1090
|
+
if (!retained) {
|
|
1091
|
+
owner.recordId = void 0;
|
|
1092
|
+
client.clearEventHandlers();
|
|
1093
|
+
await client.close();
|
|
1094
|
+
}
|
|
866
1095
|
}
|
|
867
1096
|
}
|
|
868
|
-
async
|
|
869
|
-
const { input, client, agentCommand, agentArgv, cwd, session } = params;
|
|
1097
|
+
async prepareInitialRuntimeRecord(params) {
|
|
1098
|
+
const { input, client, owner, agentCommand, agentArgv, cwd, session } = params;
|
|
870
1099
|
const record = createInitialRecord({
|
|
871
1100
|
recordId: createRecordId(input.sessionKey, input.mode),
|
|
872
1101
|
sessionName: input.sessionKey,
|
|
@@ -879,6 +1108,7 @@ var AcpRuntimeManager = class {
|
|
|
879
1108
|
this.closingActiveRecords.delete(record.acpxRecordId);
|
|
880
1109
|
record.protocolVersion = client.initializeResult?.protocolVersion;
|
|
881
1110
|
record.agentCapabilities = client.initializeResult?.agentCapabilities;
|
|
1111
|
+
this.attachIdleProjection(owner, record);
|
|
882
1112
|
applyConfigOptionsToRecord(record, session.sessionResult);
|
|
883
1113
|
const modelApplication = await applyRequestedModelIfAdvertised({
|
|
884
1114
|
client,
|
|
@@ -893,20 +1123,31 @@ var AcpRuntimeManager = class {
|
|
|
893
1123
|
if (modelApplication.applied) setCurrentModelId(record, currentModelIdFromSetModelResponse(modelApplication.response, input.sessionOptions?.model));
|
|
894
1124
|
applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
|
|
895
1125
|
persistSessionOptions(record, input.sessionOptions);
|
|
896
|
-
await this.options.sessionStore.save(record);
|
|
897
1126
|
return record;
|
|
898
1127
|
}
|
|
899
|
-
async
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
this.
|
|
903
|
-
|
|
904
|
-
|
|
1128
|
+
async retainInitializedSessionOwner(owner, record) {
|
|
1129
|
+
owner.recordId = record.acpxRecordId;
|
|
1130
|
+
await owner.projection?.checkpoint.checkpoint();
|
|
1131
|
+
const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
|
|
1132
|
+
this.retainedSessionOwners.set(record.acpxRecordId, owner);
|
|
1133
|
+
if (owner.mode === "oneshot") this.pendingOneShotRecordIds.set(owner.sessionKey, record.acpxRecordId);
|
|
1134
|
+
if (previousOwner && previousOwner !== owner) {
|
|
1135
|
+
this.removeRetainedSessionOwner(previousOwner);
|
|
1136
|
+
await this.stopSessionOwner(previousOwner);
|
|
1137
|
+
}
|
|
905
1138
|
}
|
|
906
1139
|
startTurn(input) {
|
|
907
|
-
|
|
1140
|
+
let promptInput;
|
|
1141
|
+
try {
|
|
1142
|
+
promptInput = toPromptInput(input.text, input.attachments);
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
this.closeRetainedOneShotHandle(input.handle).catch(() => {});
|
|
1145
|
+
throw error;
|
|
1146
|
+
}
|
|
908
1147
|
const queue = new AsyncEventQueue();
|
|
909
1148
|
const result = createDeferred();
|
|
1149
|
+
const promptStarted = createDeferred();
|
|
1150
|
+
promptStarted.promise.catch(() => {});
|
|
910
1151
|
const sessionReady = createDeferred();
|
|
911
1152
|
sessionReady.promise.catch(() => {});
|
|
912
1153
|
let resultSettled = false;
|
|
@@ -938,13 +1179,17 @@ var AcpRuntimeManager = class {
|
|
|
938
1179
|
};
|
|
939
1180
|
if (input.signal) {
|
|
940
1181
|
if (input.signal.aborted) {
|
|
1182
|
+
promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
|
|
941
1183
|
closeStream();
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1184
|
+
this.closeRetainedOneShotHandle(input.handle).catch(() => {}).then(() => {
|
|
1185
|
+
settleResult({
|
|
1186
|
+
status: "cancelled",
|
|
1187
|
+
stopReason: "cancelled"
|
|
1188
|
+
});
|
|
945
1189
|
});
|
|
946
1190
|
return {
|
|
947
1191
|
requestId: input.requestId,
|
|
1192
|
+
promptStarted: promptStarted.promise,
|
|
948
1193
|
events: queue.iterate(),
|
|
949
1194
|
result: result.promise,
|
|
950
1195
|
cancel: async () => {},
|
|
@@ -957,6 +1202,7 @@ var AcpRuntimeManager = class {
|
|
|
957
1202
|
input,
|
|
958
1203
|
promptInput,
|
|
959
1204
|
queue,
|
|
1205
|
+
promptStarted,
|
|
960
1206
|
sessionReady,
|
|
961
1207
|
state,
|
|
962
1208
|
settleResult,
|
|
@@ -964,6 +1210,7 @@ var AcpRuntimeManager = class {
|
|
|
964
1210
|
});
|
|
965
1211
|
return {
|
|
966
1212
|
requestId: input.requestId,
|
|
1213
|
+
promptStarted: promptStarted.promise,
|
|
967
1214
|
events: queue.iterate(),
|
|
968
1215
|
result: result.promise,
|
|
969
1216
|
cancel: async () => {
|
|
@@ -974,61 +1221,159 @@ var AcpRuntimeManager = class {
|
|
|
974
1221
|
}
|
|
975
1222
|
};
|
|
976
1223
|
}
|
|
1224
|
+
async closeRetainedOneShotHandle(handle) {
|
|
1225
|
+
const recordId = handle.acpxRecordId ?? handle.sessionKey;
|
|
1226
|
+
if (this.retainedSessionOwners.get(recordId)?.mode === "oneshot") await this.closeRetainedSessionOwner(recordId);
|
|
1227
|
+
}
|
|
977
1228
|
async runRuntimeTurnTask(task) {
|
|
978
1229
|
let turn;
|
|
1230
|
+
let terminalResult;
|
|
979
1231
|
try {
|
|
980
1232
|
turn = await this.prepareRuntimeTurn(task);
|
|
981
1233
|
const { sessionId, resumed, loadError } = await this.connectRuntimeTurn(task, turn);
|
|
982
1234
|
await this.resolveRuntimeTurnReady(task, turn, resumed, loadError);
|
|
983
|
-
if (this.cancelRuntimeTurnBeforePrompt(task))
|
|
984
|
-
|
|
985
|
-
|
|
1235
|
+
if (this.cancelRuntimeTurnBeforePrompt(task)) terminalResult = {
|
|
1236
|
+
status: "cancelled",
|
|
1237
|
+
stopReason: "cancelled"
|
|
1238
|
+
};
|
|
1239
|
+
else {
|
|
1240
|
+
await this.applyPendingRuntimeTurnCancel(task, turn);
|
|
1241
|
+
const response = await this.runRuntimePrompt(task, turn, sessionId);
|
|
1242
|
+
await this.saveCompletedRuntimeTurn(turn, response.stopReason);
|
|
1243
|
+
terminalResult = {
|
|
1244
|
+
status: response.stopReason === "cancelled" ? "cancelled" : "completed",
|
|
1245
|
+
...response.stopReason ? { stopReason: response.stopReason } : {}
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
terminalResult = this.failRuntimeTurn(task, error);
|
|
1250
|
+
}
|
|
1251
|
+
try {
|
|
1252
|
+
await this.finalizeRuntimeTurn(task, turn);
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
terminalResult = this.failRuntimeTurn(task, error);
|
|
1255
|
+
}
|
|
1256
|
+
task.settleResult(terminalResult);
|
|
1257
|
+
}
|
|
1258
|
+
async runRuntimePrompt(task, turn, sessionId) {
|
|
1259
|
+
try {
|
|
1260
|
+
return await runPromptTurn({
|
|
986
1261
|
client: turn.client,
|
|
987
1262
|
sessionId,
|
|
988
1263
|
prompt: task.promptInput,
|
|
989
1264
|
timeoutMs: task.input.timeoutMs ?? this.options.timeoutMs,
|
|
990
1265
|
conversation: turn.conversation,
|
|
991
|
-
promptMessageId: turn.promptMessageId
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
task.settleResult({
|
|
995
|
-
status: response.stopReason === "cancelled" ? "cancelled" : "completed",
|
|
996
|
-
...response.stopReason ? { stopReason: response.stopReason } : {}
|
|
1266
|
+
promptMessageId: turn.promptMessageId,
|
|
1267
|
+
onPromptRequestStarted: () => task.promptStarted.resolve(),
|
|
1268
|
+
onElicitation: task.input.onElicitation
|
|
997
1269
|
});
|
|
998
|
-
} catch (error) {
|
|
999
|
-
this.failRuntimeTurn(task, error);
|
|
1000
1270
|
} finally {
|
|
1001
|
-
|
|
1271
|
+
turn.client.endPromptElicitation?.(sessionId);
|
|
1002
1272
|
}
|
|
1003
1273
|
}
|
|
1004
1274
|
async prepareRuntimeTurn(task) {
|
|
1005
|
-
const
|
|
1275
|
+
const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
|
|
1276
|
+
return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.prepareRuntimeTurnWithOwnership(task));
|
|
1277
|
+
}
|
|
1278
|
+
async prepareRuntimeTurnWithOwnership(task) {
|
|
1279
|
+
const { record, retainedOwner, conversation, acpxState, promptMessageId } = await this.prepareRuntimeTurnState(task);
|
|
1280
|
+
try {
|
|
1281
|
+
const client = retainedOwner?.client ?? this.createTurnClient(record);
|
|
1282
|
+
const owner = this.resolveRuntimeTurnOwner(task, record, client, retainedOwner);
|
|
1283
|
+
const turn = this.createRunningRuntimeTurn({
|
|
1284
|
+
record,
|
|
1285
|
+
conversation,
|
|
1286
|
+
acpxState,
|
|
1287
|
+
client,
|
|
1288
|
+
owner,
|
|
1289
|
+
connected: retainedOwner !== void 0,
|
|
1290
|
+
promptMessageId
|
|
1291
|
+
});
|
|
1292
|
+
this.activateRuntimeTurn(task, turn);
|
|
1293
|
+
return turn;
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
this.restoreBufferedSessionOwner(retainedOwner);
|
|
1296
|
+
throw error;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
async prepareRuntimeTurnState(task) {
|
|
1300
|
+
const { record, retainedOwner } = await this.acquireRuntimeTurnState(task);
|
|
1006
1301
|
const conversation = cloneSessionConversation(record);
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1302
|
+
const acpxState = cloneSessionAcpxState(record.acpx);
|
|
1303
|
+
try {
|
|
1304
|
+
const promptStartedAt = isoNow();
|
|
1305
|
+
const promptMessageId = recordPromptSubmission(conversation, task.promptInput, promptStartedAt);
|
|
1306
|
+
trimConversationForRuntime(conversation);
|
|
1307
|
+
record.lastPromptAt = promptStartedAt;
|
|
1308
|
+
record.lastUsedAt = promptStartedAt;
|
|
1309
|
+
record.acpx = acpxState;
|
|
1310
|
+
applyConversation(record, conversation);
|
|
1311
|
+
await this.options.sessionStore.save(record);
|
|
1312
|
+
return {
|
|
1313
|
+
record,
|
|
1314
|
+
retainedOwner,
|
|
1315
|
+
conversation,
|
|
1316
|
+
acpxState,
|
|
1317
|
+
promptMessageId
|
|
1318
|
+
};
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
this.restoreBufferedSessionOwner(retainedOwner);
|
|
1321
|
+
throw error;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
async acquireRuntimeTurnState(task) {
|
|
1325
|
+
const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
|
|
1326
|
+
let record = await this.requireRecord(recordId);
|
|
1327
|
+
const retainedOwner = await this.readRetainedSessionOwner(record, { consume: false });
|
|
1328
|
+
if (!retainedOwner) return { record };
|
|
1329
|
+
const projection = retainedOwner.projection;
|
|
1330
|
+
if (projection) record = structuredClone(projection.record);
|
|
1331
|
+
retainedOwner.bufferSessionUpdates = true;
|
|
1332
|
+
return {
|
|
1333
|
+
record,
|
|
1334
|
+
retainedOwner
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
restoreBufferedSessionOwner(owner) {
|
|
1338
|
+
if (!owner) return;
|
|
1339
|
+
owner.bufferSessionUpdates = false;
|
|
1340
|
+
this.drainPendingSessionUpdates(owner);
|
|
1341
|
+
}
|
|
1342
|
+
resolveRuntimeTurnOwner(task, record, client, retainedOwner) {
|
|
1343
|
+
return retainedOwner ?? this.createSessionOwner({
|
|
1344
|
+
client,
|
|
1345
|
+
sessionKey: record.name ?? task.input.handle.sessionKey,
|
|
1346
|
+
mode: task.input.sessionMode
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
createRunningRuntimeTurn(input) {
|
|
1350
|
+
const { record, conversation, acpxState, client, owner, connected, promptMessageId } = input;
|
|
1018
1351
|
const turn = {
|
|
1019
1352
|
record,
|
|
1020
1353
|
conversation,
|
|
1021
1354
|
acpxState,
|
|
1022
1355
|
liveCheckpoint: this.createRuntimeTurnCheckpoint(record, conversation, () => turn.acpxState),
|
|
1023
1356
|
client,
|
|
1024
|
-
|
|
1357
|
+
owner,
|
|
1358
|
+
connected,
|
|
1025
1359
|
promptMessageId,
|
|
1026
1360
|
activeSessionId: record.acpSessionId
|
|
1027
1361
|
};
|
|
1362
|
+
return turn;
|
|
1363
|
+
}
|
|
1364
|
+
activateRuntimeTurn(task, turn) {
|
|
1365
|
+
const { owner, record } = turn;
|
|
1366
|
+
this.removeRetainedSessionOwner(owner);
|
|
1367
|
+
owner.recordId = record.acpxRecordId;
|
|
1028
1368
|
task.state.activeController = this.buildRuntimeTurnController(task, turn);
|
|
1029
1369
|
this.activeControllers.set(record.acpxRecordId, task.state.activeController);
|
|
1030
|
-
|
|
1031
|
-
|
|
1370
|
+
owner.projection = void 0;
|
|
1371
|
+
owner.activeTurn = {
|
|
1372
|
+
task,
|
|
1373
|
+
turn
|
|
1374
|
+
};
|
|
1375
|
+
owner.bufferSessionUpdates = false;
|
|
1376
|
+
this.drainPendingSessionUpdates(owner);
|
|
1032
1377
|
}
|
|
1033
1378
|
createTurnClient(record) {
|
|
1034
1379
|
return this.createClient({
|
|
@@ -1038,7 +1383,9 @@ var AcpRuntimeManager = class {
|
|
|
1038
1383
|
mcpServers: [...this.options.mcpServers ?? []],
|
|
1039
1384
|
permissionMode: this.options.permissionMode,
|
|
1040
1385
|
nonInteractivePermissions: this.options.nonInteractivePermissions,
|
|
1386
|
+
permissionPolicy: this.options.permissionPolicy,
|
|
1041
1387
|
onPermissionRequest: this.options.onPermissionRequest,
|
|
1388
|
+
elicitationModes: this.options.elicitationModes,
|
|
1042
1389
|
verbose: this.options.verbose,
|
|
1043
1390
|
sessionOptions: sessionOptionsFromRecord(record)
|
|
1044
1391
|
});
|
|
@@ -1067,15 +1414,7 @@ var AcpRuntimeManager = class {
|
|
|
1067
1414
|
await this.waitForRuntimeControlSession(task, turn);
|
|
1068
1415
|
const models = advertisedModelState(turn.acpxState);
|
|
1069
1416
|
const response = await turn.client.setSessionModel(turn.activeSessionId, modelId, models);
|
|
1070
|
-
|
|
1071
|
-
const nextState = cloneSessionAcpxState(turn.acpxState) ?? {};
|
|
1072
|
-
nextState.session_options = {
|
|
1073
|
-
...nextState.session_options,
|
|
1074
|
-
model: modelId
|
|
1075
|
-
};
|
|
1076
|
-
nextState.current_model_id = currentModelIdFromSetModelResponse(response, modelId);
|
|
1077
|
-
clearDesiredConfigOption(nextState, models?.configId);
|
|
1078
|
-
turn.acpxState = nextState;
|
|
1417
|
+
turn.acpxState = applyModelSelection(turn.acpxState, modelId, response);
|
|
1079
1418
|
return response;
|
|
1080
1419
|
},
|
|
1081
1420
|
setSessionConfigOption: async (configId, value) => {
|
|
@@ -1100,52 +1439,28 @@ var AcpRuntimeManager = class {
|
|
|
1100
1439
|
...turn.record,
|
|
1101
1440
|
acpx: turn.acpxState ?? void 0
|
|
1102
1441
|
}, configId);
|
|
1442
|
+
const modelConfigId = advertisedModelState(turn.acpxState)?.configId;
|
|
1103
1443
|
const response = await turn.client.setSessionConfigOption(turn.activeSessionId, resolvedConfigId, value);
|
|
1104
|
-
|
|
1444
|
+
turn.acpxState = applyConfigOptionSelection(turn.acpxState, resolvedConfigId, value, response, modelConfigId);
|
|
1105
1445
|
return {
|
|
1106
1446
|
configId: resolvedConfigId,
|
|
1107
1447
|
response
|
|
1108
1448
|
};
|
|
1109
1449
|
}
|
|
1110
|
-
applyRuntimeConfigOptionState(turn, configId, value, response) {
|
|
1111
|
-
applyConfigOptionResponseToTurn(turn, response);
|
|
1112
|
-
applyDesiredConfigOptionToTurn(turn, configId, value);
|
|
1113
|
-
}
|
|
1114
|
-
installRuntimeTurnEventHandlers(task, turn) {
|
|
1115
|
-
turn.client.setEventHandlers({
|
|
1116
|
-
onSessionUpdate: (notification) => {
|
|
1117
|
-
turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
|
|
1118
|
-
trimConversationForRuntime(turn.conversation);
|
|
1119
|
-
turn.liveCheckpoint.request();
|
|
1120
|
-
this.emitRuntimeTurnEvent(task, {
|
|
1121
|
-
jsonrpc: "2.0",
|
|
1122
|
-
method: "session/update",
|
|
1123
|
-
params: notification
|
|
1124
|
-
});
|
|
1125
|
-
},
|
|
1126
|
-
onClientOperation: (operation) => {
|
|
1127
|
-
turn.acpxState = recordClientOperation(turn.conversation, turn.acpxState, operation);
|
|
1128
|
-
trimConversationForRuntime(turn.conversation);
|
|
1129
|
-
turn.liveCheckpoint.request();
|
|
1130
|
-
this.emitRuntimeTurnEvent(task, {
|
|
1131
|
-
type: "client_operation",
|
|
1132
|
-
...operation
|
|
1133
|
-
});
|
|
1134
|
-
}
|
|
1135
|
-
});
|
|
1136
|
-
}
|
|
1137
1450
|
emitRuntimeTurnEvent(task, payload) {
|
|
1138
1451
|
const parsed = parsePromptEventLine(JSON.stringify(payload));
|
|
1139
1452
|
if (!parsed) return;
|
|
1140
1453
|
task.queue.push(parsed);
|
|
1141
1454
|
}
|
|
1142
1455
|
async connectRuntimeTurn(task, turn) {
|
|
1143
|
-
|
|
1456
|
+
if (turn.connected) return {
|
|
1144
1457
|
sessionId: turn.record.acpSessionId,
|
|
1145
1458
|
resumed: false,
|
|
1146
1459
|
loadError: void 0
|
|
1147
|
-
}
|
|
1460
|
+
};
|
|
1461
|
+
const loaded = await this.connectRuntimeTurnClient(task, turn);
|
|
1148
1462
|
turn.acpxState = cloneSessionAcpxState(turn.record.acpx);
|
|
1463
|
+
turn.connected = true;
|
|
1149
1464
|
return loaded;
|
|
1150
1465
|
}
|
|
1151
1466
|
async connectRuntimeTurnClient(task, turn) {
|
|
@@ -1188,10 +1503,7 @@ var AcpRuntimeManager = class {
|
|
|
1188
1503
|
cancelRuntimeTurnBeforePrompt(task) {
|
|
1189
1504
|
if (!task.state.pendingCancel && !task.input.signal?.aborted) return false;
|
|
1190
1505
|
task.state.pendingCancel = false;
|
|
1191
|
-
task.
|
|
1192
|
-
status: "cancelled",
|
|
1193
|
-
stopReason: "cancelled"
|
|
1194
|
-
});
|
|
1506
|
+
task.promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
|
|
1195
1507
|
return true;
|
|
1196
1508
|
}
|
|
1197
1509
|
async applyPendingRuntimeTurnCancel(task, turn) {
|
|
@@ -1211,9 +1523,10 @@ var AcpRuntimeManager = class {
|
|
|
1211
1523
|
await this.options.sessionStore.save(turn.record);
|
|
1212
1524
|
}
|
|
1213
1525
|
failRuntimeTurn(task, error) {
|
|
1526
|
+
task.promptStarted.reject(error);
|
|
1214
1527
|
task.sessionReady.reject(error);
|
|
1215
1528
|
const normalized = normalizeOutputError(error, { origin: "runtime" });
|
|
1216
|
-
|
|
1529
|
+
return {
|
|
1217
1530
|
status: "failed",
|
|
1218
1531
|
error: {
|
|
1219
1532
|
message: normalized.message,
|
|
@@ -1221,13 +1534,36 @@ var AcpRuntimeManager = class {
|
|
|
1221
1534
|
...normalized.detailCode ? { detailCode: normalized.detailCode } : {},
|
|
1222
1535
|
...normalized.retryable !== void 0 ? { retryable: normalized.retryable } : {}
|
|
1223
1536
|
}
|
|
1224
|
-
}
|
|
1537
|
+
};
|
|
1225
1538
|
}
|
|
1226
1539
|
async finalizeRuntimeTurn(task, turn) {
|
|
1227
1540
|
task.state.turnActive = false;
|
|
1228
|
-
task.input.signal?.removeEventListener("abort", task.abortHandler);
|
|
1229
|
-
turn
|
|
1230
|
-
|
|
1541
|
+
const abortHandlerAttempt = await settleAttempt(() => task.input.signal?.removeEventListener("abort", task.abortHandler));
|
|
1542
|
+
const recordAttempt = await settleAttempt(async () => turn ? await this.finalizeRuntimeTurnRecord(turn) : false);
|
|
1543
|
+
let failure = firstFailedAttempt([abortHandlerAttempt, recordAttempt]);
|
|
1544
|
+
let pooled = recordAttempt.ok ? recordAttempt.value : false;
|
|
1545
|
+
if (failure) {
|
|
1546
|
+
this.discardRetainedRuntimeTurnOwner(turn);
|
|
1547
|
+
pooled = false;
|
|
1548
|
+
}
|
|
1549
|
+
const closeAttempt = await settleAttempt(async () => this.closeRuntimeTurnClient(turn, pooled));
|
|
1550
|
+
this.cleanupRuntimeTurn(task, turn);
|
|
1551
|
+
failure ??= firstFailedAttempt([closeAttempt]);
|
|
1552
|
+
if (failure) throw failure.error;
|
|
1553
|
+
}
|
|
1554
|
+
discardRetainedRuntimeTurnOwner(turn) {
|
|
1555
|
+
if (turn) {
|
|
1556
|
+
this.removeRetainedSessionOwner(turn.owner);
|
|
1557
|
+
turn.owner.activeTurn = void 0;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
async closeRuntimeTurnClient(turn, pooled) {
|
|
1561
|
+
if (!turn || pooled) return;
|
|
1562
|
+
turn.owner.activeTurn = void 0;
|
|
1563
|
+
const failure = firstFailedAttempt([await settleAttempt(() => turn.client.clearEventHandlers()), await settleAttempt(async () => turn.client.close())]);
|
|
1564
|
+
if (failure) throw failure.error;
|
|
1565
|
+
}
|
|
1566
|
+
cleanupRuntimeTurn(task, turn) {
|
|
1231
1567
|
if (turn) {
|
|
1232
1568
|
this.activeControllers.delete(turn.record.acpxRecordId);
|
|
1233
1569
|
this.closingActiveRecords.delete(turn.record.acpxRecordId);
|
|
@@ -1235,17 +1571,20 @@ var AcpRuntimeManager = class {
|
|
|
1235
1571
|
task.queue.close();
|
|
1236
1572
|
}
|
|
1237
1573
|
async finalizeRuntimeTurnRecord(turn) {
|
|
1574
|
+
if (!turn.connected) turn.acpxState = cloneSessionAcpxState(turn.record.acpx);
|
|
1238
1575
|
applyLifecycleSnapshotToRecord(turn.record, turn.client.getAgentLifecycleSnapshot());
|
|
1239
1576
|
turn.record.acpx = turn.acpxState;
|
|
1240
1577
|
applyConversation(turn.record, turn.conversation);
|
|
1241
1578
|
turn.record.lastUsedAt = isoNow();
|
|
1242
|
-
await turn.liveCheckpoint.flush()
|
|
1579
|
+
await turn.liveCheckpoint.flush();
|
|
1243
1580
|
const closed = await this.refreshClosedState(turn.record);
|
|
1244
|
-
await this.options.sessionStore.save(turn.record)
|
|
1245
|
-
if (closed) return false;
|
|
1246
|
-
return await this.
|
|
1581
|
+
await this.options.sessionStore.save(turn.record);
|
|
1582
|
+
if (closed || !turn.connected) return false;
|
|
1583
|
+
return await this.retainPersistentSessionOwnerAfterTurn({
|
|
1247
1584
|
record: turn.record,
|
|
1248
|
-
|
|
1585
|
+
owner: turn.owner,
|
|
1586
|
+
conversation: turn.conversation,
|
|
1587
|
+
acpxState: turn.acpxState
|
|
1249
1588
|
});
|
|
1250
1589
|
}
|
|
1251
1590
|
async *runTurn(input) {
|
|
@@ -1254,7 +1593,10 @@ var AcpRuntimeManager = class {
|
|
|
1254
1593
|
yield legacyTerminalEventFromTurnResult(await turn.result);
|
|
1255
1594
|
}
|
|
1256
1595
|
async getStatus(handle) {
|
|
1257
|
-
const
|
|
1596
|
+
const recordId = handle.acpxRecordId ?? handle.sessionKey;
|
|
1597
|
+
const owner = this.retainedSessionOwners.get(recordId);
|
|
1598
|
+
if (owner) await this.flushSessionOwner(owner);
|
|
1599
|
+
const record = await this.requireRecord(recordId);
|
|
1258
1600
|
return {
|
|
1259
1601
|
summary: statusSummary(record),
|
|
1260
1602
|
acpxRecordId: record.acpxRecordId,
|
|
@@ -1272,6 +1614,10 @@ var AcpRuntimeManager = class {
|
|
|
1272
1614
|
};
|
|
1273
1615
|
}
|
|
1274
1616
|
async setMode(handle, mode, sessionMode = "persistent") {
|
|
1617
|
+
const recordId = handle.acpxRecordId ?? handle.sessionKey;
|
|
1618
|
+
await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setModeWithOwnership(handle, mode, sessionMode));
|
|
1619
|
+
}
|
|
1620
|
+
async setModeWithOwnership(handle, mode, sessionMode) {
|
|
1275
1621
|
const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
|
|
1276
1622
|
const controller = this.activeControllers.get(record.acpxRecordId);
|
|
1277
1623
|
let targetRecord = record;
|
|
@@ -1283,63 +1629,109 @@ var AcpRuntimeManager = class {
|
|
|
1283
1629
|
await this.options.sessionStore.save(targetRecord);
|
|
1284
1630
|
}
|
|
1285
1631
|
async setConfigOption(handle, key, value, sessionMode = "persistent") {
|
|
1632
|
+
const recordId = handle.acpxRecordId ?? handle.sessionKey;
|
|
1633
|
+
return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setConfigOptionWithOwnership(handle, key, value, sessionMode));
|
|
1634
|
+
}
|
|
1635
|
+
async setConfigOptionWithOwnership(handle, key, value, sessionMode) {
|
|
1286
1636
|
const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
|
|
1287
1637
|
const controller = this.activeControllers.get(record.acpxRecordId);
|
|
1288
1638
|
if (controller) {
|
|
1289
1639
|
const { configId, response } = await controller.setResolvedSessionConfigOption(key, value);
|
|
1290
|
-
|
|
1291
|
-
applyDesiredConfigOptionToRecord(record, configId, value);
|
|
1640
|
+
record.acpx = applyConfigOptionSelection(record.acpx, configId, value, response);
|
|
1292
1641
|
await this.options.sessionStore.save(record);
|
|
1293
|
-
return;
|
|
1642
|
+
return response;
|
|
1294
1643
|
}
|
|
1295
1644
|
const result = await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
|
|
1296
1645
|
const configId = resolveSupportedConfigOptionId(connectedRecord, key);
|
|
1297
|
-
|
|
1298
|
-
|
|
1646
|
+
const modelConfigId = advertisedModelState(connectedRecord.acpx)?.configId;
|
|
1647
|
+
const response = await client.setSessionConfigOption(sessionId, configId, value);
|
|
1648
|
+
connectedRecord.acpx = applyConfigOptionSelection(connectedRecord.acpx, configId, value, response, modelConfigId);
|
|
1649
|
+
return response;
|
|
1650
|
+
}, {
|
|
1651
|
+
key,
|
|
1652
|
+
resolve: (connectedRecord) => resolveSupportedConfigOptionId(connectedRecord, key)
|
|
1299
1653
|
});
|
|
1300
1654
|
await this.options.sessionStore.save(result.record);
|
|
1655
|
+
return result.value;
|
|
1301
1656
|
}
|
|
1302
1657
|
async cancel(handle) {
|
|
1303
1658
|
await this.activeControllers.get(handle.acpxRecordId ?? handle.sessionKey)?.requestCancelActivePrompt();
|
|
1304
1659
|
}
|
|
1305
1660
|
async close(handle, options = {}) {
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1661
|
+
const recordId = handle.acpxRecordId ?? handle.sessionKey;
|
|
1662
|
+
const record = await this.resolveRuntimeRecordForClose(recordId);
|
|
1663
|
+
this.markActiveRuntimeRecordClosing(record);
|
|
1308
1664
|
await this.cancel(handle);
|
|
1309
|
-
|
|
1665
|
+
await this.closeRuntimeRecordOwnership(record, options.discardPersistentState === true);
|
|
1666
|
+
record.closed = true;
|
|
1667
|
+
record.closedAt = isoNow();
|
|
1668
|
+
await this.options.sessionStore.save(record);
|
|
1669
|
+
}
|
|
1670
|
+
async resolveRuntimeRecordForClose(recordId) {
|
|
1671
|
+
const retainedOwner = this.retainedSessionOwners.get(recordId);
|
|
1672
|
+
if (retainedOwner) await this.flushSessionOwner(retainedOwner);
|
|
1673
|
+
return retainedOwner?.projection?.record ?? await this.requireRecord(recordId);
|
|
1674
|
+
}
|
|
1675
|
+
markActiveRuntimeRecordClosing(record) {
|
|
1676
|
+
if (this.activeControllers.has(record.acpxRecordId)) this.closingActiveRecords.add(record.acpxRecordId);
|
|
1677
|
+
}
|
|
1678
|
+
async closeRuntimeRecordOwnership(record, discardPersistentState) {
|
|
1679
|
+
if (discardPersistentState) {
|
|
1310
1680
|
await this.closeBackendSession(record);
|
|
1311
1681
|
record.acpx = {
|
|
1312
1682
|
...record.acpx,
|
|
1313
1683
|
reset_on_next_ensure: true
|
|
1314
1684
|
};
|
|
1315
|
-
} else await this.
|
|
1316
|
-
record.closed = true;
|
|
1317
|
-
record.closedAt = isoNow();
|
|
1318
|
-
await this.options.sessionStore.save(record);
|
|
1685
|
+
} else await this.closeRetainedSessionOwner(record.acpxRecordId);
|
|
1319
1686
|
}
|
|
1320
1687
|
async closeBackendSession(record) {
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1688
|
+
const connection = await this.acquireBackendCloseConnection(record);
|
|
1689
|
+
try {
|
|
1690
|
+
await this.requestBackendSessionClose(record, connection);
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
this.handleBackendSessionCloseError(record, error);
|
|
1693
|
+
} finally {
|
|
1694
|
+
await this.finalizeBackendCloseConnection(connection);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
async finalizeBackendCloseConnection(connection) {
|
|
1698
|
+
const failure = firstFailedAttempt([
|
|
1699
|
+
await settleAttempt(async () => {
|
|
1700
|
+
if (connection.owner) await this.flushSessionOwner(connection.owner);
|
|
1701
|
+
}),
|
|
1702
|
+
await settleAttempt(() => connection.owner?.client.clearEventHandlers()),
|
|
1703
|
+
await settleAttempt(async () => connection.client.close())
|
|
1704
|
+
]);
|
|
1705
|
+
if (failure) throw failure.error;
|
|
1706
|
+
}
|
|
1707
|
+
async acquireBackendCloseConnection(record) {
|
|
1708
|
+
const owner = await this.readRetainedSessionOwner(record, { consume: true });
|
|
1709
|
+
if (owner) return {
|
|
1710
|
+
client: owner.client,
|
|
1711
|
+
owner
|
|
1712
|
+
};
|
|
1713
|
+
return { client: this.createClient({
|
|
1323
1714
|
agentCommand: record.agentCommand,
|
|
1324
1715
|
agentArgv: record.agentArgv,
|
|
1325
1716
|
cwd: record.cwd,
|
|
1326
1717
|
mcpServers: [...this.options.mcpServers ?? []],
|
|
1327
1718
|
permissionMode: this.options.permissionMode,
|
|
1328
1719
|
nonInteractivePermissions: this.options.nonInteractivePermissions,
|
|
1720
|
+
permissionPolicy: this.options.permissionPolicy,
|
|
1329
1721
|
onPermissionRequest: this.options.onPermissionRequest,
|
|
1722
|
+
elicitationModes: this.options.elicitationModes,
|
|
1330
1723
|
verbose: this.options.verbose
|
|
1331
|
-
});
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
}
|
|
1724
|
+
}) };
|
|
1725
|
+
}
|
|
1726
|
+
async requestBackendSessionClose(record, connection) {
|
|
1727
|
+
if (!connection.owner) await withTimeout(connection.client.start(), this.options.timeoutMs);
|
|
1728
|
+
if (!connection.client.supportsCloseSession()) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`);
|
|
1729
|
+
await withTimeout(connection.client.closeSession(record.acpSessionId), this.options.timeoutMs);
|
|
1730
|
+
}
|
|
1731
|
+
handleBackendSessionCloseError(record, error) {
|
|
1732
|
+
if (isAcpResourceNotFoundError(error)) return;
|
|
1733
|
+
if (isUnsupportedSessionCloseError(error)) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`, { cause: error });
|
|
1734
|
+
throw error;
|
|
1343
1735
|
}
|
|
1344
1736
|
async requireRecord(sessionId) {
|
|
1345
1737
|
const record = await this.options.sessionStore.load(sessionId);
|
|
@@ -1506,6 +1898,7 @@ function createProbeClient(options, agentCommand, deps) {
|
|
|
1506
1898
|
mcpServers: [...options.mcpServers ?? []],
|
|
1507
1899
|
permissionMode: options.permissionMode,
|
|
1508
1900
|
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1901
|
+
permissionPolicy: options.permissionPolicy,
|
|
1509
1902
|
verbose: options.verbose
|
|
1510
1903
|
};
|
|
1511
1904
|
return deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
|
|
@@ -1614,10 +2007,14 @@ var AcpxRuntime = class {
|
|
|
1614
2007
|
sessionMode: state.mode,
|
|
1615
2008
|
requestId: input.requestId,
|
|
1616
2009
|
timeoutMs: input.timeoutMs,
|
|
1617
|
-
signal: input.signal
|
|
2010
|
+
signal: input.signal,
|
|
2011
|
+
onElicitation: input.onElicitation
|
|
1618
2012
|
}));
|
|
1619
2013
|
return {
|
|
1620
2014
|
requestId: input.requestId,
|
|
2015
|
+
get promptStarted() {
|
|
2016
|
+
return turnPromise.then((turn) => turn.promptStarted);
|
|
2017
|
+
},
|
|
1621
2018
|
events: { async *[Symbol.asyncIterator]() {
|
|
1622
2019
|
yield* (await turnPromise).events;
|
|
1623
2020
|
} },
|
|
@@ -1642,7 +2039,8 @@ var AcpxRuntime = class {
|
|
|
1642
2039
|
sessionMode: state.mode,
|
|
1643
2040
|
requestId: input.requestId,
|
|
1644
2041
|
timeoutMs: input.timeoutMs,
|
|
1645
|
-
signal: input.signal
|
|
2042
|
+
signal: input.signal,
|
|
2043
|
+
onElicitation: input.onElicitation
|
|
1646
2044
|
});
|
|
1647
2045
|
}
|
|
1648
2046
|
async getCapabilities(input) {
|
|
@@ -1666,7 +2064,7 @@ var AcpxRuntime = class {
|
|
|
1666
2064
|
}
|
|
1667
2065
|
async setConfigOption(input) {
|
|
1668
2066
|
const { handle, state } = this.resolveManagerHandle(input.handle);
|
|
1669
|
-
await (await this.getManager()).setConfigOption(handle, input.key, input.value, state.mode);
|
|
2067
|
+
return await (await this.getManager()).setConfigOption(handle, input.key, input.value, state.mode);
|
|
1670
2068
|
}
|
|
1671
2069
|
async cancel(input) {
|
|
1672
2070
|
const { handle } = this.resolveManagerHandle(input.handle);
|