@ash-cloud/ash-ai 0.1.18 → 0.1.20
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.cjs +488 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1453 -1277
- package/dist/index.d.ts +1453 -1277
- package/dist/index.js +488 -80
- package/dist/index.js.map +1 -1
- package/dist/playground/components/NormalizedMessageList.d.ts +7 -1
- package/dist/playground/components/NormalizedMessageList.d.ts.map +1 -1
- package/dist/playground/contexts/ThemeContext.d.ts +12 -8
- package/dist/playground/contexts/ThemeContext.d.ts.map +1 -1
- package/dist/playground/index.d.ts +1 -1
- package/dist/playground/index.d.ts.map +1 -1
- package/dist/playground.js +1477 -1382
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -913,6 +913,32 @@ var init_mcp = __esm({
|
|
|
913
913
|
});
|
|
914
914
|
|
|
915
915
|
// src/agent/claude-sdk.ts
|
|
916
|
+
function isStandardMcpServerConfig(config) {
|
|
917
|
+
if (!config || typeof config !== "object") return false;
|
|
918
|
+
const candidate = config;
|
|
919
|
+
if (typeof candidate.command === "string") return true;
|
|
920
|
+
if (typeof candidate.url === "string") return true;
|
|
921
|
+
if (candidate.type === "stdio" || candidate.type === "http" || candidate.type === "sse") return true;
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
function hasCustomMcpServers(mcpServers) {
|
|
925
|
+
if (!mcpServers) return false;
|
|
926
|
+
return Object.values(mcpServers).some((config) => !isStandardMcpServerConfig(config));
|
|
927
|
+
}
|
|
928
|
+
async function* toStreamingPrompt(prompt) {
|
|
929
|
+
yield {
|
|
930
|
+
type: "user",
|
|
931
|
+
message: {
|
|
932
|
+
role: "user",
|
|
933
|
+
content: prompt
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function normalizePromptForSdk(prompt, mcpServers) {
|
|
938
|
+
if (typeof prompt !== "string") return prompt;
|
|
939
|
+
if (!hasCustomMcpServers(mcpServers)) return prompt;
|
|
940
|
+
return toStreamingPrompt(prompt);
|
|
941
|
+
}
|
|
916
942
|
function convertClaudeMessage(claudeMessage, sessionId) {
|
|
917
943
|
if (claudeMessage.type !== "assistant" || !claudeMessage.message) {
|
|
918
944
|
return null;
|
|
@@ -956,16 +982,17 @@ function normalizeMcpServers(mcpServers) {
|
|
|
956
982
|
if (!mcpServers) return mcpServers;
|
|
957
983
|
return Object.fromEntries(
|
|
958
984
|
Object.entries(mcpServers).map(([name, config]) => {
|
|
959
|
-
|
|
985
|
+
const configWithAuth = config;
|
|
986
|
+
if (!configWithAuth.auth) {
|
|
960
987
|
return [name, config];
|
|
961
988
|
}
|
|
962
|
-
const authHeaders = mcpAuthToHeaders(
|
|
963
|
-
const { auth: _auth, ...rest } =
|
|
989
|
+
const authHeaders = mcpAuthToHeaders(configWithAuth.auth);
|
|
990
|
+
const { auth: _auth, ...rest } = configWithAuth;
|
|
964
991
|
return [name, {
|
|
965
992
|
...rest,
|
|
966
993
|
headers: {
|
|
967
994
|
...authHeaders,
|
|
968
|
-
...
|
|
995
|
+
...configWithAuth.headers
|
|
969
996
|
// Explicit headers take precedence
|
|
970
997
|
}
|
|
971
998
|
}];
|
|
@@ -992,8 +1019,22 @@ var init_claude_sdk = __esm({
|
|
|
992
1019
|
async *query(prompt, options = {}) {
|
|
993
1020
|
const model = options.model ?? this.defaultModel;
|
|
994
1021
|
if (await this.checkSdkAvailable()) {
|
|
995
|
-
|
|
1022
|
+
const normalizedOptions = { ...options, model };
|
|
1023
|
+
if (normalizedOptions.agents) {
|
|
1024
|
+
const allowedTools = normalizedOptions.allowedTools ?? [];
|
|
1025
|
+
if (!allowedTools.includes("Task")) {
|
|
1026
|
+
normalizedOptions.allowedTools = [...allowedTools, "Task"];
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
yield* this.executeRealQuery(prompt, normalizedOptions);
|
|
996
1030
|
} else {
|
|
1031
|
+
if (typeof prompt !== "string") {
|
|
1032
|
+
yield {
|
|
1033
|
+
type: "error",
|
|
1034
|
+
error: "Streaming prompts require the real Claude Agent SDK to be installed."
|
|
1035
|
+
};
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
997
1038
|
yield* this.executeSimulatedQuery(prompt, { ...options, model });
|
|
998
1039
|
}
|
|
999
1040
|
}
|
|
@@ -1007,8 +1048,9 @@ var init_claude_sdk = __esm({
|
|
|
1007
1048
|
}
|
|
1008
1049
|
try {
|
|
1009
1050
|
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
1051
|
+
const normalizedMcpServers = normalizeMcpServers(options.mcpServers);
|
|
1010
1052
|
const queryOptions = {
|
|
1011
|
-
prompt,
|
|
1053
|
+
prompt: normalizePromptForSdk(prompt, options.mcpServers),
|
|
1012
1054
|
options: {
|
|
1013
1055
|
model: options.model,
|
|
1014
1056
|
allowedTools: options.allowedTools,
|
|
@@ -1017,10 +1059,12 @@ var init_claude_sdk = __esm({
|
|
|
1017
1059
|
maxTurns: options.maxTurns,
|
|
1018
1060
|
resume: options.resume,
|
|
1019
1061
|
forkSession: options.forkSession,
|
|
1020
|
-
mcpServers:
|
|
1062
|
+
mcpServers: normalizedMcpServers,
|
|
1021
1063
|
agents: options.agents,
|
|
1022
1064
|
hooks: options.hooks,
|
|
1065
|
+
plugins: options.plugins,
|
|
1023
1066
|
settingSources: options.settingSources,
|
|
1067
|
+
outputFormat: options.outputFormat,
|
|
1024
1068
|
// Enable streaming partial messages for real-time text deltas
|
|
1025
1069
|
includePartialMessages: true,
|
|
1026
1070
|
// Environment variables for the SDK
|
|
@@ -1043,7 +1087,8 @@ var init_claude_sdk = __esm({
|
|
|
1043
1087
|
if (message.type === "system" && message.subtype === "init") {
|
|
1044
1088
|
yield {
|
|
1045
1089
|
type: "session_init",
|
|
1046
|
-
sessionId: message.session_id
|
|
1090
|
+
sessionId: message.session_id,
|
|
1091
|
+
slashCommands: message.slash_commands
|
|
1047
1092
|
};
|
|
1048
1093
|
continue;
|
|
1049
1094
|
}
|
|
@@ -1094,12 +1139,17 @@ var init_claude_sdk = __esm({
|
|
|
1094
1139
|
}
|
|
1095
1140
|
}
|
|
1096
1141
|
if (message.type === "result") {
|
|
1142
|
+
const usage = message.usage;
|
|
1143
|
+
const hasUsageTokens = !!usage && (usage.input_tokens !== void 0 || usage.output_tokens !== void 0);
|
|
1144
|
+
const totalTokens = hasUsageTokens ? (usage?.input_tokens ?? 0) + (usage?.output_tokens ?? 0) : message.tokens ? message.tokens.input + message.tokens.output : void 0;
|
|
1145
|
+
const totalCost = usage?.total_cost_usd ?? message.cost ?? message.total_cost_usd;
|
|
1097
1146
|
yield {
|
|
1098
1147
|
type: "complete",
|
|
1099
1148
|
sessionId: message.session_id,
|
|
1100
1149
|
result: message.result,
|
|
1101
|
-
|
|
1102
|
-
|
|
1150
|
+
structured_output: message.structured_output,
|
|
1151
|
+
totalCost,
|
|
1152
|
+
totalTokens
|
|
1103
1153
|
};
|
|
1104
1154
|
}
|
|
1105
1155
|
}
|
|
@@ -1107,6 +1157,13 @@ var init_claude_sdk = __esm({
|
|
|
1107
1157
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1108
1158
|
if (errorMessage.includes("Cannot find module") || errorMessage.includes("MODULE_NOT_FOUND")) {
|
|
1109
1159
|
console.warn("Claude Agent SDK not installed, using simulation mode");
|
|
1160
|
+
if (typeof prompt !== "string") {
|
|
1161
|
+
yield {
|
|
1162
|
+
type: "error",
|
|
1163
|
+
error: "Streaming prompts require the real Claude Agent SDK to be installed."
|
|
1164
|
+
};
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1110
1167
|
yield* this.executeSimulatedQuery(prompt, options);
|
|
1111
1168
|
} else {
|
|
1112
1169
|
yield {
|
|
@@ -1284,7 +1341,11 @@ var init_claude_sdk = __esm({
|
|
|
1284
1341
|
this.sessionId = msg.session_id;
|
|
1285
1342
|
}
|
|
1286
1343
|
if (msg.type === "system" && msg.subtype === "init") {
|
|
1287
|
-
yield {
|
|
1344
|
+
yield {
|
|
1345
|
+
type: "session_init",
|
|
1346
|
+
sessionId: msg.session_id,
|
|
1347
|
+
slashCommands: msg.slash_commands
|
|
1348
|
+
};
|
|
1288
1349
|
} else if (msg.type === "assistant" && msg.message) {
|
|
1289
1350
|
for (const block of msg.message.content) {
|
|
1290
1351
|
if (block.type === "text") {
|
|
@@ -1712,6 +1773,13 @@ function createGeminiBackendExecutor(options) {
|
|
|
1712
1773
|
defaultModel: options.model ?? exports.DEFAULT_MODELS.gemini
|
|
1713
1774
|
});
|
|
1714
1775
|
return async function* (prompt, queryOptions) {
|
|
1776
|
+
if (typeof prompt !== "string") {
|
|
1777
|
+
yield {
|
|
1778
|
+
type: "error",
|
|
1779
|
+
error: "Gemini backend does not support streaming prompt inputs."
|
|
1780
|
+
};
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1715
1783
|
const geminiOptions = mapClaudeOptionsToGemini(queryOptions);
|
|
1716
1784
|
if (queryOptions.signal) {
|
|
1717
1785
|
geminiOptions.signal = queryOptions.signal;
|
|
@@ -1876,6 +1944,11 @@ var init_sandbox_logger = __esm({
|
|
|
1876
1944
|
};
|
|
1877
1945
|
}
|
|
1878
1946
|
});
|
|
1947
|
+
function getClaudeSdkOverrides(config) {
|
|
1948
|
+
const raw = config?.claudeSdkOptions;
|
|
1949
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1950
|
+
return raw;
|
|
1951
|
+
}
|
|
1879
1952
|
exports.AgentHarness = void 0;
|
|
1880
1953
|
var init_harness = __esm({
|
|
1881
1954
|
"src/agent/harness.ts"() {
|
|
@@ -2132,6 +2205,7 @@ var init_harness = __esm({
|
|
|
2132
2205
|
yield sessionStartEvent;
|
|
2133
2206
|
yield* yieldQueuedLogs();
|
|
2134
2207
|
const assistantContent = [];
|
|
2208
|
+
let structuredOutput;
|
|
2135
2209
|
let wasAborted = false;
|
|
2136
2210
|
try {
|
|
2137
2211
|
logger3.info("execution", "Starting Claude Agent SDK query");
|
|
@@ -2222,6 +2296,10 @@ var init_harness = __esm({
|
|
|
2222
2296
|
};
|
|
2223
2297
|
writeEvent?.(toolResultEvent);
|
|
2224
2298
|
yield toolResultEvent;
|
|
2299
|
+
} else if (event.type === "complete") {
|
|
2300
|
+
if (event.structured_output !== void 0) {
|
|
2301
|
+
structuredOutput = event.structured_output;
|
|
2302
|
+
}
|
|
2225
2303
|
}
|
|
2226
2304
|
}
|
|
2227
2305
|
if (wasAborted || controller.signal.aborted) {
|
|
@@ -2273,7 +2351,8 @@ var init_harness = __esm({
|
|
|
2273
2351
|
[
|
|
2274
2352
|
{
|
|
2275
2353
|
role: "assistant",
|
|
2276
|
-
content: assistantContent
|
|
2354
|
+
content: assistantContent,
|
|
2355
|
+
...structuredOutput !== void 0 ? { metadata: { structured_output: structuredOutput } } : {}
|
|
2277
2356
|
}
|
|
2278
2357
|
]
|
|
2279
2358
|
);
|
|
@@ -2394,11 +2473,15 @@ var init_harness = __esm({
|
|
|
2394
2473
|
async *executeAgentQuery(session, prompt, options, signal, _logger) {
|
|
2395
2474
|
const sessionEnvVars = session.metadata?.envVars;
|
|
2396
2475
|
const sessionStartupScript = session.metadata?.startupScript;
|
|
2476
|
+
const sdkOverrides = getClaudeSdkOverrides(this.config.config);
|
|
2397
2477
|
const mergedEnvVars = {
|
|
2398
2478
|
MAX_THINKING_TOKENS: "1024",
|
|
2399
2479
|
...this.config.envVars,
|
|
2400
2480
|
...sessionEnvVars
|
|
2401
2481
|
};
|
|
2482
|
+
if (sdkOverrides?.enableFileCheckpointing) {
|
|
2483
|
+
mergedEnvVars.CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING = "1";
|
|
2484
|
+
}
|
|
2402
2485
|
const hasEnvVars = Object.keys(mergedEnvVars).length > 0;
|
|
2403
2486
|
const startupScript = sessionStartupScript ?? this.config.startupScript;
|
|
2404
2487
|
const queryOptions = {
|
|
@@ -2407,7 +2490,9 @@ var init_harness = __esm({
|
|
|
2407
2490
|
disallowedTools: this.config.disallowedTools,
|
|
2408
2491
|
permissionMode: this.config.permissionMode,
|
|
2409
2492
|
maxTurns: this.config.maxTurns,
|
|
2493
|
+
outputFormat: options.outputFormat,
|
|
2410
2494
|
mcpServers: this.config.mcpServers,
|
|
2495
|
+
settingSources: this.config.settingSources ?? ["project"],
|
|
2411
2496
|
// Pass the harness session ID for sandbox caching
|
|
2412
2497
|
harnessSessionId: session.id,
|
|
2413
2498
|
// Pass environment and startup configuration
|
|
@@ -2416,6 +2501,17 @@ var init_harness = __esm({
|
|
|
2416
2501
|
// Pass config file URL for cloud-hosted .claude directory (downloaded in sandbox)
|
|
2417
2502
|
...this.config.configFileUrl && { configFileUrl: this.config.configFileUrl }
|
|
2418
2503
|
};
|
|
2504
|
+
if (sdkOverrides) {
|
|
2505
|
+
if (sdkOverrides.hooks) queryOptions.hooks = sdkOverrides.hooks;
|
|
2506
|
+
if (typeof sdkOverrides.enableFileCheckpointing === "boolean") {
|
|
2507
|
+
queryOptions.enableFileCheckpointing = sdkOverrides.enableFileCheckpointing;
|
|
2508
|
+
if (sdkOverrides.enableFileCheckpointing && !sdkOverrides.extraArgs) {
|
|
2509
|
+
queryOptions.extraArgs = { "replay-user-messages": null };
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
if (sdkOverrides.extraArgs) queryOptions.extraArgs = sdkOverrides.extraArgs;
|
|
2513
|
+
if (sdkOverrides.permissionMode) queryOptions.permissionMode = sdkOverrides.permissionMode;
|
|
2514
|
+
}
|
|
2419
2515
|
const sessionApiKey = this.sessionApiKeys.get(session.id);
|
|
2420
2516
|
if (sessionApiKey) {
|
|
2421
2517
|
queryOptions.apiKey = sessionApiKey;
|
|
@@ -2451,7 +2547,9 @@ ${prompt}`;
|
|
|
2451
2547
|
const skillsDir = this.sessionSkillDirs.get(session.id);
|
|
2452
2548
|
if (skillsDir) {
|
|
2453
2549
|
queryOptions.cwd = skillsDir;
|
|
2454
|
-
queryOptions.settingSources
|
|
2550
|
+
if (queryOptions.settingSources === void 0) {
|
|
2551
|
+
queryOptions.settingSources = ["project"];
|
|
2552
|
+
}
|
|
2455
2553
|
if (queryOptions.allowedTools && !queryOptions.allowedTools.includes("Skill")) {
|
|
2456
2554
|
queryOptions.allowedTools = [...queryOptions.allowedTools, "Skill"];
|
|
2457
2555
|
}
|
|
@@ -2480,6 +2578,7 @@ ${prompt}`;
|
|
|
2480
2578
|
await this.sessionManager.updateSession(session.id, {
|
|
2481
2579
|
sdkSessionId: event.sessionId
|
|
2482
2580
|
});
|
|
2581
|
+
yield { type: "session_init", sessionId: event.sessionId, slashCommands: event.slashCommands };
|
|
2483
2582
|
} else if (event.type === "text_delta" && event.delta) {
|
|
2484
2583
|
yield { type: "text_delta", delta: event.delta };
|
|
2485
2584
|
} else if (event.type === "thinking_delta" && event.delta) {
|
|
@@ -2500,6 +2599,11 @@ ${prompt}`;
|
|
|
2500
2599
|
content: event.content,
|
|
2501
2600
|
isError: event.isError
|
|
2502
2601
|
};
|
|
2602
|
+
} else if (event.type === "complete") {
|
|
2603
|
+
yield {
|
|
2604
|
+
type: "complete",
|
|
2605
|
+
structured_output: event.structured_output
|
|
2606
|
+
};
|
|
2503
2607
|
} else if (event.type === "error") {
|
|
2504
2608
|
throw new Error(event.error ?? "Unknown error from Claude SDK");
|
|
2505
2609
|
}
|
|
@@ -4639,6 +4743,14 @@ echo "[warmup] Warmup complete!"
|
|
|
4639
4743
|
lastMaintenanceAt = null;
|
|
4640
4744
|
metricsCallback;
|
|
4641
4745
|
startPromise = null;
|
|
4746
|
+
/** Registered warmup specs by tag (e.g. agentId -> spec) */
|
|
4747
|
+
warmupSpecs = /* @__PURE__ */ new Map();
|
|
4748
|
+
/** Tags currently being warmed (prevents duplicate warming of same spec) */
|
|
4749
|
+
warmingTags = /* @__PURE__ */ new Set();
|
|
4750
|
+
/** Max warmup specs to keep (LRU eviction above this) */
|
|
4751
|
+
static MAX_SPECS = 10;
|
|
4752
|
+
/** Timeout for spec setup in ms (prevents hanging S3 pulls / install.sh) */
|
|
4753
|
+
static SPEC_SETUP_TIMEOUT_MS = 12e4;
|
|
4642
4754
|
/** Consecutive warmup failure count (reset on success) */
|
|
4643
4755
|
consecutiveFailures = 0;
|
|
4644
4756
|
/** Timestamp of last warmup attempt — used for backoff */
|
|
@@ -4651,7 +4763,7 @@ echo "[warmup] Warmup complete!"
|
|
|
4651
4763
|
this.config = {
|
|
4652
4764
|
minPoolSize: config.minPoolSize ?? parseInt(process.env.SANDBOX_POOL_MIN_SIZE ?? "2"),
|
|
4653
4765
|
maxPoolSize: config.maxPoolSize ?? parseInt(process.env.SANDBOX_POOL_MAX_SIZE ?? "5"),
|
|
4654
|
-
sandboxTimeout: config.sandboxTimeout ?? parseInt(process.env.SANDBOX_TIMEOUT ?? "
|
|
4766
|
+
sandboxTimeout: config.sandboxTimeout ?? parseInt(process.env.SANDBOX_TIMEOUT ?? "300"),
|
|
4655
4767
|
expiryThresholdMs: config.expiryThresholdMs ?? parseInt(process.env.SANDBOX_EXPIRY_THRESHOLD_MS ?? "120000"),
|
|
4656
4768
|
maintenanceIntervalMs: config.maintenanceIntervalMs ?? parseInt(process.env.SANDBOX_POOL_MAINTENANCE_MS ?? "30000"),
|
|
4657
4769
|
runtime: config.runtime ?? "node22",
|
|
@@ -4695,11 +4807,13 @@ echo "[warmup] Warmup complete!"
|
|
|
4695
4807
|
if (this.maintenanceIntervalId.unref) {
|
|
4696
4808
|
this.maintenanceIntervalId.unref();
|
|
4697
4809
|
}
|
|
4810
|
+
const initialSpecs = this.selectSpecsForReplenishment(this.config.minPoolSize);
|
|
4698
4811
|
console.log(`[POOL] Spawning ${this.config.minPoolSize} initial sandbox(es)...`);
|
|
4699
4812
|
const warmupPromises = [];
|
|
4700
4813
|
for (let i = 0; i < this.config.minPoolSize; i++) {
|
|
4814
|
+
const spec = initialSpecs[i];
|
|
4701
4815
|
warmupPromises.push(
|
|
4702
|
-
this.warmSandbox().then((sandbox) => {
|
|
4816
|
+
this.warmSandbox(spec).then((sandbox) => {
|
|
4703
4817
|
this.pool.set(sandbox.sandboxId, sandbox);
|
|
4704
4818
|
console.log(`[POOL] Initial sandbox ready: ${sandbox.sandboxId}`);
|
|
4705
4819
|
}).catch((error) => {
|
|
@@ -4743,9 +4857,11 @@ echo "[warmup] Warmup complete!"
|
|
|
4743
4857
|
}
|
|
4744
4858
|
/**
|
|
4745
4859
|
* Acquire a pre-warmed sandbox for a session.
|
|
4860
|
+
* If preferTag is provided, tries to find a sandbox warmed for that tag first.
|
|
4861
|
+
* Falls back to a generic (untagged) sandbox if no tag match is found.
|
|
4746
4862
|
* If no eligible sandbox is available, creates one on-demand.
|
|
4747
4863
|
*/
|
|
4748
|
-
async acquire(sessionId) {
|
|
4864
|
+
async acquire(sessionId, preferTag) {
|
|
4749
4865
|
if (!this.running) {
|
|
4750
4866
|
throw new Error("Sandbox pool is not running");
|
|
4751
4867
|
}
|
|
@@ -4755,26 +4871,34 @@ echo "[warmup] Warmup complete!"
|
|
|
4755
4871
|
return pooled;
|
|
4756
4872
|
}
|
|
4757
4873
|
}
|
|
4758
|
-
const available = this.getAvailableSandbox();
|
|
4874
|
+
const available = this.getAvailableSandbox(preferTag);
|
|
4759
4875
|
if (available) {
|
|
4760
4876
|
available.assignedTo = sessionId;
|
|
4761
|
-
|
|
4877
|
+
const tagInfo = available.warmupTag ? ` [tag=${available.warmupTag}, agentSetupComplete=${available.agentSetupComplete}]` : " [generic]";
|
|
4878
|
+
console.log(`[POOL] Acquired sandbox ${available.sandboxId} for session ${sessionId}${tagInfo}`);
|
|
4762
4879
|
this.emitMetric("sandbox_assigned", {
|
|
4763
4880
|
sandboxId: available.sandboxId,
|
|
4764
4881
|
sessionId,
|
|
4765
|
-
poolAvailable: this.getAvailableCount()
|
|
4882
|
+
poolAvailable: this.getAvailableCount(),
|
|
4883
|
+
warmupTag: available.warmupTag,
|
|
4884
|
+
agentSetupComplete: available.agentSetupComplete,
|
|
4885
|
+
preferTag
|
|
4766
4886
|
});
|
|
4767
4887
|
this.triggerReplenishment();
|
|
4768
4888
|
return available;
|
|
4769
4889
|
}
|
|
4770
|
-
|
|
4771
|
-
|
|
4890
|
+
const spec = preferTag ? this.warmupSpecs.get(preferTag) : void 0;
|
|
4891
|
+
console.log(`[POOL] No available sandbox, creating on-demand for session ${sessionId}${spec ? ` [spec=${preferTag}]` : ""}...`);
|
|
4892
|
+
const sandbox = await this.warmSandbox(spec);
|
|
4772
4893
|
sandbox.assignedTo = sessionId;
|
|
4773
4894
|
this.pool.set(sandbox.sandboxId, sandbox);
|
|
4774
4895
|
this.emitMetric("sandbox_assigned", {
|
|
4775
4896
|
sandboxId: sandbox.sandboxId,
|
|
4776
4897
|
sessionId,
|
|
4777
|
-
onDemand: true
|
|
4898
|
+
onDemand: true,
|
|
4899
|
+
warmupTag: sandbox.warmupTag,
|
|
4900
|
+
agentSetupComplete: sandbox.agentSetupComplete,
|
|
4901
|
+
preferTag
|
|
4778
4902
|
});
|
|
4779
4903
|
return sandbox;
|
|
4780
4904
|
}
|
|
@@ -4805,11 +4929,14 @@ echo "[warmup] Warmup complete!"
|
|
|
4805
4929
|
let available = 0;
|
|
4806
4930
|
let assigned = 0;
|
|
4807
4931
|
let ineligible = 0;
|
|
4932
|
+
const availableByTag = {};
|
|
4808
4933
|
for (const pooled of this.pool.values()) {
|
|
4809
4934
|
if (pooled.assignedTo) {
|
|
4810
4935
|
assigned++;
|
|
4811
4936
|
} else if (pooled.eligible) {
|
|
4812
4937
|
available++;
|
|
4938
|
+
const tagKey = pooled.warmupTag || "generic";
|
|
4939
|
+
availableByTag[tagKey] = (availableByTag[tagKey] || 0) + 1;
|
|
4813
4940
|
} else {
|
|
4814
4941
|
ineligible++;
|
|
4815
4942
|
}
|
|
@@ -4824,6 +4951,8 @@ echo "[warmup] Warmup complete!"
|
|
|
4824
4951
|
lastMaintenanceAt: this.lastMaintenanceAt,
|
|
4825
4952
|
consecutiveFailures: this.consecutiveFailures,
|
|
4826
4953
|
warmupSuspended: this.consecutiveFailures >= _SandboxPool.MAX_CONSECUTIVE_FAILURES,
|
|
4954
|
+
registeredSpecs: this.warmupSpecs.size,
|
|
4955
|
+
availableByTag,
|
|
4827
4956
|
config: {
|
|
4828
4957
|
minPoolSize: this.config.minPoolSize,
|
|
4829
4958
|
maxPoolSize: this.config.maxPoolSize,
|
|
@@ -4838,13 +4967,61 @@ echo "[warmup] Warmup complete!"
|
|
|
4838
4967
|
onMetrics(callback) {
|
|
4839
4968
|
this.metricsCallback = callback;
|
|
4840
4969
|
}
|
|
4970
|
+
/**
|
|
4971
|
+
* Register a warmup spec so the pool can pre-warm agent-specific sandboxes.
|
|
4972
|
+
* If a spec with the same tag and configHash already exists, only updates priority (skip #7).
|
|
4973
|
+
* Evicts lowest-priority specs when exceeding MAX_SPECS (fix #2).
|
|
4974
|
+
* Triggers replenishment to warm a sandbox for this spec.
|
|
4975
|
+
*/
|
|
4976
|
+
registerWarmupSpec(spec) {
|
|
4977
|
+
const existing = this.warmupSpecs.get(spec.tag);
|
|
4978
|
+
if (existing && spec.configHash && existing.configHash === spec.configHash) {
|
|
4979
|
+
existing.priority = spec.priority;
|
|
4980
|
+
return;
|
|
4981
|
+
}
|
|
4982
|
+
const isNew = !existing;
|
|
4983
|
+
this.warmupSpecs.set(spec.tag, spec);
|
|
4984
|
+
if (this.warmupSpecs.size > _SandboxPool.MAX_SPECS) {
|
|
4985
|
+
this.evictLowestPrioritySpecs();
|
|
4986
|
+
}
|
|
4987
|
+
console.log(`[POOL] ${isNew ? "Registered" : "Updated"} warmup spec: ${spec.tag} (priority=${spec.priority}, specs=${this.warmupSpecs.size})`);
|
|
4988
|
+
this.emitMetric("spec_registered", {
|
|
4989
|
+
tag: spec.tag,
|
|
4990
|
+
priority: spec.priority,
|
|
4991
|
+
isNew,
|
|
4992
|
+
totalSpecs: this.warmupSpecs.size,
|
|
4993
|
+
configHash: spec.configHash
|
|
4994
|
+
});
|
|
4995
|
+
if (this.running) {
|
|
4996
|
+
this.triggerReplenishment();
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
/**
|
|
5000
|
+
* Remove a warmup spec. Existing tagged sandboxes remain but won't be replaced.
|
|
5001
|
+
*/
|
|
5002
|
+
unregisterWarmupSpec(tag) {
|
|
5003
|
+
this.warmupSpecs.delete(tag);
|
|
5004
|
+
console.log(`[POOL] Unregistered warmup spec: ${tag}`);
|
|
5005
|
+
}
|
|
5006
|
+
/**
|
|
5007
|
+
* Update the priority of a warmup spec (e.g. for MRU tracking).
|
|
5008
|
+
* Higher priority = more likely to get a warm sandbox during replenishment.
|
|
5009
|
+
*/
|
|
5010
|
+
updateSpecPriority(tag, priority) {
|
|
5011
|
+
const spec = this.warmupSpecs.get(tag);
|
|
5012
|
+
if (spec) {
|
|
5013
|
+
spec.priority = priority;
|
|
5014
|
+
}
|
|
5015
|
+
}
|
|
4841
5016
|
// ===========================================================================
|
|
4842
5017
|
// PRIVATE METHODS
|
|
4843
5018
|
// ===========================================================================
|
|
4844
5019
|
/**
|
|
4845
|
-
* Create and warm a new sandbox
|
|
5020
|
+
* Create and warm a new sandbox.
|
|
5021
|
+
* If a spec is provided, runs the spec's setup function after SDK installation.
|
|
5022
|
+
* On spec setup failure, the sandbox remains generic (graceful degradation).
|
|
4846
5023
|
*/
|
|
4847
|
-
async warmSandbox() {
|
|
5024
|
+
async warmSandbox(spec) {
|
|
4848
5025
|
const warmupId = `warming-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
4849
5026
|
this.warmingInProgress.add(warmupId);
|
|
4850
5027
|
this.emitMetric("warmup_started", { warmupId });
|
|
@@ -4938,6 +5115,60 @@ echo "[warmup] Warmup complete!"
|
|
|
4938
5115
|
throw new Error(`Warmup failed: ${stderr}`);
|
|
4939
5116
|
}
|
|
4940
5117
|
}
|
|
5118
|
+
let warmupTag;
|
|
5119
|
+
let agentSetupComplete = false;
|
|
5120
|
+
let warmupInstallRan = false;
|
|
5121
|
+
let warmupStartupRan = false;
|
|
5122
|
+
if (spec) {
|
|
5123
|
+
this.warmingTags.add(spec.tag);
|
|
5124
|
+
console.log(`[POOL] Running spec setup for tag=${spec.tag} on sandbox ${sandbox.sandboxId}...`);
|
|
5125
|
+
this.emitMetric("spec_setup_started", { tag: spec.tag, sandboxId: sandbox.sandboxId });
|
|
5126
|
+
const specStartTime = Date.now();
|
|
5127
|
+
try {
|
|
5128
|
+
const setupResult = await new Promise((resolve3, reject) => {
|
|
5129
|
+
const timer = setTimeout(
|
|
5130
|
+
() => reject(new Error(`Spec setup timed out after ${_SandboxPool.SPEC_SETUP_TIMEOUT_MS / 1e3}s`)),
|
|
5131
|
+
_SandboxPool.SPEC_SETUP_TIMEOUT_MS
|
|
5132
|
+
);
|
|
5133
|
+
spec.setup(sandbox).then((result) => {
|
|
5134
|
+
clearTimeout(timer);
|
|
5135
|
+
resolve3(result);
|
|
5136
|
+
}).catch((error) => {
|
|
5137
|
+
clearTimeout(timer);
|
|
5138
|
+
reject(error);
|
|
5139
|
+
});
|
|
5140
|
+
});
|
|
5141
|
+
warmupTag = spec.tag;
|
|
5142
|
+
agentSetupComplete = true;
|
|
5143
|
+
if (setupResult && typeof setupResult === "object") {
|
|
5144
|
+
warmupInstallRan = setupResult.installRan === true;
|
|
5145
|
+
warmupStartupRan = setupResult.startupRan === true;
|
|
5146
|
+
}
|
|
5147
|
+
const specDuration = Date.now() - specStartTime;
|
|
5148
|
+
console.log(`[POOL] Spec setup completed for tag=${spec.tag} on sandbox ${sandbox.sandboxId} (${specDuration}ms, install=${warmupInstallRan}, startup=${warmupStartupRan})`);
|
|
5149
|
+
this.emitMetric("spec_setup_completed", {
|
|
5150
|
+
tag: spec.tag,
|
|
5151
|
+
sandboxId: sandbox.sandboxId,
|
|
5152
|
+
durationMs: specDuration,
|
|
5153
|
+
warmupInstallRan,
|
|
5154
|
+
warmupStartupRan
|
|
5155
|
+
});
|
|
5156
|
+
} catch (specError) {
|
|
5157
|
+
const specDuration = Date.now() - specStartTime;
|
|
5158
|
+
const specErrorMessage = specError instanceof Error ? specError.message : "Unknown";
|
|
5159
|
+
console.warn(
|
|
5160
|
+
`[POOL] Spec setup failed for tag=${spec.tag} on sandbox ${sandbox.sandboxId} (${specDuration}ms): ${specErrorMessage}. Sandbox stays generic.`
|
|
5161
|
+
);
|
|
5162
|
+
this.emitMetric("spec_setup_failed", {
|
|
5163
|
+
tag: spec.tag,
|
|
5164
|
+
sandboxId: sandbox.sandboxId,
|
|
5165
|
+
durationMs: specDuration,
|
|
5166
|
+
error: specErrorMessage
|
|
5167
|
+
});
|
|
5168
|
+
} finally {
|
|
5169
|
+
this.warmingTags.delete(spec.tag);
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
4941
5172
|
const warmupTime = Date.now() - startTime;
|
|
4942
5173
|
const now = Date.now();
|
|
4943
5174
|
const pooled = {
|
|
@@ -4947,14 +5178,21 @@ echo "[warmup] Warmup complete!"
|
|
|
4947
5178
|
expiresAt: now + this.config.sandboxTimeout * 1e3,
|
|
4948
5179
|
sdkInstalled: true,
|
|
4949
5180
|
eligible: true,
|
|
4950
|
-
lastHeartbeat: now
|
|
5181
|
+
lastHeartbeat: now,
|
|
5182
|
+
warmupTag,
|
|
5183
|
+
agentSetupComplete,
|
|
5184
|
+
warmupInstallRan,
|
|
5185
|
+
warmupStartupRan
|
|
4951
5186
|
};
|
|
4952
|
-
|
|
5187
|
+
const tagInfo = warmupTag ? ` [tag=${warmupTag}]` : "";
|
|
5188
|
+
console.log(`[POOL] Warmup completed for ${sandbox.sandboxId} (took ${warmupTime}ms)${useTarball ? " [tarball]" : ""}${tagInfo}`);
|
|
4953
5189
|
this.consecutiveFailures = 0;
|
|
4954
5190
|
this.emitMetric("warmup_completed", {
|
|
4955
5191
|
sandboxId: pooled.sandboxId,
|
|
4956
5192
|
warmupTimeMs: warmupTime,
|
|
4957
|
-
usedTarball: useTarball
|
|
5193
|
+
usedTarball: useTarball,
|
|
5194
|
+
warmupTag,
|
|
5195
|
+
agentSetupComplete
|
|
4958
5196
|
});
|
|
4959
5197
|
this.emitMetric("sandbox_created", { sandboxId: pooled.sandboxId });
|
|
4960
5198
|
return pooled;
|
|
@@ -5035,18 +5273,29 @@ echo "[warmup] Warmup complete!"
|
|
|
5035
5273
|
}
|
|
5036
5274
|
}
|
|
5037
5275
|
/**
|
|
5038
|
-
* Get an available eligible sandbox for assignment
|
|
5276
|
+
* Get an available eligible sandbox for assignment.
|
|
5277
|
+
* If preferTag is provided:
|
|
5278
|
+
* - First tries to find a sandbox tagged for that specific agent
|
|
5279
|
+
* - Falls back to a generic (untagged) sandbox
|
|
5280
|
+
* - Tagged sandboxes for OTHER agents are NOT used as fallback (reserved)
|
|
5039
5281
|
*/
|
|
5040
|
-
getAvailableSandbox() {
|
|
5041
|
-
let
|
|
5282
|
+
getAvailableSandbox(preferTag) {
|
|
5283
|
+
let bestTagged = null;
|
|
5284
|
+
let bestGeneric = null;
|
|
5042
5285
|
for (const pooled of this.pool.values()) {
|
|
5043
5286
|
if (!pooled.assignedTo && pooled.eligible) {
|
|
5044
|
-
if (
|
|
5045
|
-
|
|
5287
|
+
if (preferTag && pooled.warmupTag === preferTag) {
|
|
5288
|
+
if (!bestTagged || pooled.expiresAt > bestTagged.expiresAt) {
|
|
5289
|
+
bestTagged = pooled;
|
|
5290
|
+
}
|
|
5291
|
+
} else if (!pooled.warmupTag) {
|
|
5292
|
+
if (!bestGeneric || pooled.expiresAt > bestGeneric.expiresAt) {
|
|
5293
|
+
bestGeneric = pooled;
|
|
5294
|
+
}
|
|
5046
5295
|
}
|
|
5047
5296
|
}
|
|
5048
5297
|
}
|
|
5049
|
-
return
|
|
5298
|
+
return bestTagged || bestGeneric;
|
|
5050
5299
|
}
|
|
5051
5300
|
/**
|
|
5052
5301
|
* Get count of available sandboxes
|
|
@@ -5113,13 +5362,16 @@ echo "[warmup] Warmup complete!"
|
|
|
5113
5362
|
this.config.maxPoolSize - this.pool.size - warmingCount
|
|
5114
5363
|
);
|
|
5115
5364
|
if (needed <= 0) return;
|
|
5365
|
+
const specAssignments = this.selectSpecsForReplenishment(needed);
|
|
5116
5366
|
console.log(`[POOL] Spawning ${needed} sandbox(es) to maintain pool...`);
|
|
5117
5367
|
const promises = [];
|
|
5118
5368
|
for (let i = 0; i < needed; i++) {
|
|
5369
|
+
const spec = specAssignments[i];
|
|
5119
5370
|
promises.push(
|
|
5120
|
-
this.warmSandbox().then((sandbox) => {
|
|
5371
|
+
this.warmSandbox(spec).then((sandbox) => {
|
|
5121
5372
|
this.pool.set(sandbox.sandboxId, sandbox);
|
|
5122
|
-
|
|
5373
|
+
const tagInfo = sandbox.warmupTag ? ` [tag=${sandbox.warmupTag}]` : "";
|
|
5374
|
+
console.log(`[POOL] Replenishment sandbox ready: ${sandbox.sandboxId}${tagInfo}`);
|
|
5123
5375
|
}).catch((error) => {
|
|
5124
5376
|
console.error("[POOL] Failed to warm replenishment sandbox:", error);
|
|
5125
5377
|
})
|
|
@@ -5130,6 +5382,65 @@ echo "[warmup] Warmup complete!"
|
|
|
5130
5382
|
}
|
|
5131
5383
|
await Promise.all(promises);
|
|
5132
5384
|
}
|
|
5385
|
+
/**
|
|
5386
|
+
* Decide which specs to apply to new sandboxes during replenishment.
|
|
5387
|
+
* Strategy:
|
|
5388
|
+
* - Always reserve at least 1 slot for generic (fix #3)
|
|
5389
|
+
* - Cover uncovered specs first (highest priority), skipping in-flight tags (fix #4)
|
|
5390
|
+
* - Fill remaining as generic
|
|
5391
|
+
* Returns an array of length `needed`, where each element is a spec or undefined (generic).
|
|
5392
|
+
*/
|
|
5393
|
+
selectSpecsForReplenishment(needed) {
|
|
5394
|
+
if (this.warmupSpecs.size === 0 || needed === 0) {
|
|
5395
|
+
return new Array(needed).fill(void 0);
|
|
5396
|
+
}
|
|
5397
|
+
const maxTaggedSlots = Math.max(0, needed - 1);
|
|
5398
|
+
const uncoveredSpecs = [];
|
|
5399
|
+
for (const spec of this.warmupSpecs.values()) {
|
|
5400
|
+
if (this.warmingTags.has(spec.tag)) continue;
|
|
5401
|
+
let hasCoverage = false;
|
|
5402
|
+
for (const pooled of this.pool.values()) {
|
|
5403
|
+
if (pooled.warmupTag === spec.tag && pooled.eligible && !pooled.assignedTo) {
|
|
5404
|
+
hasCoverage = true;
|
|
5405
|
+
break;
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
if (!hasCoverage) {
|
|
5409
|
+
uncoveredSpecs.push(spec);
|
|
5410
|
+
}
|
|
5411
|
+
}
|
|
5412
|
+
uncoveredSpecs.sort((a, b) => b.priority - a.priority);
|
|
5413
|
+
const assignments = [];
|
|
5414
|
+
for (const spec of uncoveredSpecs) {
|
|
5415
|
+
if (assignments.length >= maxTaggedSlots) break;
|
|
5416
|
+
assignments.push(spec);
|
|
5417
|
+
}
|
|
5418
|
+
while (assignments.length < needed) {
|
|
5419
|
+
assignments.push(void 0);
|
|
5420
|
+
}
|
|
5421
|
+
return assignments;
|
|
5422
|
+
}
|
|
5423
|
+
/**
|
|
5424
|
+
* Evict lowest-priority specs when over MAX_SPECS capacity (fix #2).
|
|
5425
|
+
*/
|
|
5426
|
+
evictLowestPrioritySpecs() {
|
|
5427
|
+
while (this.warmupSpecs.size > _SandboxPool.MAX_SPECS) {
|
|
5428
|
+
let lowestTag;
|
|
5429
|
+
let lowestPriority = Infinity;
|
|
5430
|
+
for (const [tag, spec] of this.warmupSpecs.entries()) {
|
|
5431
|
+
if (spec.priority < lowestPriority) {
|
|
5432
|
+
lowestPriority = spec.priority;
|
|
5433
|
+
lowestTag = tag;
|
|
5434
|
+
}
|
|
5435
|
+
}
|
|
5436
|
+
if (lowestTag) {
|
|
5437
|
+
this.warmupSpecs.delete(lowestTag);
|
|
5438
|
+
console.log(`[POOL] Evicted warmup spec: ${lowestTag} (priority=${lowestPriority}, specs=${this.warmupSpecs.size})`);
|
|
5439
|
+
} else {
|
|
5440
|
+
break;
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5133
5444
|
/**
|
|
5134
5445
|
* Destroy a sandbox and clean up
|
|
5135
5446
|
*/
|
|
@@ -5361,7 +5672,7 @@ function removeExpiredSandbox(sessionId) {
|
|
|
5361
5672
|
}
|
|
5362
5673
|
}
|
|
5363
5674
|
async function getOrCreateSandbox(options) {
|
|
5364
|
-
const { sessionId, runtime = "node22", timeout = 300, vcpus = 4, existingSandboxId } = options;
|
|
5675
|
+
const { sessionId, runtime = "node22", timeout = 300, vcpus = 4, existingSandboxId, preferTag } = options;
|
|
5365
5676
|
ensureCleanupRunning();
|
|
5366
5677
|
ensureHeartbeatRunning();
|
|
5367
5678
|
const { Sandbox } = await import('@vercel/sandbox');
|
|
@@ -5378,6 +5689,8 @@ async function getOrCreateSandbox(options) {
|
|
|
5378
5689
|
sdkInstalled: cached.sdkInstalled,
|
|
5379
5690
|
startupScriptRan: cached.startupScriptRan,
|
|
5380
5691
|
startupScriptHash: cached.startupScriptHash,
|
|
5692
|
+
installScriptRan: cached.installScriptRan,
|
|
5693
|
+
installScriptHash: cached.installScriptHash,
|
|
5381
5694
|
isNew: false,
|
|
5382
5695
|
configFileUrl: cached.configFileUrl,
|
|
5383
5696
|
configInstalledAt: cached.configInstalledAt
|
|
@@ -5386,41 +5699,6 @@ async function getOrCreateSandbox(options) {
|
|
|
5386
5699
|
console.log("[SANDBOX] Cached sandbox has expired (HTTP 410 or similar), creating new sandbox");
|
|
5387
5700
|
removeExpiredSandbox(sessionId);
|
|
5388
5701
|
}
|
|
5389
|
-
const pool = await ensureSandboxPoolInitialized();
|
|
5390
|
-
if (pool && pool.isRunning()) {
|
|
5391
|
-
try {
|
|
5392
|
-
console.log("[SANDBOX] Attempting to acquire from pre-warmed pool...");
|
|
5393
|
-
const pooled = await pool.acquire(sessionId);
|
|
5394
|
-
console.log("[SANDBOX] Acquired pre-warmed sandbox:", pooled.sandboxId);
|
|
5395
|
-
const now2 = Date.now();
|
|
5396
|
-
const entry2 = {
|
|
5397
|
-
sandbox: pooled.sandbox,
|
|
5398
|
-
sessionId,
|
|
5399
|
-
createdAt: pooled.createdAt,
|
|
5400
|
-
lastUsedAt: now2,
|
|
5401
|
-
sdkInstalled: pooled.sdkInstalled,
|
|
5402
|
-
startupScriptRan: false
|
|
5403
|
-
// User script hasn't run yet
|
|
5404
|
-
};
|
|
5405
|
-
sandboxCache.set(sessionId, entry2);
|
|
5406
|
-
return {
|
|
5407
|
-
sandbox: pooled.sandbox,
|
|
5408
|
-
sandboxId: pooled.sandboxId,
|
|
5409
|
-
sdkInstalled: pooled.sdkInstalled,
|
|
5410
|
-
startupScriptRan: false,
|
|
5411
|
-
startupScriptHash: void 0,
|
|
5412
|
-
isNew: false,
|
|
5413
|
-
// Not new - came from pool
|
|
5414
|
-
configFileUrl: void 0,
|
|
5415
|
-
configInstalledAt: void 0
|
|
5416
|
-
};
|
|
5417
|
-
} catch (error) {
|
|
5418
|
-
console.warn(
|
|
5419
|
-
"[SANDBOX] Failed to acquire from pool, falling back to on-demand creation:",
|
|
5420
|
-
error instanceof Error ? error.message : "Unknown error"
|
|
5421
|
-
);
|
|
5422
|
-
}
|
|
5423
|
-
}
|
|
5424
5702
|
if (existingSandboxId) {
|
|
5425
5703
|
console.log("[SANDBOX] Attempting to reconnect to existing sandbox:", existingSandboxId);
|
|
5426
5704
|
try {
|
|
@@ -5441,7 +5719,9 @@ async function getOrCreateSandbox(options) {
|
|
|
5441
5719
|
// We assume SDK is installed since this is an existing sandbox
|
|
5442
5720
|
// The caller can verify and re-mark if needed
|
|
5443
5721
|
sdkInstalled: true,
|
|
5444
|
-
startupScriptRan: true
|
|
5722
|
+
startupScriptRan: true,
|
|
5723
|
+
installScriptRan: true
|
|
5724
|
+
// Assume ran for reconnected sandboxes
|
|
5445
5725
|
};
|
|
5446
5726
|
sandboxCache.set(sessionId, entry2);
|
|
5447
5727
|
return {
|
|
@@ -5453,13 +5733,17 @@ async function getOrCreateSandbox(options) {
|
|
|
5453
5733
|
// Assume ran for reconnected sandboxes
|
|
5454
5734
|
startupScriptHash: void 0,
|
|
5455
5735
|
// Unknown — caller should not re-run based on hash mismatch alone
|
|
5736
|
+
installScriptRan: true,
|
|
5737
|
+
// Assume ran for reconnected sandboxes
|
|
5738
|
+
installScriptHash: void 0,
|
|
5739
|
+
// Unknown — same logic as startup script
|
|
5456
5740
|
isNew: false,
|
|
5457
5741
|
configFileUrl: void 0,
|
|
5458
5742
|
configInstalledAt: now2
|
|
5459
5743
|
// Assume config was installed — prevents unnecessary re-install on reconnection
|
|
5460
5744
|
};
|
|
5461
5745
|
} else {
|
|
5462
|
-
console.log("[SANDBOX] Reconnected sandbox failed health check, will create new");
|
|
5746
|
+
console.log("[SANDBOX] Reconnected sandbox failed health check, will try pool or create new");
|
|
5463
5747
|
}
|
|
5464
5748
|
} catch (error) {
|
|
5465
5749
|
console.log(
|
|
@@ -5469,6 +5753,51 @@ async function getOrCreateSandbox(options) {
|
|
|
5469
5753
|
);
|
|
5470
5754
|
}
|
|
5471
5755
|
}
|
|
5756
|
+
const pool = await ensureSandboxPoolInitialized();
|
|
5757
|
+
if (pool && pool.isRunning()) {
|
|
5758
|
+
try {
|
|
5759
|
+
console.log(`[SANDBOX] Attempting to acquire from pre-warmed pool...${preferTag ? ` [preferTag=${preferTag}]` : ""}`);
|
|
5760
|
+
const pooled = await pool.acquire(sessionId, preferTag);
|
|
5761
|
+
const tagInfo = pooled.warmupTag ? ` [tag=${pooled.warmupTag}, agentSetup=${pooled.agentSetupComplete}]` : "";
|
|
5762
|
+
console.log(`[SANDBOX] Acquired pre-warmed sandbox: ${pooled.sandboxId}${tagInfo}`);
|
|
5763
|
+
const installDone = pooled.warmupInstallRan === true;
|
|
5764
|
+
const startupDone = pooled.warmupStartupRan === true;
|
|
5765
|
+
const agentSetupDone = pooled.agentSetupComplete === true;
|
|
5766
|
+
const now2 = Date.now();
|
|
5767
|
+
const entry2 = {
|
|
5768
|
+
sandbox: pooled.sandbox,
|
|
5769
|
+
sessionId,
|
|
5770
|
+
createdAt: pooled.createdAt,
|
|
5771
|
+
lastUsedAt: now2,
|
|
5772
|
+
sdkInstalled: pooled.sdkInstalled,
|
|
5773
|
+
startupScriptRan: startupDone,
|
|
5774
|
+
installScriptRan: installDone
|
|
5775
|
+
};
|
|
5776
|
+
sandboxCache.set(sessionId, entry2);
|
|
5777
|
+
return {
|
|
5778
|
+
sandbox: pooled.sandbox,
|
|
5779
|
+
sandboxId: pooled.sandboxId,
|
|
5780
|
+
sdkInstalled: pooled.sdkInstalled,
|
|
5781
|
+
startupScriptRan: startupDone,
|
|
5782
|
+
startupScriptHash: void 0,
|
|
5783
|
+
installScriptRan: installDone,
|
|
5784
|
+
installScriptHash: void 0,
|
|
5785
|
+
isNew: false,
|
|
5786
|
+
// Not new - came from pool
|
|
5787
|
+
configFileUrl: void 0,
|
|
5788
|
+
configInstalledAt: agentSetupDone ? now2 : void 0,
|
|
5789
|
+
warmupTag: pooled.warmupTag,
|
|
5790
|
+
agentSetupComplete: pooled.agentSetupComplete,
|
|
5791
|
+
warmupInstallRan: pooled.warmupInstallRan,
|
|
5792
|
+
warmupStartupRan: pooled.warmupStartupRan
|
|
5793
|
+
};
|
|
5794
|
+
} catch (error) {
|
|
5795
|
+
console.warn(
|
|
5796
|
+
"[SANDBOX] Failed to acquire from pool, falling back to on-demand creation:",
|
|
5797
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
5798
|
+
);
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5472
5801
|
console.log("[SANDBOX] Creating new sandbox for session:", sessionId);
|
|
5473
5802
|
const baseTarballUrl = process.env.SANDBOX_BASE_TARBALL_URL;
|
|
5474
5803
|
const useTarball = !!baseTarballUrl;
|
|
@@ -5502,7 +5831,8 @@ async function getOrCreateSandbox(options) {
|
|
|
5502
5831
|
lastUsedAt: now,
|
|
5503
5832
|
// If we used tarball, SDK is pre-installed
|
|
5504
5833
|
sdkInstalled: useTarball,
|
|
5505
|
-
startupScriptRan: false
|
|
5834
|
+
startupScriptRan: false,
|
|
5835
|
+
installScriptRan: false
|
|
5506
5836
|
};
|
|
5507
5837
|
sandboxCache.set(sessionId, entry);
|
|
5508
5838
|
return {
|
|
@@ -5512,6 +5842,8 @@ async function getOrCreateSandbox(options) {
|
|
|
5512
5842
|
sdkInstalled: useTarball,
|
|
5513
5843
|
startupScriptRan: false,
|
|
5514
5844
|
startupScriptHash: void 0,
|
|
5845
|
+
installScriptRan: false,
|
|
5846
|
+
installScriptHash: void 0,
|
|
5515
5847
|
isNew: true,
|
|
5516
5848
|
configFileUrl: void 0,
|
|
5517
5849
|
configInstalledAt: void 0
|
|
@@ -5551,6 +5883,14 @@ function markStartupScriptRan(sessionId, scriptHash) {
|
|
|
5551
5883
|
cached.lastUsedAt = Date.now();
|
|
5552
5884
|
}
|
|
5553
5885
|
}
|
|
5886
|
+
function markInstallScriptRan(sessionId, scriptHash) {
|
|
5887
|
+
const cached = sandboxCache.get(sessionId);
|
|
5888
|
+
if (cached) {
|
|
5889
|
+
cached.installScriptRan = true;
|
|
5890
|
+
cached.installScriptHash = scriptHash;
|
|
5891
|
+
cached.lastUsedAt = Date.now();
|
|
5892
|
+
}
|
|
5893
|
+
}
|
|
5554
5894
|
function needsStartupScriptRerun(sessionId, newScript) {
|
|
5555
5895
|
const cached = sandboxCache.get(sessionId);
|
|
5556
5896
|
if (!cached) return true;
|
|
@@ -5618,6 +5958,8 @@ function getCachedSandbox(sessionId) {
|
|
|
5618
5958
|
sdkInstalled: cached.sdkInstalled,
|
|
5619
5959
|
startupScriptRan: cached.startupScriptRan,
|
|
5620
5960
|
startupScriptHash: cached.startupScriptHash,
|
|
5961
|
+
installScriptRan: cached.installScriptRan,
|
|
5962
|
+
installScriptHash: cached.installScriptHash,
|
|
5621
5963
|
isNew: false,
|
|
5622
5964
|
configFileUrl: cached.configFileUrl,
|
|
5623
5965
|
configInstalledAt: cached.configInstalledAt
|
|
@@ -5792,6 +6134,13 @@ function createVercelSandboxExecutor(apiKey) {
|
|
|
5792
6134
|
};
|
|
5793
6135
|
}
|
|
5794
6136
|
async function* executeInSandbox(prompt, apiKey, options) {
|
|
6137
|
+
if (typeof prompt !== "string") {
|
|
6138
|
+
yield {
|
|
6139
|
+
type: "error",
|
|
6140
|
+
error: "Vercel sandbox executor does not support streaming prompt inputs."
|
|
6141
|
+
};
|
|
6142
|
+
return;
|
|
6143
|
+
}
|
|
5795
6144
|
const sessionId = options.harnessSessionId || `temp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
5796
6145
|
try {
|
|
5797
6146
|
const { sandbox, sdkInstalled, startupScriptRan, startupScriptHash: cachedScriptHash, configFileUrl: cachedConfigUrl } = await getOrCreateSandbox({
|
|
@@ -5933,9 +6282,13 @@ async function* executeInSandbox(prompt, apiKey, options) {
|
|
|
5933
6282
|
permissionMode: options.permissionMode || "bypassPermissions",
|
|
5934
6283
|
includePartialMessages: true
|
|
5935
6284
|
};
|
|
6285
|
+
if (options.settingSources !== void 0) {
|
|
6286
|
+
sdkOptions.settingSources = options.settingSources;
|
|
6287
|
+
} else {
|
|
6288
|
+
sdkOptions.settingSources = ["project"];
|
|
6289
|
+
}
|
|
5936
6290
|
const hasConfig = options.configFileUrl || cachedConfigUrl;
|
|
5937
6291
|
if (hasConfig) {
|
|
5938
|
-
sdkOptions.settingSources = ["project"];
|
|
5939
6292
|
if (options.allowedTools && options.allowedTools.length > 0) {
|
|
5940
6293
|
const allowedTools = [...options.allowedTools];
|
|
5941
6294
|
if (!allowedTools.includes("Skill")) {
|
|
@@ -5971,12 +6324,47 @@ async function* executeInSandbox(prompt, apiKey, options) {
|
|
|
5971
6324
|
if (options.resume) {
|
|
5972
6325
|
sdkOptions.resume = options.resume;
|
|
5973
6326
|
}
|
|
6327
|
+
try {
|
|
6328
|
+
const pluginFindResult = await sandbox.runCommand({
|
|
6329
|
+
cmd: "bash",
|
|
6330
|
+
args: [
|
|
6331
|
+
"-c",
|
|
6332
|
+
[
|
|
6333
|
+
'for base in ".claude/plugins" "$HOME/.claude/plugins"; do',
|
|
6334
|
+
' if [ -d "$base" ]; then',
|
|
6335
|
+
' find "$base" -type f -path "*/.claude-plugin/plugin.json" -print',
|
|
6336
|
+
" fi",
|
|
6337
|
+
"done | sed 's#/.claude-plugin/plugin.json$##'"
|
|
6338
|
+
].join("\n")
|
|
6339
|
+
]
|
|
6340
|
+
});
|
|
6341
|
+
const rawPluginRoots = (await pluginFindResult.stdout()).trim();
|
|
6342
|
+
const pluginRoots = rawPluginRoots ? rawPluginRoots.split("\n").map((line) => line.trim()).filter(Boolean) : [];
|
|
6343
|
+
const uniquePluginRoots = Array.from(new Set(pluginRoots));
|
|
6344
|
+
if (uniquePluginRoots.length > 0) {
|
|
6345
|
+
sdkOptions.plugins = uniquePluginRoots.map((path15) => ({ type: "local", path: path15 }));
|
|
6346
|
+
console.log("[SANDBOX] Plugins detected:", uniquePluginRoots);
|
|
6347
|
+
}
|
|
6348
|
+
} catch (pluginError) {
|
|
6349
|
+
console.warn("[SANDBOX] Failed to detect plugins:", pluginError);
|
|
6350
|
+
}
|
|
5974
6351
|
const agentScript = `
|
|
5975
6352
|
const { query } = require('@anthropic-ai/claude-agent-sdk');
|
|
6353
|
+
const fs = require('fs');
|
|
5976
6354
|
|
|
5977
6355
|
const prompt = ${JSON.stringify(prompt)};
|
|
5978
6356
|
const options = ${JSON.stringify(sdkOptions)};
|
|
5979
6357
|
|
|
6358
|
+
// Enable subagents if .claude/agents exists (requires Task tool)
|
|
6359
|
+
if (fs.existsSync('.claude/agents')) {
|
|
6360
|
+
if (!Array.isArray(options.allowedTools)) {
|
|
6361
|
+
options.allowedTools = [];
|
|
6362
|
+
}
|
|
6363
|
+
if (!options.allowedTools.includes('Task')) {
|
|
6364
|
+
options.allowedTools.push('Task');
|
|
6365
|
+
}
|
|
6366
|
+
}
|
|
6367
|
+
|
|
5980
6368
|
let queryCompleted = false;
|
|
5981
6369
|
|
|
5982
6370
|
async function run() {
|
|
@@ -6073,7 +6461,11 @@ SCRIPT_EOF`]
|
|
|
6073
6461
|
});
|
|
6074
6462
|
}
|
|
6075
6463
|
if (event.type === "system" && event.subtype === "init") {
|
|
6076
|
-
events.push({
|
|
6464
|
+
events.push({
|
|
6465
|
+
type: "session_init",
|
|
6466
|
+
sessionId: event.session_id || "",
|
|
6467
|
+
slashCommands: event.slash_commands
|
|
6468
|
+
});
|
|
6077
6469
|
} else if (event.type === "stream_event" && event.event) {
|
|
6078
6470
|
const streamEvent = event.event;
|
|
6079
6471
|
if (streamEvent.type === "content_block_delta") {
|
|
@@ -6108,12 +6500,16 @@ SCRIPT_EOF`]
|
|
|
6108
6500
|
}
|
|
6109
6501
|
}
|
|
6110
6502
|
} else if (event.type === "result") {
|
|
6503
|
+
const usage = event.usage;
|
|
6504
|
+
const hasUsageTokens = !!usage && (usage.input_tokens !== void 0 || usage.output_tokens !== void 0);
|
|
6505
|
+
const totalTokens = hasUsageTokens ? (usage?.input_tokens ?? 0) + (usage?.output_tokens ?? 0) : event.total_tokens ?? (event.tokens ? event.tokens.input + event.tokens.output : void 0);
|
|
6506
|
+
const totalCost = usage?.total_cost_usd ?? event.total_cost_usd ?? event.cost ?? event.total_cost;
|
|
6111
6507
|
events.push({
|
|
6112
6508
|
type: "complete",
|
|
6113
6509
|
sessionId: event.session_id,
|
|
6114
6510
|
result: event.result,
|
|
6115
|
-
totalCost
|
|
6116
|
-
totalTokens
|
|
6511
|
+
totalCost,
|
|
6512
|
+
totalTokens
|
|
6117
6513
|
});
|
|
6118
6514
|
}
|
|
6119
6515
|
return events;
|
|
@@ -12692,6 +13088,7 @@ __export(schemas_exports, {
|
|
|
12692
13088
|
RunAgentRequestSchema: () => RunAgentRequestSchema,
|
|
12693
13089
|
SendMessageRequestSchema: () => SendMessageRequestSchema,
|
|
12694
13090
|
SessionEndEventSchema: () => SessionEndEventSchema,
|
|
13091
|
+
SessionInitEventSchema: () => SessionInitEventSchema,
|
|
12695
13092
|
SessionSchema: () => SessionSchema,
|
|
12696
13093
|
SessionStartEventSchema: () => SessionStartEventSchema,
|
|
12697
13094
|
SessionStatusSchema: () => SessionStatusSchema,
|
|
@@ -12711,7 +13108,7 @@ __export(schemas_exports, {
|
|
|
12711
13108
|
TurnCompleteEventSchema: () => TurnCompleteEventSchema,
|
|
12712
13109
|
UpdateAgentRequestSchema: () => UpdateAgentRequestSchema
|
|
12713
13110
|
});
|
|
12714
|
-
var ErrorResponseSchema, SuccessResponseSchema, PaginationQuerySchema, OrderQuerySchema, TextContentSchema, ToolUseContentSchema, ToolResultContentSchema, ImageContentSchema, FileContentSchema, MessageContentSchema, MessageSchema, PaginatedMessagesSchema, SessionStatusSchema, SessionSchema, PaginatedSessionsSchema, CreateSessionRequestSchema, SendMessageRequestSchema, ResumeSessionRequestSchema, ListSessionsQuerySchema, AgentStatusSchema, PermissionModeSchema, McpServerConfigSchema, StoredAgentSchema, SimpleAgentSchema, PaginatedAgentsSchema, CreateAgentRequestSchema, UpdateAgentRequestSchema, RunAgentRequestSchema, ListAgentsQuerySchema, GitHubSkillSourceSchema, LocalSkillSourceSchema, SkillSourceSchema, BrowseSkillsRequestSchema, ReadSkillFileRequestSchema, FileEntrySchema, BrowseSkillsResponseSchema, SkillFileContentSchema, ReadSkillFileResponseSchema, SessionStartEventSchema, TextDeltaEventSchema, ThinkingDeltaEventSchema, MessageEventSchema, ToolUseEventSchema, ToolResultEventSchema, SessionEndEventSchema, TurnCompleteEventSchema, StreamErrorEventSchema, HealthResponseSchema;
|
|
13111
|
+
var ErrorResponseSchema, SuccessResponseSchema, PaginationQuerySchema, OrderQuerySchema, TextContentSchema, ToolUseContentSchema, ToolResultContentSchema, ImageContentSchema, FileContentSchema, MessageContentSchema, MessageSchema, PaginatedMessagesSchema, SessionStatusSchema, SessionSchema, PaginatedSessionsSchema, CreateSessionRequestSchema, SendMessageRequestSchema, ResumeSessionRequestSchema, ListSessionsQuerySchema, AgentStatusSchema, PermissionModeSchema, McpServerConfigSchema, StoredAgentSchema, SimpleAgentSchema, PaginatedAgentsSchema, CreateAgentRequestSchema, UpdateAgentRequestSchema, RunAgentRequestSchema, ListAgentsQuerySchema, GitHubSkillSourceSchema, LocalSkillSourceSchema, SkillSourceSchema, BrowseSkillsRequestSchema, ReadSkillFileRequestSchema, FileEntrySchema, BrowseSkillsResponseSchema, SkillFileContentSchema, ReadSkillFileResponseSchema, SessionStartEventSchema, SessionInitEventSchema, TextDeltaEventSchema, ThinkingDeltaEventSchema, MessageEventSchema, ToolUseEventSchema, ToolResultEventSchema, SessionEndEventSchema, TurnCompleteEventSchema, StreamErrorEventSchema, HealthResponseSchema;
|
|
12715
13112
|
var init_schemas = __esm({
|
|
12716
13113
|
"src/server/openapi/schemas.ts"() {
|
|
12717
13114
|
init_dist3();
|
|
@@ -12937,6 +13334,15 @@ var init_schemas = __esm({
|
|
|
12937
13334
|
sessionId: zod.z.string().openapi({ description: "Session ID" }),
|
|
12938
13335
|
claudeSessionId: zod.z.string().openapi({ description: "Claude SDK session ID" })
|
|
12939
13336
|
}).openapi("SessionStartEvent");
|
|
13337
|
+
SessionInitEventSchema = zod.z.object({
|
|
13338
|
+
type: zod.z.literal("session_init"),
|
|
13339
|
+
sessionId: zod.z.string().openapi({ description: "Claude SDK session ID" }),
|
|
13340
|
+
slashCommands: zod.z.array(zod.z.object({
|
|
13341
|
+
name: zod.z.string().openapi({ description: "Slash command name" }),
|
|
13342
|
+
description: zod.z.string().optional().openapi({ description: "Slash command description" }),
|
|
13343
|
+
prompt: zod.z.string().optional().openapi({ description: "Slash command prompt" })
|
|
13344
|
+
})).optional().openapi({ description: "Slash commands advertised by the SDK" })
|
|
13345
|
+
}).openapi("SessionInitEvent");
|
|
12940
13346
|
TextDeltaEventSchema = zod.z.object({
|
|
12941
13347
|
type: zod.z.literal("text_delta"),
|
|
12942
13348
|
delta: zod.z.string().openapi({ description: "Text chunk", example: "Hello" })
|
|
@@ -13263,6 +13669,7 @@ var init_sessions2 = __esm({
|
|
|
13263
13669
|
The stream emits the following event types:
|
|
13264
13670
|
|
|
13265
13671
|
- \`session_start\` - Session started, includes sessionId and sdkSessionId
|
|
13672
|
+
- \`session_init\` - Claude SDK session initialized (includes slash commands)
|
|
13266
13673
|
- \`text_delta\` - Text chunk being generated
|
|
13267
13674
|
- \`thinking_delta\` - Thinking/reasoning text chunk
|
|
13268
13675
|
- \`message\` - Complete message saved to storage
|
|
@@ -13711,6 +14118,7 @@ This is a convenience endpoint that combines session creation and message sendin
|
|
|
13711
14118
|
The stream emits the following event types:
|
|
13712
14119
|
|
|
13713
14120
|
- \`session_start\` - Session started
|
|
14121
|
+
- \`session_init\` - Claude SDK session initialized (includes slash commands)
|
|
13714
14122
|
- \`text_delta\` - Text chunk being generated
|
|
13715
14123
|
- \`thinking_delta\` - Thinking/reasoning text chunk
|
|
13716
14124
|
- \`message\` - Complete message saved
|
|
@@ -19986,6 +20394,7 @@ exports.loadWorkspaceState = loadWorkspaceState;
|
|
|
19986
20394
|
exports.mapClaudeOptionsToGemini = mapClaudeOptionsToGemini;
|
|
19987
20395
|
exports.mapToolToActionType = mapToolToActionType;
|
|
19988
20396
|
exports.markConfigInstalled = markConfigInstalled;
|
|
20397
|
+
exports.markInstallScriptRan = markInstallScriptRan;
|
|
19989
20398
|
exports.markSdkInstalled = markSdkInstalled;
|
|
19990
20399
|
exports.markStartupScriptRan = markStartupScriptRan;
|
|
19991
20400
|
exports.mcpAuthToHeaders = mcpAuthToHeaders;
|