@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 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
  }
@@ -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 ?? "900"),
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
- console.log(`[POOL] Acquired sandbox ${available.sandboxId} for session ${sessionId}`);
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
- console.log(`[POOL] No available sandbox, creating on-demand for session ${sessionId}...`);
4771
- const sandbox = await this.warmSandbox();
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
- console.log(`[POOL] Warmup completed for ${sandbox.sandboxId} (took ${warmupTime}ms)${useTarball ? " [tarball]" : ""}`);
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 best = null;
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 (!best || pooled.expiresAt > best.expiresAt) {
5045
- best = pooled;
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 best;
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
- console.log(`[POOL] Replenishment sandbox ready: ${sandbox.sandboxId}`);
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({ 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
+ });
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: event.total_cost_usd,
6116
- totalTokens: event.total_tokens
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;