@ash-cloud/ash-ai 0.1.19 → 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 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
- if (!config.auth) {
985
+ const configWithAuth = config;
986
+ if (!configWithAuth.auth) {
960
987
  return [name, config];
961
988
  }
962
- const authHeaders = mcpAuthToHeaders(config.auth);
963
- const { auth: _auth, ...rest } = config;
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
- ...config.headers
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
- yield* this.executeRealQuery(prompt, { ...options, model });
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: options.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
- totalCost: message.cost,
1102
- totalTokens: message.tokens ? message.tokens.input + message.tokens.output : void 0
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 { type: "session_init", sessionId: msg.session_id };
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 = ["project"];
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
  }
@@ -4641,6 +4745,12 @@ echo "[warmup] Warmup complete!"
4641
4745
  startPromise = null;
4642
4746
  /** Registered warmup specs by tag (e.g. agentId -> spec) */
4643
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;
4644
4754
  /** Consecutive warmup failure count (reset on success) */
4645
4755
  consecutiveFailures = 0;
4646
4756
  /** Timestamp of last warmup attempt — used for backoff */
@@ -4653,7 +4763,7 @@ echo "[warmup] Warmup complete!"
4653
4763
  this.config = {
4654
4764
  minPoolSize: config.minPoolSize ?? parseInt(process.env.SANDBOX_POOL_MIN_SIZE ?? "2"),
4655
4765
  maxPoolSize: config.maxPoolSize ?? parseInt(process.env.SANDBOX_POOL_MAX_SIZE ?? "5"),
4656
- sandboxTimeout: config.sandboxTimeout ?? parseInt(process.env.SANDBOX_TIMEOUT ?? "900"),
4766
+ sandboxTimeout: config.sandboxTimeout ?? parseInt(process.env.SANDBOX_TIMEOUT ?? "300"),
4657
4767
  expiryThresholdMs: config.expiryThresholdMs ?? parseInt(process.env.SANDBOX_EXPIRY_THRESHOLD_MS ?? "120000"),
4658
4768
  maintenanceIntervalMs: config.maintenanceIntervalMs ?? parseInt(process.env.SANDBOX_POOL_MAINTENANCE_MS ?? "30000"),
4659
4769
  runtime: config.runtime ?? "node22",
@@ -4859,18 +4969,28 @@ echo "[warmup] Warmup complete!"
4859
4969
  }
4860
4970
  /**
4861
4971
  * Register a warmup spec so the pool can pre-warm agent-specific sandboxes.
4862
- * If a spec with the same tag already exists, it is replaced.
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).
4863
4974
  * Triggers replenishment to warm a sandbox for this spec.
4864
4975
  */
4865
4976
  registerWarmupSpec(spec) {
4866
- const isNew = !this.warmupSpecs.has(spec.tag);
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;
4867
4983
  this.warmupSpecs.set(spec.tag, spec);
4868
- console.log(`[POOL] ${isNew ? "Registered" : "Updated"} warmup spec: ${spec.tag} (priority=${spec.priority})`);
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})`);
4869
4988
  this.emitMetric("spec_registered", {
4870
4989
  tag: spec.tag,
4871
4990
  priority: spec.priority,
4872
4991
  isNew,
4873
- totalSpecs: this.warmupSpecs.size
4992
+ totalSpecs: this.warmupSpecs.size,
4993
+ configHash: spec.configHash
4874
4994
  });
4875
4995
  if (this.running) {
4876
4996
  this.triggerReplenishment();
@@ -4997,20 +5117,41 @@ echo "[warmup] Warmup complete!"
4997
5117
  }
4998
5118
  let warmupTag;
4999
5119
  let agentSetupComplete = false;
5120
+ let warmupInstallRan = false;
5121
+ let warmupStartupRan = false;
5000
5122
  if (spec) {
5123
+ this.warmingTags.add(spec.tag);
5001
5124
  console.log(`[POOL] Running spec setup for tag=${spec.tag} on sandbox ${sandbox.sandboxId}...`);
5002
5125
  this.emitMetric("spec_setup_started", { tag: spec.tag, sandboxId: sandbox.sandboxId });
5003
5126
  const specStartTime = Date.now();
5004
5127
  try {
5005
- await spec.setup(sandbox);
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
+ });
5006
5141
  warmupTag = spec.tag;
5007
5142
  agentSetupComplete = true;
5143
+ if (setupResult && typeof setupResult === "object") {
5144
+ warmupInstallRan = setupResult.installRan === true;
5145
+ warmupStartupRan = setupResult.startupRan === true;
5146
+ }
5008
5147
  const specDuration = Date.now() - specStartTime;
5009
- console.log(`[POOL] Spec setup completed for tag=${spec.tag} on sandbox ${sandbox.sandboxId} (${specDuration}ms)`);
5148
+ console.log(`[POOL] Spec setup completed for tag=${spec.tag} on sandbox ${sandbox.sandboxId} (${specDuration}ms, install=${warmupInstallRan}, startup=${warmupStartupRan})`);
5010
5149
  this.emitMetric("spec_setup_completed", {
5011
5150
  tag: spec.tag,
5012
5151
  sandboxId: sandbox.sandboxId,
5013
- durationMs: specDuration
5152
+ durationMs: specDuration,
5153
+ warmupInstallRan,
5154
+ warmupStartupRan
5014
5155
  });
5015
5156
  } catch (specError) {
5016
5157
  const specDuration = Date.now() - specStartTime;
@@ -5024,6 +5165,8 @@ echo "[warmup] Warmup complete!"
5024
5165
  durationMs: specDuration,
5025
5166
  error: specErrorMessage
5026
5167
  });
5168
+ } finally {
5169
+ this.warmingTags.delete(spec.tag);
5027
5170
  }
5028
5171
  }
5029
5172
  const warmupTime = Date.now() - startTime;
@@ -5037,7 +5180,9 @@ echo "[warmup] Warmup complete!"
5037
5180
  eligible: true,
5038
5181
  lastHeartbeat: now,
5039
5182
  warmupTag,
5040
- agentSetupComplete
5183
+ agentSetupComplete,
5184
+ warmupInstallRan,
5185
+ warmupStartupRan
5041
5186
  };
5042
5187
  const tagInfo = warmupTag ? ` [tag=${warmupTag}]` : "";
5043
5188
  console.log(`[POOL] Warmup completed for ${sandbox.sandboxId} (took ${warmupTime}ms)${useTarball ? " [tarball]" : ""}${tagInfo}`);
@@ -5239,15 +5384,20 @@ echo "[warmup] Warmup complete!"
5239
5384
  }
5240
5385
  /**
5241
5386
  * Decide which specs to apply to new sandboxes during replenishment.
5242
- * Strategy: cover uncovered specs first (highest priority), then fill remaining as generic.
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
5243
5391
  * Returns an array of length `needed`, where each element is a spec or undefined (generic).
5244
5392
  */
5245
5393
  selectSpecsForReplenishment(needed) {
5246
- if (this.warmupSpecs.size === 0) {
5394
+ if (this.warmupSpecs.size === 0 || needed === 0) {
5247
5395
  return new Array(needed).fill(void 0);
5248
5396
  }
5397
+ const maxTaggedSlots = Math.max(0, needed - 1);
5249
5398
  const uncoveredSpecs = [];
5250
5399
  for (const spec of this.warmupSpecs.values()) {
5400
+ if (this.warmingTags.has(spec.tag)) continue;
5251
5401
  let hasCoverage = false;
5252
5402
  for (const pooled of this.pool.values()) {
5253
5403
  if (pooled.warmupTag === spec.tag && pooled.eligible && !pooled.assignedTo) {
@@ -5262,7 +5412,7 @@ echo "[warmup] Warmup complete!"
5262
5412
  uncoveredSpecs.sort((a, b) => b.priority - a.priority);
5263
5413
  const assignments = [];
5264
5414
  for (const spec of uncoveredSpecs) {
5265
- if (assignments.length >= needed) break;
5415
+ if (assignments.length >= maxTaggedSlots) break;
5266
5416
  assignments.push(spec);
5267
5417
  }
5268
5418
  while (assignments.length < needed) {
@@ -5270,6 +5420,27 @@ echo "[warmup] Warmup complete!"
5270
5420
  }
5271
5421
  return assignments;
5272
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
+ }
5273
5444
  /**
5274
5445
  * Destroy a sandbox and clean up
5275
5446
  */
@@ -5589,6 +5760,8 @@ async function getOrCreateSandbox(options) {
5589
5760
  const pooled = await pool.acquire(sessionId, preferTag);
5590
5761
  const tagInfo = pooled.warmupTag ? ` [tag=${pooled.warmupTag}, agentSetup=${pooled.agentSetupComplete}]` : "";
5591
5762
  console.log(`[SANDBOX] Acquired pre-warmed sandbox: ${pooled.sandboxId}${tagInfo}`);
5763
+ const installDone = pooled.warmupInstallRan === true;
5764
+ const startupDone = pooled.warmupStartupRan === true;
5592
5765
  const agentSetupDone = pooled.agentSetupComplete === true;
5593
5766
  const now2 = Date.now();
5594
5767
  const entry2 = {
@@ -5597,24 +5770,26 @@ async function getOrCreateSandbox(options) {
5597
5770
  createdAt: pooled.createdAt,
5598
5771
  lastUsedAt: now2,
5599
5772
  sdkInstalled: pooled.sdkInstalled,
5600
- startupScriptRan: agentSetupDone,
5601
- installScriptRan: agentSetupDone
5773
+ startupScriptRan: startupDone,
5774
+ installScriptRan: installDone
5602
5775
  };
5603
5776
  sandboxCache.set(sessionId, entry2);
5604
5777
  return {
5605
5778
  sandbox: pooled.sandbox,
5606
5779
  sandboxId: pooled.sandboxId,
5607
5780
  sdkInstalled: pooled.sdkInstalled,
5608
- startupScriptRan: agentSetupDone,
5781
+ startupScriptRan: startupDone,
5609
5782
  startupScriptHash: void 0,
5610
- installScriptRan: agentSetupDone,
5783
+ installScriptRan: installDone,
5611
5784
  installScriptHash: void 0,
5612
5785
  isNew: false,
5613
5786
  // Not new - came from pool
5614
5787
  configFileUrl: void 0,
5615
5788
  configInstalledAt: agentSetupDone ? now2 : void 0,
5616
5789
  warmupTag: pooled.warmupTag,
5617
- agentSetupComplete: pooled.agentSetupComplete
5790
+ agentSetupComplete: pooled.agentSetupComplete,
5791
+ warmupInstallRan: pooled.warmupInstallRan,
5792
+ warmupStartupRan: pooled.warmupStartupRan
5618
5793
  };
5619
5794
  } catch (error) {
5620
5795
  console.warn(
@@ -5959,6 +6134,13 @@ function createVercelSandboxExecutor(apiKey) {
5959
6134
  };
5960
6135
  }
5961
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
+ }
5962
6144
  const sessionId = options.harnessSessionId || `temp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
5963
6145
  try {
5964
6146
  const { sandbox, sdkInstalled, startupScriptRan, startupScriptHash: cachedScriptHash, configFileUrl: cachedConfigUrl } = await getOrCreateSandbox({
@@ -6100,9 +6282,13 @@ async function* executeInSandbox(prompt, apiKey, options) {
6100
6282
  permissionMode: options.permissionMode || "bypassPermissions",
6101
6283
  includePartialMessages: true
6102
6284
  };
6285
+ if (options.settingSources !== void 0) {
6286
+ sdkOptions.settingSources = options.settingSources;
6287
+ } else {
6288
+ sdkOptions.settingSources = ["project"];
6289
+ }
6103
6290
  const hasConfig = options.configFileUrl || cachedConfigUrl;
6104
6291
  if (hasConfig) {
6105
- sdkOptions.settingSources = ["project"];
6106
6292
  if (options.allowedTools && options.allowedTools.length > 0) {
6107
6293
  const allowedTools = [...options.allowedTools];
6108
6294
  if (!allowedTools.includes("Skill")) {
@@ -6138,12 +6324,47 @@ async function* executeInSandbox(prompt, apiKey, options) {
6138
6324
  if (options.resume) {
6139
6325
  sdkOptions.resume = options.resume;
6140
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
+ }
6141
6351
  const agentScript = `
6142
6352
  const { query } = require('@anthropic-ai/claude-agent-sdk');
6353
+ const fs = require('fs');
6143
6354
 
6144
6355
  const prompt = ${JSON.stringify(prompt)};
6145
6356
  const options = ${JSON.stringify(sdkOptions)};
6146
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
+
6147
6368
  let queryCompleted = false;
6148
6369
 
6149
6370
  async function run() {
@@ -6240,7 +6461,11 @@ SCRIPT_EOF`]
6240
6461
  });
6241
6462
  }
6242
6463
  if (event.type === "system" && event.subtype === "init") {
6243
- events.push({ type: "session_init", sessionId: event.session_id || "" });
6464
+ events.push({
6465
+ type: "session_init",
6466
+ sessionId: event.session_id || "",
6467
+ slashCommands: event.slash_commands
6468
+ });
6244
6469
  } else if (event.type === "stream_event" && event.event) {
6245
6470
  const streamEvent = event.event;
6246
6471
  if (streamEvent.type === "content_block_delta") {
@@ -6275,12 +6500,16 @@ SCRIPT_EOF`]
6275
6500
  }
6276
6501
  }
6277
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;
6278
6507
  events.push({
6279
6508
  type: "complete",
6280
6509
  sessionId: event.session_id,
6281
6510
  result: event.result,
6282
- totalCost: event.total_cost_usd,
6283
- totalTokens: event.total_tokens
6511
+ totalCost,
6512
+ totalTokens
6284
6513
  });
6285
6514
  }
6286
6515
  return events;
@@ -12859,6 +13088,7 @@ __export(schemas_exports, {
12859
13088
  RunAgentRequestSchema: () => RunAgentRequestSchema,
12860
13089
  SendMessageRequestSchema: () => SendMessageRequestSchema,
12861
13090
  SessionEndEventSchema: () => SessionEndEventSchema,
13091
+ SessionInitEventSchema: () => SessionInitEventSchema,
12862
13092
  SessionSchema: () => SessionSchema,
12863
13093
  SessionStartEventSchema: () => SessionStartEventSchema,
12864
13094
  SessionStatusSchema: () => SessionStatusSchema,
@@ -12878,7 +13108,7 @@ __export(schemas_exports, {
12878
13108
  TurnCompleteEventSchema: () => TurnCompleteEventSchema,
12879
13109
  UpdateAgentRequestSchema: () => UpdateAgentRequestSchema
12880
13110
  });
12881
- 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;
12882
13112
  var init_schemas = __esm({
12883
13113
  "src/server/openapi/schemas.ts"() {
12884
13114
  init_dist3();
@@ -13104,6 +13334,15 @@ var init_schemas = __esm({
13104
13334
  sessionId: zod.z.string().openapi({ description: "Session ID" }),
13105
13335
  claudeSessionId: zod.z.string().openapi({ description: "Claude SDK session ID" })
13106
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");
13107
13346
  TextDeltaEventSchema = zod.z.object({
13108
13347
  type: zod.z.literal("text_delta"),
13109
13348
  delta: zod.z.string().openapi({ description: "Text chunk", example: "Hello" })
@@ -13430,6 +13669,7 @@ var init_sessions2 = __esm({
13430
13669
  The stream emits the following event types:
13431
13670
 
13432
13671
  - \`session_start\` - Session started, includes sessionId and sdkSessionId
13672
+ - \`session_init\` - Claude SDK session initialized (includes slash commands)
13433
13673
  - \`text_delta\` - Text chunk being generated
13434
13674
  - \`thinking_delta\` - Thinking/reasoning text chunk
13435
13675
  - \`message\` - Complete message saved to storage
@@ -13878,6 +14118,7 @@ This is a convenience endpoint that combines session creation and message sendin
13878
14118
  The stream emits the following event types:
13879
14119
 
13880
14120
  - \`session_start\` - Session started
14121
+ - \`session_init\` - Claude SDK session initialized (includes slash commands)
13881
14122
  - \`text_delta\` - Text chunk being generated
13882
14123
  - \`thinking_delta\` - Thinking/reasoning text chunk
13883
14124
  - \`message\` - Complete message saved