@tangle-network/sdk-provider-cli-base 0.12.0 → 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/dist/index.d.mts +187 -555
- package/dist/index.mjs +952 -785
- package/package.json +8 -8
package/dist/index.mjs
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
import { DEFAULT_INTERACTION_DECISION_TIMEOUT_MS as PERMISSION_DECISION_TIMEOUT_MS, InteractionBroker, InteractionBroker as InteractionBroker$1, InteractionHttpBridge, brokerInteractionTools, brokerInteractionTools as brokerInteractionTools$1 } from "@tangle-network/agent-core/interactions";
|
|
2
|
+
import { CODEX_DISABLED_FEATURE_ARGS, UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER, applyWorkspacePlanWithSecretProvider, bindLaunchToolBinDirs, bindSecretSessionView, codexDeveloperInstructionArgs, enforceWorkspacePlanSupport, ensurePublicConcurrentProfileRoots, joinSystemPromptAdditions, keepsHarnessSystemPrompt, materializeProfile, modelIdsMatch, parseSandboxToolBinDirs, preparePublicConcurrentProfileRevision, prepareSecretConcurrentProfileRevision, resolveAgentProfileResources, secretSessionViewWorkspace } from "@tangle-network/agent-profile-materialize";
|
|
2
3
|
import { BridgeClosedError, InvalidAttachmentError, JsonRpcError, RpcRequestTimeoutError, execCli, launchCliProcess, resolveRawCapturePath, spawnCliProcess, spawnRpcBridge } from "@tangle-network/sdk-cli-runner";
|
|
3
4
|
import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
4
5
|
import { AgentExecutionError, AgentExecutionOutcomeSchema, AgentRuntimeAttachmentsSchema, InteractionExecutionBindingSchema, InteractionKind, REASONING_EFFORTS, agentProfileSchema, nativeReasoningControl, normalizeInputParts } from "@tangle-network/agent-interface";
|
|
5
|
-
import { UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER, applyWorkspacePlanWithSecretProvider, bindLaunchToolBinDirs, enforceWorkspacePlanSupport, ensurePublicConcurrentProfileRoots, joinSystemPromptAdditions, keepsHarnessSystemPrompt, materializeProfile, modelIdsMatch, parseSandboxToolBinDirs, preparePublicConcurrentProfileRevision, prepareSecretConcurrentProfileRevision, resolveAgentProfileResources } from "@tangle-network/agent-profile-materialize";
|
|
6
6
|
import { HarnessSessionContractError, assertArgvHonoursSessionContract, assertFlagsHonourSessionContract, assertStoreLocationHonoursSessionContract, compileSessionStoreGlob } from "@tangle-network/cli-agent-registry/session-contract";
|
|
7
7
|
import { MAX_SECRET_PROFILE_REPLAY_CHARS, RESTORED_SESSION_CONTEXT_PREAMBLE, RESTORED_SESSION_CONTEXT_TAG, buildCompleteForkContextBlock, buildForkContextBlock, createMessageStore, createSessionStore, forkSessionWithMessages, readProviderMessageIds } from "@tangle-network/sdk-session-persistence";
|
|
8
8
|
import { mkdir, open, readdir, writeFile } from "node:fs/promises";
|
|
9
|
-
import path, { basename, isAbsolute, normalize, resolve } from "node:path";
|
|
10
|
-
import
|
|
9
|
+
import path, { basename, dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
10
|
+
import * as fs$1 from "node:fs";
|
|
11
|
+
import fs, { constants, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
11
12
|
import { execFile } from "node:child_process";
|
|
12
13
|
import { isDeepStrictEqual, promisify } from "node:util";
|
|
13
14
|
import { messageStorageDirOf } from "@tangle-network/cli-agent-registry";
|
|
14
|
-
import {
|
|
15
|
+
import { NATIVE_SESSION_CONTRACTS, NATIVE_SESSION_CONTRACTS as NATIVE_SESSION_CONTRACTS$1, configDirHomeEnv } from "@tangle-network/cli-agent-registry/native-session-contracts";
|
|
15
16
|
import { z } from "zod";
|
|
16
17
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
|
+
import { DatabaseSync } from "node:sqlite";
|
|
17
19
|
import { StringDecoder } from "node:string_decoder";
|
|
18
20
|
import { DEFAULT_INTERACTION_MCP_SERVER_NAME, InteractionMcpServer as InteractionMcpServer$1, interactionMcpToolName } from "@tangle-network/agent-core/interactions/mcp";
|
|
21
|
+
import { isRecord, isRecord as isPlainRecord } from "@tangle-network/agent-core";
|
|
19
22
|
//#region src/acp-process-launch.ts
|
|
20
|
-
/** Spawn an ACP agent over NDJSON stdio. The adapter owns abort, so no signal. */
|
|
23
|
+
/** Spawn an ACP agent over NDJSON stdio from `env` alone, never the sidecar's own environment. The adapter owns abort, so no signal. */
|
|
21
24
|
function spawnAcpBridge(request) {
|
|
22
25
|
return spawnRpcBridge({
|
|
23
26
|
...request,
|
|
24
|
-
|
|
27
|
+
inheritEnv: false
|
|
25
28
|
});
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
@@ -34,13 +37,12 @@ function jsonRpcErrorText(err) {
|
|
|
34
37
|
return cause && cause !== err.message ? `${err.message}: ${cause}` : err.message;
|
|
35
38
|
}
|
|
36
39
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
40
|
+
* An ACP process env: the spec's keys with the agent environment over them,
|
|
41
|
+
* unset keys dropped. The agent environment is the whole inherited part.
|
|
39
42
|
*/
|
|
40
43
|
function withAgentEnvironment(env, agent) {
|
|
41
|
-
if (Object.keys(agent).length === 0) return env;
|
|
42
44
|
const merged = {};
|
|
43
|
-
for (const [key, value] of Object.entries(env ??
|
|
45
|
+
for (const [key, value] of Object.entries(env ?? {})) if (value !== void 0) merged[key] = value;
|
|
44
46
|
for (const [key, value] of Object.entries(agent)) if (value !== void 0) merged[key] = value;
|
|
45
47
|
return merged;
|
|
46
48
|
}
|
|
@@ -61,32 +63,6 @@ function acpContentText(content) {
|
|
|
61
63
|
}
|
|
62
64
|
return parts.join("");
|
|
63
65
|
}
|
|
64
|
-
/** Map an ACP stop reason onto a turn status. */
|
|
65
|
-
function mapAcpStopReason(reason) {
|
|
66
|
-
switch (reason) {
|
|
67
|
-
case "end_turn": return { status: "completed" };
|
|
68
|
-
case "max_tokens": return {
|
|
69
|
-
status: "completed",
|
|
70
|
-
detail: "Stopped at max tokens (output may be truncated)"
|
|
71
|
-
};
|
|
72
|
-
case "max_turn_requests": return {
|
|
73
|
-
status: "failed",
|
|
74
|
-
detail: "Hit max turn-requests limit"
|
|
75
|
-
};
|
|
76
|
-
case "refusal": return {
|
|
77
|
-
status: "failed",
|
|
78
|
-
detail: "Agent refused the request"
|
|
79
|
-
};
|
|
80
|
-
case "cancelled": return {
|
|
81
|
-
status: "failed",
|
|
82
|
-
detail: "Cancelled"
|
|
83
|
-
};
|
|
84
|
-
default: return {
|
|
85
|
-
status: "failed",
|
|
86
|
-
detail: `Unknown stop reason: ${reason ?? "<missing>"}`
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
66
|
//#endregion
|
|
91
67
|
//#region src/reasoning-parts.ts
|
|
92
68
|
/**
|
|
@@ -578,23 +554,6 @@ function nearestExistingAncestor(target) {
|
|
|
578
554
|
current = parent;
|
|
579
555
|
}
|
|
580
556
|
}
|
|
581
|
-
/**
|
|
582
|
-
* True when a CLI can create and write `target`.
|
|
583
|
-
*
|
|
584
|
-
* A read-only mount is the case this exists for: the shared tool-cache root is
|
|
585
|
-
* mounted read-only with only named subdirectories bound read-write, so a path
|
|
586
|
-
* under it that has no bind mount refuses `mkdir` with `EROFS`. A CLI that
|
|
587
|
-
* creates its cache directory at startup then dies before it runs, and a
|
|
588
|
-
* caller waiting on that run observes silence, not an error.
|
|
589
|
-
*
|
|
590
|
-
* The check reads permissions and never creates anything. Creating the
|
|
591
|
-
* directory here would run as the sidecar's own user and leave a directory the
|
|
592
|
-
* agent user cannot write, which trades one unusable path for another.
|
|
593
|
-
*
|
|
594
|
-
* A directory the calling process may write but the agent subprocess may not
|
|
595
|
-
* still reads as usable. Ownership is the spawn layer's concern; this answers
|
|
596
|
-
* the mount-level question that silently kills a CLI.
|
|
597
|
-
*/
|
|
598
557
|
function isWritableTarget(target) {
|
|
599
558
|
const ancestor = nearestExistingAncestor(target);
|
|
600
559
|
if (!ancestor) return false;
|
|
@@ -659,9 +618,7 @@ function homeXdgEnv(home) {
|
|
|
659
618
|
//#region src/adapter-lifecycle.ts
|
|
660
619
|
const DEFAULT_CONTEXT_WINDOW = 2e5;
|
|
661
620
|
const execFileAsync = promisify(execFile);
|
|
662
|
-
/**
|
|
663
|
-
* Console-based logger fallback.
|
|
664
|
-
*/
|
|
621
|
+
/** Console-based logger fallback. */
|
|
665
622
|
const consoleLogger = {
|
|
666
623
|
debug: (msg, meta) => console.debug(`[cli-provider] ${msg}`, meta ?? ""),
|
|
667
624
|
info: (msg, meta) => console.info(`[cli-provider] ${msg}`, meta ?? ""),
|
|
@@ -670,13 +627,9 @@ const consoleLogger = {
|
|
|
670
627
|
};
|
|
671
628
|
/** Owns configuration, initialization, cancellation, and subprocess identity. */
|
|
672
629
|
var CliAdapterLifecycle = class {
|
|
673
|
-
/**
|
|
674
|
-
* The message log directory when the caller names none. Unset, it is the
|
|
675
|
-
* registry spec's `messageStorageDir` for `provider`, the one source; only
|
|
676
|
-
* an adapter for a provider the registry does not know sets it.
|
|
677
|
-
*/
|
|
630
|
+
/** The message log directory when the caller names none. */
|
|
678
631
|
defaultStorageDir;
|
|
679
|
-
inheritProcessEnv
|
|
632
|
+
inheritProcessEnv;
|
|
680
633
|
config;
|
|
681
634
|
initialConfig;
|
|
682
635
|
logger;
|
|
@@ -700,12 +653,10 @@ var CliAdapterLifecycle = class {
|
|
|
700
653
|
this.storagePath = options.storagePath ?? process.cwd();
|
|
701
654
|
this._storageDir = options.storageDir;
|
|
702
655
|
this.agentEnvironment = options.agentEnvironment;
|
|
656
|
+
this.inheritProcessEnv = options.agentEnvironment === void 0;
|
|
703
657
|
this.launcher = options.launcher;
|
|
704
658
|
this.ownProcessGroup = options.ownProcessGroup ?? false;
|
|
705
659
|
}
|
|
706
|
-
/**
|
|
707
|
-
* Get the storage directory. Uses provided storageDir or falls back to defaultStorageDir.
|
|
708
|
-
*/
|
|
709
660
|
get storageDir() {
|
|
710
661
|
return this._storageDir ?? this.defaultStorageDir ?? messageStorageDirOf(this.provider);
|
|
711
662
|
}
|
|
@@ -717,17 +668,7 @@ var CliAdapterLifecycle = class {
|
|
|
717
668
|
if (binding.sessionId !== sessionId) throw new Error(`Interaction binding session "${binding.sessionId}" does not match adapter session "${sessionId}"`);
|
|
718
669
|
return binding;
|
|
719
670
|
}
|
|
720
|
-
/**
|
|
721
|
-
* The uid/gid every CLI process this adapter spawns runs as, or undefined
|
|
722
|
-
* when the host process is already unprivileged.
|
|
723
|
-
*
|
|
724
|
-
* This is an adapter-wide invariant, not a per-callsite option, because
|
|
725
|
-
* these CLIs seed their own state on first use — a sqlite database, a log
|
|
726
|
-
* directory, a downloaded-binary cache under the harness HOME. An auxiliary
|
|
727
|
-
* invocation left at root (`mcp add`, `mcp list`, `--version`) creates that
|
|
728
|
-
* tree root-owned, and the streamed run, which does drop privileges, then
|
|
729
|
-
* cannot open its own log and exits before emitting a single event.
|
|
730
|
-
*/
|
|
671
|
+
/** The uid/gid every CLI process this adapter spawns runs as, or undefined when the host process is already unprivileged. */
|
|
731
672
|
subprocessIdentity() {
|
|
732
673
|
const assigned = this.config.subprocessUser;
|
|
733
674
|
if (assigned) {
|
|
@@ -749,16 +690,11 @@ var CliAdapterLifecycle = class {
|
|
|
749
690
|
subprocessSpawnOptions() {
|
|
750
691
|
return this.subprocessIdentity() ?? {};
|
|
751
692
|
}
|
|
752
|
-
/**
|
|
753
|
-
* Run a one-shot CLI command under this adapter's subprocess identity.
|
|
754
|
-
*
|
|
755
|
-
* Every auxiliary invocation an adapter makes goes through here so the
|
|
756
|
-
* identity cannot be forgotten at a call site. Auxiliary output stays out
|
|
757
|
-
* of the streamed turn's raw evidence.
|
|
758
|
-
*/
|
|
693
|
+
/** Run a one-shot CLI command under this adapter's subprocess identity. */
|
|
759
694
|
runCli(config) {
|
|
760
695
|
return execCli({
|
|
761
696
|
...config,
|
|
697
|
+
inheritEnv: this.inheritProcessEnv && config.inheritEnv !== false,
|
|
762
698
|
rawCapturePath: void 0,
|
|
763
699
|
...this.config.subprocessUser ? { isolatedIdentity: this.config.subprocessUser } : {},
|
|
764
700
|
spawnOptions: {
|
|
@@ -767,14 +703,7 @@ var CliAdapterLifecycle = class {
|
|
|
767
703
|
}
|
|
768
704
|
});
|
|
769
705
|
}
|
|
770
|
-
/**
|
|
771
|
-
* Run a builtin-only Node script under this adapter's subprocess identity
|
|
772
|
-
* and return its stdout. File work inside a directory the CLI user owns goes
|
|
773
|
-
* through here, so a root sidecar never creates, reads or writes there with
|
|
774
|
-
* its own rights: a link the agent plants reaches only what the agent could
|
|
775
|
-
* already reach. The script reads `args` from argv after `--` and `input`
|
|
776
|
-
* from stdin. A failure carries the script's stderr, not the script text.
|
|
777
|
-
*/
|
|
706
|
+
/** Run a builtin-only Node script under this adapter's subprocess identity and return its stdout. */
|
|
778
707
|
async runNodeAsSubprocess(script, args, input = "") {
|
|
779
708
|
try {
|
|
780
709
|
const pending = execFileAsync(process.execPath, [
|
|
@@ -884,26 +813,26 @@ var CliAdapterLifecycle = class {
|
|
|
884
813
|
model: this.config.model
|
|
885
814
|
});
|
|
886
815
|
}
|
|
887
|
-
/**
|
|
888
|
-
* Auth/config files the adapter needs on disk that do NOT arrive through
|
|
889
|
-
* `config.model.authFiles` — e.g. a synthesized `~/.openclaw/openclaw.json`
|
|
890
|
-
* pointing at the managed router. The sidecar materializes these through the
|
|
891
|
-
* hardened `materializeCliAuthFiles` path AFTER `initialize()`, so an adapter
|
|
892
|
-
* can derive them from the resolved config/env. Default: none.
|
|
893
|
-
*/
|
|
816
|
+
/** Auth/config files the adapter needs on disk that do NOT arrive through `config.model.authFiles` — e.g. */
|
|
894
817
|
getManagedAuthFiles() {
|
|
895
818
|
return [];
|
|
896
819
|
}
|
|
897
820
|
/** Provider-specific initialization runs inside the tracked lifecycle. */
|
|
898
821
|
async afterInitialize(_config) {}
|
|
899
|
-
/**
|
|
900
|
-
* The harness session contract this adapter honours (cli-agent-registry
|
|
901
|
-
* NATIVE_SESSION_CONTRACTS), or null for a provider that declares none.
|
|
902
|
-
* Registration refuses a harness spec without one; ACP overrides this per
|
|
903
|
-
* sub-agent.
|
|
904
|
-
*/
|
|
822
|
+
/** The harness session contract this adapter honours (cli-agent-registry NATIVE_SESSION_CONTRACTS), or null for a provider that declares none. */
|
|
905
823
|
sessionContract() {
|
|
906
|
-
return NATIVE_SESSION_CONTRACTS[this.provider] ?? null;
|
|
824
|
+
return NATIVE_SESSION_CONTRACTS$1[this.provider] ?? null;
|
|
825
|
+
}
|
|
826
|
+
async withExecution(signal, run) {
|
|
827
|
+
const execution = beginExecution(this, signal);
|
|
828
|
+
try {
|
|
829
|
+
return await run(execution.controller.signal);
|
|
830
|
+
} catch (error) {
|
|
831
|
+
execution.controller.abort();
|
|
832
|
+
throw error;
|
|
833
|
+
} finally {
|
|
834
|
+
finishExecution(this, execution);
|
|
835
|
+
}
|
|
907
836
|
}
|
|
908
837
|
async shutdown() {
|
|
909
838
|
this.initialization.cancel();
|
|
@@ -981,7 +910,8 @@ var CliAdapterLifecycle = class {
|
|
|
981
910
|
}
|
|
982
911
|
/** The agent environment the host supplied (CliProviderOptions.agentEnvironment). */
|
|
983
912
|
agentEnvironmentValues() {
|
|
984
|
-
|
|
913
|
+
const secretProfile = !!this.config.secretProfileRevisionRoot;
|
|
914
|
+
return this.agentEnvironment?.({ secretProfile }) ?? (secretProfile ? {} : { ...process.env });
|
|
985
915
|
}
|
|
986
916
|
applyWritableHomeDefaults(adapterEnv, workspaceRoot) {
|
|
987
917
|
const env = {
|
|
@@ -1068,15 +998,19 @@ function runtimeSecretsOverlay(runtimeSecrets, fallback) {
|
|
|
1068
998
|
function hasPortableProfileControls(profile) {
|
|
1069
999
|
return profile.prompt !== void 0 || profile.model !== void 0 || profile.harness !== void 0 || profile.permissions !== void 0 || profile.tools !== void 0 || profile.mcp !== void 0 || profile.connections !== void 0 || profile.subagents !== void 0 || profile.resources !== void 0 || profile.hooks !== void 0 || profile.modes !== void 0 || profile.confidential !== void 0 || profile.extensions !== void 0;
|
|
1070
1000
|
}
|
|
1001
|
+
function systemPromptConflict(request, profile) {
|
|
1002
|
+
return /* @__PURE__ */ new Error(`execution system prompt (${request.length} chars) conflicts with the materialized AgentProfile prompt.systemPrompt (${profile.length} chars); send no systemPrompt, or move the addition to appendSystemPrompt`);
|
|
1003
|
+
}
|
|
1004
|
+
/** A blank profile prompt declares none, so the request prompt stands. */
|
|
1071
1005
|
function resolveSystemPrompt(requestPrompt, profilePrompt) {
|
|
1072
|
-
if (profilePrompt
|
|
1073
|
-
if (requestPrompt
|
|
1006
|
+
if (!profilePrompt?.trim()) return requestPrompt;
|
|
1007
|
+
if (requestPrompt && requestPrompt !== profilePrompt) throw systemPromptConflict(requestPrompt, profilePrompt);
|
|
1074
1008
|
return profilePrompt;
|
|
1075
1009
|
}
|
|
1076
1010
|
function profileForExecution(profile, requestPrompt) {
|
|
1077
1011
|
const profilePrompt = profile?.prompt?.systemPrompt;
|
|
1078
|
-
if (!profile || profilePrompt
|
|
1079
|
-
if (!requestPrompt.startsWith(`${profilePrompt}\n\n`)) throw
|
|
1012
|
+
if (!profile || !profilePrompt?.trim() || !requestPrompt || requestPrompt === profilePrompt) return profile;
|
|
1013
|
+
if (!requestPrompt.startsWith(`${profilePrompt}\n\n`)) throw systemPromptConflict(requestPrompt, profilePrompt);
|
|
1080
1014
|
return {
|
|
1081
1015
|
...profile,
|
|
1082
1016
|
prompt: {
|
|
@@ -1101,10 +1035,10 @@ function resolveModel(requestModel, profileModel, configuredModel, profileProvid
|
|
|
1101
1035
|
}
|
|
1102
1036
|
//#endregion
|
|
1103
1037
|
//#region src/profile-revision-receipts.ts
|
|
1104
|
-
/** Receipts belong to their adapter;
|
|
1038
|
+
/** Receipts belong to their adapter; resolved secrets never enter them. */
|
|
1105
1039
|
var ProfileRevisionReceipts = class {
|
|
1106
|
-
|
|
1107
|
-
|
|
1040
|
+
public = /* @__PURE__ */ new WeakMap();
|
|
1041
|
+
secret = /* @__PURE__ */ new WeakMap();
|
|
1108
1042
|
};
|
|
1109
1043
|
const receiptsByAdapter = /* @__PURE__ */ new WeakMap();
|
|
1110
1044
|
function profileRevisionReceipts(owner) {
|
|
@@ -1116,18 +1050,31 @@ function profileRevisionReceipts(owner) {
|
|
|
1116
1050
|
return receipts;
|
|
1117
1051
|
}
|
|
1118
1052
|
function assertExecutionProfileRevision(owner, getConfig, input, profilePlan) {
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1053
|
+
const config = getConfig();
|
|
1054
|
+
const receipts = profileRevisionReceipts(owner);
|
|
1055
|
+
const actualPublic = profilePlan ? receipts.public.get(profilePlan) : void 0;
|
|
1056
|
+
const actualSecret = profilePlan ? receipts.secret.get(profilePlan) : void 0;
|
|
1057
|
+
for (const [root, receipt, kind] of [[
|
|
1058
|
+
config.publicProfileRevisionRoot,
|
|
1059
|
+
actualPublic,
|
|
1060
|
+
"public"
|
|
1061
|
+
], [
|
|
1062
|
+
config.secretProfileRevisionRoot,
|
|
1063
|
+
actualSecret,
|
|
1064
|
+
"secret"
|
|
1065
|
+
]]) if (root && profilePlan && !receipt) throw new Error(`Provider added ${kind} profile launch controls after immutable revision preparation`);
|
|
1066
|
+
const { expectedSecretProfileReceipt, expectedPublicProfileRevision } = input;
|
|
1067
|
+
if (config.secretProfileRevisionRoot) {
|
|
1068
|
+
if (!expectedSecretProfileReceipt || !actualSecret || actualSecret.executorReceipt !== expectedSecretProfileReceipt) throw new Error("Secret native profile receipt mismatch before native spawn");
|
|
1069
|
+
} else if (expectedSecretProfileReceipt !== void 0) throw new Error("Secret native profile execution is unavailable");
|
|
1070
|
+
assertPublicReceipt(actualPublic, expectedPublicProfileRevision);
|
|
1071
|
+
}
|
|
1072
|
+
function assertPublicReceipt(actual, expected) {
|
|
1073
|
+
if (expected !== void 0 && (!actual || [
|
|
1074
|
+
"workspacePlanDigest",
|
|
1075
|
+
"materializedDigest",
|
|
1076
|
+
"launchDigest"
|
|
1077
|
+
].some((key) => actual[key] !== expected[key]))) throw new Error("Public native profile revision mismatch before native spawn");
|
|
1131
1078
|
}
|
|
1132
1079
|
//#endregion
|
|
1133
1080
|
//#region src/errors.ts
|
|
@@ -1230,23 +1177,6 @@ function buildFileUrl(input) {
|
|
|
1230
1177
|
function deterministicFilePartId(messageId, key) {
|
|
1231
1178
|
return `file-${sha1Hex(`${messageId}|${key}`).slice(0, 16)}`;
|
|
1232
1179
|
}
|
|
1233
|
-
/**
|
|
1234
|
-
* Stateless derivation of the canonical file-part fields for one observed
|
|
1235
|
-
* path: resolve-once (normalize, then anchor a relative path to
|
|
1236
|
-
* `workspaceRoot`), and from that single resolved path derive the dedupe
|
|
1237
|
-
* `key`, the deterministic `id`, the `filename`, the inferred `mediaType`,
|
|
1238
|
-
* and the percent-encoded `file://` `url` — so every consumer that surfaces
|
|
1239
|
-
* the same file agrees on all five.
|
|
1240
|
-
*
|
|
1241
|
-
* Returns `null` for a relative path with no `workspaceRoot` to anchor it:
|
|
1242
|
-
* detection is opportunistic and a fabricated location is worse than a
|
|
1243
|
-
* missing part (see `trackFilePart` for the full rationale).
|
|
1244
|
-
*
|
|
1245
|
-
* This is the ONE derivation chain. `trackFilePart` (exec path, with
|
|
1246
|
-
* ExecutionState memoization) and stateless mappers that own no
|
|
1247
|
-
* ExecutionState (codex's app-server item mapper) both call it — never
|
|
1248
|
-
* re-implement the chain, or the two paths drift.
|
|
1249
|
-
*/
|
|
1250
1180
|
function deriveFilePart(input) {
|
|
1251
1181
|
const normalized = normalize(input.path);
|
|
1252
1182
|
let resolvedPath;
|
|
@@ -1262,33 +1192,6 @@ function deriveFilePart(input) {
|
|
|
1262
1192
|
url: buildFileUrl({ path: resolvedPath })
|
|
1263
1193
|
};
|
|
1264
1194
|
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Tracks a file an adapter observed being created/modified (e.g. from a
|
|
1267
|
-
* write-tool call) and returns the canonical `FilePart` fields to stream,
|
|
1268
|
-
* deduping by resolved-absolute-path or data-URI content so repeated writes
|
|
1269
|
-
* to the same file update one part instead of appending duplicates.
|
|
1270
|
-
*
|
|
1271
|
-
* The path is normalized and resolved to an absolute form ONCE, and that same
|
|
1272
|
-
* resolved value drives both the dedupe key and the `file://` url — so two
|
|
1273
|
-
* observations of the same file that differ only in spelling (`./chart.png`
|
|
1274
|
-
* vs `/workspace/chart.png`, or `/workspace/sub/../chart.png` vs
|
|
1275
|
-
* `/workspace/chart.png`) unify on one part with one stable url, satisfying
|
|
1276
|
-
* the stream invariant that a given part id keeps a stable url.
|
|
1277
|
-
*
|
|
1278
|
-
* A relative path can only be resolved against `state.workspaceRoot` (set by
|
|
1279
|
-
* the adapter from the CLI's cwd). Detection here is opportunistic — a
|
|
1280
|
-
* tool-call observation, not a guarantee of an absolute path — so when no
|
|
1281
|
-
* workspaceRoot is available to anchor a relative path, we drop the
|
|
1282
|
-
* observation (`null`) rather than fabricate an anchor (e.g. resolving
|
|
1283
|
-
* against "/"): a wrong url is worse than a missing part, and a caller that
|
|
1284
|
-
* genuinely lost the file would get a wrong, permanent url once it's on the
|
|
1285
|
-
* stream.
|
|
1286
|
-
*
|
|
1287
|
-
* Detection is opportunistic for other reasons too: a delete, or an
|
|
1288
|
-
* observation carrying neither a path nor a data URI, also yields no part
|
|
1289
|
-
* (`null`) rather than an error — callers are not required to have a file
|
|
1290
|
-
* signal on every tool result.
|
|
1291
|
-
*/
|
|
1292
1195
|
function trackFilePart(state, args) {
|
|
1293
1196
|
if (args.changeKind === "delete") return null;
|
|
1294
1197
|
if (args.path === void 0 && args.dataUri === void 0) return null;
|
|
@@ -1536,51 +1439,23 @@ function enabledInteractionKinds(config, supported) {
|
|
|
1536
1439
|
if (typeof config !== "object" || config === null) return [];
|
|
1537
1440
|
return supported.filter((kind) => config[kind] === true);
|
|
1538
1441
|
}
|
|
1539
|
-
/**
|
|
1540
|
-
* Read a turn's ephemeral context, rejecting a wrong-typed value loudly.
|
|
1541
|
-
*
|
|
1542
|
-
* The published `AgentExecutionInput` does not declare the field, so nothing
|
|
1543
|
-
* type-checks it at the process boundary. Left unchecked, a caller passing a
|
|
1544
|
-
* non-string gets it stringified into the model prompt (`[object Object]`) —
|
|
1545
|
-
* a corrupted turn with no error anywhere. `undefined`/`null` mean "no context
|
|
1546
|
-
* this turn"; an empty string is likewise nothing to append.
|
|
1547
|
-
*/
|
|
1548
1442
|
function readEphemeralContext(input) {
|
|
1549
1443
|
const context = input.ephemeralContext;
|
|
1550
1444
|
if (context === void 0 || context === null) return void 0;
|
|
1551
1445
|
if (typeof context !== "string") throw new TypeError(`ephemeralContext must be a string, received ${typeof context}`);
|
|
1552
1446
|
return context || void 0;
|
|
1553
1447
|
}
|
|
1554
|
-
/**
|
|
1555
|
-
* Compose the model-facing prompt for a turn: the caller's own text, then this
|
|
1556
|
-
* turn's ephemeral context. Call it at the point the prompt is handed to the
|
|
1557
|
-
* provider — NEVER on the parts handed to `recordUserMessage` or the message
|
|
1558
|
-
* store.
|
|
1559
|
-
*/
|
|
1448
|
+
/** Compose the model-facing prompt for a turn: the caller's own text, then this turn's ephemeral context. */
|
|
1560
1449
|
function withEphemeralContext(promptText, input) {
|
|
1561
1450
|
const context = readEphemeralContext(input);
|
|
1562
1451
|
if (!context) return promptText;
|
|
1563
1452
|
return promptText ? `${promptText}\n\n${context}` : context;
|
|
1564
1453
|
}
|
|
1565
|
-
/**
|
|
1566
|
-
* Resolve whether an interaction kind is active for THIS turn. When the turn
|
|
1567
|
-
* carries `interactions` it is authoritative — a complete set that can also
|
|
1568
|
-
* turn a kind OFF mid-session; otherwise the adapter falls back to its latched
|
|
1569
|
-
* config (the initialize-time wiring, or a direct non-sidecar caller's option).
|
|
1570
|
-
*/
|
|
1571
1454
|
function turnInteractionEnabled(input, latched, kind) {
|
|
1572
1455
|
const perTurn = input.interactions;
|
|
1573
1456
|
return interactionToggleEnabled(perTurn !== void 0 ? perTurn : latched, kind);
|
|
1574
1457
|
}
|
|
1575
|
-
/**
|
|
1576
|
-
* Overlay this turn's interactions onto a latched config, returning a NEW
|
|
1577
|
-
* object (no shared mutation — safe when one adapter instance serves concurrent
|
|
1578
|
-
* turns from different sessions). When the turn omits `interactions` the config
|
|
1579
|
-
* is returned unchanged, preserving behavior for direct non-sidecar callers.
|
|
1580
|
-
* Adapters read plan/permission/question posture off the returned config so a
|
|
1581
|
-
* mid-session toggle takes effect and can turn a kind off (which the latched
|
|
1582
|
-
* config, only ever merged by updateConfig, cannot).
|
|
1583
|
-
*/
|
|
1458
|
+
/** Overlay this turn's interactions onto a latched config, returning a NEW object (no shared mutation — safe when. */
|
|
1584
1459
|
function withTurnInteractions(config, input) {
|
|
1585
1460
|
if (input.interactions === void 0) return config;
|
|
1586
1461
|
return {
|
|
@@ -1588,29 +1463,30 @@ function withTurnInteractions(config, input) {
|
|
|
1588
1463
|
interactions: input.interactions
|
|
1589
1464
|
};
|
|
1590
1465
|
}
|
|
1591
|
-
//#endregion
|
|
1592
|
-
//#region src/adapter-profile.ts
|
|
1593
1466
|
/**
|
|
1594
|
-
*
|
|
1595
|
-
* the
|
|
1596
|
-
*
|
|
1597
|
-
* those bytes and that CLI, not of the session or subprocess UID that will
|
|
1598
|
-
* read them (the ACL grant that publishes a revision to a UID throws when it
|
|
1599
|
-
* fails). A session's identity changes whenever its profile variant or turn
|
|
1600
|
-
* credential does, so keying by the session's receipt checked an unchanged
|
|
1601
|
-
* profile again every turn. Secret values are excluded from the digest.
|
|
1602
|
-
* Agents are rebuilt per request, so this outlives any adapter.
|
|
1603
|
-
* Insertion-ordered and bounded: the oldest entry is forgotten first.
|
|
1467
|
+
* The one precedence every engine names a failed turn's end with (#16404): an
|
|
1468
|
+
* end the abort, the timeout or the harness named is theirs; only an
|
|
1469
|
+
* unexplained death is the link's, the memory limit's or the harness's.
|
|
1604
1470
|
*/
|
|
1471
|
+
function turnEndReason(end) {
|
|
1472
|
+
if (end.aborted) return "abort";
|
|
1473
|
+
if (end.timedOut) return "timeout";
|
|
1474
|
+
if (end.harnessFailed) return "failed";
|
|
1475
|
+
if (end.linkLost) return "gateway_disconnect";
|
|
1476
|
+
if (end.memoryKilled) return "memory_limit";
|
|
1477
|
+
return end.signal ? "harness_exit" : "failed";
|
|
1478
|
+
}
|
|
1479
|
+
//#endregion
|
|
1480
|
+
//#region src/adapter-profile.ts
|
|
1481
|
+
/** Revisions that already passed the native health check in this process, by the verifier's scope and the revision's materialized digest. */
|
|
1605
1482
|
const verifiedSecretProfileRevisions = /* @__PURE__ */ new Set();
|
|
1606
1483
|
const VERIFIED_SECRET_PROFILE_REVISIONS_MAX = 1024;
|
|
1607
1484
|
function rememberVerifiedSecretProfileRevision(key) {
|
|
1608
1485
|
verifiedSecretProfileRevisions.delete(key);
|
|
1609
1486
|
verifiedSecretProfileRevisions.add(key);
|
|
1610
|
-
|
|
1487
|
+
if (verifiedSecretProfileRevisions.size > VERIFIED_SECRET_PROFILE_REVISIONS_MAX) {
|
|
1611
1488
|
const oldest = verifiedSecretProfileRevisions.values().next().value;
|
|
1612
|
-
if (oldest
|
|
1613
|
-
verifiedSecretProfileRevisions.delete(oldest);
|
|
1489
|
+
if (oldest !== void 0) verifiedSecretProfileRevisions.delete(oldest);
|
|
1614
1490
|
}
|
|
1615
1491
|
}
|
|
1616
1492
|
/** The executor's naming key, refused unless root owns it alone. */
|
|
@@ -1628,35 +1504,19 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
|
|
|
1628
1504
|
supportsSecretProfileRevisions = false;
|
|
1629
1505
|
profileSecretProvider;
|
|
1630
1506
|
secretProfileRuntimeKeyProvider;
|
|
1631
|
-
/**
|
|
1632
|
-
|
|
1633
|
-
* executor receipt is all a caller sees of a revision's resolved contents.
|
|
1634
|
-
*/
|
|
1635
|
-
secretProfileMaterializedDigests = /* @__PURE__ */ new WeakMap();
|
|
1636
|
-
/**
|
|
1637
|
-
* The value behind each `{env:NAME}` reference in a secret revision's
|
|
1638
|
-
* config, by that revision's application. Executor memory only: no launch
|
|
1639
|
-
* environment, argv or file ever carries them.
|
|
1640
|
-
*/
|
|
1641
|
-
secretProfileReferenceValues = /* @__PURE__ */ new WeakMap();
|
|
1507
|
+
/** Private native preparation state; never part of a returned receipt. */
|
|
1508
|
+
secretProfileRevisions = /* @__PURE__ */ new WeakMap();
|
|
1642
1509
|
constructor(options = {}) {
|
|
1643
1510
|
super(options);
|
|
1644
|
-
profileRevisionReceipts(this);
|
|
1645
1511
|
this.profileSecretProvider = options.profileSecretProvider ?? UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER;
|
|
1646
1512
|
this.secretProfileRuntimeKeyProvider = options.secretProfileRuntimeKeyProvider ?? UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER;
|
|
1647
1513
|
}
|
|
1648
|
-
/** Apply captured diagnostic identity to this spawn, after profile composition. */
|
|
1649
1514
|
/** The exact child environment a turn uses; shared with prewarm paths. */
|
|
1650
1515
|
buildTurnEnvironment(executionConfig, profilePlan, workspaceRoot, correlationHeaders) {
|
|
1651
1516
|
const merged = this.mergeProfilePlanEnvironment(this.buildNativeExecutionEnvironment(executionConfig), profilePlan, executionConfig.nativeAuthEnv !== void 0 || (executionConfig.authFiles?.length ?? 0) > 0);
|
|
1652
1517
|
return this.bindExecutionCorrelation(this.applyWritableHomeDefaults(this.bindProfileToolBinDirs(merged, profilePlan), workspaceRoot), correlationHeaders);
|
|
1653
1518
|
}
|
|
1654
|
-
/**
|
|
1655
|
-
* A profile's declared executable tools, sealed in its revision, stand in
|
|
1656
|
-
* for the app's own tool dirs on this launch's PATH tail. A secret profile's
|
|
1657
|
-
* env no longer names the app's dirs (app env is scrubbed), so the sidecar's
|
|
1658
|
-
* own SANDBOX_TOOL_BIN_DIRS says which inherited PATH entries they are.
|
|
1659
|
-
*/
|
|
1519
|
+
/** A profile's declared executable tools, sealed in its revision, stand in for the app's own tool dirs on this launch's PATH tail. */
|
|
1660
1520
|
bindProfileToolBinDirs(env, profilePlan) {
|
|
1661
1521
|
const toolBinDirs = profilePlan?.spawn.toolBinDirs;
|
|
1662
1522
|
if (!toolBinDirs?.length) return env;
|
|
@@ -1669,35 +1529,45 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
|
|
|
1669
1529
|
if (correlationHeaders !== void 0 && Object.keys(correlationHeaders).some((name) => name !== "X-Tangle-Trace-ID")) this.logger.warn("Native model request correlation is unsupported", { provider: this.provider });
|
|
1670
1530
|
return environment;
|
|
1671
1531
|
}
|
|
1672
|
-
/**
|
|
1673
|
-
* Provision the inline profile's files and native execution controls with the
|
|
1674
|
-
* shared materializer used by cli-bridge. Unsupported portable controls fail
|
|
1675
|
-
* before spawn unless the profile is explicitly best-effort
|
|
1676
|
-
* (resources.failOnError: false), which materializes the supported subset
|
|
1677
|
-
* and warns about the rest.
|
|
1678
|
-
*/
|
|
1679
|
-
/**
|
|
1680
|
-
* Launcher controls this adapter OWNS on the spawn it starts — see
|
|
1681
|
-
* `BINDABLE_LAUNCH_CONTROLS` in the materializer. Empty by default: a plan
|
|
1682
|
-
* whose intent needs a launcher binding then fails closed instead of being
|
|
1683
|
-
* silently dropped. An adapter that really applies the plan's launch flags
|
|
1684
|
-
* to its own spawn (sdk-provider-opencode selecting the plan's primary
|
|
1685
|
-
* agent via `--agent`) overrides this with the controls it honors.
|
|
1686
|
-
*/
|
|
1532
|
+
/** Launcher controls this adapter OWNS on the spawn it starts — see `BINDABLE_LAUNCH_CONTROLS` in the materializer. */
|
|
1687
1533
|
profileMaterializationBinds() {
|
|
1688
1534
|
return [];
|
|
1689
1535
|
}
|
|
1690
|
-
/** Optional native instruction root.
|
|
1536
|
+
/** Optional native instruction root. */
|
|
1691
1537
|
profileAgentDirectory(_workspaceRoot) {}
|
|
1692
1538
|
/** Provider transport bound into the private immutable revision and receipt. */
|
|
1693
1539
|
secretProfileProviderConfig() {}
|
|
1694
|
-
/**
|
|
1695
|
-
* The values behind a secret revision's `{env:NAME}` config references, for
|
|
1696
|
-
* an executor that serves those references itself. Never pass them to the
|
|
1697
|
-
* native process.
|
|
1698
|
-
*/
|
|
1540
|
+
/** The values behind a secret revision's `{env:NAME}` config references, for an executor that serves those references itself. */
|
|
1699
1541
|
secretProfileReferences(application) {
|
|
1700
|
-
return application ? this.
|
|
1542
|
+
return application ? this.secretProfileRevisions.get(application)?.references : void 0;
|
|
1543
|
+
}
|
|
1544
|
+
/** The one sealed session view repointed only while its native turn owns the queue. */
|
|
1545
|
+
secretProfileSessionView(sessionId) {
|
|
1546
|
+
const root = this.config.secretProfileRevisionRoot;
|
|
1547
|
+
const subprocessUid = this.config.subprocessUser?.uid;
|
|
1548
|
+
if (!root || subprocessUid === void 0) throw new Error("Secret profile session view is unavailable");
|
|
1549
|
+
const launchRoot = join(root, "launch");
|
|
1550
|
+
return {
|
|
1551
|
+
launchRoot,
|
|
1552
|
+
view: secretSessionViewWorkspace({
|
|
1553
|
+
launchRoot,
|
|
1554
|
+
sharedWorkspaceDir: this.storagePath,
|
|
1555
|
+
sessionId,
|
|
1556
|
+
subprocessUid
|
|
1557
|
+
}),
|
|
1558
|
+
subprocessUid
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
/** Bind the private revision into the session's stable native cwd. */
|
|
1562
|
+
bindSecretProfileSessionView(revisionWorkspaceDir, sessionId) {
|
|
1563
|
+
const { launchRoot, view, subprocessUid } = this.secretProfileSessionView(sessionId);
|
|
1564
|
+
bindSecretSessionView({
|
|
1565
|
+
launchRoot,
|
|
1566
|
+
viewWorkspaceDir: view,
|
|
1567
|
+
revisionWorkspaceDir,
|
|
1568
|
+
subprocessUid
|
|
1569
|
+
});
|
|
1570
|
+
return view;
|
|
1701
1571
|
}
|
|
1702
1572
|
async materializeProfileResources(inputProfile, workspaceRoot, ownerId, sessionId, scope = "turn") {
|
|
1703
1573
|
if (!inputProfile && !this.config.runtimeAttachments) return void 0;
|
|
@@ -1743,7 +1613,7 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
|
|
|
1743
1613
|
});
|
|
1744
1614
|
const launchDigest = revision.manifest.launchDigest;
|
|
1745
1615
|
if (!launchDigest) throw new Error("Concurrent profile launch digest was not recorded");
|
|
1746
|
-
profileRevisionReceipts(this).
|
|
1616
|
+
profileRevisionReceipts(this).public.set(revision.application, {
|
|
1747
1617
|
workspacePlanDigest: revision.manifest.workspacePlanDigest,
|
|
1748
1618
|
materializedDigest: revision.manifest.materializedDigest,
|
|
1749
1619
|
launchDigest,
|
|
@@ -1763,10 +1633,7 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
|
|
|
1763
1633
|
});
|
|
1764
1634
|
return application;
|
|
1765
1635
|
}
|
|
1766
|
-
/**
|
|
1767
|
-
* Publish the profile as an executor-sealed revision for this session's
|
|
1768
|
-
* subprocess identity, and record the receipt that binds it to the turn.
|
|
1769
|
-
*/
|
|
1636
|
+
/** Publish the profile as an executor-sealed revision for this session's subprocess identity, and record the receipt that binds it to the turn. */
|
|
1770
1637
|
async applySecretProfileRevision(plan, secretProfileRevisionRoot, workspaceRoot, ownerId, sessionId) {
|
|
1771
1638
|
if (!this.supportsSecretProfileRevisions || !ownerId || !sessionId || !this.config.subprocessUser) throw new Error("Secret concurrent profile needs an opted-in adapter, a session, and per-turn runtime secrets only");
|
|
1772
1639
|
const namingKey = readSecretProfileNamingKey(this.config.secretProfileNamingKeyPath);
|
|
@@ -1784,25 +1651,24 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
|
|
|
1784
1651
|
const launchDigest = revision.manifest.launchDigest;
|
|
1785
1652
|
if (!launchDigest) throw new Error("Secret concurrent profile launch digest is absent");
|
|
1786
1653
|
const executorReceipt = `hmac-sha256:${createHmac("sha256", namingKey).update(ownerId).update("\0").update(sessionId).update("\0").update(String(this.config.subprocessUser.uid)).update("\0").update(revision.manifest.workspacePlanDigest).update("\0").update(revision.manifest.materializedDigest).update("\0").update(launchDigest).digest("hex")}`;
|
|
1787
|
-
this.
|
|
1788
|
-
|
|
1789
|
-
|
|
1654
|
+
this.secretProfileRevisions.set(revision.application, {
|
|
1655
|
+
materializedDigest: revision.manifest.materializedDigest,
|
|
1656
|
+
references: revision.secretReferences
|
|
1657
|
+
});
|
|
1658
|
+
profileRevisionReceipts(this).secret.set(revision.application, {
|
|
1790
1659
|
workspacePlanDigest: revision.manifest.workspacePlanDigest,
|
|
1791
1660
|
executorReceipt,
|
|
1792
1661
|
conflicts: revision.manifest.conflicts.map((conflict) => ({ ...conflict }))
|
|
1793
1662
|
});
|
|
1794
1663
|
return revision.application;
|
|
1795
1664
|
}
|
|
1796
|
-
/**
|
|
1797
|
-
* Prepare the same provider-composed native plan that execute() will check.
|
|
1798
|
-
* This may publish an immutable public revision, but never starts a model turn.
|
|
1799
|
-
*/
|
|
1665
|
+
/** Prepare the same provider-composed native plan that execute() will check. */
|
|
1800
1666
|
async preparePublicProfileRevision(input) {
|
|
1801
1667
|
if (!this.config.publicProfileRevisionRoot) throw new Error("Concurrent public profile materialization is disabled");
|
|
1802
1668
|
const sessionScope = input.userId ?? input.sessionId;
|
|
1803
1669
|
if (!sessionScope) throw new Error("Concurrent public profile preparation needs a session scope");
|
|
1804
1670
|
const application = await this.materializeProfileResources(this.resolveExecutionProfileForInput(input), input.workspaceRoot ?? this.storagePath, sessionScope, input.sessionId);
|
|
1805
|
-
const receipt = application ? profileRevisionReceipts(this).
|
|
1671
|
+
const receipt = application ? profileRevisionReceipts(this).public.get(application) : void 0;
|
|
1806
1672
|
if (!receipt) throw new Error("Concurrent public profile preparation produced no native plan");
|
|
1807
1673
|
return receipt;
|
|
1808
1674
|
}
|
|
@@ -1814,9 +1680,9 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
|
|
|
1814
1680
|
const startedAt = performance.now();
|
|
1815
1681
|
const application = await this.materializeProfileResources(this.resolveExecutionProfileForInput(input), input.workspaceRoot ?? this.storagePath, sessionScope, input.sessionId);
|
|
1816
1682
|
const materializedAt = performance.now();
|
|
1817
|
-
const receipt = application ? profileRevisionReceipts(this).
|
|
1683
|
+
const receipt = application ? profileRevisionReceipts(this).secret.get(application) : void 0;
|
|
1818
1684
|
if (!application || !receipt) throw new Error("Secret profile preparation produced no native plan");
|
|
1819
|
-
const materializedDigest = this.
|
|
1685
|
+
const materializedDigest = this.secretProfileRevisions.get(application)?.materializedDigest;
|
|
1820
1686
|
if (!materializedDigest) throw new Error("Secret profile preparation produced no revision digest");
|
|
1821
1687
|
const verification = `${this.secretProfileVerificationScope()}\0${materializedDigest}`;
|
|
1822
1688
|
const verifyCached = verifiedSecretProfileRevisions.has(verification);
|
|
@@ -1829,11 +1695,7 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
|
|
|
1829
1695
|
});
|
|
1830
1696
|
return receipt;
|
|
1831
1697
|
}
|
|
1832
|
-
/**
|
|
1833
|
-
* What the native health check's verdict depends on besides a revision's
|
|
1834
|
-
* bytes: the executable that loads them. A provider whose check runs
|
|
1835
|
-
* through a configurable binary names it here.
|
|
1836
|
-
*/
|
|
1698
|
+
/** What the native health check's verdict depends on besides a revision's bytes: the executable that loads them. */
|
|
1837
1699
|
secretProfileVerificationScope() {
|
|
1838
1700
|
return this.provider;
|
|
1839
1701
|
}
|
|
@@ -1862,18 +1724,18 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
|
|
|
1862
1724
|
extraFlags: [...config.extraFlags ?? [], ...profilePlan.spawn.flags]
|
|
1863
1725
|
};
|
|
1864
1726
|
}
|
|
1727
|
+
/** A profile's addition precedes host context while preserving its harness prompt. */
|
|
1728
|
+
resolveProfilePrompts(input, plan, profile = this.resolveExecutionProfileForInput(input)) {
|
|
1729
|
+
const addition = plan?.spawn.appendSystemPrompt;
|
|
1730
|
+
const keep = keepsHarnessSystemPrompt(profile, this.profileHarness);
|
|
1731
|
+
return {
|
|
1732
|
+
systemPrompt: keep ? void 0 : this.resolveProfileSystemPrompt(input.systemPrompt, plan),
|
|
1733
|
+
appendSystemPrompt: keep ? joinSystemPromptAdditions(addition, input.systemPrompt, input.appendSystemPrompt) : input.appendSystemPrompt ? joinSystemPromptAdditions(addition, input.appendSystemPrompt) : addition
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1865
1736
|
resolveProfileSystemPrompt(requestPrompt, profilePlan) {
|
|
1866
1737
|
return resolveSystemPrompt(requestPrompt, profilePlan?.spawn.systemPrompt);
|
|
1867
1738
|
}
|
|
1868
|
-
/**
|
|
1869
|
-
* The plan's additive prompt intent for this spawn. No request-level
|
|
1870
|
-
* counterpart exists (`AgentExecutionInput` carries only the replacement),
|
|
1871
|
-
* so unlike {@link resolveProfileSystemPrompt} there is nothing to
|
|
1872
|
-
* conflict-check against.
|
|
1873
|
-
*/
|
|
1874
|
-
resolveProfileAppendSystemPrompt(profilePlan) {
|
|
1875
|
-
return profilePlan?.spawn.appendSystemPrompt;
|
|
1876
|
-
}
|
|
1877
1739
|
resolveProfileModel(requestModel) {
|
|
1878
1740
|
return resolveModel(requestModel, this.config.profile?.model?.default, this.config.model, this.config.profile?.model?.provider);
|
|
1879
1741
|
}
|
|
@@ -1893,26 +1755,20 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
|
|
|
1893
1755
|
}
|
|
1894
1756
|
async preparePromptInput(parts, workspaceRoot, sessionId, options) {
|
|
1895
1757
|
return preparePromptInputParts(parts, workspaceRoot, sessionId, {
|
|
1896
|
-
|
|
1897
|
-
|
|
1758
|
+
...options,
|
|
1759
|
+
nativeAttachmentKinds: options?.nativeAttachmentKinds ?? this.nativeAttachmentKinds()
|
|
1898
1760
|
});
|
|
1899
1761
|
}
|
|
1900
|
-
/**
|
|
1901
|
-
* Attachment kinds this adapter forwards natively via buildExecArgs (argv
|
|
1902
|
-
* flag / stdin content block) instead of the `[Image: ...]` / `[File: ...]`
|
|
1903
|
-
* text marker. The base adapter omits those kinds' markers from the prompt
|
|
1904
|
-
* text — an adapter that opts a kind in MUST deliver every attachment of
|
|
1905
|
-
* that kind itself, or the model never sees it.
|
|
1906
|
-
*/
|
|
1762
|
+
/** Attachment kinds this adapter forwards natively via buildExecArgs (argv flag / stdin content block) instead of the `[Image: ...]` / `[File: ...]` text marker. */
|
|
1907
1763
|
nativeAttachmentKinds() {
|
|
1908
1764
|
return [];
|
|
1909
1765
|
}
|
|
1910
1766
|
};
|
|
1911
1767
|
//#endregion
|
|
1912
1768
|
//#region src/native-session-enforcement.ts
|
|
1913
|
-
/**
|
|
1914
|
-
function sessionContractFullyEnforced(
|
|
1915
|
-
return
|
|
1769
|
+
/** Every shipped harness is enforced. A missing receipt is a failed proof, not a skip. */
|
|
1770
|
+
function sessionContractFullyEnforced(_provider) {
|
|
1771
|
+
return true;
|
|
1916
1772
|
}
|
|
1917
1773
|
//#endregion
|
|
1918
1774
|
//#region src/adapter-sessions.ts
|
|
@@ -2177,30 +2033,25 @@ const FAILURE_STDERR_TAIL_LIMIT = 600;
|
|
|
2177
2033
|
* message.
|
|
2178
2034
|
*
|
|
2179
2035
|
* A provider that reports its OWN terminal failure — an empty event stream, an
|
|
2180
|
-
* unfinished model step — explains what it saw but not how the process ended
|
|
2181
|
-
*
|
|
2182
|
-
*
|
|
2183
|
-
*
|
|
2184
|
-
*
|
|
2185
|
-
* caller got a bare sentence to debug from.
|
|
2186
|
-
*
|
|
2187
|
-
* Each part is skipped when the message already states it, so a message
|
|
2188
|
-
* derived FROM stderr or from the exit code is never doubled.
|
|
2036
|
+
* unfinished model step — explains what it saw but not how the process ended:
|
|
2037
|
+
* a CLI that exited 0 having emitted nothing is misconfigured, one killed on the
|
|
2038
|
+
* timeout is stuck, one that exited 9 with stderr crashed. Each part is skipped
|
|
2039
|
+
* when the message already states it, so a message derived FROM stderr or the
|
|
2040
|
+
* exit code is never doubled.
|
|
2189
2041
|
*/
|
|
2190
2042
|
function withCliExitDiagnostics(message, exit, stderr) {
|
|
2191
2043
|
const parts = [];
|
|
2192
2044
|
if (exit.stdinError && !message.includes(exit.stdinError)) parts.push(`stdin failed: ${exit.stdinError}`);
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
if (timeoutDetail) {
|
|
2196
|
-
if (!message.includes(timeoutDetail)) parts.push(timeoutDetail);
|
|
2197
|
-
} else if (!/timed out/i.test(message)) parts.push("timed out");
|
|
2198
|
-
}
|
|
2045
|
+
const timeout = exit.timedOut ? exit.timeoutDiagnostic ?? "timed out" : "";
|
|
2046
|
+
if (timeout && !message.toLowerCase().includes(timeout.toLowerCase())) parts.push(timeout);
|
|
2199
2047
|
if (exit.code !== null) {
|
|
2200
2048
|
if (!message.includes(`code ${exit.code}`)) parts.push(`exit code ${exit.code}`);
|
|
2201
2049
|
} else if (exit.signal) parts.push(`killed by ${exit.signal}`);
|
|
2202
2050
|
const trimmedStderr = stderr.trim();
|
|
2203
|
-
if (trimmedStderr && !message.includes(trimmedStderr))
|
|
2051
|
+
if (trimmedStderr && !message.includes(trimmedStderr)) {
|
|
2052
|
+
const tail = trimmedStderr.slice(-FAILURE_STDERR_TAIL_LIMIT);
|
|
2053
|
+
parts.push(`stderr: ${tail === trimmedStderr ? "" : "…"}${tail}`);
|
|
2054
|
+
}
|
|
2204
2055
|
return parts.length === 0 ? message : `${message} (${parts.join("; ")})`;
|
|
2205
2056
|
}
|
|
2206
2057
|
//#endregion
|
|
@@ -2366,9 +2217,20 @@ var CliAdapterMcp = class extends CliAdapterSessions {
|
|
|
2366
2217
|
};
|
|
2367
2218
|
//#endregion
|
|
2368
2219
|
//#region src/execution-state.ts
|
|
2369
|
-
/**
|
|
2370
|
-
|
|
2371
|
-
|
|
2220
|
+
/** A file of the sandbox's cgroup (#7571); undefined off Linux or outside one. */
|
|
2221
|
+
function readCgroupFile(name) {
|
|
2222
|
+
try {
|
|
2223
|
+
return readFileSync(`/sys/fs/cgroup/${name}`, "utf8").trim();
|
|
2224
|
+
} catch {
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
/** The sandbox's OOM-kill count: a running total for the whole box. */
|
|
2229
|
+
function oomKills() {
|
|
2230
|
+
const events = readCgroupFile("memory.events") ?? "";
|
|
2231
|
+
return Number(/^oom_kill (\d+)$/m.exec(events)?.[1] ?? 0);
|
|
2232
|
+
}
|
|
2233
|
+
/** Creates a new execution state. */
|
|
2372
2234
|
function createExecutionState(sessionId, messageId, workspaceRoot) {
|
|
2373
2235
|
return {
|
|
2374
2236
|
sessionId,
|
|
@@ -2376,7 +2238,8 @@ function createExecutionState(sessionId, messageId, workspaceRoot) {
|
|
|
2376
2238
|
workspaceRoot,
|
|
2377
2239
|
toolCalls: /* @__PURE__ */ new Map(),
|
|
2378
2240
|
textBuffer: "",
|
|
2379
|
-
reasoningBuffer: []
|
|
2241
|
+
reasoningBuffer: [],
|
|
2242
|
+
oomKillsAtStart: oomKills()
|
|
2380
2243
|
};
|
|
2381
2244
|
}
|
|
2382
2245
|
//#endregion
|
|
@@ -2438,10 +2301,9 @@ async function findCachedTurn(recorder, input, logger, sessionId) {
|
|
|
2438
2301
|
turnId: input.turnId,
|
|
2439
2302
|
sessionId
|
|
2440
2303
|
});
|
|
2441
|
-
const outcome = cached.outcome === void 0 ? { type: "completed" } : AgentExecutionOutcomeSchema.parse(cached.outcome);
|
|
2442
2304
|
return {
|
|
2443
2305
|
...cached,
|
|
2444
|
-
outcome
|
|
2306
|
+
outcome: AgentExecutionOutcomeSchema.parse(cached.outcome)
|
|
2445
2307
|
};
|
|
2446
2308
|
}
|
|
2447
2309
|
/**
|
|
@@ -2482,7 +2344,98 @@ function armNativeSessionDeadline(params) {
|
|
|
2482
2344
|
deadline.unref();
|
|
2483
2345
|
return deadline;
|
|
2484
2346
|
}
|
|
2485
|
-
|
|
2347
|
+
function idIn(glob, rel) {
|
|
2348
|
+
const g = glob.split("/");
|
|
2349
|
+
const r = rel.split("/");
|
|
2350
|
+
let gi = 0;
|
|
2351
|
+
let ri = 0;
|
|
2352
|
+
let id = null;
|
|
2353
|
+
while (gi < g.length && ri < r.length) {
|
|
2354
|
+
const part = g[gi];
|
|
2355
|
+
if (part === "**") {
|
|
2356
|
+
const rest = g.slice(gi + 1).join("/");
|
|
2357
|
+
if (!rest) return id;
|
|
2358
|
+
for (let k = ri; k < r.length; k++) {
|
|
2359
|
+
const found = idIn(rest, r.slice(k).join("/"));
|
|
2360
|
+
if (found) return found;
|
|
2361
|
+
}
|
|
2362
|
+
return null;
|
|
2363
|
+
}
|
|
2364
|
+
if (part?.includes("{id}")) {
|
|
2365
|
+
const [pre, post] = part.split("{id}");
|
|
2366
|
+
const name = r[ri] ?? "";
|
|
2367
|
+
if (!name.startsWith(pre ?? "") || !name.endsWith(post ?? "")) return null;
|
|
2368
|
+
id = name.slice((pre ?? "").length, name.length - (post ?? "").length);
|
|
2369
|
+
} else if (part !== "*" && part !== r[ri]) return null;
|
|
2370
|
+
gi += 1;
|
|
2371
|
+
ri += 1;
|
|
2372
|
+
}
|
|
2373
|
+
return gi === g.length && ri === r.length ? id : null;
|
|
2374
|
+
}
|
|
2375
|
+
function filesUnder(dir, prefix, out) {
|
|
2376
|
+
let entries;
|
|
2377
|
+
try {
|
|
2378
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2379
|
+
} catch {
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
for (const entry of entries) {
|
|
2383
|
+
const name = String(entry.name);
|
|
2384
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
2385
|
+
if (entry.isDirectory()) filesUnder(join(dir, name), rel, out);
|
|
2386
|
+
else out.push(rel);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
function idsNow(home, stores) {
|
|
2390
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2391
|
+
for (const store of stores) {
|
|
2392
|
+
const root = join(home, store.root);
|
|
2393
|
+
if (store.shared && store.write === "sqlite-wal") {
|
|
2394
|
+
try {
|
|
2395
|
+
const db = new DatabaseSync(join(root, store.files[0] ?? "state.db"), { readOnly: true });
|
|
2396
|
+
try {
|
|
2397
|
+
let rows;
|
|
2398
|
+
try {
|
|
2399
|
+
rows = db.prepare("SELECT id FROM sessions").all();
|
|
2400
|
+
} catch {
|
|
2401
|
+
rows = db.prepare("SELECT session_id AS id FROM session_windows").all();
|
|
2402
|
+
}
|
|
2403
|
+
for (const row of rows) if (row.id) ids.add(row.id);
|
|
2404
|
+
} finally {
|
|
2405
|
+
db.close();
|
|
2406
|
+
}
|
|
2407
|
+
} catch {}
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
const found = [];
|
|
2411
|
+
filesUnder(root, "", found);
|
|
2412
|
+
for (const rel of found) for (const glob of store.files) {
|
|
2413
|
+
const id = idIn(glob, rel);
|
|
2414
|
+
if (id) ids.add(id);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
return ids;
|
|
2418
|
+
}
|
|
2419
|
+
function watchNewSessionIds(params) {
|
|
2420
|
+
const home = params.runtimeHome?.trim().replace(/\/+$/u, "");
|
|
2421
|
+
const contract = params.sessionContract;
|
|
2422
|
+
if (!home || contract?.identity.kind !== "first-event") return () => {};
|
|
2423
|
+
const seen = idsNow(home, contract.stores);
|
|
2424
|
+
const timer = setInterval(() => {
|
|
2425
|
+
if (params.observer.nativeSessionObserved) return;
|
|
2426
|
+
for (const id of idsNow(home, contract.stores)) {
|
|
2427
|
+
if (seen.has(id)) continue;
|
|
2428
|
+
seen.add(id);
|
|
2429
|
+
params.observer.onEvent({
|
|
2430
|
+
type: "native.session.observed",
|
|
2431
|
+
provider: params.provider,
|
|
2432
|
+
nativeSessionId: id
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
}, 200);
|
|
2436
|
+
timer.unref();
|
|
2437
|
+
return () => clearInterval(timer);
|
|
2438
|
+
}
|
|
2486
2439
|
function collectStderr(cliProcess) {
|
|
2487
2440
|
let text = "";
|
|
2488
2441
|
const stderrDecoder = new StringDecoder("utf8");
|
|
@@ -2496,16 +2449,46 @@ function collectStderr(cliProcess) {
|
|
|
2496
2449
|
return text;
|
|
2497
2450
|
} };
|
|
2498
2451
|
}
|
|
2499
|
-
/**
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2452
|
+
/** A turn's stderr or ACP error when its link to its harness's server dropped (#8819). */
|
|
2453
|
+
const HARNESS_LINK_LOST = /\b(?:ECONNRESET|ECONNREFUSED|EPIPE|UND_ERR_SOCKET)\b|ended before (?:it|the turn) completed|Gateway disconnected/u;
|
|
2454
|
+
/**
|
|
2455
|
+
* Names a SIGKILL from the memory limit (#7571): the CLI runs in the sandbox's
|
|
2456
|
+
* cgroup, whose `memory.events` counts OOM kills under the `memory.max` limit.
|
|
2457
|
+
* The count is the box's running total, so only a kill since the turn started
|
|
2458
|
+
* is this turn's.
|
|
2459
|
+
*/
|
|
2460
|
+
function memoryLimitKill(killsAtStart = 0) {
|
|
2461
|
+
const kills = oomKills() - killsAtStart;
|
|
2462
|
+
if (kills < 1) return void 0;
|
|
2463
|
+
const max = readCgroupFile("memory.max") ?? "max";
|
|
2464
|
+
return `The sandbox's ${max === "max" ? "host" : `${Number(max) / 2 ** 20} MiB`} memory limit OOM-killed ${kills} process(es) during this turn: give the sandbox more memory or make the tool use less`;
|
|
2505
2465
|
}
|
|
2506
|
-
/**
|
|
2507
|
-
|
|
2508
|
-
|
|
2466
|
+
/**
|
|
2467
|
+
* A failed CLI turn's end and error (#16404): a guard's end is its own, the
|
|
2468
|
+
* rest is `turnEndReason`'s; a session death and a memory-limit kill carry
|
|
2469
|
+
* their wire code.
|
|
2470
|
+
*/
|
|
2471
|
+
function endFailedTurn(params) {
|
|
2472
|
+
const { provider, state, exitResult, stderr, startTime, endTime } = params;
|
|
2473
|
+
const oom = exitResult.signal === "SIGKILL" && memoryLimitKill(state.oomKillsAtStart);
|
|
2474
|
+
state.endReason ??= turnEndReason({
|
|
2475
|
+
aborted: params.aborted,
|
|
2476
|
+
timedOut: exitResult.timedOut,
|
|
2477
|
+
harnessFailed: Boolean(state.terminalStatus),
|
|
2478
|
+
linkLost: HARNESS_LINK_LOST.test(stderr),
|
|
2479
|
+
memoryKilled: Boolean(oom),
|
|
2480
|
+
signal: exitResult.signal
|
|
2481
|
+
});
|
|
2482
|
+
if (oom && state.endReason === "memory_limit") state.failure ??= {
|
|
2483
|
+
code: "MEMORY_LIMIT_KILLED",
|
|
2484
|
+
message: oom
|
|
2485
|
+
};
|
|
2486
|
+
if (["gateway_disconnect", "harness_exit"].includes(state.endReason)) state.failure ??= {
|
|
2487
|
+
code: "SESSION_DIED",
|
|
2488
|
+
message: `Harness session died (${state.endReason}): start a new session, or send the next turn to this session to resume it`
|
|
2489
|
+
};
|
|
2490
|
+
const nix = params.nixBinPath ? ` (NIX_BIN_PATH=${params.nixBinPath})` : " (NIX_BIN_PATH unset — is the Nix profile mounted?)";
|
|
2491
|
+
const errorMessage = exitResult.spawnError ? `Failed to spawn "${params.command}": ${exitResult.spawnError}${exitResult.spawnError.includes("ENOENT") ? ` — binary not found in PATH${nix}` : ""}` : withCliExitDiagnostics(state.failure?.message ?? state.lastDiagnostic ?? (exitResult.timedOut ? exitResult.timeoutDiagnostic ?? "Execution timed out" : void 0) ?? (stderr.trim() || (state.terminalStatus === "failed" ? `${provider} reported a failed terminal status` : `Process exited with code ${exitResult.code}`)), exitResult, stderr);
|
|
2509
2492
|
const failure = new AgentExecutionError(`${provider} execution failed: ${errorMessage}`, {
|
|
2510
2493
|
...state.tokenUsage ? { tokenUsage: state.tokenUsage } : {},
|
|
2511
2494
|
timing: {
|
|
@@ -2514,14 +2497,19 @@ function buildTurnFailure(params) {
|
|
|
2514
2497
|
durationMs: endTime - startTime
|
|
2515
2498
|
}
|
|
2516
2499
|
});
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
...state.failure
|
|
2500
|
+
Object.assign(failure, {
|
|
2501
|
+
reason: state.endReason,
|
|
2502
|
+
...state.failure?.code ? { code: state.failure.code } : {},
|
|
2503
|
+
...state.failure?.statusCode === void 0 ? {} : {
|
|
2520
2504
|
statusCode: state.failure.statusCode,
|
|
2521
2505
|
status: state.failure.statusCode
|
|
2522
|
-
}
|
|
2506
|
+
},
|
|
2507
|
+
...state.failure?.resetAt ? { resetAt: state.failure.resetAt } : {}
|
|
2523
2508
|
});
|
|
2524
|
-
return
|
|
2509
|
+
return {
|
|
2510
|
+
errorMessage,
|
|
2511
|
+
failure
|
|
2512
|
+
};
|
|
2525
2513
|
}
|
|
2526
2514
|
/**
|
|
2527
2515
|
* Best-effort: stamp the assistant message interrupted so reattach can
|
|
@@ -2570,14 +2558,12 @@ function buildExecutionResult(params) {
|
|
|
2570
2558
|
}
|
|
2571
2559
|
};
|
|
2572
2560
|
}
|
|
2573
|
-
/** A turn whose provider session record failed to persist ends failed. */
|
|
2574
2561
|
function failOnProviderSessionPersistence(observer, state) {
|
|
2575
2562
|
if (!observer.providerSessionPersistenceError) return;
|
|
2576
2563
|
const message = observer.providerSessionPersistenceError instanceof Error ? observer.providerSessionPersistenceError.message : String(observer.providerSessionPersistenceError);
|
|
2577
2564
|
state.terminalStatus = "failed";
|
|
2578
2565
|
state.lastDiagnostic = `Provider session persistence failed: ${message}`;
|
|
2579
2566
|
}
|
|
2580
|
-
/** Log a non-zero exit the provider accepted, and the stderr a successful turn left. */
|
|
2581
2567
|
function logAcceptedCliExit(logger, exitResult, stderr) {
|
|
2582
2568
|
if (!exitResult.success) logger.info("CLI process exited non-zero but was treated as success by provider", {
|
|
2583
2569
|
exitCode: exitResult.code,
|
|
@@ -2683,14 +2669,20 @@ function describeFailure(part, repairToolNames) {
|
|
|
2683
2669
|
}
|
|
2684
2670
|
function createRepeatedToolFailureGuard(options) {
|
|
2685
2671
|
const { limit } = options;
|
|
2686
|
-
if (!Number.isInteger(limit) || limit < 1) throw new Error(`Repeated tool-failure limit must be a positive whole number (got ${limit})`);
|
|
2687
2672
|
const repairToolNames = options.repairToolNames ?? NO_REPAIR_TOOL_NAMES;
|
|
2688
2673
|
let signature;
|
|
2689
2674
|
let previousInput;
|
|
2690
2675
|
let count = 0;
|
|
2691
2676
|
let lastScoredPartId;
|
|
2692
2677
|
let tripped = false;
|
|
2693
|
-
return {
|
|
2678
|
+
return {
|
|
2679
|
+
/**
|
|
2680
|
+
* Feed one tool part. Returns the trip on the call that reaches the limit,
|
|
2681
|
+
* and `undefined` every other time. A tripped guard stays tripped and
|
|
2682
|
+
* reports once, so a caller can kill the turn without racing the events
|
|
2683
|
+
* still in flight behind it.
|
|
2684
|
+
*/
|
|
2685
|
+
observe(part) {
|
|
2694
2686
|
if (tripped) return void 0;
|
|
2695
2687
|
if (isUnresolved(part.state)) return void 0;
|
|
2696
2688
|
if (part.id === lastScoredPartId) return void 0;
|
|
@@ -2716,22 +2708,6 @@ function createRepeatedToolFailureGuard(options) {
|
|
|
2716
2708
|
}
|
|
2717
2709
|
//#endregion
|
|
2718
2710
|
//#region src/turn-liveness.ts
|
|
2719
|
-
/**
|
|
2720
|
-
* How long the provider stream stays quiet before the adapter emits a liveness
|
|
2721
|
-
* event, and how often it re-checks that quiet window.
|
|
2722
|
-
*
|
|
2723
|
-
* A model that reasons for a minute and a run that died look the same on the
|
|
2724
|
-
* wire: an open stream with nothing on it. A consumer that must show progress
|
|
2725
|
-
* resolves the ambiguity with an inactivity timeout. The Blueprint Agent
|
|
2726
|
-
* frontend allows 20 s (blueprint-agent
|
|
2727
|
-
* apps/web/src/ui-provider/devcontainer-events/hooks/useStreamWatchdog.ts).
|
|
2728
|
-
* It then aborts a turn that the CLI is still running.
|
|
2729
|
-
*
|
|
2730
|
-
* A ping on quiet feeds that timeout without inventing content. The check
|
|
2731
|
-
* interval bounds the longest silence at quiet plus check, so these values put
|
|
2732
|
-
* the worst case at 6 s. A provider that emits more often than the quiet
|
|
2733
|
-
* window never trips the ticker, so nothing double-emits.
|
|
2734
|
-
*/
|
|
2735
2711
|
const STREAM_LIVENESS_QUIET_MS = 5e3;
|
|
2736
2712
|
const STREAM_LIVENESS_CHECK_MS = 1e3;
|
|
2737
2713
|
/**
|
|
@@ -2821,7 +2797,6 @@ const REPEATED_TOOL_FAILURE_LIMIT = 3;
|
|
|
2821
2797
|
var TurnObserver = class {
|
|
2822
2798
|
providerSessionPersistence = Promise.resolve();
|
|
2823
2799
|
providerSessionPersistenceError;
|
|
2824
|
-
repeatedToolFailureTripped = false;
|
|
2825
2800
|
nativeSessionObserved = false;
|
|
2826
2801
|
partUpdates;
|
|
2827
2802
|
liveness;
|
|
@@ -2875,7 +2850,7 @@ var TurnObserver = class {
|
|
|
2875
2850
|
});
|
|
2876
2851
|
if (repeated) {
|
|
2877
2852
|
const detail = `${provider} sent the same ${repeated.tool} call ${repeated.count} times and every one was rejected. The turn stopped before it ran the model again: ${repeated.detail}`;
|
|
2878
|
-
|
|
2853
|
+
state.endReason = "repeated_tool_failure";
|
|
2879
2854
|
state.terminalStatus = "failed";
|
|
2880
2855
|
state.lastDiagnostic = detail;
|
|
2881
2856
|
logger.error("Repeated tool-call rejection stopped the turn", {
|
|
@@ -2951,26 +2926,7 @@ function cliTurnProcess(child, logger, spawnStarted) {
|
|
|
2951
2926
|
}
|
|
2952
2927
|
//#endregion
|
|
2953
2928
|
//#region src/base-adapter.ts
|
|
2954
|
-
/**
|
|
2955
|
-
* Base adapter for CLI-based AI agent providers.
|
|
2956
|
-
*
|
|
2957
|
-
* Subclasses must implement:
|
|
2958
|
-
* - provider: string identifier
|
|
2959
|
-
* - defaultCliPath: default CLI executable name
|
|
2960
|
-
* - buildExecArgs(): build CLI execution arguments
|
|
2961
|
-
* - buildExecEnv(): build environment variables
|
|
2962
|
-
* - createEventParser(): create JSONL event parser
|
|
2963
|
-
* - mapEventToStreamEvents(): map parsed events to StreamEvents
|
|
2964
|
-
*
|
|
2965
|
-
* The message log directory is the registry spec's `messageStorageDir` for
|
|
2966
|
-
* `provider` (cli-agent-registry `messageStorageDirOf`); only an adapter for
|
|
2967
|
-
* a provider the registry does not know sets `defaultStorageDir`.
|
|
2968
|
-
*
|
|
2969
|
-
* Optional overrides:
|
|
2970
|
-
* - buildMcpAddArgs(): MCP add command arguments
|
|
2971
|
-
* - buildMcpListArgs(): MCP list command arguments (if supported)
|
|
2972
|
-
* - parseMcpListOutput(): parse MCP list output
|
|
2973
|
-
*/
|
|
2929
|
+
/** Base adapter for CLI-based AI agent providers. */
|
|
2974
2930
|
var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
2975
2931
|
supportsManagedQuote = false;
|
|
2976
2932
|
async execute(input, services, onEvent) {
|
|
@@ -2978,13 +2934,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
2978
2934
|
const quoteVersion = input.managedQuoteVersion;
|
|
2979
2935
|
if (quoteVersion !== void 0 && !this.supportsManagedQuote) throw new Error(`${this.provider} does not support the managed-quote native model transport`);
|
|
2980
2936
|
const correlationHeaders = executionCorrelationHeaders(input.interactionBinding, input.traceId, quoteVersion);
|
|
2981
|
-
|
|
2982
|
-
const result = this.executeInternal(input, services, onEvent, execution.controller.signal, correlationHeaders);
|
|
2983
|
-
result.then(() => finishExecution(this, execution), () => {
|
|
2984
|
-
execution.controller.abort();
|
|
2985
|
-
finishExecution(this, execution);
|
|
2986
|
-
});
|
|
2987
|
-
return result;
|
|
2937
|
+
return this.withExecution(input.abortSignal, (signal) => this.executeInternal(input, services, onEvent, signal, correlationHeaders));
|
|
2988
2938
|
}
|
|
2989
2939
|
async executeInternal(input, services, onEvent, executionSignal, correlationHeaders) {
|
|
2990
2940
|
const startTime = Date.now();
|
|
@@ -3046,7 +2996,8 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3046
2996
|
stderr,
|
|
3047
2997
|
state
|
|
3048
2998
|
});
|
|
3049
|
-
if (!exitResult.success && !benignNonZeroExit || state.terminalStatus === "failed" ||
|
|
2999
|
+
if (!exitResult.success && !benignNonZeroExit || state.terminalStatus === "failed" || state.endReason) return this.failCliTurn({
|
|
3000
|
+
aborted: executionSignal.aborted,
|
|
3050
3001
|
onEvent,
|
|
3051
3002
|
recorder,
|
|
3052
3003
|
observer,
|
|
@@ -3070,10 +3021,6 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3070
3021
|
endTime
|
|
3071
3022
|
});
|
|
3072
3023
|
}
|
|
3073
|
-
/**
|
|
3074
|
-
* Record a successful turn: the provider session id, the assistant message,
|
|
3075
|
-
* and the result, cached for a same-turnId retry.
|
|
3076
|
-
*/
|
|
3077
3024
|
async completeTurn(params) {
|
|
3078
3025
|
const { input, sessionId, state, recorder, profilePlan } = params;
|
|
3079
3026
|
if (state.sessionId && sessionId && state.sessionId !== sessionId) await this.getSessionStore().update(sessionId, {
|
|
@@ -3093,7 +3040,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3093
3040
|
outputTokens: state.tokenUsage.outputTokens
|
|
3094
3041
|
} : void 0
|
|
3095
3042
|
});
|
|
3096
|
-
const publicProfileRevision = profilePlan ? profileRevisionReceipts(this).
|
|
3043
|
+
const publicProfileRevision = profilePlan ? profileRevisionReceipts(this).public.get(profilePlan) : void 0;
|
|
3097
3044
|
const result = buildExecutionResult({
|
|
3098
3045
|
outcome: await this.executionOutcome(state, input),
|
|
3099
3046
|
state,
|
|
@@ -3110,14 +3057,16 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3110
3057
|
async failCliTurn(params) {
|
|
3111
3058
|
const { onEvent, recorder, observer, state, exitResult, stderr } = params;
|
|
3112
3059
|
const { launch, startTime, endTime } = params;
|
|
3113
|
-
const {
|
|
3114
|
-
const errorMessage = describeCliFailure({
|
|
3060
|
+
const { errorMessage, failure } = endFailedTurn({
|
|
3115
3061
|
provider: this.provider,
|
|
3116
3062
|
state,
|
|
3117
3063
|
exitResult,
|
|
3118
3064
|
stderr,
|
|
3119
|
-
|
|
3120
|
-
|
|
3065
|
+
aborted: params.aborted,
|
|
3066
|
+
command: launch.execArgs.command,
|
|
3067
|
+
nixBinPath: launch.env.NIX_BIN_PATH,
|
|
3068
|
+
startTime,
|
|
3069
|
+
endTime
|
|
3121
3070
|
});
|
|
3122
3071
|
this.logger.error("CLI process failed", {
|
|
3123
3072
|
exitCode: exitResult.code,
|
|
@@ -3129,7 +3078,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3129
3078
|
stderr: stderr.slice(0, 2e3),
|
|
3130
3079
|
diagnostic: state.lastDiagnostic,
|
|
3131
3080
|
durationMs: endTime - startTime,
|
|
3132
|
-
command: execArgs.command
|
|
3081
|
+
command: launch.execArgs.command
|
|
3133
3082
|
});
|
|
3134
3083
|
if (exitResult.timedOut) onEvent?.({
|
|
3135
3084
|
type: "warning",
|
|
@@ -3141,13 +3090,6 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3141
3090
|
status: "failed",
|
|
3142
3091
|
detail: errorMessage
|
|
3143
3092
|
});
|
|
3144
|
-
const failure = buildTurnFailure({
|
|
3145
|
-
provider: this.provider,
|
|
3146
|
-
errorMessage,
|
|
3147
|
-
state,
|
|
3148
|
-
startTime,
|
|
3149
|
-
endTime
|
|
3150
|
-
});
|
|
3151
3093
|
await recordTurnInterruption({
|
|
3152
3094
|
recorder,
|
|
3153
3095
|
observer,
|
|
@@ -3158,11 +3100,6 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3158
3100
|
});
|
|
3159
3101
|
throw failure;
|
|
3160
3102
|
}
|
|
3161
|
-
/**
|
|
3162
|
-
* Open the turn's session (sidecar/provider persistence only; native CLI
|
|
3163
|
-
* startup is measured separately). A same-turnId retry of a completed turn
|
|
3164
|
-
* gets that turn's cached result instead of running again.
|
|
3165
|
-
*/
|
|
3166
3103
|
async openTurnSession(input, recorder, onEvent) {
|
|
3167
3104
|
const sessionPrepareStarted = performance.now();
|
|
3168
3105
|
const adapter = this;
|
|
@@ -3204,14 +3141,13 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3204
3141
|
cached
|
|
3205
3142
|
};
|
|
3206
3143
|
}
|
|
3207
|
-
if (input.turnId
|
|
3144
|
+
if (input.turnId) recorder.setTurnId?.(input.turnId);
|
|
3208
3145
|
return {
|
|
3209
3146
|
...prepared,
|
|
3210
3147
|
providerMessageIds,
|
|
3211
3148
|
cached: void 0
|
|
3212
3149
|
};
|
|
3213
3150
|
}
|
|
3214
|
-
/** Provision the profile's cwd-native dimensions for this turn. */
|
|
3215
3151
|
async materializeTurnProfile(input, sessionId) {
|
|
3216
3152
|
const executionProfile = this.resolveExecutionProfileForInput(input);
|
|
3217
3153
|
const profilePlan = await this.materializeProfileResources(executionProfile, input.workspaceRoot ?? this.storagePath, input.userId ?? sessionId, sessionId);
|
|
@@ -3221,14 +3157,9 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3221
3157
|
profilePlan
|
|
3222
3158
|
};
|
|
3223
3159
|
}
|
|
3224
|
-
|
|
3225
|
-
* Prepare the prompt and record the caller's own parts. `promptText`
|
|
3226
|
-
* additionally carries the replayed transcript and this turn's ephemeral
|
|
3227
|
-
* context, which are execution-only and stay out of both stores.
|
|
3228
|
-
*/
|
|
3229
|
-
async recordTurnPrompt(input, session, recorder) {
|
|
3160
|
+
async recordTurnPrompt(input, session, recorder, attachmentOptions) {
|
|
3230
3161
|
const { sessionId, providerMessageIds } = session;
|
|
3231
|
-
const preparedInput = await this.preparePromptInput(normalizeInputParts(input), input.workspaceRoot ?? this.storagePath, sessionId);
|
|
3162
|
+
const preparedInput = await this.preparePromptInput(normalizeInputParts(input), input.workspaceRoot ?? this.storagePath, sessionId, attachmentOptions);
|
|
3232
3163
|
const transcriptContext = await buildTranscriptContext({
|
|
3233
3164
|
provider: this.provider,
|
|
3234
3165
|
sessionId,
|
|
@@ -3265,13 +3196,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3265
3196
|
sessionId: providerSessionId,
|
|
3266
3197
|
...mintedSessionId ? { mintedSessionId } : {},
|
|
3267
3198
|
workspaceRoot,
|
|
3268
|
-
...
|
|
3269
|
-
systemPrompt: void 0,
|
|
3270
|
-
appendSystemPrompt: joinSystemPromptAdditions(this.resolveProfileAppendSystemPrompt(profilePlan), input.systemPrompt, input.appendSystemPrompt)
|
|
3271
|
-
} : {
|
|
3272
|
-
systemPrompt: this.resolveProfileSystemPrompt(input.systemPrompt, profilePlan),
|
|
3273
|
-
appendSystemPrompt: input.appendSystemPrompt ? joinSystemPromptAdditions(this.resolveProfileAppendSystemPrompt(profilePlan), input.appendSystemPrompt) : this.resolveProfileAppendSystemPrompt(profilePlan)
|
|
3274
|
-
},
|
|
3199
|
+
...this.resolveProfilePrompts(input, profilePlan, executionProfile),
|
|
3275
3200
|
attachments,
|
|
3276
3201
|
profilePlan
|
|
3277
3202
|
});
|
|
@@ -3329,10 +3254,9 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3329
3254
|
});
|
|
3330
3255
|
await observer.providerSessionPersistence;
|
|
3331
3256
|
if (observer.providerSessionPersistenceError) throw observer.providerSessionPersistenceError;
|
|
3257
|
+
this.logger.observePhase?.("native_session_bind", performance.now() - nativeBindStarted);
|
|
3332
3258
|
}
|
|
3333
|
-
if (knownNativeSessionId) this.logger.observePhase?.("native_session_bind", performance.now() - nativeBindStarted);
|
|
3334
3259
|
}
|
|
3335
|
-
/** Spawn the turn's process, feed its input, and wait for it to exit. */
|
|
3336
3260
|
async runCliTurn(params) {
|
|
3337
3261
|
const { input, sessionId, providerSessionId, executionSignal } = params;
|
|
3338
3262
|
const { launch, promptText, state, observer, startTime } = params;
|
|
@@ -3353,6 +3277,17 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3353
3277
|
const cliProcess = spawnedProcess instanceof Promise ? await spawnedProcess : spawnedProcess;
|
|
3354
3278
|
this.activeProcesses.add(cliProcess);
|
|
3355
3279
|
observer.attachProcess(cliProcess);
|
|
3280
|
+
const stopSessionWatch = watchNewSessionIds({
|
|
3281
|
+
provider: this.provider,
|
|
3282
|
+
runtimeHome: this.config.runtimeHome,
|
|
3283
|
+
sessionContract: launch.sessionContract,
|
|
3284
|
+
observer
|
|
3285
|
+
});
|
|
3286
|
+
if (providerSessionId && launch.sessionContract?.identity.kind === "first-event") observer.onEvent({
|
|
3287
|
+
type: "native.session.observed",
|
|
3288
|
+
provider: this.provider,
|
|
3289
|
+
nativeSessionId: providerSessionId
|
|
3290
|
+
});
|
|
3356
3291
|
const identityDeadline = armNativeSessionDeadline({
|
|
3357
3292
|
provider: this.provider,
|
|
3358
3293
|
sessionId,
|
|
@@ -3367,8 +3302,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3367
3302
|
let exitResult;
|
|
3368
3303
|
try {
|
|
3369
3304
|
if (execArgs.useStdin) {
|
|
3370
|
-
|
|
3371
|
-
cliProcess.write(this.formatStdinMessage(stdinMessage));
|
|
3305
|
+
cliProcess.write(this.formatStdinMessage(execArgs.stdin || promptText));
|
|
3372
3306
|
cliProcess.closeInput();
|
|
3373
3307
|
}
|
|
3374
3308
|
exitResult = await cliProcess.exitPromise;
|
|
@@ -3377,6 +3311,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3377
3311
|
await cliProcess.exitPromise;
|
|
3378
3312
|
throw error;
|
|
3379
3313
|
} finally {
|
|
3314
|
+
stopSessionWatch();
|
|
3380
3315
|
clearInterval(livenessTicker);
|
|
3381
3316
|
if (identityDeadline) clearTimeout(identityDeadline);
|
|
3382
3317
|
this.activeProcesses.delete(cliProcess);
|
|
@@ -3407,13 +3342,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3407
3342
|
subprocessSpawnOptions: this.subprocessSpawnOptions()
|
|
3408
3343
|
});
|
|
3409
3344
|
}
|
|
3410
|
-
/**
|
|
3411
|
-
* The launch one turn would make, without starting it: the spawn config
|
|
3412
|
-
* (argv, cwd, environment, identity or launcher), the stdin payload, and
|
|
3413
|
-
* the profile files written into the workspace. Session and message
|
|
3414
|
-
* stores are untouched and no held process is reserved, so a caller that
|
|
3415
|
-
* runs the process itself (`execCli`) owns its output and lifecycle.
|
|
3416
|
-
*/
|
|
3345
|
+
/** The launch one turn would make, without starting it: the spawn config (argv, cwd, environment, identity or launcher). */
|
|
3417
3346
|
async prepareLaunch(input, options = {}) {
|
|
3418
3347
|
this.ensureInitialized();
|
|
3419
3348
|
const sessionId = input.sessionId ?? randomUUID();
|
|
@@ -3436,12 +3365,11 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3436
3365
|
const { execArgs } = launch;
|
|
3437
3366
|
return {
|
|
3438
3367
|
spawn: this.turnSpawnConfig(execArgs, launch.workspaceRoot, launch.env),
|
|
3439
|
-
...execArgs.useStdin ? { stdin: this.formatStdinMessage(execArgs.stdin
|
|
3368
|
+
...execArgs.useStdin ? { stdin: this.formatStdinMessage(execArgs.stdin || promptText) } : {},
|
|
3440
3369
|
...launch.mintedSessionId ? { nativeSessionId: launch.mintedSessionId } : {},
|
|
3441
3370
|
...profilePlan ? { workspacePlan: profilePlan.receipt } : {}
|
|
3442
3371
|
};
|
|
3443
3372
|
}
|
|
3444
|
-
/** Override only when the provider has a turn transport besides a CLI child. */
|
|
3445
3373
|
spawnExecutionProcess(request) {
|
|
3446
3374
|
const { execArgs, workspaceRoot, env, signal, timeoutMs, parser } = request;
|
|
3447
3375
|
const spawnStarted = performance.now();
|
|
@@ -3459,103 +3387,245 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
|
3459
3387
|
}
|
|
3460
3388
|
};
|
|
3461
3389
|
//#endregion
|
|
3462
|
-
//#region src/
|
|
3463
|
-
const
|
|
3464
|
-
const
|
|
3465
|
-
const
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
const
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3390
|
+
//#region src/metering.ts
|
|
3391
|
+
const UsageRecordSchema = z.record(z.string(), z.unknown());
|
|
3392
|
+
const UsageNumberSchema = z.union([z.number(), z.string().trim().min(1)]).transform(Number).pipe(z.number().finite().nonnegative());
|
|
3393
|
+
const TOKEN_USAGE_INPUT_KEYS = Object.freeze([
|
|
3394
|
+
"inputTokens",
|
|
3395
|
+
"input",
|
|
3396
|
+
"input_tokens",
|
|
3397
|
+
"promptTokens",
|
|
3398
|
+
"prompt_tokens"
|
|
3399
|
+
]);
|
|
3400
|
+
const TOKEN_USAGE_OUTPUT_KEYS = Object.freeze([
|
|
3401
|
+
"outputTokens",
|
|
3402
|
+
"output",
|
|
3403
|
+
"output_tokens",
|
|
3404
|
+
"completionTokens",
|
|
3405
|
+
"completion_tokens"
|
|
3406
|
+
]);
|
|
3407
|
+
const TOKEN_USAGE_TOTAL_KEYS = Object.freeze([
|
|
3408
|
+
"totalTokens",
|
|
3409
|
+
"total_tokens",
|
|
3410
|
+
"total"
|
|
3411
|
+
]);
|
|
3412
|
+
const TOKEN_USAGE_REASONING_KEYS = Object.freeze([
|
|
3413
|
+
"reasoningTokens",
|
|
3414
|
+
"reasoning_tokens",
|
|
3415
|
+
"reasoning",
|
|
3416
|
+
"reasoning_output_tokens"
|
|
3417
|
+
]);
|
|
3418
|
+
const TOKEN_USAGE_COST_KEYS = Object.freeze([
|
|
3419
|
+
"totalCostUsd",
|
|
3420
|
+
"costUsd",
|
|
3421
|
+
"total_cost_usd",
|
|
3422
|
+
"cost_usd",
|
|
3423
|
+
"cost"
|
|
3424
|
+
]);
|
|
3425
|
+
const CACHE_READ_INPUT_KEYS = Object.freeze([
|
|
3426
|
+
"cacheReadInputTokens",
|
|
3427
|
+
"cache_read_input_tokens",
|
|
3428
|
+
"cacheRead",
|
|
3429
|
+
"cache_read",
|
|
3430
|
+
"cached_input_tokens"
|
|
3431
|
+
]);
|
|
3432
|
+
const CACHE_CREATION_INPUT_KEYS = Object.freeze([
|
|
3433
|
+
"cacheCreationInputTokens",
|
|
3434
|
+
"cache_creation_input_tokens",
|
|
3435
|
+
"cacheWrite",
|
|
3436
|
+
"cache_write"
|
|
3437
|
+
]);
|
|
3438
|
+
const NESTED_COST_KEYS = Object.freeze([
|
|
3439
|
+
"total",
|
|
3440
|
+
"totalUsd",
|
|
3441
|
+
"total_usd",
|
|
3442
|
+
"usd"
|
|
3443
|
+
]);
|
|
3444
|
+
function asRecord(raw) {
|
|
3445
|
+
const parsed = UsageRecordSchema.safeParse(raw);
|
|
3446
|
+
return parsed.success ? parsed.data : void 0;
|
|
3447
|
+
}
|
|
3448
|
+
function tokenCount(raw) {
|
|
3449
|
+
const parsed = UsageNumberSchema.safeParse(raw);
|
|
3450
|
+
return parsed.success ? Math.trunc(parsed.data) : void 0;
|
|
3451
|
+
}
|
|
3452
|
+
function firstTokenCount(source, keys) {
|
|
3453
|
+
for (const key of keys) {
|
|
3454
|
+
const count = tokenCount(source[key]);
|
|
3455
|
+
if (count !== void 0) return count;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
function firstUsageCostUsd(source, keys = TOKEN_USAGE_COST_KEYS) {
|
|
3459
|
+
for (const key of keys) {
|
|
3460
|
+
const parsed = UsageNumberSchema.safeParse(source[key]);
|
|
3461
|
+
if (parsed.success) return parsed.data;
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
function usageRecord(source) {
|
|
3465
|
+
return asRecord(source.tokenUsage) ?? asRecord(source.usage) ?? source;
|
|
3466
|
+
}
|
|
3467
|
+
function readTokenUsage(source) {
|
|
3468
|
+
const usageSource = usageRecord(source);
|
|
3469
|
+
const inputTokens = firstTokenCount(usageSource, TOKEN_USAGE_INPUT_KEYS);
|
|
3470
|
+
const outputTokens = firstTokenCount(usageSource, TOKEN_USAGE_OUTPUT_KEYS);
|
|
3471
|
+
return inputTokens !== void 0 || outputTokens !== void 0 ? {
|
|
3472
|
+
inputTokens,
|
|
3473
|
+
outputTokens
|
|
3474
|
+
} : void 0;
|
|
3475
|
+
}
|
|
3476
|
+
function readTokenCostUsd(source) {
|
|
3477
|
+
const usageSource = usageRecord(source);
|
|
3478
|
+
return firstUsageCostUsd(source) ?? firstUsageCostUsd(usageSource) ?? readNestedCostUsd(source) ?? readNestedCostUsd(usageSource);
|
|
3479
|
+
}
|
|
3480
|
+
function readNestedCostUsd(source) {
|
|
3481
|
+
const cost = asRecord(source.cost);
|
|
3482
|
+
return cost ? firstUsageCostUsd(cost, NESTED_COST_KEYS) : void 0;
|
|
3483
|
+
}
|
|
3484
|
+
function readCliTokenUsage(raw) {
|
|
3485
|
+
const parsed = UsageRecordSchema.safeParse(raw);
|
|
3486
|
+
if (!parsed.success) return void 0;
|
|
3487
|
+
const source = parsed.data;
|
|
3488
|
+
const usageSource = usageRecord(source);
|
|
3489
|
+
const usage = readTokenUsage(source) ?? readTokenUsage(usageSource);
|
|
3490
|
+
const cost = readTokenCostUsd(source);
|
|
3491
|
+
const totalTokens = firstTokenCount(usageSource, TOKEN_USAGE_TOTAL_KEYS);
|
|
3492
|
+
const reasoningTokens = firstTokenCount(usageSource, TOKEN_USAGE_REASONING_KEYS);
|
|
3493
|
+
const cacheReadInputTokens = firstTokenCount(usageSource, CACHE_READ_INPUT_KEYS);
|
|
3494
|
+
const cacheCreationInputTokens = firstTokenCount(usageSource, CACHE_CREATION_INPUT_KEYS);
|
|
3495
|
+
if (!usage && totalTokens === void 0 && reasoningTokens === void 0 && cost === void 0 && cacheReadInputTokens === void 0 && cacheCreationInputTokens === void 0) return;
|
|
3496
|
+
return {
|
|
3497
|
+
inputTokens: usage?.inputTokens,
|
|
3498
|
+
outputTokens: usage?.outputTokens,
|
|
3499
|
+
totalTokens,
|
|
3500
|
+
reasoningTokens,
|
|
3501
|
+
cacheReadInputTokens,
|
|
3502
|
+
cacheCreationInputTokens,
|
|
3503
|
+
cost
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
function recordCliTokenUsage(state, ...sources) {
|
|
3507
|
+
for (const source of sources) {
|
|
3508
|
+
const tokenUsage = readCliTokenUsage(source);
|
|
3509
|
+
if (tokenUsage) {
|
|
3510
|
+
state.tokenUsage = tokenUsage;
|
|
3511
|
+
return true;
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
return false;
|
|
3515
|
+
}
|
|
3516
|
+
function addUsage(left, right) {
|
|
3517
|
+
if (left === void 0) return right;
|
|
3518
|
+
if (right === void 0) return left;
|
|
3519
|
+
const sum = left + right;
|
|
3520
|
+
return Number.isFinite(sum) ? sum : Math.max(left, right);
|
|
3521
|
+
}
|
|
3522
|
+
/**
|
|
3523
|
+
* Adds one usage report to the running total instead of replacing it.
|
|
3524
|
+
*
|
|
3525
|
+
* For a CLI that reports usage PER TURN (pi) rather than once per run,
|
|
3526
|
+
* `recordCliTokenUsage` would leave the execution billed as its final turn
|
|
3527
|
+
* alone. Use this wherever a single execution can produce more than one usage
|
|
3528
|
+
* report; use `recordCliTokenUsage` when the CLI emits one cumulative total.
|
|
3529
|
+
*
|
|
3530
|
+
* Returns true when a usage report was found in `sources` and folded in.
|
|
3531
|
+
*/
|
|
3532
|
+
function accumulateCliTokenUsage(state, ...sources) {
|
|
3533
|
+
for (const source of sources) {
|
|
3534
|
+
const tokenUsage = readCliTokenUsage(source);
|
|
3535
|
+
if (!tokenUsage) continue;
|
|
3536
|
+
const total = state.tokenUsage;
|
|
3537
|
+
state.tokenUsage = total ? {
|
|
3538
|
+
inputTokens: addUsage(total.inputTokens, tokenUsage.inputTokens),
|
|
3539
|
+
outputTokens: addUsage(total.outputTokens, tokenUsage.outputTokens),
|
|
3540
|
+
totalTokens: addUsage(total.totalTokens, tokenUsage.totalTokens),
|
|
3541
|
+
reasoningTokens: addUsage(total.reasoningTokens, tokenUsage.reasoningTokens),
|
|
3542
|
+
cacheReadInputTokens: addUsage(total.cacheReadInputTokens, tokenUsage.cacheReadInputTokens),
|
|
3543
|
+
cacheCreationInputTokens: addUsage(total.cacheCreationInputTokens, tokenUsage.cacheCreationInputTokens),
|
|
3544
|
+
cost: addUsage(total.cost, tokenUsage.cost)
|
|
3545
|
+
} : tokenUsage;
|
|
3546
|
+
return true;
|
|
3547
|
+
}
|
|
3548
|
+
return false;
|
|
3549
|
+
}
|
|
3550
|
+
function parseCliUsageLine(line) {
|
|
3551
|
+
if (!line.startsWith("usage:")) return void 0;
|
|
3552
|
+
const raw = line.slice(6).trim();
|
|
3553
|
+
if (!raw) return void 0;
|
|
3554
|
+
if (raw.startsWith("{")) return asRecord(JSON.parse(raw));
|
|
3555
|
+
const fields = {};
|
|
3556
|
+
for (const part of raw.split(/[,\s]+/)) {
|
|
3557
|
+
if (!part) continue;
|
|
3558
|
+
const separator = part.includes("=") ? "=" : part.includes(":") ? ":" : "";
|
|
3559
|
+
if (!separator) continue;
|
|
3560
|
+
const [key, value] = part.split(separator, 2);
|
|
3561
|
+
if (!key || value === void 0) continue;
|
|
3562
|
+
const numeric = Number(value);
|
|
3563
|
+
fields[key] = Number.isFinite(numeric) ? numeric : value;
|
|
3564
|
+
}
|
|
3565
|
+
return Object.keys(fields).length > 0 ? fields : void 0;
|
|
3566
|
+
}
|
|
3567
|
+
//#endregion
|
|
3568
|
+
//#region src/acp-session-adapter.ts
|
|
3569
|
+
const DEFAULT_PROMPT_TIMEOUT_MS = 6e5;
|
|
3570
|
+
const DEFAULT_BOOTSTRAP_TIMEOUT_MS = 12e4;
|
|
3571
|
+
const DEFAULT_BRIDGE_IDLE_MS = 300 * 1e3;
|
|
3572
|
+
const ABORT_GRACE_MS = 250;
|
|
3573
|
+
var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
|
|
3574
|
+
acpSessions = /* @__PURE__ */ new Map();
|
|
3575
|
+
spawnBridge(request) {
|
|
3576
|
+
return spawnAcpBridge(request);
|
|
3577
|
+
}
|
|
3578
|
+
checkAgentProtocolVersion(_advertised, _turn) {}
|
|
3579
|
+
resumeSessionId() {}
|
|
3580
|
+
async onSessionOpened(_bridge, _acpSessionId, _turn, _options) {}
|
|
3581
|
+
onBridgeSpawned(_bridge, _sessionId, _turn) {}
|
|
3582
|
+
beginTurn(_sessionId, _input, _onEvent) {
|
|
3583
|
+
return () => {};
|
|
3584
|
+
}
|
|
3585
|
+
onToolFailed(_sessionId, _failureText) {
|
|
3586
|
+
return [];
|
|
3587
|
+
}
|
|
3588
|
+
takeTurnFailure(_sessionId) {}
|
|
3589
|
+
onSessionEvicted(_sessionId) {}
|
|
3590
|
+
turnLogContext(_turn) {
|
|
3591
|
+
return {};
|
|
3592
|
+
}
|
|
3593
|
+
async execute(input, services, onEvent) {
|
|
3594
|
+
this.ensureInitialized();
|
|
3595
|
+
const turn = this.resolveTurn(input);
|
|
3596
|
+
const { config } = turn;
|
|
3597
|
+
const { recorder } = services;
|
|
3598
|
+
const startTime = Date.now();
|
|
3599
|
+
const workspaceRoot = input.workspaceRoot ?? this.storagePath;
|
|
3600
|
+
const turnInput = input.sessionId ? input : {
|
|
3601
|
+
...input,
|
|
3602
|
+
sessionId: input.interactionBinding?.sessionId
|
|
3603
|
+
};
|
|
3604
|
+
const opened = await this.openTurnSession(turnInput, recorder, onEvent);
|
|
3605
|
+
if (opened.cached) return opened.cached;
|
|
3606
|
+
const { sessionId, providerSessionId, providerMessageIds } = opened;
|
|
3607
|
+
const endTurn = this.beginTurn(sessionId, input, onEvent);
|
|
3608
|
+
try {
|
|
3609
|
+
const { preparedInput, promptText } = await this.recordTurnPrompt(turnInput, opened, recorder);
|
|
3610
|
+
onEvent?.({
|
|
3611
|
+
type: "status",
|
|
3612
|
+
status: "started"
|
|
3613
|
+
});
|
|
3614
|
+
const state = createExecutionState(sessionId, providerMessageIds?.assistantMessageId);
|
|
3615
|
+
let entry;
|
|
3616
|
+
try {
|
|
3617
|
+
entry = await this.acquireAcpSession({
|
|
3618
|
+
sessionId,
|
|
3619
|
+
...this.loadsNativeSessions && providerSessionId ? { providerSessionId } : {},
|
|
3620
|
+
turn,
|
|
3621
|
+
workspaceRoot,
|
|
3622
|
+
onEvent
|
|
3623
|
+
});
|
|
3624
|
+
} catch (err) {
|
|
3625
|
+
onEvent?.({
|
|
3626
|
+
type: "status",
|
|
3627
|
+
status: "failed",
|
|
3628
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
3559
3629
|
});
|
|
3560
3630
|
throw err instanceof Error ? err : new Error(String(err));
|
|
3561
3631
|
}
|
|
@@ -3569,28 +3639,38 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
|
|
|
3569
3639
|
provider: this.provider,
|
|
3570
3640
|
nativeSessionId: acpSessionId
|
|
3571
3641
|
});
|
|
3642
|
+
const timeoutMs = config.promptTimeoutMs ?? DEFAULT_PROMPT_TIMEOUT_MS;
|
|
3643
|
+
const prompt = this.promptBlocks(input, {
|
|
3644
|
+
...preparedInput,
|
|
3645
|
+
promptText
|
|
3646
|
+
}, turn);
|
|
3647
|
+
let promptResult;
|
|
3572
3648
|
try {
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
})
|
|
3649
|
+
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
3650
|
+
promptResult = await this.promptAcpSession(entry, {
|
|
3651
|
+
prompt,
|
|
3652
|
+
timeoutMs: Math.min(timeoutMs, 12e4),
|
|
3653
|
+
abortSignal: input.abortSignal,
|
|
3654
|
+
onUpdate: (params) => {
|
|
3655
|
+
for (const event of this.mapAcpUpdate(params, state, sessionId)) onEvent?.(event);
|
|
3656
|
+
}
|
|
3657
|
+
});
|
|
3658
|
+
break;
|
|
3659
|
+
} catch (err) {
|
|
3660
|
+
const detail = this.formatBridgeError(err);
|
|
3661
|
+
if (detail.includes("timed out")) this.interruptStuckTurn();
|
|
3662
|
+
if (attempt === 0 && detail.includes("timed out")) continue;
|
|
3663
|
+
if (attempt < 2 && await this.acknowledgeLostTurn(detail)) continue;
|
|
3664
|
+
throw err;
|
|
3665
|
+
}
|
|
3584
3666
|
const recorded = this.takeTurnFailure(sessionId);
|
|
3585
3667
|
if (recorded) throw recorded;
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
detail: finalStatus.detail
|
|
3591
|
-
} : {
|
|
3668
|
+
recordCliTokenUsage(state, promptResult);
|
|
3669
|
+
const stop = promptResult?.stopReason;
|
|
3670
|
+
if (stop !== "end_turn" && stop !== "max_tokens") throw new Error(`${this.provider} stopped the turn unfinished (stop reason ${stop ?? "missing"}): send the next turn to continue it`);
|
|
3671
|
+
onEvent?.({
|
|
3592
3672
|
type: "status",
|
|
3593
|
-
status:
|
|
3673
|
+
status: "completed"
|
|
3594
3674
|
});
|
|
3595
3675
|
await this.getSessionStore().update(sessionId, { providerSessionId: acpSessionId });
|
|
3596
3676
|
return await this.completeTurn({
|
|
@@ -3603,7 +3683,25 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
|
|
|
3603
3683
|
endTime: Date.now()
|
|
3604
3684
|
});
|
|
3605
3685
|
} catch (err) {
|
|
3606
|
-
|
|
3686
|
+
const finalError = this.takeTurnFailure(sessionId) ?? err;
|
|
3687
|
+
const detail = this.formatBridgeError(finalError);
|
|
3688
|
+
this.logger.error(`${this.provider} execute failed`, {
|
|
3689
|
+
...this.turnLogContext(turn),
|
|
3690
|
+
error: detail,
|
|
3691
|
+
stderrTail: bridge.getStderrTail().slice(-500)
|
|
3692
|
+
});
|
|
3693
|
+
onEvent?.({
|
|
3694
|
+
type: "status",
|
|
3695
|
+
status: "failed",
|
|
3696
|
+
detail
|
|
3697
|
+
});
|
|
3698
|
+
this.evictAcpSession(sessionId, "execute-error");
|
|
3699
|
+
const error = finalError instanceof JsonRpcError && detail !== finalError.message ? new Error(detail, { cause: finalError }) : finalError instanceof Error ? finalError : new Error(detail);
|
|
3700
|
+
throw Object.assign(error, { reason: error.reason ?? turnEndReason({
|
|
3701
|
+
aborted: input.abortSignal?.aborted,
|
|
3702
|
+
timedOut: finalError instanceof RpcRequestTimeoutError,
|
|
3703
|
+
linkLost: finalError instanceof BridgeClosedError || HARNESS_LINK_LOST.test(detail)
|
|
3704
|
+
}) });
|
|
3607
3705
|
} finally {
|
|
3608
3706
|
this.scheduleIdleReap(sessionId, config.bridgeIdleMs);
|
|
3609
3707
|
}
|
|
@@ -3611,23 +3709,9 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
|
|
|
3611
3709
|
endTurn();
|
|
3612
3710
|
}
|
|
3613
3711
|
}
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
const detail = this.formatBridgeError(finalError);
|
|
3618
|
-
this.logger.error(`${this.provider} execute failed`, {
|
|
3619
|
-
...this.turnLogContext(turn),
|
|
3620
|
-
error: detail,
|
|
3621
|
-
stderrTail: bridge.getStderrTail().slice(-500)
|
|
3622
|
-
});
|
|
3623
|
-
onEvent?.({
|
|
3624
|
-
type: "status",
|
|
3625
|
-
status: "failed",
|
|
3626
|
-
detail
|
|
3627
|
-
});
|
|
3628
|
-
this.evictAcpSession(sessionId, "execute-error");
|
|
3629
|
-
if (finalError instanceof JsonRpcError && detail !== finalError.message) throw new Error(detail, { cause: finalError });
|
|
3630
|
-
throw finalError instanceof Error ? finalError : new Error(detail);
|
|
3712
|
+
interruptStuckTurn() {}
|
|
3713
|
+
async acknowledgeLostTurn(_detail) {
|
|
3714
|
+
return false;
|
|
3631
3715
|
}
|
|
3632
3716
|
async acquireAcpSession(opts) {
|
|
3633
3717
|
const { sessionId, turn, workspaceRoot, onEvent } = opts;
|
|
@@ -3640,6 +3724,7 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
|
|
|
3640
3724
|
this.logger.warn(`${this.provider}: cached session was started for another process configuration — respawning`, { sessionId });
|
|
3641
3725
|
this.evictAcpSession(sessionId, "process-change");
|
|
3642
3726
|
}
|
|
3727
|
+
const storedSessionId = opts.providerSessionId ? this.resumeSessionId() : void 0;
|
|
3643
3728
|
const spec = await this.processSpec(turn, {
|
|
3644
3729
|
sessionId,
|
|
3645
3730
|
workspaceRoot
|
|
@@ -3684,9 +3769,11 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
|
|
|
3684
3769
|
this.checkProtocolVersion(init, requested, turn);
|
|
3685
3770
|
acpSessionId = await this.openNativeSession(bridge, {
|
|
3686
3771
|
providerSessionId: opts.providerSessionId,
|
|
3772
|
+
storedSessionId,
|
|
3687
3773
|
cwd: workspaceRoot,
|
|
3688
3774
|
timeoutMs
|
|
3689
3775
|
});
|
|
3776
|
+
await this.getSessionStore().update(sessionId, { providerSessionId: acpSessionId });
|
|
3690
3777
|
await this.onSessionOpened(bridge, acpSessionId, turn, { timeoutMs });
|
|
3691
3778
|
this.logger.observePhase?.("cli_initialize", performance.now() - initializeStarted);
|
|
3692
3779
|
} catch (err) {
|
|
@@ -3721,31 +3808,35 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
|
|
|
3721
3808
|
if (advertised < 1) throw new Error(`${this.provider}: agent advertises protocolVersion ${advertised} but client requires at least 1. The backing agent binary is too old; upgrade it.`);
|
|
3722
3809
|
this.checkAgentProtocolVersion(advertised, turn);
|
|
3723
3810
|
}
|
|
3724
|
-
/**
|
|
3725
|
-
* Open the native ACP session. A recorded session is loaded when the agent
|
|
3726
|
-
* can; an agent that refuses the load starts a new native session, and the
|
|
3727
|
-
* Tangle transcript stays the record of the earlier turns.
|
|
3728
|
-
*/
|
|
3729
3811
|
async openNativeSession(bridge, opts) {
|
|
3730
|
-
const
|
|
3731
|
-
|
|
3732
|
-
|
|
3812
|
+
const load = async (sessionId) => {
|
|
3813
|
+
try {
|
|
3814
|
+
const loaded = await bridge.request("session/load", {
|
|
3815
|
+
sessionId,
|
|
3816
|
+
cwd: opts.cwd,
|
|
3817
|
+
mcpServers: []
|
|
3818
|
+
}, { timeoutMs: opts.timeoutMs });
|
|
3819
|
+
if (!loaded || typeof loaded !== "object") return void 0;
|
|
3820
|
+
return loaded.sessionId ?? sessionId;
|
|
3821
|
+
} catch (err) {
|
|
3822
|
+
this.logger.debug("session/load failed", { error: err instanceof Error ? err.message : String(err) });
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3733
3825
|
};
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
}, { timeoutMs: opts.timeoutMs }))?.sessionId ?? opts.providerSessionId;
|
|
3739
|
-
} catch (err) {
|
|
3740
|
-
this.logger.debug("session/load failed, creating new", { error: err instanceof Error ? err.message : String(err) });
|
|
3826
|
+
const stored = opts.storedSessionId;
|
|
3827
|
+
if (stored) {
|
|
3828
|
+
const loaded = await load(stored);
|
|
3829
|
+
if (loaded) return loaded;
|
|
3741
3830
|
}
|
|
3742
|
-
|
|
3831
|
+
if (opts.providerSessionId && opts.providerSessionId !== stored) {
|
|
3832
|
+
const loaded = await load(opts.providerSessionId);
|
|
3833
|
+
if (loaded) return loaded;
|
|
3834
|
+
}
|
|
3835
|
+
return (await bridge.request("session/new", {
|
|
3836
|
+
cwd: opts.cwd,
|
|
3837
|
+
mcpServers: []
|
|
3838
|
+
}, { timeoutMs: opts.timeoutMs })).sessionId;
|
|
3743
3839
|
}
|
|
3744
|
-
/**
|
|
3745
|
-
* One `session/prompt` turn. Abort sends `session/cancel` before closing so
|
|
3746
|
-
* the agent can settle the turn; the bridge's own signal handling would
|
|
3747
|
-
* close first and lose the notify.
|
|
3748
|
-
*/
|
|
3749
3840
|
async promptAcpSession(entry, turn) {
|
|
3750
3841
|
const { bridge, acpSessionId } = entry;
|
|
3751
3842
|
const unsubscribe = bridge.onNotification("session/update", (params) => {
|
|
@@ -3805,7 +3896,6 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
|
|
|
3805
3896
|
entry.idleTimer = void 0;
|
|
3806
3897
|
}
|
|
3807
3898
|
}
|
|
3808
|
-
/** Close a session's agent process and drop it. Idempotent. */
|
|
3809
3899
|
evictAcpSession(sessionId, reason) {
|
|
3810
3900
|
const entry = this.acpSessions.get(sessionId);
|
|
3811
3901
|
if (!entry) return;
|
|
@@ -3823,11 +3913,9 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
|
|
|
3823
3913
|
});
|
|
3824
3914
|
});
|
|
3825
3915
|
}
|
|
3826
|
-
/** Evict every cached session whose process key matches. */
|
|
3827
3916
|
evictAcpSessionsWhere(matches, reason) {
|
|
3828
3917
|
for (const [sessionId, entry] of [...this.acpSessions]) if (matches(entry.processKey)) this.evictAcpSession(sessionId, reason);
|
|
3829
3918
|
}
|
|
3830
|
-
/** Translate one `session/update` notification into stream events. */
|
|
3831
3919
|
mapAcpUpdate(params, state, sessionId) {
|
|
3832
3920
|
const update = params.update;
|
|
3833
3921
|
if (!update || typeof update !== "object") return [];
|
|
@@ -3879,12 +3967,6 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
|
|
|
3879
3967
|
if (raw instanceof Error) return raw.message;
|
|
3880
3968
|
return String(raw);
|
|
3881
3969
|
}
|
|
3882
|
-
/**
|
|
3883
|
-
* The error a failed process start surfaces. An agent that exits before its
|
|
3884
|
-
* session opens reports why only on stderr, and an agent's JSON-RPC error
|
|
3885
|
-
* carries its cause in `data`; a bare "process-exit" or "Internal error"
|
|
3886
|
-
* hides both from the caller.
|
|
3887
|
-
*/
|
|
3888
3970
|
sessionSetupError(err, bridge) {
|
|
3889
3971
|
if (err instanceof BridgeClosedError) {
|
|
3890
3972
|
const stderr = bridge.getStderrTail().trim().slice(-500);
|
|
@@ -3910,7 +3992,7 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
|
|
|
3910
3992
|
throw new Error(`${this.provider}: buildExecArgs is not used — ACP turns run over the RPC bridge`);
|
|
3911
3993
|
}
|
|
3912
3994
|
buildExecEnv(_config) {
|
|
3913
|
-
return {
|
|
3995
|
+
return {};
|
|
3914
3996
|
}
|
|
3915
3997
|
createEventParser() {
|
|
3916
3998
|
throw new Error(`${this.provider}: createEventParser is not used — session/update notifications arrive over the RPC bridge`);
|
|
@@ -3959,7 +4041,7 @@ function jsonEventParser(logger, onEvent, spec) {
|
|
|
3959
4041
|
}
|
|
3960
4042
|
/**
|
|
3961
4043
|
* Parser for harnesses that print their reply as plain stdout lines (hermes,
|
|
3962
|
-
* nanoclaw
|
|
4044
|
+
* nanoclaw). Lines classify into provider events; the reply is
|
|
3963
4045
|
* emitted as one text part when stdout closes. A turn that printed errors
|
|
3964
4046
|
* but no reply ends `failed` with the last error instead of an empty
|
|
3965
4047
|
* `completed`.
|
|
@@ -4078,22 +4160,6 @@ var TerminalPlanSupport = class {
|
|
|
4078
4160
|
};
|
|
4079
4161
|
//#endregion
|
|
4080
4162
|
//#region src/interaction-support.ts
|
|
4081
|
-
/**
|
|
4082
|
-
* Per-execution lifecycle wrapper for an MCP-capable harness. Permission and
|
|
4083
|
-
* question tools use the live {@link InteractionBroker}; plan tools commit
|
|
4084
|
-
* through {@link TerminalPlanSupport} and end the provider turn.
|
|
4085
|
-
*
|
|
4086
|
-
* Mirrors the claude-code permission-server pattern, but for the GENERIC
|
|
4087
|
-
* `request_permission` / `ask_user` / `request_plan` tools the agent calls
|
|
4088
|
-
* itself — there is no harness-specific reply contract, so any MCP-capable CLI
|
|
4089
|
-
* (factory-droids, …) composes this with ~10 lines: declare the capability
|
|
4090
|
-
* kinds, start/stop around the spawn, inject the server via the CLI's MCP
|
|
4091
|
-
* mechanism, and forward `respondToInteraction` here.
|
|
4092
|
-
*
|
|
4093
|
-
* Unlike claude-code's `--permission-prompt-tool`, these generic tools fire
|
|
4094
|
-
* only when the model chooses to call them. Permission is therefore advisory,
|
|
4095
|
-
* while plan-capable adapters separately prove the terminal turn boundary.
|
|
4096
|
-
*/
|
|
4097
4163
|
var McpInteractionSupport = class {
|
|
4098
4164
|
/** The shared emit → block → respond → fail-closed round-trip. */
|
|
4099
4165
|
broker = new InteractionBroker$1();
|
|
@@ -4184,182 +4250,129 @@ var McpInteractionSupport = class {
|
|
|
4184
4250
|
}
|
|
4185
4251
|
};
|
|
4186
4252
|
//#endregion
|
|
4187
|
-
//#region src/
|
|
4188
|
-
const
|
|
4189
|
-
const
|
|
4190
|
-
const
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
"
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
const
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
]);
|
|
4215
|
-
const TOKEN_USAGE_COST_KEYS = Object.freeze([
|
|
4216
|
-
"totalCostUsd",
|
|
4217
|
-
"costUsd",
|
|
4218
|
-
"total_cost_usd",
|
|
4219
|
-
"cost_usd",
|
|
4220
|
-
"cost"
|
|
4221
|
-
]);
|
|
4222
|
-
const CACHE_READ_INPUT_KEYS = Object.freeze([
|
|
4223
|
-
"cacheReadInputTokens",
|
|
4224
|
-
"cache_read_input_tokens",
|
|
4225
|
-
"cacheRead",
|
|
4226
|
-
"cache_read",
|
|
4227
|
-
"cached_input_tokens"
|
|
4228
|
-
]);
|
|
4229
|
-
const CACHE_CREATION_INPUT_KEYS = Object.freeze([
|
|
4230
|
-
"cacheCreationInputTokens",
|
|
4231
|
-
"cache_creation_input_tokens",
|
|
4232
|
-
"cacheWrite",
|
|
4233
|
-
"cache_write"
|
|
4234
|
-
]);
|
|
4235
|
-
const NESTED_COST_KEYS = Object.freeze([
|
|
4236
|
-
"total",
|
|
4237
|
-
"totalUsd",
|
|
4238
|
-
"total_usd",
|
|
4239
|
-
"usd"
|
|
4240
|
-
]);
|
|
4241
|
-
function asRecord(raw) {
|
|
4242
|
-
const parsed = UsageRecordSchema.safeParse(raw);
|
|
4243
|
-
return parsed.success ? parsed.data : void 0;
|
|
4244
|
-
}
|
|
4245
|
-
function tokenCount(raw) {
|
|
4246
|
-
const parsed = UsageNumberSchema.safeParse(raw);
|
|
4247
|
-
return parsed.success ? Math.trunc(parsed.data) : void 0;
|
|
4248
|
-
}
|
|
4249
|
-
function firstTokenCount(source, keys) {
|
|
4250
|
-
for (const key of keys) {
|
|
4251
|
-
const count = tokenCount(source[key]);
|
|
4252
|
-
if (count !== void 0) return count;
|
|
4253
|
-
}
|
|
4254
|
-
}
|
|
4255
|
-
function firstUsageCostUsd(source, keys = TOKEN_USAGE_COST_KEYS) {
|
|
4256
|
-
for (const key of keys) {
|
|
4257
|
-
const parsed = UsageNumberSchema.safeParse(source[key]);
|
|
4258
|
-
if (parsed.success) return parsed.data;
|
|
4259
|
-
}
|
|
4260
|
-
}
|
|
4261
|
-
function usageRecord(source) {
|
|
4262
|
-
return asRecord(source.tokenUsage) ?? asRecord(source.usage) ?? source;
|
|
4263
|
-
}
|
|
4264
|
-
function readTokenUsage(source) {
|
|
4265
|
-
const usageSource = usageRecord(source);
|
|
4266
|
-
const inputTokens = firstTokenCount(usageSource, TOKEN_USAGE_INPUT_KEYS);
|
|
4267
|
-
const outputTokens = firstTokenCount(usageSource, TOKEN_USAGE_OUTPUT_KEYS);
|
|
4268
|
-
return inputTokens !== void 0 || outputTokens !== void 0 ? {
|
|
4269
|
-
inputTokens,
|
|
4270
|
-
outputTokens
|
|
4271
|
-
} : void 0;
|
|
4272
|
-
}
|
|
4273
|
-
function readTokenCostUsd(source) {
|
|
4274
|
-
const usageSource = usageRecord(source);
|
|
4275
|
-
return firstUsageCostUsd(source) ?? firstUsageCostUsd(usageSource) ?? readNestedCostUsd(source) ?? readNestedCostUsd(usageSource);
|
|
4276
|
-
}
|
|
4277
|
-
function readNestedCostUsd(source) {
|
|
4278
|
-
const cost = asRecord(source.cost);
|
|
4279
|
-
return cost ? firstUsageCostUsd(cost, NESTED_COST_KEYS) : void 0;
|
|
4280
|
-
}
|
|
4281
|
-
function readCliTokenUsage(raw) {
|
|
4282
|
-
const parsed = UsageRecordSchema.safeParse(raw);
|
|
4283
|
-
if (!parsed.success) return void 0;
|
|
4284
|
-
const source = parsed.data;
|
|
4285
|
-
const usageSource = usageRecord(source);
|
|
4286
|
-
const usage = readTokenUsage(source) ?? readTokenUsage(usageSource);
|
|
4287
|
-
const cost = readTokenCostUsd(source);
|
|
4288
|
-
const totalTokens = firstTokenCount(usageSource, TOKEN_USAGE_TOTAL_KEYS);
|
|
4289
|
-
const reasoningTokens = firstTokenCount(usageSource, TOKEN_USAGE_REASONING_KEYS);
|
|
4290
|
-
const cacheReadInputTokens = firstTokenCount(usageSource, CACHE_READ_INPUT_KEYS);
|
|
4291
|
-
const cacheCreationInputTokens = firstTokenCount(usageSource, CACHE_CREATION_INPUT_KEYS);
|
|
4292
|
-
if (!usage && totalTokens === void 0 && reasoningTokens === void 0 && cost === void 0 && cacheReadInputTokens === void 0 && cacheCreationInputTokens === void 0) return;
|
|
4293
|
-
return {
|
|
4294
|
-
inputTokens: usage?.inputTokens,
|
|
4295
|
-
outputTokens: usage?.outputTokens,
|
|
4296
|
-
totalTokens,
|
|
4297
|
-
reasoningTokens,
|
|
4298
|
-
cacheReadInputTokens,
|
|
4299
|
-
cacheCreationInputTokens,
|
|
4300
|
-
cost
|
|
4301
|
-
};
|
|
4253
|
+
//#region src/secret-native-proof.ts
|
|
4254
|
+
const proofKey = Symbol.for("tangle.secret-native-binary-proofs");
|
|
4255
|
+
const runtime$1 = globalThis;
|
|
4256
|
+
const proven = runtime$1[proofKey] ?? /* @__PURE__ */ new Set();
|
|
4257
|
+
runtime$1[proofKey] = proven;
|
|
4258
|
+
/** Prove an immutable executable, under the launch's own identity and environment. */
|
|
4259
|
+
async function provenStoreBinary(expected, config) {
|
|
4260
|
+
if (!config.isolatedIdentity || config.inheritEnv !== false) refuseSecretLaunch("Secret native binary proof requires an isolated identity and environment", { path: config.command });
|
|
4261
|
+
const command = resolveExecutableRealpath(config.command, config.env?.PATH);
|
|
4262
|
+
if (!command || !isTrustedStoreBinary(command)) refuseSecretLaunch("Secret native launch requires a root-owned Nix store binary", { path: command ?? config.command });
|
|
4263
|
+
const key = `${command}\0${expected}`;
|
|
4264
|
+
if (proven.has(key)) return command;
|
|
4265
|
+
const probe = await execCli({
|
|
4266
|
+
command,
|
|
4267
|
+
cwd: config.cwd,
|
|
4268
|
+
env: config.env,
|
|
4269
|
+
inheritEnv: config.inheritEnv,
|
|
4270
|
+
signal: config.signal,
|
|
4271
|
+
isolatedIdentity: config.isolatedIdentity,
|
|
4272
|
+
args: ["--version"],
|
|
4273
|
+
spawnOptions: config.isolatedIdentity,
|
|
4274
|
+
timeoutMs: 15e3
|
|
4275
|
+
});
|
|
4276
|
+
const version = probe.stdout?.trim();
|
|
4277
|
+
if (!probe.exit.success || version !== expected) refuseSecretLaunch(`Secret native launch expected ${expected} at ${command}, received no matching version`, { exitCode: probe.exit.code });
|
|
4278
|
+
proven.add(key);
|
|
4279
|
+
return command;
|
|
4302
4280
|
}
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4281
|
+
/** A reported version alone cannot establish an immutable executable. */
|
|
4282
|
+
function isTrustedStoreBinary(realpath) {
|
|
4283
|
+
if (!realpath.startsWith("/nix/store/")) return false;
|
|
4284
|
+
try {
|
|
4285
|
+
for (const path of [realpath, dirname(realpath)]) {
|
|
4286
|
+
const stat = fs$1.lstatSync(path);
|
|
4287
|
+
if (stat.uid !== 0 || (stat.mode & 18) !== 0) return false;
|
|
4309
4288
|
}
|
|
4289
|
+
return true;
|
|
4290
|
+
} catch {
|
|
4291
|
+
return false;
|
|
4310
4292
|
}
|
|
4311
|
-
return false;
|
|
4312
4293
|
}
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
if (
|
|
4316
|
-
const
|
|
4317
|
-
|
|
4294
|
+
/** The real file a launch of `command` executes under `pathEnv`. */
|
|
4295
|
+
function resolveExecutableRealpath(command, pathEnv) {
|
|
4296
|
+
if (command.includes("/") && !isAbsolute(command)) return void 0;
|
|
4297
|
+
const candidates = command.includes("/") ? [command] : (pathEnv ?? "").split(":").filter(isAbsolute).map((dir) => join(dir, command));
|
|
4298
|
+
for (const candidate of candidates) try {
|
|
4299
|
+
fs$1.accessSync(candidate, fs$1.constants.X_OK);
|
|
4300
|
+
if (fs$1.statSync(candidate).isFile()) return fs$1.realpathSync(candidate);
|
|
4301
|
+
} catch {}
|
|
4318
4302
|
}
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4303
|
+
const SEALED_NATIVE_HOME_LAYOUT = {
|
|
4304
|
+
codex: [
|
|
4305
|
+
[
|
|
4306
|
+
".",
|
|
4307
|
+
1016,
|
|
4308
|
+
true
|
|
4309
|
+
],
|
|
4310
|
+
[
|
|
4311
|
+
".codex",
|
|
4312
|
+
1016,
|
|
4313
|
+
true
|
|
4314
|
+
],
|
|
4315
|
+
[
|
|
4316
|
+
".agents",
|
|
4317
|
+
360,
|
|
4318
|
+
true
|
|
4319
|
+
],
|
|
4320
|
+
[
|
|
4321
|
+
".codex/skills",
|
|
4322
|
+
360,
|
|
4323
|
+
true
|
|
4324
|
+
],
|
|
4325
|
+
[
|
|
4326
|
+
".codex/rules",
|
|
4327
|
+
360,
|
|
4328
|
+
true
|
|
4329
|
+
],
|
|
4330
|
+
[
|
|
4331
|
+
".codex/config.toml",
|
|
4332
|
+
288,
|
|
4333
|
+
false
|
|
4334
|
+
],
|
|
4335
|
+
[
|
|
4336
|
+
".codex/AGENTS.md",
|
|
4337
|
+
288,
|
|
4338
|
+
false
|
|
4339
|
+
],
|
|
4340
|
+
[
|
|
4341
|
+
".codex/AGENTS.override.md",
|
|
4342
|
+
288,
|
|
4343
|
+
false
|
|
4344
|
+
],
|
|
4345
|
+
[
|
|
4346
|
+
".codex/hooks.json",
|
|
4347
|
+
288,
|
|
4348
|
+
false
|
|
4349
|
+
]
|
|
4350
|
+
],
|
|
4351
|
+
opencode: [[
|
|
4352
|
+
".",
|
|
4353
|
+
456,
|
|
4354
|
+
true
|
|
4355
|
+
], [
|
|
4356
|
+
".opencode",
|
|
4357
|
+
360,
|
|
4358
|
+
true
|
|
4359
|
+
]]
|
|
4360
|
+
};
|
|
4361
|
+
function assertSealedNativeHome(home, gid, layout) {
|
|
4362
|
+
for (const [relative, mode, directory] of layout) {
|
|
4363
|
+
const path = join(home, relative);
|
|
4364
|
+
const stat = fs$1.lstatSync(path);
|
|
4365
|
+
if (fs$1.realpathSync(path) !== path || stat.uid !== 0 || stat.gid !== gid || (stat.mode & 4095) !== mode || stat.isDirectory() !== directory) refuseSecretLaunch(`Secret native HOME authority is not sealed at ${path}: expected uid0 gid${gid} mode${mode.toString(8)} directory=${directory}; actual uid${stat.uid} gid${stat.gid} mode${(stat.mode & 4095).toString(8)} directory=${stat.isDirectory()}`);
|
|
4344
4366
|
}
|
|
4345
|
-
return false;
|
|
4346
4367
|
}
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
const
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
const separator = part.includes("=") ? "=" : part.includes(":") ? ":" : "";
|
|
4356
|
-
if (!separator) continue;
|
|
4357
|
-
const [key, value] = part.split(separator, 2);
|
|
4358
|
-
if (!key || value === void 0) continue;
|
|
4359
|
-
const numeric = Number(value);
|
|
4360
|
-
fields[key] = Number.isFinite(numeric) ? numeric : value;
|
|
4361
|
-
}
|
|
4362
|
-
return Object.keys(fields).length > 0 ? fields : void 0;
|
|
4368
|
+
/** Only selected structural facts enter a refusal; payloads and causes never do. */
|
|
4369
|
+
function refuseSecretLaunch(reason, { path, expected, actual, exitCode } = {}) {
|
|
4370
|
+
const detail = JSON.stringify({
|
|
4371
|
+
path,
|
|
4372
|
+
expected,
|
|
4373
|
+
actual
|
|
4374
|
+
}).slice(0, 768);
|
|
4375
|
+
throw new Error(detail === "{}" ? reason : `${reason} ${detail}`, { cause: typeof exitCode === "number" || exitCode === null ? { exitCode } : void 0 });
|
|
4363
4376
|
}
|
|
4364
4377
|
//#endregion
|
|
4365
4378
|
//#region src/reasoning-effort.ts
|
|
@@ -4389,6 +4402,160 @@ function kimiThinkingArgs(level, modelId) {
|
|
|
4389
4402
|
return off ? ["--no-thinking"] : ["--thinking"];
|
|
4390
4403
|
}
|
|
4391
4404
|
//#endregion
|
|
4392
|
-
|
|
4405
|
+
//#region src/secret-profile-mcp.ts
|
|
4406
|
+
/** Executor-held MCP credentials shared by native private-profile adapters. */
|
|
4407
|
+
const SECRET_REFERENCE_PREFIX = "TANGLE_PROFILE_SECRET_";
|
|
4408
|
+
const SECRET_REFERENCE = /\{env:(TANGLE_PROFILE_SECRET_[0-9A-F]{24})\}/g;
|
|
4409
|
+
function requireRecord(raw, message) {
|
|
4410
|
+
return isRecord(raw) ? raw : refuseSecretLaunch(message);
|
|
4411
|
+
}
|
|
4412
|
+
/** A config value with its secret references resolved, or undefined. */
|
|
4413
|
+
function resolveReferences(value, references) {
|
|
4414
|
+
let unresolved = false;
|
|
4415
|
+
const resolved = value.replace(SECRET_REFERENCE, (_match, reference) => {
|
|
4416
|
+
const secret = references[reference];
|
|
4417
|
+
if (secret === void 0) unresolved = true;
|
|
4418
|
+
return secret ?? "";
|
|
4419
|
+
});
|
|
4420
|
+
return unresolved || /\{(?:env|file):/.test(resolved) ? void 0 : resolved;
|
|
4421
|
+
}
|
|
4422
|
+
/** A stdio server the executor can run: its command and its environment with every secret reference resolved. */
|
|
4423
|
+
function servableStdioServer(name, entry, references) {
|
|
4424
|
+
const command = entry.command;
|
|
4425
|
+
if (!Array.isArray(command) || command.length === 0 || command.some((part) => typeof part !== "string" || /\{(?:env|file):/.test(part))) return "its command is not a fixed argument list";
|
|
4426
|
+
if (entry.cwd !== void 0) return "it names a working directory";
|
|
4427
|
+
const declared = entry.environment === void 0 ? {} : entry.environment;
|
|
4428
|
+
if (!isRecord(declared)) return "its environment is not a map";
|
|
4429
|
+
const environment = {};
|
|
4430
|
+
const secrets = /* @__PURE__ */ new Set();
|
|
4431
|
+
for (const [variable, value] of Object.entries(declared)) {
|
|
4432
|
+
if (typeof value !== "string") return `its ${variable} is not text`;
|
|
4433
|
+
const resolved = resolveReferences(value, references);
|
|
4434
|
+
if (resolved === void 0) return `its ${variable} names a value the executor cannot resolve`;
|
|
4435
|
+
environment[variable] = resolved;
|
|
4436
|
+
for (const [, reference] of value.matchAll(SECRET_REFERENCE)) {
|
|
4437
|
+
const secret = references[reference];
|
|
4438
|
+
if (secret) secrets.add(secret);
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
return {
|
|
4442
|
+
name,
|
|
4443
|
+
command,
|
|
4444
|
+
environment,
|
|
4445
|
+
secrets: [...secrets]
|
|
4446
|
+
};
|
|
4447
|
+
}
|
|
4448
|
+
/** A remote server the executor can serve: its fixed upstream URL and every header with its secret references resolved. */
|
|
4449
|
+
function servableMcpServer(name, entry, references) {
|
|
4450
|
+
const url = entry.url;
|
|
4451
|
+
if (typeof url !== "string" || /\{(?:env|file):/.test(url)) return "its URL is not a fixed address";
|
|
4452
|
+
try {
|
|
4453
|
+
const parsed = new URL(url);
|
|
4454
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return "its URL is not http(s)";
|
|
4455
|
+
} catch {
|
|
4456
|
+
return "its URL is not a fixed address";
|
|
4457
|
+
}
|
|
4458
|
+
const headers = {};
|
|
4459
|
+
const declared = entry.headers === void 0 ? {} : entry.headers;
|
|
4460
|
+
if (!isRecord(declared)) return "its headers are not a map";
|
|
4461
|
+
for (const [header, value] of Object.entries(declared)) {
|
|
4462
|
+
if (typeof value !== "string") return `its ${header} header is not text`;
|
|
4463
|
+
const resolved = resolveReferences(value, references);
|
|
4464
|
+
if (resolved === void 0) return `its ${header} header names a value the executor cannot resolve`;
|
|
4465
|
+
headers[header] = resolved;
|
|
4466
|
+
}
|
|
4467
|
+
return {
|
|
4468
|
+
name,
|
|
4469
|
+
url,
|
|
4470
|
+
headers
|
|
4471
|
+
};
|
|
4472
|
+
}
|
|
4473
|
+
/** Join the queue at `key`; `previous` settles when earlier holders release. */
|
|
4474
|
+
function joinQueue(queues, key) {
|
|
4475
|
+
const previous = queues.get(key) ?? Promise.resolve();
|
|
4476
|
+
let release;
|
|
4477
|
+
const finished = new Promise((resolve) => {
|
|
4478
|
+
release = resolve;
|
|
4479
|
+
});
|
|
4480
|
+
const tail = previous.then(() => finished);
|
|
4481
|
+
queues.set(key, tail);
|
|
4482
|
+
tail.then(() => {
|
|
4483
|
+
if (queues.get(key) === tail) queues.delete(key);
|
|
4484
|
+
});
|
|
4485
|
+
return {
|
|
4486
|
+
previous,
|
|
4487
|
+
release
|
|
4488
|
+
};
|
|
4489
|
+
}
|
|
4490
|
+
function untilSettledOrAborted(previous, signal) {
|
|
4491
|
+
const aborted = () => signal.reason ?? /* @__PURE__ */ new Error("Secret native turn aborted before its session view was free");
|
|
4492
|
+
if (signal.aborted) return Promise.reject(aborted());
|
|
4493
|
+
return new Promise((resolve, reject) => {
|
|
4494
|
+
const onAbort = () => reject(aborted());
|
|
4495
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4496
|
+
previous.then(() => {
|
|
4497
|
+
signal.removeEventListener("abort", onAbort);
|
|
4498
|
+
resolve();
|
|
4499
|
+
});
|
|
4500
|
+
});
|
|
4501
|
+
}
|
|
4502
|
+
/** Every native discovery ancestor is sealed and free of ambient configuration. */
|
|
4503
|
+
function assertSealedNativeSearchPath(cwd, configNames) {
|
|
4504
|
+
for (let child = cwd, parent = dirname(cwd);; child = parent, parent = dirname(parent)) {
|
|
4505
|
+
const stat = lstatSync(child);
|
|
4506
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(child) !== child || stat.uid !== 0 || (stat.mode & 18) !== 0 || child !== cwd && configNames.some((name) => lstatSync(join(child, name), { throwIfNoEntry: false }))) throw new Error("Secret native launch has mutable or ambient discovery authority");
|
|
4507
|
+
if (parent === child) break;
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
/** One session view changes only after its previous physical native process exits. */
|
|
4511
|
+
const turnsKey = Symbol.for("tangle.secret-native-session-turns");
|
|
4512
|
+
const turnsRuntime = globalThis;
|
|
4513
|
+
const sessionTurns = turnsRuntime[turnsKey] ?? /* @__PURE__ */ new Map();
|
|
4514
|
+
turnsRuntime[turnsKey] = sessionTurns;
|
|
4515
|
+
function secretSessionTurnQueue(view) {
|
|
4516
|
+
return joinQueue(sessionTurns, view);
|
|
4517
|
+
}
|
|
4518
|
+
/** An HTTP URL on loopback with an explicit port and no URL credentials. */
|
|
4519
|
+
function isLoopbackEndpoint(value) {
|
|
4520
|
+
if (typeof value !== "string") return false;
|
|
4521
|
+
try {
|
|
4522
|
+
const url = new URL(value);
|
|
4523
|
+
return url.protocol === "http:" && [
|
|
4524
|
+
"127.0.0.1",
|
|
4525
|
+
"[::1]",
|
|
4526
|
+
"localhost"
|
|
4527
|
+
].includes(url.hostname) && url.port !== "" && !url.username && !url.password;
|
|
4528
|
+
} catch {
|
|
4529
|
+
return false;
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
//#endregion
|
|
4533
|
+
//#region src/secret-profile-scope.ts
|
|
4534
|
+
const key = Symbol.for("tangle.secret-profile-mcp.scopes");
|
|
4535
|
+
const runtime = globalThis;
|
|
4536
|
+
const openScopes = runtime[key] ??= /* @__PURE__ */ new Map();
|
|
4537
|
+
function secretMcpScope(sessionId, uid, owner) {
|
|
4538
|
+
if (sessionId.includes("\0")) throw new Error("A secret MCP session id has no NUL");
|
|
4539
|
+
const scope = `${sessionId}\0${uid ?? ""}`;
|
|
4540
|
+
if (owner?.proxy) {
|
|
4541
|
+
const owners = openScopes.get(scope) ?? /* @__PURE__ */ new Set();
|
|
4542
|
+
owners.add(owner);
|
|
4543
|
+
openScopes.set(scope, owners);
|
|
4544
|
+
}
|
|
4545
|
+
return scope;
|
|
4546
|
+
}
|
|
4547
|
+
async function forgetSessionScopes(target) {
|
|
4548
|
+
for (const [scope, owners] of openScopes) {
|
|
4549
|
+
if ("sessionId" in target && !scope.startsWith(`${target.sessionId}\0`)) continue;
|
|
4550
|
+
for (const owner of [...owners]) {
|
|
4551
|
+
if ("owner" in target && owner !== target.owner) continue;
|
|
4552
|
+
if (![...owners].some((other) => other !== owner && other.proxy === owner.proxy)) await owner.proxy.forget(scope);
|
|
4553
|
+
owners.delete(owner);
|
|
4554
|
+
}
|
|
4555
|
+
if (owners.size === 0) openScopes.delete(scope);
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
//#endregion
|
|
4559
|
+
export { ACP_MIN_PROTOCOL_VERSION, ACP_PROTOCOL_VERSION, AcpProviderAdapterBase, CODEX_DISABLED_FEATURE_ARGS, CliProviderAdapterBase, DEFAULT_INTERACTION_MCP_SERVER_NAME, EXECUTION_TRACE_HEADER, InteractionBroker, InteractionHttpBridge, InteractionMcpServer, InvalidAttachmentError, MANAGED_QUOTE_VERSION_HEADER, MAX_INLINE_IMAGE_BYTES, MAX_TOTAL_INLINE_IMAGE_BYTES, McpInteractionSupport, NATIVE_SESSION_CONTRACTS, PERMISSION_DECISION_TIMEOUT_MS, PROMPT_SECTION_SEPARATOR, SEALED_NATIVE_HOME_LAYOUT, SECRET_REFERENCE, SECRET_REFERENCE_PREFIX, SessionRehydrationError, TerminalPlanSupport, accumulateCliTokenUsage, acpContentText, assertSealedNativeHome, assertSealedNativeSearchPath, brokerInteractionTools, buildFileUrl, codexDeveloperInstructionArgs, createExecutionState, createRepeatedToolFailureGuard, deriveFilePart, deterministicFilePartId, enabledInteractionKinds, executionCorrelationHeaders, finalTextEvent, forgetSessionScopes, homeXdgEnv, inferMediaType, interactionMcpToolName, interactionToggleEnabled, isLocalMcpConfig, isLoopbackEndpoint, isPlainRecord, isTrustedStoreBinary, joinQueue, jsonEventParser, kimiThinkingArgs, localMcpArgv, localMcpEnv, nativeEffortArgs, parseCliUsageLine, partitionImageAttachments, plaintextResponseParser, preparePromptInputParts, prependPromptSection, provenStoreBinary, readCliTokenUsage, readEphemeralContext, reasoningEvents, recordCliTokenUsage, refuseSecretLaunch, rejectNotLaunched, requireRecord, resolveExecutableRealpath, resolveReferences, runtimeSecretsOverlay, secretMcpScope, secretSessionTurnQueue, servableMcpServer, servableStdioServer, sessionObservedEvents, spawnAcpBridge, streamPartBase, textDeltaEvents, textSnapshotEvents, toolResultEvents, toolStartEvents, trackFilePart, trackReasoningBlock, turnEndReason, turnInteractionEnabled, untilSettledOrAborted, wasNeverLaunched, withEphemeralContext, withTurnInteractions };
|
|
4393
4560
|
|
|
4394
4561
|
//# sourceMappingURL=index.mjs.map
|