@bike4mind/cli 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { $ as PermissionDeniedError, $t as extractSnippetMeta, A as FriendshipEvents, At as XAI_IMAGE_MODELS, B as InboxEvents, Bt as isZodError, C as CorruptedFileError, Ct as TransferCreditTransaction, D as FeedbackEvents, Dt as VIDEO_SIZE_CONSTRAINTS, E as FavoriteDocumentType, Et as UnprocessableEntityError, F as HTTPError, Ft as getViewById, G as LLMEvents, Gt as sanitizeTelemetryError, H as InviteEvents, Ht as obfuscateApiKey, I as HttpStatus, It as isGPTImage2Model, J as ModelBackend, Jt as substituteArguments$1, K as MiscEvents, Kt as secureParameters, L as ImageEditUsageTransaction, Lt as isGPTImageModel, M as GenerateImageToolCallSchema, Mt as dayjsConfig_default, N as GenericCreditAddTransaction, Nt as getAccessibleDataLakes, O as FileEvents, Ot as VideoGenerationUsageTransaction, P as GenericCreditDeductTransaction, Pt as getMcpProviderMetadata, Q as Permission, Qt as buildRateLimitLogEntry, R as ImageGenerationUsageTransaction, Rt as isModelAccessible, S as CompletionApiUsageTransaction, St as ToolUsageTransaction, T as ElabsEvents, Tt as UnauthorizedError, U as InviteType, Ut as parseSkillArguments, V as InternalServerError, Vt as mapMimeTypeToArtifactType, W as KnowledgeType, Wt as resolveNavigationIntents, X as OpenAIEmbeddingModel, Y as NotFoundError, Yt as toDataLakeConfig, Z as OpenAIImageGenerationInput, _ as BadRequestError, _t as SupportedFabFileMimeTypes, at as QuestMasterParamsSchema, b as ChatModels, bt as TextGenerationUsageTransaction, c as AiEvents, ct as RechartsChartTypeList, d as ApiKeyType, dt as ResearchTaskExecutionType, en as isNearLimit, et as ProfileEvents, f as AppFileEvents, ft as ResearchTaskPeriodicFrequencyType, g as BFL_SAFETY_TOLERANCE, gt as SubscriptionCreditTransaction, h as BFL_IMAGE_MODELS, ht as SpeechToTextUsageTransaction, it as PurchaseTransaction, j as GEMINI_IMAGE_MODELS, jt as b4mLLMTools, k as ForbiddenError, kt as VideoModels, l as ApiKeyEvents, lt as RegInviteEvents, m as AuthEvents, mt as SessionEvents, n as logger, nn as CollectionType, nt as PromptIntentSchema, ot as RealtimeVoiceUsageTransaction, p as ArtifactTypeSchema, pt as ResearchTaskType, q as ModalEvents, qt as settingsMap, rt as PromptMetaZodSchema, s as ALERT_THRESHOLDS, st as ReceivedCreditTransaction, t as ConfigStore, tn as parseRateLimitHeaders, tt as ProjectEvents, u as ApiKeyScope, ut as ResearchModeParamsSchema, vt as TagType, w as DashboardParamsSchema, wt as UiNavigationEvents, x as ClaudeArtifactMimeTypes, xt as TooManyRequestsError, y as ChatCompletionCreateInputSchema, yt as TaskScheduleHandler, z as ImageModels, zt as isSupportedEmbeddingModel } from "./ConfigStore-yJU1dxpP.mjs";
2
+ import { $ as PermissionDeniedError, $t as wrapUntrustedSkillBody, A as FriendshipEvents, At as XAI_IMAGE_MODELS, B as InboxEvents, Bt as isSupportedEmbeddingModel, C as CorruptedFileError, Ct as TransferCreditTransaction, D as FeedbackEvents, Dt as VIDEO_SIZE_CONSTRAINTS, E as FavoriteDocumentType, Et as UnprocessableEntityError, F as HTTPError, Ft as getViewById, G as LLMEvents, Gt as resolveNavigationIntents, H as InviteEvents, Ht as mapMimeTypeToArtifactType, I as HttpStatus, It as intersectAllowedTools, J as ModelBackend, Jt as settingsMap, K as MiscEvents, Kt as sanitizeTelemetryError, L as ImageEditUsageTransaction, Lt as isGPTImage2Model, M as GenerateImageToolCallSchema, Mt as dayjsConfig_default, N as GenericCreditAddTransaction, Nt as getAccessibleDataLakes, O as FileEvents, Ot as VideoGenerationUsageTransaction, P as GenericCreditDeductTransaction, Pt as getMcpProviderMetadata, Q as Permission, R as ImageGenerationUsageTransaction, Rt as isGPTImageModel, S as CompletionApiUsageTransaction, St as ToolUsageTransaction, T as ElabsEvents, Tt as UnauthorizedError, U as InviteType, Ut as obfuscateApiKey, V as InternalServerError, Vt as isZodError, W as KnowledgeType, Wt as parseSkillArguments, X as OpenAIEmbeddingModel, Xt as toDataLakeConfig, Y as NotFoundError, Yt as substituteArguments$1, Z as OpenAIImageGenerationInput, _ as BadRequestError, _t as SupportedFabFileMimeTypes, at as QuestMasterParamsSchema, b as ChatModels, bt as TextGenerationUsageTransaction, c as AiEvents, ct as RechartsChartTypeList, d as ApiKeyType, dt as ResearchTaskExecutionType, en as buildRateLimitLogEntry, et as ProfileEvents, f as AppFileEvents, ft as ResearchTaskPeriodicFrequencyType, g as BFL_SAFETY_TOLERANCE, gt as SubscriptionCreditTransaction, h as BFL_IMAGE_MODELS, ht as SpeechToTextUsageTransaction, in as CollectionType, it as PurchaseTransaction, j as GEMINI_IMAGE_MODELS, jt as b4mLLMTools, k as ForbiddenError, kt as VideoModels, l as ApiKeyEvents, lt as RegInviteEvents, m as AuthEvents, mt as SessionEvents, n as logger, nn as isNearLimit, nt as PromptIntentSchema, ot as RealtimeVoiceUsageTransaction, p as ArtifactTypeSchema, pt as ResearchTaskType, q as ModalEvents, qt as secureParameters, rn as parseRateLimitHeaders, rt as PromptMetaZodSchema, s as ALERT_THRESHOLDS, st as ReceivedCreditTransaction, t as ConfigStore, tn as extractSnippetMeta, tt as ProjectEvents, u as ApiKeyScope, ut as ResearchModeParamsSchema, vt as TagType, w as DashboardParamsSchema, wt as UiNavigationEvents, x as ClaudeArtifactMimeTypes, xt as TooManyRequestsError, y as ChatCompletionCreateInputSchema, yt as TaskScheduleHandler, z as ImageModels, zt as isModelAccessible } from "./ConfigStore-DL7p3ZiY.mjs";
3
3
  import { a as isUserLockedOut, c as userCanDisableMFA, d as userRequiresMFA, f as verifyBackupCode, i as getLockoutTimeRemaining, l as userEligibleForMFA, n as generateBackupCodes, o as recordFailedAttempt, p as verifyTOTPToken, r as generateTOTPSetup, s as shouldResetFailedAttempts, t as clearFailedAttempts, u as userHasMFAConfigured } from "./utils-fqhJmuB8.mjs";
4
4
  import { n as isPathAllowed, t as assertPathAllowed } from "./pathValidation-D8tjkQXE-1HwvsuYT.mjs";
5
- import { t as version } from "./package-B3f78bSl.mjs";
5
+ import { t as version } from "./package-WpJJMKy_.mjs";
6
6
  import { execFile, execFileSync, spawn } from "child_process";
7
7
  import crypto, { createHash, randomBytes, randomUUID } from "crypto";
8
8
  import { existsSync, promises, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "fs";
@@ -7086,7 +7086,9 @@ function shouldTriggerFallback(error) {
7086
7086
  "api key",
7087
7087
  "quota exceeded",
7088
7088
  "model not found",
7089
- "does not exist"
7089
+ "does not exist",
7090
+ "is not available",
7091
+ "safety classifier refusal"
7090
7092
  ].some((trigger) => message.includes(trigger));
7091
7093
  }
7092
7094
  /**
@@ -7149,17 +7151,20 @@ function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logg
7149
7151
  "claude-opus-4-8",
7150
7152
  "claude-opus-4-7",
7151
7153
  "claude-opus-4-6",
7154
+ "claude-sonnet-5",
7152
7155
  "claude-sonnet-4-6",
7153
7156
  "gpt-5"
7154
7157
  ],
7155
7158
  "claude-opus-4-8": [
7156
7159
  "claude-opus-4-7",
7157
7160
  "claude-opus-4-6",
7161
+ "claude-sonnet-5",
7158
7162
  "claude-sonnet-4-6",
7159
7163
  "gpt-5"
7160
7164
  ],
7161
7165
  "claude-opus-4-7": [
7162
7166
  "claude-opus-4-6",
7167
+ "claude-sonnet-5",
7163
7168
  "claude-sonnet-4-6",
7164
7169
  "gpt-5"
7165
7170
  ],
@@ -7170,17 +7175,26 @@ function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logg
7170
7175
  "claude-haiku-4-5-20251001"
7171
7176
  ],
7172
7177
  "claude-opus-4-6": [
7178
+ "claude-sonnet-5",
7173
7179
  "claude-sonnet-4-6",
7174
7180
  "claude-sonnet-4-5-20250929",
7175
7181
  "gpt-5",
7176
7182
  "claude-haiku-4-5-20251001"
7177
7183
  ],
7184
+ "claude-sonnet-5": [
7185
+ "claude-sonnet-4-6",
7186
+ "claude-sonnet-4-5-20250929",
7187
+ "gpt-5"
7188
+ ],
7189
+ "global.anthropic.claude-sonnet-5": ["global.anthropic.claude-sonnet-4-6", "gpt-5"],
7178
7190
  "claude-sonnet-4-6": [
7191
+ "claude-sonnet-5",
7179
7192
  "claude-sonnet-4-5-20250929",
7180
7193
  "claude-opus-4-5-20251101",
7181
7194
  "gpt-5"
7182
7195
  ],
7183
7196
  "claude-sonnet-4-5-20250929": [
7197
+ "claude-sonnet-5",
7184
7198
  "claude-sonnet-4-6",
7185
7199
  "claude-haiku-4-5-20251001",
7186
7200
  "gpt-5"
@@ -7212,7 +7226,7 @@ function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logg
7212
7226
  ],
7213
7227
  "gpt-4o-mini": ["claude-haiku-4-5-20251001", "gpt-3.5-turbo"]
7214
7228
  }[originalModel.id] || [];
7215
- if (preferences.length === 0) preferences.push("claude-sonnet-4-6", "claude-sonnet-4-5-20250929", "gpt-5", "claude-haiku-4-5-20251001");
7229
+ if (preferences.length === 0) preferences.push("claude-sonnet-5", "claude-sonnet-4-6", "claude-sonnet-4-5-20250929", "gpt-5", "claude-haiku-4-5-20251001");
7216
7230
  for (const modelId of preferences) {
7217
7231
  const fallbackModel = availableModels.find((m) => m.id === modelId);
7218
7232
  if (fallbackModel) {
@@ -7869,7 +7883,7 @@ function classifyError(error) {
7869
7883
  return "unhandled_rejection";
7870
7884
  }
7871
7885
  /**
7872
- * Builds a structured log entry for Lambda error handlers.
7886
+ * Builds a structured log entry for the global error handlers.
7873
7887
  */
7874
7888
  function buildLogEntry(error, category) {
7875
7889
  return {
@@ -7884,29 +7898,33 @@ function buildLogEntry(error, category) {
7884
7898
  /**
7885
7899
  * Registers global handlers for unhandled promise rejections and uncaught exceptions.
7886
7900
  *
7887
- * IMPORTANT: This is for OBSERVABILITY ONLY. AWS Lambda's default handlers will still
7888
- * cause the invocation to fail. These handlers allow us to:
7889
- * 1. Log errors with structured context before Lambda exits
7901
+ * Provider-agnostic core shared by Lambda handlers and the always-on
7902
+ * QuestProcessorService container. The handlers:
7903
+ * 1. Log errors with structured context
7890
7904
  * 2. Classify error types for easier debugging
7891
- * 3. Distinguish network errors from application errors
7905
+ * 3. Distinguish network errors from application errors and swallow the network
7906
+ * class (mark the rejection handled / don't re-throw) so a single transient
7907
+ * network failure doesn't escalate
7892
7908
  *
7893
- * DO NOT call process.exit() - let Lambda manage the lifecycle.
7909
+ * CRITICAL for long-running processes: these handlers NEVER call process.exit().
7910
+ * On AWS Lambda that lets the runtime own the lifecycle. On a long-running Fargate
7911
+ * task it means one quest's mid-stream EPIPE / orphaned undici rejection (#8055,
7912
+ * #8197) is logged and swallowed instead of taking Node's default
7913
+ * `uncaughtException` path, which would terminate the whole container and kill
7914
+ * every other in-flight quest.
7894
7915
  *
7895
7916
  * @param logger - Optional logger instance. Falls back to console if not provided.
7917
+ * @param context - Short label used in log message prefixes (default 'Process').
7896
7918
  *
7897
7919
  * @example
7898
7920
  * ```typescript
7899
- * import { registerLambdaErrorHandlers } from '@bike4mind/utils';
7921
+ * import { registerProcessErrorHandlers } from '@bike4mind/utils';
7900
7922
  *
7901
- * // Call at the top of your Lambda handler file (after imports)
7902
- * registerLambdaErrorHandlers();
7903
- *
7904
- * export const handler = async (event) => {
7905
- * // Your handler code
7906
- * };
7923
+ * // Call once at startup, after imports
7924
+ * registerProcessErrorHandlers(logger, 'QuestProcessorService');
7907
7925
  * ```
7908
7926
  */
7909
- function registerLambdaErrorHandlers(logger) {
7927
+ function registerProcessErrorHandlers(logger, context = "Process") {
7910
7928
  if (isRegistered) return;
7911
7929
  isRegistered = true;
7912
7930
  const log = logger || console;
@@ -7915,17 +7933,27 @@ function registerLambdaErrorHandlers(logger) {
7915
7933
  const isNetworkError = category.startsWith("network_");
7916
7934
  const logEntry = buildLogEntry(reason, category);
7917
7935
  if (isNetworkError) {
7918
- log.warn("[Lambda] Network error (unhandled rejection)", logEntry);
7936
+ log.warn(`[${context}] Network error (unhandled rejection)`, logEntry);
7919
7937
  promise.catch(() => {});
7920
- } else log.error("[Lambda] Unhandled promise rejection", logEntry);
7938
+ } else log.error(`[${context}] Unhandled promise rejection`, logEntry);
7921
7939
  });
7922
7940
  process.on("uncaughtException", (error) => {
7923
7941
  const category = classifyError(error);
7924
- if (category.startsWith("network_")) log.warn("[Lambda] Network error (uncaught exception)", buildLogEntry(error, category));
7925
- else log.error("[Lambda] Uncaught exception", buildLogEntry(error, "uncaught_exception"));
7942
+ if (category.startsWith("network_")) log.warn(`[${context}] Network error (uncaught exception)`, buildLogEntry(error, category));
7943
+ else log.error(`[${context}] Uncaught exception`, buildLogEntry(error, "uncaught_exception"));
7926
7944
  });
7927
7945
  }
7928
7946
  /**
7947
+ * Back-compat alias for Lambda handlers. Preserves the historical `[Lambda]` log
7948
+ * prefix so existing log-based filters/alerts are unaffected. New code on
7949
+ * non-Lambda runtimes should call {@link registerProcessErrorHandlers} directly.
7950
+ *
7951
+ * @param logger - Optional logger instance. Falls back to console if not provided.
7952
+ */
7953
+ function registerLambdaErrorHandlers(logger) {
7954
+ registerProcessErrorHandlers(logger, "Lambda");
7955
+ }
7956
+ /**
7929
7957
  * Reset the registration state. Only for testing purposes.
7930
7958
  * @internal
7931
7959
  */
@@ -8055,6 +8083,7 @@ __reExport(/* @__PURE__ */ __exportAll$1({
8055
8083
  processFabFilesServer: () => processFabFilesServer,
8056
8084
  processUrlsFromPrompt: () => processUrlsFromPrompt,
8057
8085
  registerLambdaErrorHandlers: () => registerLambdaErrorHandlers,
8086
+ registerProcessErrorHandlers: () => registerProcessErrorHandlers,
8058
8087
  secureParameters: () => secureParameters,
8059
8088
  sendToConnection: () => sendToConnection,
8060
8089
  shouldTriggerFallback: () => shouldTriggerFallback,
@@ -8876,22 +8905,22 @@ function makeSchedulingResult(args) {
8876
8905
  const OperationSchema = z.object({
8877
8906
  jobId: z.number(),
8878
8907
  machineId: z.number(),
8879
- duration: z.number().positive()
8908
+ duration: z.number().positive().max(1e6)
8880
8909
  });
8881
8910
  const JobSchema = z.object({
8882
8911
  id: z.number(),
8883
- name: z.string(),
8884
- operations: z.array(OperationSchema)
8912
+ name: z.string().max(200),
8913
+ operations: z.array(OperationSchema).max(1e3)
8885
8914
  });
8886
8915
  const MachineSchema = z.object({
8887
8916
  id: z.number(),
8888
- name: z.string()
8917
+ name: z.string().max(200)
8889
8918
  });
8890
8919
  const SchedulingProblemSchema = z.object({
8891
- name: z.string(),
8892
- description: z.string().optional(),
8893
- jobs: z.array(JobSchema),
8894
- machines: z.array(MachineSchema),
8920
+ name: z.string().max(200),
8921
+ description: z.string().max(2e3).optional(),
8922
+ jobs: z.array(JobSchema).max(1e3),
8923
+ machines: z.array(MachineSchema).max(1e3),
8895
8924
  solvedJobUrl: z.string().url().optional()
8896
8925
  });
8897
8926
  const SolverIdSchema = z.enum([
@@ -8955,13 +8984,18 @@ const greedySolver = {
8955
8984
  totalIterations: totalOps
8956
8985
  });
8957
8986
  }
8987
+ const makespan = Math.max(...schedule.map((op) => op.endTime));
8958
8988
  return makeSchedulingResult({
8959
8989
  solverId: "greedy",
8960
8990
  solverName: "Greedy (Earliest Start)",
8961
- makespan: Math.max(...schedule.map((op) => op.endTime)),
8991
+ makespan,
8962
8992
  schedule,
8963
8993
  elapsedMs: performance.now() - startTime,
8964
- iterations: totalOps
8994
+ iterations: totalOps,
8995
+ trace: [{
8996
+ solution: schedule,
8997
+ objective: makespan
8998
+ }]
8965
8999
  });
8966
9000
  }
8967
9001
  };
@@ -9092,6 +9126,34 @@ function validateScheduleFeasibility(schedule) {
9092
9126
  return true;
9093
9127
  }
9094
9128
  /**
9129
+ * Append a best-so-far snapshot, thinning the early history when over cap so the
9130
+ * trace stays bounded while preserving the first point and recent progress.
9131
+ * (Same thinning policy as the family engine's pushTrace.)
9132
+ */
9133
+ function pushScheduleTrace(trace, schedule, makespan) {
9134
+ trace.push({
9135
+ solution: schedule,
9136
+ objective: makespan
9137
+ });
9138
+ if (trace.length > 48) {
9139
+ const first = trace[0];
9140
+ const rest = trace.slice(1).filter((_, i) => i % 2 === 1);
9141
+ trace.length = 0;
9142
+ trace.push(first, ...rest);
9143
+ }
9144
+ }
9145
+ const nowMs = () => typeof performance !== "undefined" ? performance.now() : Date.now();
9146
+ /**
9147
+ * Combine the timeoutMs budget with cooperative cancellation into one stop check
9148
+ * that iterative solvers poll between moves — so a UI cancel or a timeout actually
9149
+ * halts a long run instead of being decorative. `defaultTimeoutMs` preserves each
9150
+ * solver's historical default when the caller omits timeoutMs.
9151
+ */
9152
+ function makeScheduleStopper(options, startTime, defaultTimeoutMs) {
9153
+ const deadline = startTime + (options?.timeoutMs ?? defaultTimeoutMs);
9154
+ return () => (options?.shouldCancel?.() ?? false) || nowMs() > deadline;
9155
+ }
9156
+ /**
9095
9157
  * Simple seeded pseudo-random number generator (mulberry32).
9096
9158
  */
9097
9159
  function createRng$1(seed) {
@@ -9127,19 +9189,25 @@ const naiveSolver = {
9127
9189
  description: "Evaluates all permutations to find the optimal schedule. Only feasible for small problems.",
9128
9190
  async solve(problem, options) {
9129
9191
  const startTime = performance.now();
9192
+ const stop = makeScheduleStopper(options, startTime, 15e3);
9130
9193
  const ops = buildOperationList(problem);
9131
9194
  const totalPerms = factorial(ops.length);
9132
9195
  const cappedTotal = Math.min(totalPerms, MAX_PERMUTATIONS);
9133
- let bestMakespan = Infinity;
9134
9196
  let bestSchedule = decodePermutation(ops, problem);
9197
+ let bestMakespan = computeMakespan(bestSchedule);
9135
9198
  let count = 0;
9199
+ const trace = [{
9200
+ solution: bestSchedule,
9201
+ objective: bestMakespan
9202
+ }];
9136
9203
  for (const perm of permutations(ops)) {
9137
- if (count >= MAX_PERMUTATIONS) break;
9204
+ if (count >= MAX_PERMUTATIONS || stop()) break;
9138
9205
  const schedule = decodePermutation(perm, problem);
9139
9206
  const makespan = computeMakespan(schedule);
9140
9207
  if (makespan < bestMakespan) {
9141
9208
  bestMakespan = makespan;
9142
9209
  bestSchedule = schedule;
9210
+ pushScheduleTrace(trace, bestSchedule, bestMakespan);
9143
9211
  }
9144
9212
  count++;
9145
9213
  if (options?.onProgress && count % 100 === 0) options.onProgress({
@@ -9163,7 +9231,8 @@ const naiveSolver = {
9163
9231
  makespan: bestMakespan,
9164
9232
  schedule: bestSchedule,
9165
9233
  elapsedMs: performance.now() - startTime,
9166
- iterations: count
9234
+ iterations: count,
9235
+ trace
9167
9236
  });
9168
9237
  }
9169
9238
  };
@@ -9188,18 +9257,22 @@ const randomRestartSolver = {
9188
9257
  description: "Multiple random starts with local search improvement via operation swaps.",
9189
9258
  async solve(problem, options) {
9190
9259
  const startTime = performance.now();
9191
- const timeoutMs = options?.timeoutMs ?? 1e4;
9260
+ const stop = makeScheduleStopper(options, startTime, 1e4);
9192
9261
  const rng = createRng$1(42);
9193
- let globalBestMakespan = Infinity;
9194
9262
  let globalBestSchedule = decodePermutation(randomPermutation(problem, rng), problem);
9263
+ let globalBestMakespan = computeMakespan(globalBestSchedule);
9195
9264
  let totalIterations = 0;
9265
+ const trace = [{
9266
+ solution: globalBestSchedule,
9267
+ objective: globalBestMakespan
9268
+ }];
9196
9269
  for (let restart = 0; restart < RESTARTS; restart++) {
9197
- if (performance.now() - startTime > timeoutMs) break;
9270
+ if (stop()) break;
9198
9271
  let currentPerm = randomPermutation(problem, rng);
9199
9272
  let currentSchedule = decodePermutation(currentPerm, problem);
9200
9273
  let currentMakespan = computeMakespan(currentSchedule);
9201
9274
  for (let i = 0; i < HILL_CLIMB_ITERATIONS; i++) {
9202
- if (performance.now() - startTime > timeoutMs) break;
9275
+ if (stop()) break;
9203
9276
  const neighborPerm = swapNeighbor(currentPerm, rng);
9204
9277
  const neighborSchedule = decodePermutation(neighborPerm, problem);
9205
9278
  const neighborMakespan = computeMakespan(neighborSchedule);
@@ -9213,6 +9286,7 @@ const randomRestartSolver = {
9213
9286
  if (currentMakespan < globalBestMakespan) {
9214
9287
  globalBestMakespan = currentMakespan;
9215
9288
  globalBestSchedule = currentSchedule;
9289
+ pushScheduleTrace(trace, globalBestSchedule, globalBestMakespan);
9216
9290
  }
9217
9291
  options?.onProgress?.({
9218
9292
  solverId: "random-restart",
@@ -9228,7 +9302,8 @@ const randomRestartSolver = {
9228
9302
  makespan: globalBestMakespan,
9229
9303
  schedule: globalBestSchedule,
9230
9304
  elapsedMs: performance.now() - startTime,
9231
- iterations: totalIterations
9305
+ iterations: totalIterations,
9306
+ trace
9232
9307
  });
9233
9308
  }
9234
9309
  };
@@ -9265,17 +9340,21 @@ function createSASolver(config) {
9265
9340
  description: config.description,
9266
9341
  async solve(problem, options) {
9267
9342
  const startTime = performance.now();
9268
- const timeoutMs = options?.timeoutMs ?? 3e4;
9343
+ const stop = makeScheduleStopper(options, startTime, 3e4);
9269
9344
  const rng = createRng$1(42);
9270
9345
  let currentPerm = randomPermutation(problem, rng);
9271
9346
  let currentSchedule = decodePermutation(currentPerm, problem);
9272
9347
  let currentMakespan = computeMakespan(currentSchedule);
9273
9348
  let bestSchedule = currentSchedule;
9274
9349
  let bestMakespan = currentMakespan;
9350
+ const trace = [{
9351
+ solution: bestSchedule,
9352
+ objective: bestMakespan
9353
+ }];
9275
9354
  let temperature = config.initialTemp;
9276
9355
  const progressInterval = Math.max(1, Math.floor(config.iterations / 100));
9277
9356
  for (let i = 0; i < config.iterations; i++) {
9278
- if (performance.now() - startTime > timeoutMs) break;
9357
+ if (stop()) break;
9279
9358
  const neighborPerm = swapNeighbor(currentPerm, rng);
9280
9359
  const neighborSchedule = decodePermutation(neighborPerm, problem);
9281
9360
  const neighborMakespan = computeMakespan(neighborSchedule);
@@ -9287,6 +9366,7 @@ function createSASolver(config) {
9287
9366
  if (currentMakespan < bestMakespan) {
9288
9367
  bestSchedule = currentSchedule;
9289
9368
  bestMakespan = currentMakespan;
9369
+ pushScheduleTrace(trace, bestSchedule, bestMakespan);
9290
9370
  }
9291
9371
  }
9292
9372
  temperature *= config.coolingRate;
@@ -9311,7 +9391,8 @@ function createSASolver(config) {
9311
9391
  makespan: bestMakespan,
9312
9392
  schedule: bestSchedule,
9313
9393
  elapsedMs: performance.now() - startTime,
9314
- iterations: config.iterations
9394
+ iterations: config.iterations,
9395
+ trace
9315
9396
  });
9316
9397
  }
9317
9398
  };
@@ -9326,7 +9407,7 @@ const simulatedAnnealingLargeSolver = createSASolver(SA_CONFIGS[2]);
9326
9407
  * forbidden for a period. This prevents cycling and forces exploration
9327
9408
  * of new regions of the solution space.
9328
9409
  */
9329
- const ITERATIONS$1 = 1e4;
9410
+ const ITERATIONS$2 = 1e4;
9330
9411
  const TABU_TENURE = 15;
9331
9412
  const NEIGHBORHOOD_SIZE = 20;
9332
9413
  const tabuSolver = {
@@ -9335,17 +9416,21 @@ const tabuSolver = {
9335
9416
  description: "Hill climbing with short-term memory to avoid revisiting recent solutions.",
9336
9417
  async solve(problem, options) {
9337
9418
  const startTime = performance.now();
9338
- const timeoutMs = options?.timeoutMs ?? 15e3;
9419
+ const stop = makeScheduleStopper(options, startTime, 15e3);
9339
9420
  const rng = createRng$1(42);
9340
9421
  let currentPerm = randomPermutation(problem, rng);
9341
9422
  let currentSchedule = decodePermutation(currentPerm, problem);
9342
9423
  let currentMakespan = computeMakespan(currentSchedule);
9343
9424
  let bestSchedule = currentSchedule;
9344
9425
  let bestMakespan = currentMakespan;
9426
+ const trace = [{
9427
+ solution: bestSchedule,
9428
+ objective: bestMakespan
9429
+ }];
9345
9430
  const tabuList = /* @__PURE__ */ new Map();
9346
- const progressInterval = Math.max(1, Math.floor(ITERATIONS$1 / 100));
9347
- for (let iter = 0; iter < ITERATIONS$1; iter++) {
9348
- if (performance.now() - startTime > timeoutMs) break;
9431
+ const progressInterval = Math.max(1, Math.floor(ITERATIONS$2 / 100));
9432
+ for (let iter = 0; iter < ITERATIONS$2; iter++) {
9433
+ if (stop()) break;
9349
9434
  let bestNeighborPerm = null;
9350
9435
  let bestNeighborMakespan = Infinity;
9351
9436
  let bestSwapKey = "";
@@ -9371,6 +9456,7 @@ const tabuSolver = {
9371
9456
  if (currentMakespan < bestMakespan) {
9372
9457
  bestMakespan = currentMakespan;
9373
9458
  bestSchedule = currentSchedule;
9459
+ pushScheduleTrace(trace, bestSchedule, bestMakespan);
9374
9460
  }
9375
9461
  }
9376
9462
  if (iter % 100 === 0) {
@@ -9378,18 +9464,18 @@ const tabuSolver = {
9378
9464
  }
9379
9465
  if (options?.onProgress && iter % progressInterval === 0) options.onProgress({
9380
9466
  solverId: "tabu",
9381
- percentage: Math.round(iter / ITERATIONS$1 * 100),
9467
+ percentage: Math.round(iter / ITERATIONS$2 * 100),
9382
9468
  bestObjective: bestMakespan,
9383
9469
  currentIteration: iter,
9384
- totalIterations: ITERATIONS$1
9470
+ totalIterations: ITERATIONS$2
9385
9471
  });
9386
9472
  }
9387
9473
  options?.onProgress?.({
9388
9474
  solverId: "tabu",
9389
9475
  percentage: 100,
9390
9476
  bestObjective: bestMakespan,
9391
- currentIteration: ITERATIONS$1,
9392
- totalIterations: ITERATIONS$1
9477
+ currentIteration: ITERATIONS$2,
9478
+ totalIterations: ITERATIONS$2
9393
9479
  });
9394
9480
  return makeSchedulingResult({
9395
9481
  solverId: "tabu",
@@ -9397,7 +9483,8 @@ const tabuSolver = {
9397
9483
  makespan: bestMakespan,
9398
9484
  schedule: bestSchedule,
9399
9485
  elapsedMs: performance.now() - startTime,
9400
- iterations: ITERATIONS$1
9486
+ iterations: ITERATIONS$2,
9487
+ trace
9401
9488
  });
9402
9489
  }
9403
9490
  };
@@ -9417,7 +9504,7 @@ const geneticAlgorithmSolver = {
9417
9504
  description: "Population-based search with tournament selection, crossover, and mutation.",
9418
9505
  async solve(problem, options) {
9419
9506
  const startTime = performance.now();
9420
- const timeoutMs = options?.timeoutMs ?? 2e4;
9507
+ const stop = makeScheduleStopper(options, startTime, 2e4);
9421
9508
  const rng = createRng$1(42);
9422
9509
  let population = [];
9423
9510
  for (let i = 0; i < POPULATION_SIZE; i++) {
@@ -9429,8 +9516,12 @@ const geneticAlgorithmSolver = {
9429
9516
  });
9430
9517
  }
9431
9518
  let bestIndividual = population.reduce((best, ind) => ind.makespan < best.makespan ? ind : best);
9519
+ const trace = [{
9520
+ solution: decodePermutation(bestIndividual.perm, problem),
9521
+ objective: bestIndividual.makespan
9522
+ }];
9432
9523
  for (let gen = 0; gen < GENERATIONS; gen++) {
9433
- if (performance.now() - startTime > timeoutMs) break;
9524
+ if (stop()) break;
9434
9525
  const newPopulation = [];
9435
9526
  newPopulation.push({ ...bestIndividual });
9436
9527
  while (newPopulation.length < POPULATION_SIZE) {
@@ -9446,7 +9537,10 @@ const geneticAlgorithmSolver = {
9446
9537
  }
9447
9538
  population = newPopulation;
9448
9539
  const genBest = population.reduce((best, ind) => ind.makespan < best.makespan ? ind : best);
9449
- if (genBest.makespan < bestIndividual.makespan) bestIndividual = genBest;
9540
+ if (genBest.makespan < bestIndividual.makespan) {
9541
+ bestIndividual = genBest;
9542
+ pushScheduleTrace(trace, decodePermutation(bestIndividual.perm, problem), bestIndividual.makespan);
9543
+ }
9450
9544
  options?.onProgress?.({
9451
9545
  solverId: "genetic-algorithm",
9452
9546
  percentage: Math.round((gen + 1) / GENERATIONS * 100),
@@ -9462,7 +9556,8 @@ const geneticAlgorithmSolver = {
9462
9556
  makespan: bestIndividual.makespan,
9463
9557
  schedule: bestSchedule,
9464
9558
  elapsedMs: performance.now() - startTime,
9465
- iterations: GENERATIONS * POPULATION_SIZE
9559
+ iterations: GENERATIONS * POPULATION_SIZE,
9560
+ trace
9466
9561
  });
9467
9562
  }
9468
9563
  };
@@ -9507,10 +9602,10 @@ function crossover(parent1, parent2, rng) {
9507
9602
  * assignments) that appear in good solutions and evaporate over time.
9508
9603
  */
9509
9604
  const NUM_ANTS = 20;
9510
- const ITERATIONS = 100;
9605
+ const ITERATIONS$1 = 100;
9511
9606
  const ALPHA = 1;
9512
9607
  const BETA = 2;
9513
- const EVAPORATION = .1;
9608
+ const EVAPORATION$1 = .1;
9514
9609
  const Q = 100;
9515
9610
  const antColonySolver = {
9516
9611
  id: "ant-colony",
@@ -9518,7 +9613,7 @@ const antColonySolver = {
9518
9613
  description: "Swarm intelligence using pheromone trails to guide solution construction.",
9519
9614
  async solve(problem, options) {
9520
9615
  const startTime = performance.now();
9521
- const timeoutMs = options?.timeoutMs ?? 2e4;
9616
+ const stop = makeScheduleStopper(options, startTime, 2e4);
9522
9617
  const rng = createRng$1(42);
9523
9618
  const allOps = buildOperationList(problem);
9524
9619
  const numOps = allOps.length;
@@ -9526,10 +9621,14 @@ const antColonySolver = {
9526
9621
  const heuristic = allOps.map((ref) => {
9527
9622
  return 1 / problem.jobs.find((j) => j.id === ref.jobId).operations[ref.opIndex].duration;
9528
9623
  });
9529
- let bestMakespan = Infinity;
9530
9624
  let bestSchedule = decodePermutation(allOps, problem);
9531
- for (let iter = 0; iter < ITERATIONS; iter++) {
9532
- if (performance.now() - startTime > timeoutMs) break;
9625
+ let bestMakespan = computeMakespan(bestSchedule);
9626
+ const trace = [{
9627
+ solution: bestSchedule,
9628
+ objective: bestMakespan
9629
+ }];
9630
+ for (let iter = 0; iter < ITERATIONS$1; iter++) {
9631
+ if (stop()) break;
9533
9632
  const antSolutions = [];
9534
9633
  for (let ant = 0; ant < NUM_ANTS; ant++) {
9535
9634
  const perm = constructSolution(allOps, pheromone, heuristic, rng);
@@ -9542,9 +9641,10 @@ const antColonySolver = {
9542
9641
  if (makespan < bestMakespan) {
9543
9642
  bestMakespan = makespan;
9544
9643
  bestSchedule = schedule;
9644
+ pushScheduleTrace(trace, bestSchedule, bestMakespan);
9545
9645
  }
9546
9646
  }
9547
- for (let i = 0; i < numOps; i++) for (let j = 0; j < numOps; j++) pheromone[i][j] *= 1 - EVAPORATION;
9647
+ for (let i = 0; i < numOps; i++) for (let j = 0; j < numOps; j++) pheromone[i][j] *= 1 - EVAPORATION$1;
9548
9648
  for (const solution of antSolutions) {
9549
9649
  const deposit = Q / solution.makespan;
9550
9650
  for (let pos = 0; pos < solution.perm.length; pos++) {
@@ -9554,10 +9654,10 @@ const antColonySolver = {
9554
9654
  }
9555
9655
  options?.onProgress?.({
9556
9656
  solverId: "ant-colony",
9557
- percentage: Math.round((iter + 1) / ITERATIONS * 100),
9657
+ percentage: Math.round((iter + 1) / ITERATIONS$1 * 100),
9558
9658
  bestObjective: bestMakespan,
9559
9659
  currentIteration: iter + 1,
9560
- totalIterations: ITERATIONS
9660
+ totalIterations: ITERATIONS$1
9561
9661
  });
9562
9662
  }
9563
9663
  return makeSchedulingResult({
@@ -9566,7 +9666,8 @@ const antColonySolver = {
9566
9666
  makespan: bestMakespan,
9567
9667
  schedule: bestSchedule,
9568
9668
  elapsedMs: performance.now() - startTime,
9569
- iterations: ITERATIONS * NUM_ANTS
9669
+ iterations: ITERATIONS$1 * NUM_ANTS,
9670
+ trace
9570
9671
  });
9571
9672
  }
9572
9673
  };
@@ -9813,23 +9914,18 @@ function validateSchedule(problem, schedule) {
9813
9914
  errors
9814
9915
  };
9815
9916
  }
9816
- const MAX_BINARY_VARS = 150;
9817
- const MAX_TOTAL_OPS = 80;
9818
- const MAX_OPS_PER_MACHINE = 8;
9819
- /**
9820
- * Load HiGHS - creates FRESH instance each time.
9821
- *
9822
- * We intentionally do NOT cache the HiGHS instance.
9823
- * Caching causes WASM corruption errors.
9824
- */
9917
+ /** Load a fresh HiGHS WASM instance (never cache it — see file header). */
9825
9918
  async function loadHiGHS() {
9826
9919
  const highsLoader = (await import("highs")).default;
9827
- return await highsLoader({ locateFile: (file) => {
9920
+ return highsLoader({ locateFile: (file) => {
9828
9921
  const globalSelf = globalThis;
9829
9922
  if (globalSelf.location?.origin) return `${globalSelf.location.origin}/${file}`;
9830
9923
  return file;
9831
9924
  } });
9832
9925
  }
9926
+ const MAX_BINARY_VARS = 150;
9927
+ const MAX_TOTAL_OPS = 80;
9928
+ const MAX_OPS_PER_MACHINE = 8;
9833
9929
  function elapsed(startTime) {
9834
9930
  return typeof performance !== "undefined" ? performance.now() - startTime : Date.now() - startTime;
9835
9931
  }
@@ -9861,6 +9957,21 @@ const highsSolver = {
9861
9957
  const lpString = generateLPFormulation(problem);
9862
9958
  if (!lpString.includes("Subject To") || !lpString.includes("Bounds")) throw new Error("Invalid LP formulation - missing required sections");
9863
9959
  reportProgress(20);
9960
+ if (options?.shouldCancel?.()) {
9961
+ const g = await greedySolver.solve(problem, {});
9962
+ return makeSchedulingResult({
9963
+ solverId: "highs",
9964
+ solverName: "HiGHS (WASM)",
9965
+ makespan: g.objective,
9966
+ schedule: g.solution,
9967
+ elapsedMs: elapsed(startTime),
9968
+ iterations: 0,
9969
+ metadata: {
9970
+ usedFallback: true,
9971
+ reason: "cancelled before HiGHS solve"
9972
+ }
9973
+ });
9974
+ }
9864
9975
  let solution;
9865
9976
  try {
9866
9977
  solution = highs.solve(lpString, {
@@ -9899,7 +10010,11 @@ const highsSolver = {
9899
10010
  makespan,
9900
10011
  schedule,
9901
10012
  elapsedMs: elapsed(startTime),
9902
- iterations: 1
10013
+ iterations: 1,
10014
+ trace: [{
10015
+ solution: schedule,
10016
+ objective: makespan
10017
+ }]
9903
10018
  });
9904
10019
  } catch (error) {
9905
10020
  reportProgress(0);
@@ -10544,31 +10659,39 @@ const allSolvers = [
10544
10659
  solverId: "qaoa-local",
10545
10660
  percentage: pct
10546
10661
  });
10662
+ const cancelled = () => options.shouldCancel?.() ?? false;
10547
10663
  progress(0);
10548
10664
  const greedyResult = await greedySolver.solve(problem, {});
10549
10665
  progress(10);
10550
10666
  const greedyMakespan = greedyResult.objective;
10551
10667
  const greedySchedule = greedyResult.solution;
10668
+ const greedyFallback = (reason) => makeSchedulingResult({
10669
+ solverId: "qaoa-local",
10670
+ solverName: "QAOA (Local)",
10671
+ makespan: greedyMakespan,
10672
+ schedule: greedySchedule,
10673
+ elapsedMs: elapsed(),
10674
+ trace: [{
10675
+ solution: greedySchedule,
10676
+ objective: greedyMakespan
10677
+ }],
10678
+ metadata: {
10679
+ layers: LAYERS$1,
10680
+ shots: 0,
10681
+ gamma: 0,
10682
+ beta: 0,
10683
+ rawMakespan: greedyMakespan,
10684
+ usedFallback: true,
10685
+ reason
10686
+ }
10687
+ });
10552
10688
  const encoded = encodeSchedulingWithMetadata(problem, greedyMakespan);
10553
10689
  const numQubits = encoded.qubo.size;
10554
10690
  if (numQubits > MAX_QUBITS || numQubits === 0) {
10555
10691
  progress(100);
10556
- return makeSchedulingResult({
10557
- solverId: "qaoa-local",
10558
- solverName: "QAOA (Local)",
10559
- makespan: greedyMakespan,
10560
- schedule: greedySchedule,
10561
- elapsedMs: elapsed(),
10562
- metadata: {
10563
- layers: LAYERS$1,
10564
- shots: 0,
10565
- gamma: 0,
10566
- beta: 0,
10567
- rawMakespan: greedyMakespan,
10568
- usedFallback: true
10569
- }
10570
- });
10692
+ return greedyFallback(numQubits === 0 ? "no qubits to encode" : `instance needs ${numQubits} qubits (cap ${MAX_QUBITS})`);
10571
10693
  }
10694
+ if (cancelled()) return greedyFallback("cancelled");
10572
10695
  const ising = quboToIsing(encoded.qubo);
10573
10696
  const backend = new LocalSimBackend();
10574
10697
  progress(20);
@@ -10578,6 +10701,7 @@ const allSolvers = [
10578
10701
  p: LAYERS$1
10579
10702
  });
10580
10703
  progress(75);
10704
+ if (cancelled()) return greedyFallback("cancelled");
10581
10705
  const gates = buildQAOACircuit(numQubits, ising, [gamma], [beta], LAYERS$1);
10582
10706
  const { probabilities } = await backend.run(numQubits, gates);
10583
10707
  const samples = sampleFromProbabilities(probabilities, FINAL_SHOTS$1);
@@ -10594,6 +10718,13 @@ const allSolvers = [
10594
10718
  makespan: winnerMakespan,
10595
10719
  schedule: winnerSchedule,
10596
10720
  elapsedMs: elapsed(),
10721
+ trace: [{
10722
+ solution: greedySchedule,
10723
+ objective: greedyMakespan
10724
+ }, {
10725
+ solution: winnerSchedule,
10726
+ objective: winnerMakespan
10727
+ }],
10597
10728
  metadata: {
10598
10729
  layers: LAYERS$1,
10599
10730
  shots: FINAL_SHOTS$1,
@@ -10636,11 +10767,12 @@ RULES:
10636
10767
  5. Each machine can only process one operation at a time
10637
10768
  6. Assign reasonable durations if not specified (use whole numbers)
10638
10769
  7. Create meaningful names for jobs and machines based on context
10770
+ 8. "description" MUST be a single plain-English sentence summarizing the real-world scenario this models (what the user just described) — it is shown on the on-screen brief card so a human can recognize at a glance that it matches the conversation. e.g. "Scheduling the 6 high-priority sprint issues across 5 developers to minimize total completion time."
10639
10771
 
10640
10772
  RESPOND WITH ONLY A JSON OBJECT matching this schema:
10641
10773
  {
10642
- "name": "string - descriptive problem name",
10643
- "description": "string - brief description",
10774
+ "name": "string - short descriptive problem name",
10775
+ "description": "string - one plain-English sentence summarizing the scenario (see rule 8)",
10644
10776
  "jobs": [
10645
10777
  {
10646
10778
  "id": 0,
@@ -10901,54 +11033,1966 @@ No markdown, no explanation, no code blocks — just the raw JSON object.`;
10901
11033
  " \\/ <- fit here",
10902
11034
  "Minimize: sum(error^2)"
10903
11035
  ].join("\n");
10904
- /** Build a jittered grid mesh: cells at (col,row), edges between face-sharing neighbors. */
10905
- function gridMesh(cols, rows, cellName, weightAt, jitter) {
10906
- const nodes = [];
10907
- const padX = 100 / (cols + 1);
10908
- const padY = 100 / (rows + 1);
10909
- for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
10910
- const i = r * cols + c;
10911
- const { dx, dy } = jitter(i);
10912
- nodes.push({
10913
- id: i,
10914
- name: cellName(i),
10915
- x: Math.round(padX * (c + 1) + dx),
10916
- y: Math.round(padY * (r + 1) + dy)
10917
- });
11036
+ const routingSampleProblems = [
11037
+ {
11038
+ family: "routing",
11039
+ name: "Austin Food Truck Circuit",
11040
+ description: "A food truck owner wants the shortest nightly loop hitting ten Austin hotspots, starting and ending at the South Lamar commissary.",
11041
+ nodes: [
11042
+ {
11043
+ id: 0,
11044
+ name: "Commissary (S Lamar)",
11045
+ x: 38,
11046
+ y: 72
11047
+ },
11048
+ {
11049
+ id: 1,
11050
+ name: "Zilker Park",
11051
+ x: 30,
11052
+ y: 58
11053
+ },
11054
+ {
11055
+ id: 2,
11056
+ name: "Rainey St",
11057
+ x: 56,
11058
+ y: 60
11059
+ },
11060
+ {
11061
+ id: 3,
11062
+ name: "6th & Red River",
11063
+ x: 54,
11064
+ y: 48
11065
+ },
11066
+ {
11067
+ id: 4,
11068
+ name: "UT Campus",
11069
+ x: 52,
11070
+ y: 32
11071
+ },
11072
+ {
11073
+ id: 5,
11074
+ name: "Mueller Market",
11075
+ x: 72,
11076
+ y: 30
11077
+ },
11078
+ {
11079
+ id: 6,
11080
+ name: "East Cesar Chavez",
11081
+ x: 70,
11082
+ y: 56
11083
+ },
11084
+ {
11085
+ id: 7,
11086
+ name: "Barton Springs",
11087
+ x: 34,
11088
+ y: 64
11089
+ },
11090
+ {
11091
+ id: 8,
11092
+ name: "The Domain",
11093
+ x: 44,
11094
+ y: 8
11095
+ },
11096
+ {
11097
+ id: 9,
11098
+ name: "Oak Hill",
11099
+ x: 12,
11100
+ y: 86
11101
+ }
11102
+ ]
11103
+ },
11104
+ {
11105
+ family: "routing",
11106
+ name: "Kyiv Drone Resupply Run",
11107
+ description: "A logistics drone must visit twelve forward supply caches and return to base on one battery. Every wasted kilometer is flight time the battery does not have.",
11108
+ nodes: [
11109
+ {
11110
+ id: 0,
11111
+ name: "Base",
11112
+ x: 50,
11113
+ y: 92
11114
+ },
11115
+ {
11116
+ id: 1,
11117
+ name: "Cache Alpha",
11118
+ x: 18,
11119
+ y: 74
11120
+ },
11121
+ {
11122
+ id: 2,
11123
+ name: "Cache Bravo",
11124
+ x: 10,
11125
+ y: 48
11126
+ },
11127
+ {
11128
+ id: 3,
11129
+ name: "Cache Charlie",
11130
+ x: 24,
11131
+ y: 22
11132
+ },
11133
+ {
11134
+ id: 4,
11135
+ name: "Cache Delta",
11136
+ x: 44,
11137
+ y: 10
11138
+ },
11139
+ {
11140
+ id: 5,
11141
+ name: "Cache Echo",
11142
+ x: 66,
11143
+ y: 14
11144
+ },
11145
+ {
11146
+ id: 6,
11147
+ name: "Cache Foxtrot",
11148
+ x: 86,
11149
+ y: 26
11150
+ },
11151
+ {
11152
+ id: 7,
11153
+ name: "Cache Golf",
11154
+ x: 92,
11155
+ y: 52
11156
+ },
11157
+ {
11158
+ id: 8,
11159
+ name: "Cache Hotel",
11160
+ x: 80,
11161
+ y: 72
11162
+ },
11163
+ {
11164
+ id: 9,
11165
+ name: "Cache India",
11166
+ x: 62,
11167
+ y: 56
11168
+ },
11169
+ {
11170
+ id: 10,
11171
+ name: "Cache Juliet",
11172
+ x: 38,
11173
+ y: 44
11174
+ },
11175
+ {
11176
+ id: 11,
11177
+ name: "Cache Kilo",
11178
+ x: 30,
11179
+ y: 60
11180
+ }
11181
+ ]
11182
+ },
11183
+ {
11184
+ family: "routing",
11185
+ name: "Hill Country Brewery Tour",
11186
+ description: "Eight breweries, one designated-driver van, and a hard rule: end where you started. Minimize the drive so there is more time for the beer.",
11187
+ nodes: [
11188
+ {
11189
+ id: 0,
11190
+ name: "Depot (Dripping Springs)",
11191
+ x: 20,
11192
+ y: 60
11193
+ },
11194
+ {
11195
+ id: 1,
11196
+ name: "Jester King",
11197
+ x: 32,
11198
+ y: 48
11199
+ },
11200
+ {
11201
+ id: 2,
11202
+ name: "Vista Brewing",
11203
+ x: 16,
11204
+ y: 78
11205
+ },
11206
+ {
11207
+ id: 3,
11208
+ name: "Treaty Oak",
11209
+ x: 28,
11210
+ y: 70
11211
+ },
11212
+ {
11213
+ id: 4,
11214
+ name: "Last Stand",
11215
+ x: 46,
11216
+ y: 38
11217
+ },
11218
+ {
11219
+ id: 5,
11220
+ name: "Fitzhugh Brewing",
11221
+ x: 40,
11222
+ y: 56
11223
+ },
11224
+ {
11225
+ id: 6,
11226
+ name: "Family Business",
11227
+ x: 60,
11228
+ y: 30
11229
+ },
11230
+ {
11231
+ id: 7,
11232
+ name: "Suds Monkey",
11233
+ x: 24,
11234
+ y: 52
11235
+ }
11236
+ ]
10918
11237
  }
10919
- const edges = [];
10920
- for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
10921
- const i = r * cols + c;
10922
- if (c + 1 < cols) edges.push({
10923
- a: i,
10924
- b: i + 1,
10925
- w: weightAt(i, i + 1)
10926
- });
10927
- if (r + 1 < rows) edges.push({
10928
- a: i,
10929
- b: i + cols,
10930
- w: weightAt(i, i + cols)
10931
- });
11238
+ ];
11239
+ const packingSampleProblems = [
11240
+ {
11241
+ family: "packing",
11242
+ name: "Mars Cargo Manifest",
11243
+ description: "Fourteen payload modules must ride to orbit in standardized cargo pods. Every pod you do not launch saves nine figures.",
11244
+ binCapacity: 100,
11245
+ binLabel: "pod",
11246
+ items: [
11247
+ {
11248
+ id: 0,
11249
+ name: "Habitat Frame",
11250
+ size: 62
11251
+ },
11252
+ {
11253
+ id: 1,
11254
+ name: "Water Recycler",
11255
+ size: 48
11256
+ },
11257
+ {
11258
+ id: 2,
11259
+ name: "Solar Array A",
11260
+ size: 55
11261
+ },
11262
+ {
11263
+ id: 3,
11264
+ name: "Solar Array B",
11265
+ size: 55
11266
+ },
11267
+ {
11268
+ id: 4,
11269
+ name: "Rover Chassis",
11270
+ size: 70
11271
+ },
11272
+ {
11273
+ id: 5,
11274
+ name: "Lab Module",
11275
+ size: 38
11276
+ },
11277
+ {
11278
+ id: 6,
11279
+ name: "Greenhouse Kit",
11280
+ size: 42
11281
+ },
11282
+ {
11283
+ id: 7,
11284
+ name: "Battery Bank",
11285
+ size: 30
11286
+ },
11287
+ {
11288
+ id: 8,
11289
+ name: "Antenna Mast",
11290
+ size: 22
11291
+ },
11292
+ {
11293
+ id: 9,
11294
+ name: "Med Bay Kit",
11295
+ size: 26
11296
+ },
11297
+ {
11298
+ id: 10,
11299
+ name: "Spare Parts Crate",
11300
+ size: 18
11301
+ },
11302
+ {
11303
+ id: 11,
11304
+ name: "Food Stores",
11305
+ size: 44
11306
+ },
11307
+ {
11308
+ id: 12,
11309
+ name: "Drill Assembly",
11310
+ size: 34
11311
+ },
11312
+ {
11313
+ id: 13,
11314
+ name: "3D Printer",
11315
+ size: 16
11316
+ }
11317
+ ]
11318
+ },
11319
+ {
11320
+ family: "packing",
11321
+ name: "Moving Day — Oak Hill",
11322
+ description: "Everything Erik owns has to fit into rented trucks. Each truck costs $180 a day, so the optimizer IS the budget.",
11323
+ binCapacity: 80,
11324
+ binLabel: "truck",
11325
+ items: [
11326
+ {
11327
+ id: 0,
11328
+ name: "Couch",
11329
+ size: 40
11330
+ },
11331
+ {
11332
+ id: 1,
11333
+ name: "Bed Frame",
11334
+ size: 32
11335
+ },
11336
+ {
11337
+ id: 2,
11338
+ name: "Mattress",
11339
+ size: 28
11340
+ },
11341
+ {
11342
+ id: 3,
11343
+ name: "Dining Table",
11344
+ size: 30
11345
+ },
11346
+ {
11347
+ id: 4,
11348
+ name: "Bookshelves",
11349
+ size: 24
11350
+ },
11351
+ {
11352
+ id: 5,
11353
+ name: "Desk Battlestation",
11354
+ size: 36
11355
+ },
11356
+ {
11357
+ id: 6,
11358
+ name: "Arcade Cabinet",
11359
+ size: 44
11360
+ },
11361
+ {
11362
+ id: 7,
11363
+ name: "Kitchen Boxes",
11364
+ size: 20
11365
+ },
11366
+ {
11367
+ id: 8,
11368
+ name: "Book Boxes",
11369
+ size: 26
11370
+ },
11371
+ {
11372
+ id: 9,
11373
+ name: "Garage Tools",
11374
+ size: 18
11375
+ },
11376
+ {
11377
+ id: 10,
11378
+ name: "Bikes",
11379
+ size: 16
11380
+ },
11381
+ {
11382
+ id: 11,
11383
+ name: "Server Rack",
11384
+ size: 22
11385
+ }
11386
+ ]
11387
+ },
11388
+ {
11389
+ family: "packing",
11390
+ name: "Server Rack Tetris",
11391
+ description: "Thirteen VM workloads need hosts. Each host burns power whether it is full or idle — consolidate or pay.",
11392
+ binCapacity: 64,
11393
+ binLabel: "host",
11394
+ items: [
11395
+ {
11396
+ id: 0,
11397
+ name: "Postgres Primary",
11398
+ size: 32
11399
+ },
11400
+ {
11401
+ id: 1,
11402
+ name: "Postgres Replica",
11403
+ size: 24
11404
+ },
11405
+ {
11406
+ id: 2,
11407
+ name: "Web Frontend x4",
11408
+ size: 16
11409
+ },
11410
+ {
11411
+ id: 3,
11412
+ name: "API Cluster",
11413
+ size: 28
11414
+ },
11415
+ {
11416
+ id: 4,
11417
+ name: "Redis Cache",
11418
+ size: 12
11419
+ },
11420
+ {
11421
+ id: 5,
11422
+ name: "ML Inference",
11423
+ size: 48
11424
+ },
11425
+ {
11426
+ id: 6,
11427
+ name: "CI Runners",
11428
+ size: 20
11429
+ },
11430
+ {
11431
+ id: 7,
11432
+ name: "Log Pipeline",
11433
+ size: 14
11434
+ },
11435
+ {
11436
+ id: 8,
11437
+ name: "Metrics Stack",
11438
+ size: 18
11439
+ },
11440
+ {
11441
+ id: 9,
11442
+ name: "Batch ETL",
11443
+ size: 26
11444
+ },
11445
+ {
11446
+ id: 10,
11447
+ name: "Search Index",
11448
+ size: 22
11449
+ },
11450
+ {
11451
+ id: 11,
11452
+ name: "Mail Service",
11453
+ size: 8
11454
+ },
11455
+ {
11456
+ id: 12,
11457
+ name: "Backup Agent",
11458
+ size: 10
11459
+ }
11460
+ ]
10932
11461
  }
10933
- return {
10934
- nodes,
10935
- edges
10936
- };
10937
- }
10938
- const wobble = (i) => i * 7919 % 11 / 10 - .5;
10939
- const plate = gridMesh(4, 3, (i) => `cell ${i}`, (a, b) => 1 + (a * 31 + b * 17) % 4, (i) => ({
10940
- dx: wobble(i) * 6,
10941
- dy: wobble(i + 3) * 6
10942
- }));
10943
- const reservoir = gridMesh(6, 3, (i) => `block ${i}`, (a, b) => 1 + (a * 13 + b * 29) % 5, (i) => ({
10944
- dx: wobble(i + 1) * 8,
11462
+ ];
11463
+ const assignmentSampleProblems = [
11464
+ {
11465
+ family: "assignment",
11466
+ name: "Raid Roster",
11467
+ description: "Eight raiders, eight roles, one progression night. The cost matrix is each player&apos;s discomfort in each role — minimize total grumbling.",
11468
+ agents: [
11469
+ {
11470
+ id: 0,
11471
+ name: "Thorgrim"
11472
+ },
11473
+ {
11474
+ id: 1,
11475
+ name: "Lyralei"
11476
+ },
11477
+ {
11478
+ id: 2,
11479
+ name: "Mogrash"
11480
+ },
11481
+ {
11482
+ id: 3,
11483
+ name: "Seraphine"
11484
+ },
11485
+ {
11486
+ id: 4,
11487
+ name: "Kettle"
11488
+ },
11489
+ {
11490
+ id: 5,
11491
+ name: "Vexx"
11492
+ },
11493
+ {
11494
+ id: 6,
11495
+ name: "Oakenheart"
11496
+ },
11497
+ {
11498
+ id: 7,
11499
+ name: "Zinnia"
11500
+ }
11501
+ ],
11502
+ tasks: [
11503
+ {
11504
+ id: 0,
11505
+ name: "Main Tank"
11506
+ },
11507
+ {
11508
+ id: 1,
11509
+ name: "Off Tank"
11510
+ },
11511
+ {
11512
+ id: 2,
11513
+ name: "Healer 1"
11514
+ },
11515
+ {
11516
+ id: 3,
11517
+ name: "Healer 2"
11518
+ },
11519
+ {
11520
+ id: 4,
11521
+ name: "Melee DPS"
11522
+ },
11523
+ {
11524
+ id: 5,
11525
+ name: "Ranged DPS"
11526
+ },
11527
+ {
11528
+ id: 6,
11529
+ name: "Raid Lead"
11530
+ },
11531
+ {
11532
+ id: 7,
11533
+ name: "Loot Council"
11534
+ }
11535
+ ],
11536
+ cost: [
11537
+ [
11538
+ 2,
11539
+ 4,
11540
+ 9,
11541
+ 9,
11542
+ 5,
11543
+ 7,
11544
+ 6,
11545
+ 8
11546
+ ],
11547
+ [
11548
+ 9,
11549
+ 8,
11550
+ 2,
11551
+ 3,
11552
+ 7,
11553
+ 5,
11554
+ 6,
11555
+ 4
11556
+ ],
11557
+ [
11558
+ 3,
11559
+ 2,
11560
+ 9,
11561
+ 8,
11562
+ 4,
11563
+ 6,
11564
+ 7,
11565
+ 9
11566
+ ],
11567
+ [
11568
+ 8,
11569
+ 9,
11570
+ 3,
11571
+ 2,
11572
+ 6,
11573
+ 4,
11574
+ 5,
11575
+ 6
11576
+ ],
11577
+ [
11578
+ 7,
11579
+ 6,
11580
+ 5,
11581
+ 5,
11582
+ 2,
11583
+ 3,
11584
+ 8,
11585
+ 7
11586
+ ],
11587
+ [
11588
+ 6,
11589
+ 7,
11590
+ 6,
11591
+ 6,
11592
+ 3,
11593
+ 2,
11594
+ 4,
11595
+ 5
11596
+ ],
11597
+ [
11598
+ 4,
11599
+ 3,
11600
+ 7,
11601
+ 7,
11602
+ 5,
11603
+ 6,
11604
+ 2,
11605
+ 3
11606
+ ],
11607
+ [
11608
+ 9,
11609
+ 8,
11610
+ 4,
11611
+ 4,
11612
+ 6,
11613
+ 5,
11614
+ 3,
11615
+ 2
11616
+ ]
11617
+ ]
11618
+ },
11619
+ {
11620
+ family: "assignment",
11621
+ name: "Surgery Suite Staffing",
11622
+ description: "Six surgeons, six operating rooms with different specialties on the board today. Cost = mismatch between surgeon expertise and case type.",
11623
+ agents: [
11624
+ {
11625
+ id: 0,
11626
+ name: "Dr. Asante"
11627
+ },
11628
+ {
11629
+ id: 1,
11630
+ name: "Dr. Brook"
11631
+ },
11632
+ {
11633
+ id: 2,
11634
+ name: "Dr. Chen"
11635
+ },
11636
+ {
11637
+ id: 3,
11638
+ name: "Dr. Duarte"
11639
+ },
11640
+ {
11641
+ id: 4,
11642
+ name: "Dr. Eze"
11643
+ },
11644
+ {
11645
+ id: 5,
11646
+ name: "Dr. Farrow"
11647
+ }
11648
+ ],
11649
+ tasks: [
11650
+ {
11651
+ id: 0,
11652
+ name: "OR-1 Cardiac"
11653
+ },
11654
+ {
11655
+ id: 1,
11656
+ name: "OR-2 Ortho"
11657
+ },
11658
+ {
11659
+ id: 2,
11660
+ name: "OR-3 Neuro"
11661
+ },
11662
+ {
11663
+ id: 3,
11664
+ name: "OR-4 General"
11665
+ },
11666
+ {
11667
+ id: 4,
11668
+ name: "OR-5 Pediatric"
11669
+ },
11670
+ {
11671
+ id: 5,
11672
+ name: "OR-6 Trauma"
11673
+ }
11674
+ ],
11675
+ cost: [
11676
+ [
11677
+ 1,
11678
+ 8,
11679
+ 9,
11680
+ 4,
11681
+ 7,
11682
+ 5
11683
+ ],
11684
+ [
11685
+ 9,
11686
+ 1,
11687
+ 8,
11688
+ 5,
11689
+ 6,
11690
+ 3
11691
+ ],
11692
+ [
11693
+ 8,
11694
+ 9,
11695
+ 1,
11696
+ 6,
11697
+ 5,
11698
+ 7
11699
+ ],
11700
+ [
11701
+ 5,
11702
+ 4,
11703
+ 7,
11704
+ 1,
11705
+ 3,
11706
+ 2
11707
+ ],
11708
+ [
11709
+ 7,
11710
+ 6,
11711
+ 5,
11712
+ 3,
11713
+ 1,
11714
+ 4
11715
+ ],
11716
+ [
11717
+ 4,
11718
+ 3,
11719
+ 6,
11720
+ 2,
11721
+ 4,
11722
+ 1
11723
+ ]
11724
+ ]
11725
+ },
11726
+ {
11727
+ family: "assignment",
11728
+ name: "Support Ticket Triage",
11729
+ description: "Seven engineers, nine open escalations — two will wait. Cost = estimated hours for each engineer on each ticket given their domain depth.",
11730
+ agents: [
11731
+ {
11732
+ id: 0,
11733
+ name: "Imani"
11734
+ },
11735
+ {
11736
+ id: 1,
11737
+ name: "Jonas"
11738
+ },
11739
+ {
11740
+ id: 2,
11741
+ name: "Kai"
11742
+ },
11743
+ {
11744
+ id: 3,
11745
+ name: "Lena"
11746
+ },
11747
+ {
11748
+ id: 4,
11749
+ name: "Marcus"
11750
+ },
11751
+ {
11752
+ id: 5,
11753
+ name: "Noor"
11754
+ },
11755
+ {
11756
+ id: 6,
11757
+ name: "Owen"
11758
+ }
11759
+ ],
11760
+ tasks: [
11761
+ {
11762
+ id: 0,
11763
+ name: "Auth outage"
11764
+ },
11765
+ {
11766
+ id: 1,
11767
+ name: "Billing drift"
11768
+ },
11769
+ {
11770
+ id: 2,
11771
+ name: "Mobile crash"
11772
+ },
11773
+ {
11774
+ id: 3,
11775
+ name: "Search latency"
11776
+ },
11777
+ {
11778
+ id: 4,
11779
+ name: "Data export"
11780
+ },
11781
+ {
11782
+ id: 5,
11783
+ name: "Webhook storm"
11784
+ },
11785
+ {
11786
+ id: 6,
11787
+ name: "SSO config"
11788
+ },
11789
+ {
11790
+ id: 7,
11791
+ name: "Rate limiter"
11792
+ },
11793
+ {
11794
+ id: 8,
11795
+ name: "PDF renderer"
11796
+ }
11797
+ ],
11798
+ cost: [
11799
+ [
11800
+ 2,
11801
+ 7,
11802
+ 9,
11803
+ 6,
11804
+ 5,
11805
+ 4,
11806
+ 3,
11807
+ 5,
11808
+ 8
11809
+ ],
11810
+ [
11811
+ 8,
11812
+ 2,
11813
+ 7,
11814
+ 5,
11815
+ 3,
11816
+ 6,
11817
+ 7,
11818
+ 6,
11819
+ 4
11820
+ ],
11821
+ [
11822
+ 9,
11823
+ 6,
11824
+ 2,
11825
+ 7,
11826
+ 6,
11827
+ 5,
11828
+ 8,
11829
+ 7,
11830
+ 3
11831
+ ],
11832
+ [
11833
+ 4,
11834
+ 5,
11835
+ 6,
11836
+ 2,
11837
+ 4,
11838
+ 3,
11839
+ 5,
11840
+ 2,
11841
+ 7
11842
+ ],
11843
+ [
11844
+ 6,
11845
+ 3,
11846
+ 8,
11847
+ 4,
11848
+ 2,
11849
+ 7,
11850
+ 6,
11851
+ 5,
11852
+ 5
11853
+ ],
11854
+ [
11855
+ 3,
11856
+ 6,
11857
+ 5,
11858
+ 3,
11859
+ 7,
11860
+ 2,
11861
+ 2,
11862
+ 4,
11863
+ 6
11864
+ ],
11865
+ [
11866
+ 7,
11867
+ 4,
11868
+ 4,
11869
+ 8,
11870
+ 5,
11871
+ 6,
11872
+ 4,
11873
+ 3,
11874
+ 2
11875
+ ]
11876
+ ]
11877
+ }
11878
+ ];
11879
+ const networkSampleProblems = [
11880
+ {
11881
+ family: "network",
11882
+ name: "Office Seating Feud",
11883
+ description: "Ten coworkers, weighted rivalry edges (1 = mild eye-rolls, 5 = open warfare). Split them across two floors so the maximum total rivalry crosses the staircase.",
11884
+ nodes: [
11885
+ {
11886
+ id: 0,
11887
+ name: "Ava",
11888
+ x: 20,
11889
+ y: 20
11890
+ },
11891
+ {
11892
+ id: 1,
11893
+ name: "Ben",
11894
+ x: 50,
11895
+ y: 12
11896
+ },
11897
+ {
11898
+ id: 2,
11899
+ name: "Cleo",
11900
+ x: 80,
11901
+ y: 22
11902
+ },
11903
+ {
11904
+ id: 3,
11905
+ name: "Dex",
11906
+ x: 12,
11907
+ y: 50
11908
+ },
11909
+ {
11910
+ id: 4,
11911
+ name: "Elle",
11912
+ x: 42,
11913
+ y: 42
11914
+ },
11915
+ {
11916
+ id: 5,
11917
+ name: "Finn",
11918
+ x: 70,
11919
+ y: 48
11920
+ },
11921
+ {
11922
+ id: 6,
11923
+ name: "Gus",
11924
+ x: 92,
11925
+ y: 55
11926
+ },
11927
+ {
11928
+ id: 7,
11929
+ name: "Hana",
11930
+ x: 25,
11931
+ y: 80
11932
+ },
11933
+ {
11934
+ id: 8,
11935
+ name: "Iggy",
11936
+ x: 55,
11937
+ y: 84
11938
+ },
11939
+ {
11940
+ id: 9,
11941
+ name: "June",
11942
+ x: 82,
11943
+ y: 80
11944
+ }
11945
+ ],
11946
+ edges: [
11947
+ {
11948
+ a: 0,
11949
+ b: 1,
11950
+ w: 3
11951
+ },
11952
+ {
11953
+ a: 0,
11954
+ b: 3,
11955
+ w: 1
11956
+ },
11957
+ {
11958
+ a: 0,
11959
+ b: 4,
11960
+ w: 4
11961
+ },
11962
+ {
11963
+ a: 1,
11964
+ b: 2,
11965
+ w: 2
11966
+ },
11967
+ {
11968
+ a: 1,
11969
+ b: 4,
11970
+ w: 5
11971
+ },
11972
+ {
11973
+ a: 2,
11974
+ b: 5,
11975
+ w: 3
11976
+ },
11977
+ {
11978
+ a: 2,
11979
+ b: 6,
11980
+ w: 1
11981
+ },
11982
+ {
11983
+ a: 3,
11984
+ b: 4,
11985
+ w: 2
11986
+ },
11987
+ {
11988
+ a: 3,
11989
+ b: 7,
11990
+ w: 4
11991
+ },
11992
+ {
11993
+ a: 4,
11994
+ b: 5,
11995
+ w: 1
11996
+ },
11997
+ {
11998
+ a: 4,
11999
+ b: 8,
12000
+ w: 3
12001
+ },
12002
+ {
12003
+ a: 5,
12004
+ b: 6,
12005
+ w: 4
12006
+ },
12007
+ {
12008
+ a: 5,
12009
+ b: 9,
12010
+ w: 2
12011
+ },
12012
+ {
12013
+ a: 6,
12014
+ b: 9,
12015
+ w: 5
12016
+ },
12017
+ {
12018
+ a: 7,
12019
+ b: 8,
12020
+ w: 2
12021
+ },
12022
+ {
12023
+ a: 8,
12024
+ b: 9,
12025
+ w: 1
12026
+ }
12027
+ ]
12028
+ },
12029
+ {
12030
+ family: "network",
12031
+ name: "Microservice Fault Domains",
12032
+ description: "Twelve services, edges weighted by cross-talk volume. Split across two availability zones so the heaviest traffic crosses zones — forcing the chattiest pairs to prove they tolerate partition.",
12033
+ nodes: [
12034
+ {
12035
+ id: 0,
12036
+ name: "gateway",
12037
+ x: 50,
12038
+ y: 8
12039
+ },
12040
+ {
12041
+ id: 1,
12042
+ name: "auth",
12043
+ x: 25,
12044
+ y: 22
12045
+ },
12046
+ {
12047
+ id: 2,
12048
+ name: "users",
12049
+ x: 75,
12050
+ y: 22
12051
+ },
12052
+ {
12053
+ id: 3,
12054
+ name: "billing",
12055
+ x: 12,
12056
+ y: 46
12057
+ },
12058
+ {
12059
+ id: 4,
12060
+ name: "catalog",
12061
+ x: 40,
12062
+ y: 40
12063
+ },
12064
+ {
12065
+ id: 5,
12066
+ name: "orders",
12067
+ x: 64,
12068
+ y: 42
12069
+ },
12070
+ {
12071
+ id: 6,
12072
+ name: "search",
12073
+ x: 90,
12074
+ y: 44
12075
+ },
12076
+ {
12077
+ id: 7,
12078
+ name: "ledger",
12079
+ x: 20,
12080
+ y: 70
12081
+ },
12082
+ {
12083
+ id: 8,
12084
+ name: "shipping",
12085
+ x: 48,
12086
+ y: 68
12087
+ },
12088
+ {
12089
+ id: 9,
12090
+ name: "notify",
12091
+ x: 74,
12092
+ y: 70
12093
+ },
12094
+ {
12095
+ id: 10,
12096
+ name: "analytics",
12097
+ x: 34,
12098
+ y: 90
12099
+ },
12100
+ {
12101
+ id: 11,
12102
+ name: "ml-recs",
12103
+ x: 66,
12104
+ y: 90
12105
+ }
12106
+ ],
12107
+ edges: [
12108
+ {
12109
+ a: 0,
12110
+ b: 1,
12111
+ w: 5
12112
+ },
12113
+ {
12114
+ a: 0,
12115
+ b: 2,
12116
+ w: 4
12117
+ },
12118
+ {
12119
+ a: 0,
12120
+ b: 4,
12121
+ w: 3
12122
+ },
12123
+ {
12124
+ a: 1,
12125
+ b: 2,
12126
+ w: 2
12127
+ },
12128
+ {
12129
+ a: 1,
12130
+ b: 3,
12131
+ w: 3
12132
+ },
12133
+ {
12134
+ a: 2,
12135
+ b: 5,
12136
+ w: 4
12137
+ },
12138
+ {
12139
+ a: 2,
12140
+ b: 6,
12141
+ w: 2
12142
+ },
12143
+ {
12144
+ a: 3,
12145
+ b: 7,
12146
+ w: 5
12147
+ },
12148
+ {
12149
+ a: 4,
12150
+ b: 5,
12151
+ w: 3
12152
+ },
12153
+ {
12154
+ a: 4,
12155
+ b: 6,
12156
+ w: 2
12157
+ },
12158
+ {
12159
+ a: 4,
12160
+ b: 11,
12161
+ w: 2
12162
+ },
12163
+ {
12164
+ a: 5,
12165
+ b: 8,
12166
+ w: 4
12167
+ },
12168
+ {
12169
+ a: 5,
12170
+ b: 9,
12171
+ w: 2
12172
+ },
12173
+ {
12174
+ a: 7,
12175
+ b: 10,
12176
+ w: 2
12177
+ },
12178
+ {
12179
+ a: 8,
12180
+ b: 9,
12181
+ w: 3
12182
+ },
12183
+ {
12184
+ a: 8,
12185
+ b: 10,
12186
+ w: 1
12187
+ },
12188
+ {
12189
+ a: 9,
12190
+ b: 11,
12191
+ w: 2
12192
+ },
12193
+ {
12194
+ a: 10,
12195
+ b: 11,
12196
+ w: 3
12197
+ }
12198
+ ]
12199
+ },
12200
+ {
12201
+ family: "network",
12202
+ name: "Power Grid Islanding",
12203
+ description: "During a storm event, the grid must split into two self-sufficient islands. Maximize the inter-island line capacity you can afford to sever cleanly.",
12204
+ nodes: [
12205
+ {
12206
+ id: 0,
12207
+ name: "Plant North",
12208
+ x: 30,
12209
+ y: 10
12210
+ },
12211
+ {
12212
+ id: 1,
12213
+ name: "Plant South",
12214
+ x: 65,
12215
+ y: 90
12216
+ },
12217
+ {
12218
+ id: 2,
12219
+ name: "Sub Alpha",
12220
+ x: 15,
12221
+ y: 35
12222
+ },
12223
+ {
12224
+ id: 3,
12225
+ name: "Sub Bravo",
12226
+ x: 50,
12227
+ y: 28
12228
+ },
12229
+ {
12230
+ id: 4,
12231
+ name: "Sub Charlie",
12232
+ x: 85,
12233
+ y: 32
12234
+ },
12235
+ {
12236
+ id: 5,
12237
+ name: "Sub Delta",
12238
+ x: 25,
12239
+ y: 62
12240
+ },
12241
+ {
12242
+ id: 6,
12243
+ name: "Sub Echo",
12244
+ x: 58,
12245
+ y: 58
12246
+ },
12247
+ {
12248
+ id: 7,
12249
+ name: "Sub Foxtrot",
12250
+ x: 88,
12251
+ y: 66
12252
+ },
12253
+ {
12254
+ id: 8,
12255
+ name: "City West",
12256
+ x: 12,
12257
+ y: 88
12258
+ }
12259
+ ],
12260
+ edges: [
12261
+ {
12262
+ a: 0,
12263
+ b: 2,
12264
+ w: 4
12265
+ },
12266
+ {
12267
+ a: 0,
12268
+ b: 3,
12269
+ w: 5
12270
+ },
12271
+ {
12272
+ a: 0,
12273
+ b: 4,
12274
+ w: 3
12275
+ },
12276
+ {
12277
+ a: 2,
12278
+ b: 3,
12279
+ w: 2
12280
+ },
12281
+ {
12282
+ a: 2,
12283
+ b: 5,
12284
+ w: 4
12285
+ },
12286
+ {
12287
+ a: 3,
12288
+ b: 4,
12289
+ w: 2
12290
+ },
12291
+ {
12292
+ a: 3,
12293
+ b: 6,
12294
+ w: 4
12295
+ },
12296
+ {
12297
+ a: 4,
12298
+ b: 7,
12299
+ w: 4
12300
+ },
12301
+ {
12302
+ a: 5,
12303
+ b: 6,
12304
+ w: 3
12305
+ },
12306
+ {
12307
+ a: 5,
12308
+ b: 8,
12309
+ w: 5
12310
+ },
12311
+ {
12312
+ a: 6,
12313
+ b: 7,
12314
+ w: 2
12315
+ },
12316
+ {
12317
+ a: 6,
12318
+ b: 1,
12319
+ w: 5
12320
+ },
12321
+ {
12322
+ a: 7,
12323
+ b: 1,
12324
+ w: 3
12325
+ },
12326
+ {
12327
+ a: 8,
12328
+ b: 1,
12329
+ w: 2
12330
+ }
12331
+ ]
12332
+ }
12333
+ ];
12334
+ /** Build a jittered grid mesh: cells at (col,row), edges between face-sharing neighbors. */
12335
+ function gridMesh(cols, rows, cellName, weightAt, jitter) {
12336
+ const nodes = [];
12337
+ const padX = 100 / (cols + 1);
12338
+ const padY = 100 / (rows + 1);
12339
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
12340
+ const i = r * cols + c;
12341
+ const { dx, dy } = jitter(i);
12342
+ nodes.push({
12343
+ id: i,
12344
+ name: cellName(i),
12345
+ x: Math.round(padX * (c + 1) + dx),
12346
+ y: Math.round(padY * (r + 1) + dy)
12347
+ });
12348
+ }
12349
+ const edges = [];
12350
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
12351
+ const i = r * cols + c;
12352
+ if (c + 1 < cols) edges.push({
12353
+ a: i,
12354
+ b: i + 1,
12355
+ w: weightAt(i, i + 1)
12356
+ });
12357
+ if (r + 1 < rows) edges.push({
12358
+ a: i,
12359
+ b: i + cols,
12360
+ w: weightAt(i, i + cols)
12361
+ });
12362
+ }
12363
+ return {
12364
+ nodes,
12365
+ edges
12366
+ };
12367
+ }
12368
+ const wobble = (i) => i * 7919 % 11 / 10 - .5;
12369
+ const plate = gridMesh(4, 3, (i) => `cell ${i}`, (a, b) => 1 + (a * 31 + b * 17) % 4, (i) => ({
12370
+ dx: wobble(i) * 6,
12371
+ dy: wobble(i + 3) * 6
12372
+ }));
12373
+ const reservoir = gridMesh(6, 3, (i) => `block ${i}`, (a, b) => 1 + (a * 13 + b * 29) % 5, (i) => ({
12374
+ dx: wobble(i + 1) * 8,
10945
12375
  dy: wobble(i + 5) * 8
10946
12376
  }));
10947
12377
  const blade = gridMesh(6, 4, (i) => `elem ${i}`, (a, b) => 1 + (a * 23 + b * 19) % 4, (i) => ({
10948
12378
  dx: wobble(i + 2) * 7,
10949
12379
  dy: wobble(i + 7) * 7
10950
12380
  }));
10951
- plate.nodes, plate.edges, reservoir.nodes, reservoir.edges, blade.nodes, blade.edges;
12381
+ const partitioningSampleProblems = [
12382
+ {
12383
+ family: "partitioning",
12384
+ name: "Cantilever Plate Mesh",
12385
+ description: "A 4×3 finite-element plate mesh split across two solver ranks. Edge weights are the face data exchanged every timestep — each cut edge is halo traffic both ranks pay forever. Classic balanced bisection, and small enough to race on the QAOA simulator in seconds.",
12386
+ nodes: plate.nodes,
12387
+ edges: plate.edges,
12388
+ k: 2,
12389
+ balanceTolerance: 1
12390
+ },
12391
+ {
12392
+ family: "partitioning",
12393
+ name: "Reservoir Grid Sectors",
12394
+ description: "An 18-block reservoir simulation grid decomposed across three compute nodes. The big version of this problem has billions of cells; the math of where to draw the seams is identical at every scale.",
12395
+ nodes: reservoir.nodes,
12396
+ edges: reservoir.edges,
12397
+ k: 3,
12398
+ balanceTolerance: 1
12399
+ },
12400
+ {
12401
+ family: "partitioning",
12402
+ name: "Turbine Blade Mesh",
12403
+ description: "A 24-element blade section spread over four solver ranks for a transient thermal run. Four-way splits punish lazy seams — every extra cut edge multiplies across thousands of timesteps.",
12404
+ nodes: blade.nodes,
12405
+ edges: blade.edges,
12406
+ k: 4,
12407
+ balanceTolerance: 1
12408
+ }
12409
+ ];
12410
+ const selectionSampleProblems = [
12411
+ {
12412
+ family: "selection",
12413
+ name: "Indie Game Fund",
12414
+ description: "Twelve pitches, a $2M fund, and every project carries risk. Maximize expected portfolio value without betting the firm on moonshots.",
12415
+ budget: 200,
12416
+ riskWeight: 4,
12417
+ items: [
12418
+ {
12419
+ id: 0,
12420
+ name: "Cozy Farm Sim",
12421
+ value: 60,
12422
+ cost: 40,
12423
+ risk: 2
12424
+ },
12425
+ {
12426
+ id: 1,
12427
+ name: "Roguelike Deckbuilder",
12428
+ value: 85,
12429
+ cost: 55,
12430
+ risk: 4
12431
+ },
12432
+ {
12433
+ id: 2,
12434
+ name: "VR Escape Room",
12435
+ value: 95,
12436
+ cost: 70,
12437
+ risk: 8
12438
+ },
12439
+ {
12440
+ id: 3,
12441
+ name: "Pixel Metroidvania",
12442
+ value: 55,
12443
+ cost: 35,
12444
+ risk: 3
12445
+ },
12446
+ {
12447
+ id: 4,
12448
+ name: "MMO Sandbox",
12449
+ value: 160,
12450
+ cost: 120,
12451
+ risk: 10
12452
+ },
12453
+ {
12454
+ id: 5,
12455
+ name: "Mobile Puzzler",
12456
+ value: 35,
12457
+ cost: 18,
12458
+ risk: 1
12459
+ },
12460
+ {
12461
+ id: 6,
12462
+ name: "Co-op Heist Game",
12463
+ value: 75,
12464
+ cost: 50,
12465
+ risk: 5
12466
+ },
12467
+ {
12468
+ id: 7,
12469
+ name: "Narrative Adventure",
12470
+ value: 45,
12471
+ cost: 30,
12472
+ risk: 2
12473
+ },
12474
+ {
12475
+ id: 8,
12476
+ name: "Auto-Battler",
12477
+ value: 50,
12478
+ cost: 28,
12479
+ risk: 4
12480
+ },
12481
+ {
12482
+ id: 9,
12483
+ name: "Speedrun Platformer",
12484
+ value: 30,
12485
+ cost: 15,
12486
+ risk: 2
12487
+ },
12488
+ {
12489
+ id: 10,
12490
+ name: "City Builder",
12491
+ value: 90,
12492
+ cost: 65,
12493
+ risk: 5
12494
+ },
12495
+ {
12496
+ id: 11,
12497
+ name: "Horror Anthology",
12498
+ value: 40,
12499
+ cost: 25,
12500
+ risk: 6
12501
+ }
12502
+ ]
12503
+ },
12504
+ {
12505
+ family: "selection",
12506
+ name: "Drone Sensor Loadout",
12507
+ description: "A recon drone has 48 units of payload mass. Pick the sensor suite that maximizes mission value — leave the gold-plated gear on the bench.",
12508
+ budget: 48,
12509
+ riskWeight: 0,
12510
+ items: [
12511
+ {
12512
+ id: 0,
12513
+ name: "EO Camera",
12514
+ value: 30,
12515
+ cost: 12
12516
+ },
12517
+ {
12518
+ id: 1,
12519
+ name: "IR Thermal",
12520
+ value: 28,
12521
+ cost: 10
12522
+ },
12523
+ {
12524
+ id: 2,
12525
+ name: "LIDAR",
12526
+ value: 35,
12527
+ cost: 18
12528
+ },
12529
+ {
12530
+ id: 3,
12531
+ name: "SIGINT Pod",
12532
+ value: 25,
12533
+ cost: 14
12534
+ },
12535
+ {
12536
+ id: 4,
12537
+ name: "Synthetic Aperture Radar",
12538
+ value: 40,
12539
+ cost: 22
12540
+ },
12541
+ {
12542
+ id: 5,
12543
+ name: "Laser Designator",
12544
+ value: 18,
12545
+ cost: 8
12546
+ },
12547
+ {
12548
+ id: 6,
12549
+ name: "Comms Relay",
12550
+ value: 15,
12551
+ cost: 6
12552
+ },
12553
+ {
12554
+ id: 7,
12555
+ name: "Magnetometer",
12556
+ value: 10,
12557
+ cost: 4
12558
+ },
12559
+ {
12560
+ id: 8,
12561
+ name: "Air Sampler",
12562
+ value: 8,
12563
+ cost: 5
12564
+ },
12565
+ {
12566
+ id: 9,
12567
+ name: "Spare Battery",
12568
+ value: 12,
12569
+ cost: 9
12570
+ }
12571
+ ]
12572
+ },
12573
+ {
12574
+ family: "selection",
12575
+ name: "Feature Flag Quarter",
12576
+ description: "Fourteen engineering initiatives, 90 days of team capacity. Each carries delivery risk. Choose the roadmap that maximizes shipped value.",
12577
+ budget: 90,
12578
+ riskWeight: 3,
12579
+ items: [
12580
+ {
12581
+ id: 0,
12582
+ name: "Realtime Collab",
12583
+ value: 55,
12584
+ cost: 30,
12585
+ risk: 6
12586
+ },
12587
+ {
12588
+ id: 1,
12589
+ name: "Mobile Offline Mode",
12590
+ value: 40,
12591
+ cost: 25,
12592
+ risk: 4
12593
+ },
12594
+ {
12595
+ id: 2,
12596
+ name: "SSO Everywhere",
12597
+ value: 35,
12598
+ cost: 15,
12599
+ risk: 2
12600
+ },
12601
+ {
12602
+ id: 3,
12603
+ name: "Billing v2",
12604
+ value: 50,
12605
+ cost: 35,
12606
+ risk: 7
12607
+ },
12608
+ {
12609
+ id: 4,
12610
+ name: "AI Summaries",
12611
+ value: 45,
12612
+ cost: 20,
12613
+ risk: 3
12614
+ },
12615
+ {
12616
+ id: 5,
12617
+ name: "Audit Logs",
12618
+ value: 25,
12619
+ cost: 10,
12620
+ risk: 1
12621
+ },
12622
+ {
12623
+ id: 6,
12624
+ name: "Perf: Cold Start",
12625
+ value: 30,
12626
+ cost: 12,
12627
+ risk: 2
12628
+ },
12629
+ {
12630
+ id: 7,
12631
+ name: "Theme System",
12632
+ value: 15,
12633
+ cost: 8,
12634
+ risk: 1
12635
+ },
12636
+ {
12637
+ id: 8,
12638
+ name: "Webhooks v2",
12639
+ value: 28,
12640
+ cost: 14,
12641
+ risk: 3
12642
+ },
12643
+ {
12644
+ id: 9,
12645
+ name: "Data Residency",
12646
+ value: 38,
12647
+ cost: 28,
12648
+ risk: 5
12649
+ },
12650
+ {
12651
+ id: 10,
12652
+ name: "Search Rewrite",
12653
+ value: 42,
12654
+ cost: 26,
12655
+ risk: 5
12656
+ },
12657
+ {
12658
+ id: 11,
12659
+ name: "Onboarding Flow",
12660
+ value: 22,
12661
+ cost: 9,
12662
+ risk: 1
12663
+ },
12664
+ {
12665
+ id: 12,
12666
+ name: "Plugin SDK",
12667
+ value: 48,
12668
+ cost: 32,
12669
+ risk: 6
12670
+ },
12671
+ {
12672
+ id: 13,
12673
+ name: "A11y Sweep",
12674
+ value: 20,
12675
+ cost: 11,
12676
+ risk: 1
12677
+ }
12678
+ ]
12679
+ }
12680
+ ];
12681
+ const economicSampleProblems = [
12682
+ {
12683
+ family: "economic",
12684
+ name: "Spectrum Auction",
12685
+ description: "Five regional spectrum licenses, nine carriers bidding on bundles. Sell each license once, maximize treasury revenue — the classic FCC winner-determination problem.",
12686
+ goods: [
12687
+ {
12688
+ id: 0,
12689
+ name: "North Band"
12690
+ },
12691
+ {
12692
+ id: 1,
12693
+ name: "South Band"
12694
+ },
12695
+ {
12696
+ id: 2,
12697
+ name: "East Band"
12698
+ },
12699
+ {
12700
+ id: 3,
12701
+ name: "West Band"
12702
+ },
12703
+ {
12704
+ id: 4,
12705
+ name: "Metro Core"
12706
+ }
12707
+ ],
12708
+ bids: [
12709
+ {
12710
+ id: 0,
12711
+ bidder: "AstraTel",
12712
+ price: 120,
12713
+ goodIds: [0, 4]
12714
+ },
12715
+ {
12716
+ id: 1,
12717
+ bidder: "AstraTel",
12718
+ price: 60,
12719
+ goodIds: [0]
12720
+ },
12721
+ {
12722
+ id: 2,
12723
+ bidder: "Borealis",
12724
+ price: 140,
12725
+ goodIds: [1, 2]
12726
+ },
12727
+ {
12728
+ id: 3,
12729
+ bidder: "Borealis",
12730
+ price: 65,
12731
+ goodIds: [1]
12732
+ },
12733
+ {
12734
+ id: 4,
12735
+ bidder: "CometNet",
12736
+ price: 95,
12737
+ goodIds: [2, 3]
12738
+ },
12739
+ {
12740
+ id: 5,
12741
+ bidder: "CometNet",
12742
+ price: 50,
12743
+ goodIds: [3]
12744
+ },
12745
+ {
12746
+ id: 6,
12747
+ bidder: "DyneMobile",
12748
+ price: 180,
12749
+ goodIds: [
12750
+ 0,
12751
+ 1,
12752
+ 4
12753
+ ]
12754
+ },
12755
+ {
12756
+ id: 7,
12757
+ bidder: "Equinox",
12758
+ price: 75,
12759
+ goodIds: [4]
12760
+ },
12761
+ {
12762
+ id: 8,
12763
+ bidder: "Equinox",
12764
+ price: 110,
12765
+ goodIds: [2, 4]
12766
+ }
12767
+ ]
12768
+ },
12769
+ {
12770
+ family: "economic",
12771
+ name: "Cloud Spot Market",
12772
+ description: "Six GPU node pools, ten batch workloads bidding for exclusive overnight windows. Maximize the operator&apos;s take across all-or-nothing reservations.",
12773
+ goods: [
12774
+ {
12775
+ id: 0,
12776
+ name: "A100 Pool 1"
12777
+ },
12778
+ {
12779
+ id: 1,
12780
+ name: "A100 Pool 2"
12781
+ },
12782
+ {
12783
+ id: 2,
12784
+ name: "H100 Pool 1"
12785
+ },
12786
+ {
12787
+ id: 3,
12788
+ name: "H100 Pool 2"
12789
+ },
12790
+ {
12791
+ id: 4,
12792
+ name: "TPU Pod"
12793
+ },
12794
+ {
12795
+ id: 5,
12796
+ name: "CPU Farm"
12797
+ }
12798
+ ],
12799
+ bids: [
12800
+ {
12801
+ id: 0,
12802
+ bidder: "BioFold Labs",
12803
+ price: 90,
12804
+ goodIds: [2, 3]
12805
+ },
12806
+ {
12807
+ id: 1,
12808
+ bidder: "BioFold Labs",
12809
+ price: 48,
12810
+ goodIds: [2]
12811
+ },
12812
+ {
12813
+ id: 2,
12814
+ bidder: "RenderWorks",
12815
+ price: 55,
12816
+ goodIds: [0, 1]
12817
+ },
12818
+ {
12819
+ id: 3,
12820
+ bidder: "RenderWorks",
12821
+ price: 30,
12822
+ goodIds: [0]
12823
+ },
12824
+ {
12825
+ id: 4,
12826
+ bidder: "LLM Foundry",
12827
+ price: 130,
12828
+ goodIds: [
12829
+ 2,
12830
+ 3,
12831
+ 4
12832
+ ]
12833
+ },
12834
+ {
12835
+ id: 5,
12836
+ bidder: "LLM Foundry",
12837
+ price: 70,
12838
+ goodIds: [4]
12839
+ },
12840
+ {
12841
+ id: 6,
12842
+ bidder: "QuantSim",
12843
+ price: 42,
12844
+ goodIds: [1, 5]
12845
+ },
12846
+ {
12847
+ id: 7,
12848
+ bidder: "QuantSim",
12849
+ price: 22,
12850
+ goodIds: [5]
12851
+ },
12852
+ {
12853
+ id: 8,
12854
+ bidder: "WeatherNet",
12855
+ price: 65,
12856
+ goodIds: [0, 5]
12857
+ },
12858
+ {
12859
+ id: 9,
12860
+ bidder: "GenomeScan",
12861
+ price: 38,
12862
+ goodIds: [1]
12863
+ }
12864
+ ]
12865
+ },
12866
+ {
12867
+ family: "economic",
12868
+ name: "Stadium Sponsorship Bundle",
12869
+ description: "Seven sponsorship assets — naming rights, jersey patch, tunnel club — and eight brands bidding on combinations. One sale per asset; maximize the franchise&apos;s deal sheet.",
12870
+ goods: [
12871
+ {
12872
+ id: 0,
12873
+ name: "Naming Rights"
12874
+ },
12875
+ {
12876
+ id: 1,
12877
+ name: "Jersey Patch"
12878
+ },
12879
+ {
12880
+ id: 2,
12881
+ name: "Tunnel Club"
12882
+ },
12883
+ {
12884
+ id: 3,
12885
+ name: "Scoreboard"
12886
+ },
12887
+ {
12888
+ id: 4,
12889
+ name: "Practice Facility"
12890
+ },
12891
+ {
12892
+ id: 5,
12893
+ name: "Beer Pouring Rights"
12894
+ },
12895
+ {
12896
+ id: 6,
12897
+ name: "Halftime Show"
12898
+ }
12899
+ ],
12900
+ bids: [
12901
+ {
12902
+ id: 0,
12903
+ bidder: "Vertex Air",
12904
+ price: 200,
12905
+ goodIds: [0, 3]
12906
+ },
12907
+ {
12908
+ id: 1,
12909
+ bidder: "Vertex Air",
12910
+ price: 130,
12911
+ goodIds: [0]
12912
+ },
12913
+ {
12914
+ id: 2,
12915
+ bidder: "Cascade Beer",
12916
+ price: 85,
12917
+ goodIds: [5, 6]
12918
+ },
12919
+ {
12920
+ id: 3,
12921
+ bidder: "Cascade Beer",
12922
+ price: 55,
12923
+ goodIds: [5]
12924
+ },
12925
+ {
12926
+ id: 4,
12927
+ bidder: "NovaBank",
12928
+ price: 150,
12929
+ goodIds: [1, 2]
12930
+ },
12931
+ {
12932
+ id: 5,
12933
+ bidder: "NovaBank",
12934
+ price: 80,
12935
+ goodIds: [1]
12936
+ },
12937
+ {
12938
+ id: 6,
12939
+ bidder: "GrindHouse Coffee",
12940
+ price: 45,
12941
+ goodIds: [2]
12942
+ },
12943
+ {
12944
+ id: 7,
12945
+ bidder: "Pixel Motors",
12946
+ price: 110,
12947
+ goodIds: [3, 4]
12948
+ },
12949
+ {
12950
+ id: 8,
12951
+ bidder: "Pixel Motors",
12952
+ price: 60,
12953
+ goodIds: [4]
12954
+ },
12955
+ {
12956
+ id: 9,
12957
+ bidder: "StreamCo",
12958
+ price: 70,
12959
+ goodIds: [6]
12960
+ }
12961
+ ]
12962
+ }
12963
+ ];
12964
+ const continuousSampleProblems = [
12965
+ {
12966
+ family: "continuous",
12967
+ name: "Reactor Temperature Tuning",
12968
+ description: "One control dial, a yield curve full of local sweet spots, and exactly one true optimum. The wavy bowl eats naive hill climbers for breakfast.",
12969
+ functionId: "wavy-bowl",
12970
+ dims: 1,
12971
+ lo: -10,
12972
+ hi: 10,
12973
+ paramNames: ["temperature offset"]
12974
+ },
12975
+ {
12976
+ family: "continuous",
12977
+ name: "Antenna Sweet Spot",
12978
+ description: "Two placement coordinates on the roof; signal interference makes four equally good mounting points. Himmelblau&apos;s function — which optimum will your solver find?",
12979
+ functionId: "himmelblau",
12980
+ dims: 2,
12981
+ lo: -6,
12982
+ hi: 6,
12983
+ paramNames: ["east-west", "north-south"]
12984
+ },
12985
+ {
12986
+ family: "continuous",
12987
+ name: "Hyperparameter Canyon",
12988
+ description: "Two training knobs over a Rastrigin egg-carton — dozens of local minima arranged in a lattice, one global floor at the origin. Pure trap density.",
12989
+ functionId: "rastrigin",
12990
+ dims: 2,
12991
+ lo: -5.12,
12992
+ hi: 5.12,
12993
+ paramNames: ["learning rate (scaled)", "regularization (scaled)"]
12994
+ }
12995
+ ];
10952
12996
  var QuboBuilder = class {
10953
12997
  size;
10954
12998
  q = /* @__PURE__ */ new Map();
@@ -11280,8 +13324,270 @@ function continuousToy() {
11280
13324
  };
11281
13325
  }
11282
13326
  schedulingToy(), routingToy(), packingToy(), assignmentToy(), networkToy(), partitioningToy(), selectionToy(), economicToy(), continuousToy();
13327
+ /**
13328
+ * Per-family problem Zod schemas — the validation foundation for custom problem
13329
+ * EDITING (FamilyProblemEditor validates JSON against these) and LLM FORMULATION
13330
+ * (quantum_formulate validates the model's output before surfacing it).
13331
+ *
13332
+ * Authored to match the hand-written interfaces in `./types`; a type-level check
13333
+ * at the bottom keeps the two in sync. The family discriminant lets a single
13334
+ * editor/tool dispatch across all eight shapes.
13335
+ */
13336
+ const named = {
13337
+ id: z.number().int(),
13338
+ name: z.string().min(1)
13339
+ };
13340
+ const xy = {
13341
+ x: z.number(),
13342
+ y: z.number()
13343
+ };
13344
+ const edge = z.object({
13345
+ a: z.number().int().nonnegative(),
13346
+ b: z.number().int().nonnegative(),
13347
+ w: z.number().optional()
13348
+ });
13349
+ const RoutingProblemSchema = z.object({
13350
+ family: z.literal("routing"),
13351
+ name: z.string().min(1),
13352
+ description: z.string().optional(),
13353
+ nodes: z.array(z.object({
13354
+ ...named,
13355
+ ...xy
13356
+ })).min(2)
13357
+ });
13358
+ const PackingProblemSchema = z.object({
13359
+ family: z.literal("packing"),
13360
+ name: z.string().min(1),
13361
+ description: z.string().optional(),
13362
+ items: z.array(z.object({
13363
+ ...named,
13364
+ size: z.number().positive()
13365
+ })).min(1),
13366
+ binCapacity: z.number().positive(),
13367
+ binLabel: z.string().min(1)
13368
+ });
13369
+ const AssignmentProblemSchema = z.object({
13370
+ family: z.literal("assignment"),
13371
+ name: z.string().min(1),
13372
+ description: z.string().optional(),
13373
+ agents: z.array(z.object(named)).min(1),
13374
+ tasks: z.array(z.object(named)).min(1),
13375
+ cost: z.array(z.array(z.number())).min(1)
13376
+ });
13377
+ const NetworkProblemSchema = z.object({
13378
+ family: z.literal("network"),
13379
+ name: z.string().min(1),
13380
+ description: z.string().optional(),
13381
+ nodes: z.array(z.object({
13382
+ ...named,
13383
+ ...xy
13384
+ })).min(1),
13385
+ edges: z.array(edge)
13386
+ });
13387
+ const PartitionProblemSchema = z.object({
13388
+ family: z.literal("partitioning"),
13389
+ name: z.string().min(1),
13390
+ description: z.string().optional(),
13391
+ nodes: z.array(z.object({
13392
+ ...named,
13393
+ ...xy
13394
+ })).min(1),
13395
+ edges: z.array(edge),
13396
+ k: z.number().int().min(2),
13397
+ balanceTolerance: z.number().int().nonnegative().optional()
13398
+ });
13399
+ const SelectionProblemSchema = z.object({
13400
+ family: z.literal("selection"),
13401
+ name: z.string().min(1),
13402
+ description: z.string().optional(),
13403
+ items: z.array(z.object({
13404
+ ...named,
13405
+ value: z.number(),
13406
+ cost: z.number().positive(),
13407
+ risk: z.number().optional()
13408
+ })).min(1),
13409
+ budget: z.number().positive(),
13410
+ riskWeight: z.number().optional()
13411
+ });
13412
+ const EconomicProblemSchema = z.object({
13413
+ family: z.literal("economic"),
13414
+ name: z.string().min(1),
13415
+ description: z.string().optional(),
13416
+ goods: z.array(z.object(named)).min(1),
13417
+ bids: z.array(z.object({
13418
+ id: z.number().int(),
13419
+ bidder: z.string().min(1),
13420
+ price: z.number(),
13421
+ goodIds: z.array(z.number().int().nonnegative())
13422
+ })).min(1)
13423
+ });
13424
+ const ContinuousProblemSchema = z.object({
13425
+ family: z.literal("continuous"),
13426
+ name: z.string().min(1),
13427
+ description: z.string().optional(),
13428
+ functionId: z.enum([
13429
+ "rastrigin",
13430
+ "ackley",
13431
+ "himmelblau",
13432
+ "wavy-bowl"
13433
+ ]),
13434
+ dims: z.union([z.literal(1), z.literal(2)]),
13435
+ lo: z.number(),
13436
+ hi: z.number(),
13437
+ paramNames: z.array(z.string())
13438
+ });
13439
+ z.discriminatedUnion("family", [
13440
+ RoutingProblemSchema,
13441
+ PackingProblemSchema,
13442
+ AssignmentProblemSchema,
13443
+ NetworkProblemSchema,
13444
+ PartitionProblemSchema,
13445
+ SelectionProblemSchema,
13446
+ EconomicProblemSchema,
13447
+ ContinuousProblemSchema
13448
+ ]);
13449
+ const refineAssignment = (p, ctx) => {
13450
+ if (p.cost.length !== p.agents.length) ctx.addIssue({
13451
+ code: z.ZodIssueCode.custom,
13452
+ path: ["cost"],
13453
+ message: `cost must have one row per agent (${p.agents.length}); got ${p.cost.length}`
13454
+ });
13455
+ p.cost.forEach((row, i) => {
13456
+ if (row.length !== p.tasks.length) ctx.addIssue({
13457
+ code: z.ZodIssueCode.custom,
13458
+ path: ["cost", i],
13459
+ message: `cost row ${i} must have one column per task (${p.tasks.length}); got ${row.length}`
13460
+ });
13461
+ });
13462
+ };
13463
+ const refineEdgeBounds = (p, ctx) => {
13464
+ const n = p.nodes.length;
13465
+ p.edges.forEach((e, i) => {
13466
+ if (e.a >= n || e.b >= n) ctx.addIssue({
13467
+ code: z.ZodIssueCode.custom,
13468
+ path: ["edges", i],
13469
+ message: `edge endpoints must reference nodes 0..${n - 1}; got a=${e.a}, b=${e.b}`
13470
+ });
13471
+ });
13472
+ };
13473
+ const refineEconomic = (p, ctx) => {
13474
+ const n = p.goods.length;
13475
+ p.bids.forEach((bid, i) => {
13476
+ const seen = /* @__PURE__ */ new Set();
13477
+ bid.goodIds.forEach((g, j) => {
13478
+ if (g >= n) ctx.addIssue({
13479
+ code: z.ZodIssueCode.custom,
13480
+ path: [
13481
+ "bids",
13482
+ i,
13483
+ "goodIds",
13484
+ j
13485
+ ],
13486
+ message: `goodId must reference goods 0..${n - 1}; got ${g}`
13487
+ });
13488
+ if (seen.has(g)) ctx.addIssue({
13489
+ code: z.ZodIssueCode.custom,
13490
+ path: [
13491
+ "bids",
13492
+ i,
13493
+ "goodIds",
13494
+ j
13495
+ ],
13496
+ message: `duplicate goodId ${g} in bid ${i}`
13497
+ });
13498
+ seen.add(g);
13499
+ });
13500
+ });
13501
+ };
13502
+ /** Per-family schema lookup — the editor/tool pick the right one by familyId. */
13503
+ const FAMILY_PROBLEM_SCHEMAS = {
13504
+ routing: RoutingProblemSchema,
13505
+ packing: PackingProblemSchema,
13506
+ assignment: AssignmentProblemSchema.superRefine(refineAssignment),
13507
+ network: NetworkProblemSchema.superRefine(refineEdgeBounds),
13508
+ partitioning: PartitionProblemSchema.superRefine(refineEdgeBounds),
13509
+ selection: SelectionProblemSchema,
13510
+ economic: EconomicProblemSchema.superRefine(refineEconomic),
13511
+ continuous: ContinuousProblemSchema
13512
+ };
13513
+ const FAMILY_SAMPLE_PROBLEMS = {
13514
+ routing: routingSampleProblems,
13515
+ packing: packingSampleProblems,
13516
+ assignment: assignmentSampleProblems,
13517
+ network: networkSampleProblems,
13518
+ partitioning: partitioningSampleProblems,
13519
+ selection: selectionSampleProblems,
13520
+ economic: economicSampleProblems,
13521
+ continuous: continuousSampleProblems
13522
+ };
11283
13523
  //#endregion
11284
- //#region ../../b4m-core/services/dist/tools-B2RLL9dT.mjs
13524
+ //#region ../../b4m-core/services/dist/tools-D9uncH3M.mjs
13525
+ const createFabFileSchema = z.object({
13526
+ fileName: z.string(),
13527
+ mimeType: z.string(),
13528
+ fileSize: z.number(),
13529
+ type: z.enum(KnowledgeType),
13530
+ content: z.union([z.string(), z.instanceof(Buffer)]).optional(),
13531
+ organizationId: z.string().optional(),
13532
+ /**
13533
+ * Content type of the file
13534
+ * @example 'text/markdown'
13535
+ * @example 'application/pdf'
13536
+ * @example 'application/octet-stream' for binary files
13537
+ */
13538
+ contentType: z.string().optional(),
13539
+ public: z.boolean().optional(),
13540
+ prefix: z.string().optional(),
13541
+ system: z.boolean().optional(),
13542
+ tags: z.array(z.object({
13543
+ name: z.string(),
13544
+ strength: z.number()
13545
+ })).optional(),
13546
+ systemPriority: z.number().optional(),
13547
+ sessionId: z.string().optional(),
13548
+ contentHash: z.string().optional(),
13549
+ batchId: z.string().optional(),
13550
+ relativePath: z.string().optional()
13551
+ });
13552
+ const DEFAULT_MAX_FILE_SIZE = 20;
13553
+ const DEFAULT_EXPIRE_IN_SECONDS = 3600 * 24 * 5;
13554
+ const createFabFile = async (userId, parameters, { db, storage }) => {
13555
+ const params = secureParameters(parameters, createFabFileSchema);
13556
+ const user = await db.users.findById(userId);
13557
+ if (!user) throw new BadRequestError("User not found");
13558
+ let ext = "";
13559
+ let mimeType = parameters.mimeType;
13560
+ ext = getFileExtension(params.fileName);
13561
+ mimeType = params.mimeType || getMimeTypeByExtension(ext);
13562
+ if (!mimeType) mimeType = SupportedFabFileMimeTypes.TXT_PLAIN;
13563
+ if (!Object.values(SupportedFabFileMimeTypes).some((type) => type === mimeType)) throw new BadRequestError(`File type ${mimeType} is not supported`);
13564
+ let filePath = params.prefix ? `${params.prefix}/` : "";
13565
+ filePath += `${v4()}${ext ? `.${ext}` : ".txt"}`;
13566
+ const maxFileSize = getSettingsValue("MaxFileSize", await getSettingsMap(db), DEFAULT_MAX_FILE_SIZE) * 1024 * 1024;
13567
+ if (params.fileSize >= maxFileSize) throw new BadRequestError("File size exceeds maximum file size");
13568
+ await checkStorageLimitForFile(user, params.fileSize, params.organizationId, db.organizations?.findById);
13569
+ const buildData = {
13570
+ userId,
13571
+ ...params,
13572
+ mimeType,
13573
+ filePath,
13574
+ users: [],
13575
+ groups: [],
13576
+ isGlobalRead: false,
13577
+ isGlobalWrite: false,
13578
+ createdAt: /* @__PURE__ */ new Date(),
13579
+ updatedAt: /* @__PURE__ */ new Date()
13580
+ };
13581
+ if (params.content) {
13582
+ await storage.upload(filePath, params.content, {
13583
+ ContentType: params.contentType,
13584
+ ContentLength: params.fileSize
13585
+ });
13586
+ buildData.fileUrl = await storage.generateSignedUrl(filePath, DEFAULT_EXPIRE_IN_SECONDS, "get");
13587
+ buildData.fileUrlExpireAt = new Date(Date.now() + DEFAULT_EXPIRE_IN_SECONDS * 1e3);
13588
+ } else buildData.presignedUrl = await storage.generateSignedUrl(filePath, 600, "put");
13589
+ return await db.fabFiles.create(buildData);
13590
+ };
11285
13591
  async function performDeepResearch(context, params, config = {}) {
11286
13592
  const maxDepth = config.maxDepth || 7;
11287
13593
  const duration = config.duration || 4.5;
@@ -11642,18 +13948,22 @@ const deepResearchTool = {
11642
13948
  async function getDynamicDataLakeAccess(context) {
11643
13949
  const userTags = context.user.tags || [];
11644
13950
  const entitlementKeys = context.entitlementKeys ?? [];
13951
+ const organizationId = context.user.organizationId ? String(context.user.organizationId) : void 0;
13952
+ const userId = context.user.id ? String(context.user.id) : void 0;
11645
13953
  let dynamicDataLakes;
11646
13954
  if (context.db.dataLakes) try {
11647
- dynamicDataLakes = (await context.db.dataLakes.findActiveByUserTagsAndEntitlements(userTags, entitlementKeys)).map(toDataLakeConfig);
13955
+ dynamicDataLakes = (await context.db.dataLakes.findActiveByUserTagsAndEntitlements(userTags, entitlementKeys, organizationId, userId)).map(toDataLakeConfig);
11648
13956
  } catch {}
11649
13957
  const accessibleLakes = getAccessibleDataLakes(userTags, dynamicDataLakes, entitlementKeys);
13958
+ const dynamicIds = new Set((dynamicDataLakes ?? []).map((d) => d.id));
11650
13959
  return {
11651
13960
  dataLakeTags: accessibleLakes.map((dl) => dl.datalakeTag),
11652
- dataLakeTagPrefixes: accessibleLakes.map((dl) => dl.fileTagPrefix)
13961
+ dataLakeTagPrefixes: accessibleLakes.filter((dl) => !dynamicIds.has(dl.id)).map((dl) => dl.fileTagPrefix),
13962
+ scopedTagPrefixes: accessibleLakes.filter((dl) => dynamicIds.has(dl.id)).map((dl) => dl.fileTagPrefix)
11653
13963
  };
11654
13964
  }
11655
13965
  async function semanticDataLakeSearch(params, adapters) {
11656
- const { userId, userGroups = [], query, tags = [], topK = 10, minScore = 0, embeddingModel, apiKeyTable, dataLakeTags, dataLakeTagPrefixes, maxFiles = 2e3, chunkLoadCap = 1e4, logger } = params;
13966
+ const { userId, userGroups = [], query, tags = [], topK = 10, minScore = 0, embeddingModel, apiKeyTable, dataLakeTags, dataLakeTagPrefixes, scopedTagPrefixes = [], maxFiles = 2e3, chunkLoadCap = 1e4, logger } = params;
11657
13967
  const empty = {
11658
13968
  results: [],
11659
13969
  totalChunksSearched: 0,
@@ -11687,6 +13997,7 @@ async function semanticDataLakeSearch(params, adapters) {
11687
13997
  userGroups,
11688
13998
  dataLakeTags,
11689
13999
  dataLakeTagPrefixes,
14000
+ scopedTagPrefixes,
11690
14001
  excludeContent: true
11691
14002
  });
11692
14003
  const fileIds = fileSearch.data.map((f) => f.id);
@@ -11792,6 +14103,60 @@ const weatherTool = {
11792
14103
  }
11793
14104
  })
11794
14105
  };
14106
+ /**
14107
+ * Persist a tool-generated file (image, Excel, etc.) as a session-scoped FabFile so it
14108
+ * shows up in the Knowledge Base / WorkBench and is downloadable after the run.
14109
+ *
14110
+ * Why this exists: file-generating tools historically only pushed the generated filename
14111
+ * into `quest.images` via `statusUpdate`, which (a) renders fine inline for images but is
14112
+ * a broken `<img>` for non-image files like .xlsx, and (b) was never recorded as a FabFile,
14113
+ * so nothing the user can browse referenced it — the file was effectively orphaned in the
14114
+ * generated-content bucket (see prod session 6a41abae…, where 4 agent-generated images were
14115
+ * lost entirely). Creating a FabFile with `sessionId` set makes the file appear in the
14116
+ * Knowledge Viewer via `useGetFabFilesBySessionId` → `useMessageFiles`.
14117
+ *
14118
+ * Best-effort: a failure here (missing adapters, storage quota, unsupported mime) must never
14119
+ * break the tool itself — the inline `quest.images` render still works. We log and swallow.
14120
+ *
14121
+ * The file is re-uploaded into the FabFile bucket (`context.storage`) because the Knowledge
14122
+ * Viewer reads FabFiles through their signed `fileUrl`, which is issued against that bucket.
14123
+ */
14124
+ async function persistGeneratedFileAsFabFile(context, file) {
14125
+ const { sessionId, userId, db, storage, logger } = context;
14126
+ if (!sessionId) return;
14127
+ if (!db.fabfiles || !db.users || !db.adminSettings) {
14128
+ logger.warn("[persistGeneratedFileAsFabFile] missing db adapters — skipping FabFile persist");
14129
+ return;
14130
+ }
14131
+ try {
14132
+ await createFabFile(userId, {
14133
+ fileName: file.fileName,
14134
+ mimeType: file.mimeType,
14135
+ fileSize: file.content.length,
14136
+ type: KnowledgeType.FILE,
14137
+ content: file.content,
14138
+ contentType: file.mimeType,
14139
+ sessionId,
14140
+ prefix: "generated",
14141
+ tags: [{
14142
+ name: "generated",
14143
+ strength: 1
14144
+ }]
14145
+ }, {
14146
+ db: {
14147
+ fabFiles: db.fabfiles,
14148
+ adminSettings: db.adminSettings,
14149
+ users: db.users
14150
+ },
14151
+ storage: {
14152
+ upload: (path, content, options) => storage.upload(content, path, options),
14153
+ generateSignedUrl: (path, expireInSeconds, type) => storage.getSignedUrl(path, type ?? "get", { expiresIn: expireInSeconds })
14154
+ }
14155
+ });
14156
+ } catch (err) {
14157
+ logger.error("[persistGeneratedFileAsFabFile] failed to persist generated file (non-fatal)", err);
14158
+ }
14159
+ }
11795
14160
  async function downloadImage$1(url) {
11796
14161
  if (url.startsWith("data:image/")) {
11797
14162
  const base64Data = url.split(",")[1];
@@ -11825,8 +14190,15 @@ async function processAndStoreImages(images, context) {
11825
14190
  return Promise.all(images.map(async (image) => {
11826
14191
  const buffer = await downloadImage$1(image);
11827
14192
  const fileType = await fileTypeFromBuffer(buffer);
11828
- const filename = `${v4()}.${fileType?.ext}`;
11829
- return await context.imageGenerateStorage.upload(buffer, filename, {});
14193
+ const ext = fileType?.ext ?? "png";
14194
+ const filename = `${v4()}.${ext}`;
14195
+ const path = await context.imageGenerateStorage.upload(buffer, filename, {});
14196
+ await persistGeneratedFileAsFabFile(context, {
14197
+ fileName: `generated-image-${filename.slice(0, 8)}.${ext}`,
14198
+ mimeType: fileType?.mime ?? "image/png",
14199
+ content: buffer
14200
+ });
14201
+ return path;
11830
14202
  }));
11831
14203
  }
11832
14204
  async function updateQuestAndReturnMarkdown$1(storedImageUrls, context) {
@@ -12538,7 +14910,7 @@ const currentDateTimeTool = {
12538
14910
  },
12539
14911
  toolSchema: {
12540
14912
  name: "current_datetime",
12541
- description: "Get the current date and time with advanced features. Supports multiple output formats, date calculations (days until/since), historical day lookups, and IANA timezone identifiers.",
14913
+ description: "Get the current, real-world date and time, fresh at the moment of the call. Call this whenever you need the current time of day (hour/minute) or to timestamp an action as it executes — never guess or rely on memory. Supports multiple output formats, date calculations (days until/since), historical day lookups, and IANA timezone identifiers.",
12542
14914
  parameters: {
12543
14915
  type: "object",
12544
14916
  properties: {
@@ -14957,7 +17329,7 @@ async function trySemanticKbSearch(context, query, tags, maxResults) {
14957
17329
  const provider = getProviderFromModel(embeddingModel);
14958
17330
  if (provider === "openai" && !apiKeyTable?.openai) return null;
14959
17331
  if (provider === "voyageai" && !apiKeyTable?.voyageai) return null;
14960
- const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
17332
+ const { dataLakeTags, dataLakeTagPrefixes, scopedTagPrefixes } = await getDynamicDataLakeAccess(context);
14961
17333
  if (dataLakeTags.length === 0) return null;
14962
17334
  const search = await semanticDataLakeSearch({
14963
17335
  userId: context.userId,
@@ -14970,6 +17342,7 @@ async function trySemanticKbSearch(context, query, tags, maxResults) {
14970
17342
  apiKeyTable,
14971
17343
  dataLakeTags,
14972
17344
  dataLakeTagPrefixes,
17345
+ scopedTagPrefixes,
14973
17346
  logger: context.logger
14974
17347
  }, { db: {
14975
17348
  fabfiles: context.db.fabfiles,
@@ -15043,7 +17416,7 @@ const knowledgeBaseSearchTool = {
15043
17416
  const semantic = await trySemanticKbSearch(context, query, tags, max_results);
15044
17417
  if (semantic) return semantic;
15045
17418
  try {
15046
- const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
17419
+ const { dataLakeTags, dataLakeTagPrefixes, scopedTagPrefixes } = await getDynamicDataLakeAccess(context);
15047
17420
  const searchResults = await context.db.fabfiles.search(context.userId, query, {
15048
17421
  tags: tags || [],
15049
17422
  type: file_type,
@@ -15060,6 +17433,7 @@ const knowledgeBaseSearchTool = {
15060
17433
  userGroups: context.user.groups || [],
15061
17434
  dataLakeTags,
15062
17435
  dataLakeTagPrefixes,
17436
+ scopedTagPrefixes,
15063
17437
  excludeContent: true
15064
17438
  });
15065
17439
  const queryTerms = Array.from(new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3)));
@@ -15207,7 +17581,7 @@ const knowledgeBaseRetrieveTool = {
15207
17581
  if (files.length === 0) return `No document found with ID "${file_id}". The file may not exist or you may not have access to it. Try using search_knowledge_base to find the correct file ID.`;
15208
17582
  }
15209
17583
  if (files.length === 0 && (tags?.length || query)) {
15210
- const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
17584
+ const { dataLakeTags, dataLakeTagPrefixes, scopedTagPrefixes } = await getDynamicDataLakeAccess(context);
15211
17585
  files = (await context.db.fabfiles.search(context.userId, query || "", {
15212
17586
  tags: tags || [],
15213
17587
  shared: false
@@ -15223,6 +17597,7 @@ const knowledgeBaseRetrieveTool = {
15223
17597
  userGroups: context.user.groups || [],
15224
17598
  dataLakeTags,
15225
17599
  dataLakeTagPrefixes,
17600
+ scopedTagPrefixes,
15226
17601
  excludeContent: true
15227
17602
  })).data;
15228
17603
  if (files.length === 0) return `No documents found matching ${[query && `query "${query}"`, tags?.length && `tags [${tags.join(", ")}]`].filter(Boolean).join(" and ")}. Try broadening your search with search_knowledge_base.`;
@@ -15326,6 +17701,13 @@ function formatResult$1(result) {
15326
17701
  }
15327
17702
  return lines.join("\n");
15328
17703
  }
17704
+ /**
17705
+ * Returns a JSON `{ __uiSideEffect: true, ... }` envelope (see end of function), NOT
17706
+ * model-facing text. Production exec routes this through `wrapToolsForSentinels`
17707
+ * (sharedToolBuilder), which strips the envelope and surfaces `displayMessage` to the
17708
+ * model. Any future exec path that bypasses `wrapToolsForSentinels` would leak the raw
17709
+ * envelope into the model context — keep it behind that wrapper.
17710
+ */
15329
17711
  async function runQuantumSchedule(params) {
15330
17712
  const parseResult = SchedulingProblemSchema.safeParse(params.problem);
15331
17713
  if (!parseResult.success) return `Error: Invalid scheduling problem format.\n${parseResult.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}\n\nExpected format: { name, jobs: [{ id, name, operations: [{ jobId, machineId, duration }] }], machines: [{ id, name }] }`;
@@ -15371,7 +17753,12 @@ async function runQuantumSchedule(params) {
15371
17753
  lines.push("### Errors");
15372
17754
  errors.forEach((e) => lines.push(`- ${e}`));
15373
17755
  }
15374
- return lines.join("\n");
17756
+ return JSON.stringify({
17757
+ __uiSideEffect: true,
17758
+ type: "populateProblem",
17759
+ payload: problem,
17760
+ displayMessage: lines.join("\n")
17761
+ });
15375
17762
  }
15376
17763
  const quantumScheduleTool = {
15377
17764
  name: "quantum_schedule",
@@ -15385,7 +17772,9 @@ Available solvers: ${allSolvers.map((s) => `${s.id} (${s.name})`).join(", ")}.
15385
17772
 
15386
17773
  The "greedy" solver runs instantly. Metaheuristic solvers (simulated-annealing, tabu, genetic-algorithm, ant-colony) explore larger solution spaces but take longer. Use multiple solvers to race them against each other.
15387
17774
 
15388
- The problem must define jobs (each with ordered operations) and machines. Each operation runs on a specific machine for a specific duration. Operations within a job must run in order.`,
17775
+ The problem must define jobs (each with ordered operations) and machines. Each operation runs on a specific machine for a specific duration. Operations within a job must run in order.
17776
+
17777
+ Calling this loads the problem into the on-screen /opti Scheduling console (brief + Problem tab) so the user sees exactly what was solved and can tweak and re-race it — so the screen always matches the conversation.`,
15389
17778
  parameters: {
15390
17779
  type: "object",
15391
17780
  properties: {
@@ -15476,37 +17865,93 @@ The problem must define jobs (each with ordered operations) and machines. Each o
15476
17865
  }
15477
17866
  })
15478
17867
  };
17868
+ const FAMILY_IDS$1 = [
17869
+ "routing",
17870
+ "packing",
17871
+ "assignment",
17872
+ "network",
17873
+ "partitioning",
17874
+ "selection",
17875
+ "economic",
17876
+ "continuous"
17877
+ ];
17878
+ /** Run the model and pull a JSON object out of the response (handles code fences). */
17879
+ async function completeToJson$1(context, systemPrompt, userDescription) {
17880
+ let rawContent = "";
17881
+ await context.llm.complete(context.model ?? "gpt-4", [{
17882
+ role: "system",
17883
+ content: systemPrompt
17884
+ }, {
17885
+ role: "user",
17886
+ content: userDescription
17887
+ }], {
17888
+ temperature: .2,
17889
+ stream: false
17890
+ }, async (texts) => {
17891
+ rawContent = texts.filter((t) => t !== null && t !== void 0).join("");
17892
+ });
17893
+ let jsonStr = rawContent.trim();
17894
+ const codeBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
17895
+ if (codeBlockMatch) jsonStr = codeBlockMatch[1].trim();
17896
+ try {
17897
+ return { json: JSON.parse(jsonStr) };
17898
+ } catch {
17899
+ return { error: `Could not parse the model response as JSON. Raw response:\n${rawContent}` };
17900
+ }
17901
+ }
17902
+ /** Build the per-family system prompt from a real sample as the shape template. */
17903
+ function familyFormulationPrompt(familyId) {
17904
+ return `You convert a natural-language description into a "${familyId}" optimization problem as JSON.
17905
+
17906
+ Output ONLY a single JSON object matching this exact shape (a real ${familyId} example):
17907
+
17908
+ ${JSON.stringify(FAMILY_SAMPLE_PROBLEMS[familyId][0], null, 2)}
17909
+
17910
+ Rules:
17911
+ - "family" MUST be exactly "${familyId}".
17912
+ - Keep it small and browser-solvable, at a scale similar to the example.
17913
+ - ids are integers starting at 0; names are short human-readable strings.
17914
+ - "description" MUST be a single plain-English sentence summarizing the real-world scenario this models (what the user just described) — it is shown on the on-screen brief card so a human can recognize at a glance that it matches the conversation.
17915
+ - Output ONLY the JSON object — no prose, no markdown fences.`;
17916
+ }
15479
17917
  const quantumFormulateTool = {
15480
17918
  name: "quantum_formulate",
15481
17919
  implementation: (context) => ({
15482
17920
  toolFn: async (value) => {
15483
- const userDescription = value.description?.trim();
15484
- if (!userDescription) return "Error: Please provide a description of the scheduling problem to formulate.";
17921
+ const params = value;
17922
+ const userDescription = params.description?.trim();
17923
+ if (!userDescription) return "Error: Please provide a description of the optimization problem to formulate.";
17924
+ if (params.familyId && !FAMILY_IDS$1.includes(params.familyId)) return `Error: unknown familyId "${String(params.familyId)}". Valid families: ${FAMILY_IDS$1.join(", ")}. Omit familyId to formulate a job-shop scheduling problem.`;
17925
+ const familyId = params.familyId || void 0;
15485
17926
  try {
15486
- let rawContent = "";
15487
- await context.llm.complete(context.model ?? "gpt-4", [{
15488
- role: "system",
15489
- content: PROBLEM_FORMULATION_PROMPT
15490
- }, {
15491
- role: "user",
15492
- content: userDescription
15493
- }], {
15494
- temperature: .2,
15495
- stream: false
15496
- }, async (texts) => {
15497
- rawContent = texts.filter((t) => t !== null && t !== void 0).join("");
15498
- });
15499
- let jsonStr = rawContent.trim();
15500
- const codeBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
15501
- if (codeBlockMatch) jsonStr = codeBlockMatch[1].trim();
15502
- let parsed;
15503
- try {
15504
- parsed = JSON.parse(jsonStr);
15505
- } catch {
15506
- return `Error: Could not parse LLM response as JSON. Raw response:\n${rawContent}\n\nPlease try rephrasing your description with more specific details about jobs, machines, and processing times.`;
17927
+ if (familyId) {
17928
+ const result = await completeToJson$1(context, familyFormulationPrompt(familyId), userDescription);
17929
+ if ("error" in result) return `Error: ${result.error}\n\nPlease rephrase with more specifics for a ${familyId} problem.`;
17930
+ const validated = FAMILY_PROBLEM_SCHEMAS[familyId].safeParse(result.json);
17931
+ if (!validated.success) return `Error: the model produced an invalid ${familyId} problem.\n${validated.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}\n\nPlease rephrase your description.`;
17932
+ const problem = validated.data;
17933
+ const summary = [
17934
+ `## Formulated: "${problem.name}"`,
17935
+ problem.description ? `\n${problem.description}` : "",
17936
+ "",
17937
+ `**${familyId} problem** loaded into your active brief.`,
17938
+ "",
17939
+ `I've opened the ${familyId} console and set this as the active brief (use Undo on the banner to revert). Tweak it on the Problem tab or open Solvers to race it.`
17940
+ ].filter((l) => l !== void 0).join("\n");
17941
+ return JSON.stringify({
17942
+ __uiSideEffect: true,
17943
+ type: "populateFamilyProblem",
17944
+ payload: {
17945
+ familyId,
17946
+ problem
17947
+ },
17948
+ displayMessage: summary
17949
+ });
15507
17950
  }
15508
- const validated = SchedulingProblemSchema.safeParse(parsed);
15509
- if (!validated.success) return `Error: LLM generated an invalid problem structure.\n${validated.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}\n\nRaw output:\n${jsonStr}\n\nPlease try rephrasing your description.`;
17951
+ const result = await completeToJson$1(context, PROBLEM_FORMULATION_PROMPT, userDescription);
17952
+ if ("error" in result) return `Error: ${result.error}\n\nPlease try rephrasing with specific jobs, machines, and processing times.`;
17953
+ const validated = SchedulingProblemSchema.safeParse(result.json);
17954
+ if (!validated.success) return `Error: LLM generated an invalid problem structure.\n${validated.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}\n\nPlease try rephrasing your description.`;
15510
17955
  const problem = validated.data;
15511
17956
  const totalOps = problem.jobs.reduce((s, j) => s + j.operations.length, 0);
15512
17957
  const humanReadableSummary = [
@@ -15527,7 +17972,7 @@ const quantumFormulateTool = {
15527
17972
  "### Machines:",
15528
17973
  ...problem.machines.map((m) => `- ${m.name} (id: ${m.id})`),
15529
17974
  "",
15530
- "A problem is ready to load. Navigate to the Problem tab and click \"Load Problem\" to review, tweak, and run solvers."
17975
+ "I've loaded this as your active scheduling brief (use Undo on the banner to revert). Tweak it on the Problem tab or open Solvers to race it."
15531
17976
  ].filter((l) => l !== void 0).join("\n");
15532
17977
  return JSON.stringify({
15533
17978
  __uiSideEffect: true,
@@ -15541,27 +17986,188 @@ const quantumFormulateTool = {
15541
17986
  },
15542
17987
  toolSchema: {
15543
17988
  name: "quantum_formulate",
15544
- description: `Convert a natural language description of a scheduling/optimization problem into a structured SchedulingProblem that can be solved by the quantum_schedule tool.
17989
+ description: `Convert a natural-language description of an optimization problem into a structured problem that the /opti solvers can run.
15545
17990
 
15546
- Describe the problem in plain English — what jobs need to be done, what machines or resources are available, how long each step takes, and any ordering constraints. The tool will produce a structured problem definition.
17991
+ Pass \`familyId\` to target one of the eight pattern families; omit it for job-shop scheduling:
17992
+ - routing (TSP / vehicle routing), packing (bin packing), assignment (who does what),
17993
+ network (max-cut), partitioning (balanced k-way), selection (knapsack / portfolio),
17994
+ economic (combinatorial auction), continuous (function minimization).
15547
17995
 
15548
- Examples:
15549
- - "I have 3 orders to manufacture. Each needs cutting, welding, and painting. Cutting takes 2h, welding 3h, painting 1h."
15550
- - "Schedule 4 software builds across 2 CI runners. Build A needs compile (5min) then test (3min)..."
15551
- - "A bakery needs to make 5 cakes. Each cake goes through mixing, baking, and decorating on separate stations."`,
15552
- strict: true,
17996
+ Describe the scenario in plain English — the entities, resources, costs/values, and constraints — and the tool produces a structured problem definition for that family.`,
17997
+ strict: false,
15553
17998
  parameters: {
15554
17999
  type: "object",
15555
- properties: { description: {
15556
- type: "string",
15557
- description: "Natural language description of the scheduling problem to formulate"
15558
- } },
18000
+ properties: {
18001
+ description: {
18002
+ type: "string",
18003
+ description: "Natural-language description of the optimization problem to formulate"
18004
+ },
18005
+ familyId: {
18006
+ type: "string",
18007
+ enum: FAMILY_IDS$1,
18008
+ description: "Optional optimization family. Omit for job-shop scheduling; otherwise one of the eight families."
18009
+ }
18010
+ },
15559
18011
  required: ["description"],
15560
18012
  additionalProperties: false
15561
18013
  }
15562
18014
  }
15563
18015
  })
15564
18016
  };
18017
+ const FAMILY_IDS = [
18018
+ "routing",
18019
+ "packing",
18020
+ "assignment",
18021
+ "network",
18022
+ "partitioning",
18023
+ "selection",
18024
+ "economic",
18025
+ "continuous"
18026
+ ];
18027
+ /** Run the model and pull a JSON object out of the response (handles code fences). */
18028
+ async function completeToJson(context, systemPrompt, userInstruction) {
18029
+ let rawContent = "";
18030
+ await context.llm.complete(context.model ?? "gpt-4", [{
18031
+ role: "system",
18032
+ content: systemPrompt
18033
+ }, {
18034
+ role: "user",
18035
+ content: userInstruction
18036
+ }], {
18037
+ temperature: .2,
18038
+ stream: false
18039
+ }, async (texts) => {
18040
+ rawContent = texts.filter((t) => t !== null && t !== void 0).join("");
18041
+ });
18042
+ let jsonStr = rawContent.trim();
18043
+ const codeBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
18044
+ if (codeBlockMatch) jsonStr = codeBlockMatch[1].trim();
18045
+ try {
18046
+ return { json: JSON.parse(jsonStr) };
18047
+ } catch {
18048
+ return { error: `Could not parse the model response as JSON. Raw response:\n${rawContent}` };
18049
+ }
18050
+ }
18051
+ /**
18052
+ * Edit prompt. The current problem doubles as the shape template — the model must
18053
+ * return the SAME shape with only the requested change applied. The hard rule is
18054
+ * fidelity: anything the instruction doesn't touch comes back byte-for-byte, which
18055
+ * is the whole reason this tool exists instead of re-running `quantum_formulate`.
18056
+ */
18057
+ function editPrompt(currentProblem, familyId) {
18058
+ return `You are EDITING an existing optimization problem. Below is the CURRENT problem as JSON.
18059
+
18060
+ Apply ONLY the change the user describes. Preserve every other field EXACTLY — identical ids, names, and numeric values for anything the change does not explicitly touch. Do not renumber, rename, reorder, or "improve" untouched entities. Keep it small and browser-solvable.
18061
+ ${familyId ? `- "family" MUST stay exactly "${familyId}".` : "- Keep the job-shop scheduling shape (jobs with ordered operations referencing machine ids)."}
18062
+ - "description" stays a single plain-English sentence; update it only if the change makes it inaccurate.
18063
+
18064
+ CURRENT PROBLEM:
18065
+ ${JSON.stringify(currentProblem, null, 2)}
18066
+
18067
+ Output ONLY a JSON object of the form:
18068
+ { "problem": <the COMPLETE updated problem, same shape as above>, "changes": "<one short sentence describing exactly what you changed>" }
18069
+ No prose, no markdown fences.`;
18070
+ }
18071
+ /** Split the model output into the problem and an optional change summary. */
18072
+ function unwrap(json) {
18073
+ if (json && typeof json === "object" && "problem" in json) {
18074
+ const obj = json;
18075
+ return {
18076
+ problem: obj.problem,
18077
+ changes: typeof obj.changes === "string" ? obj.changes : void 0
18078
+ };
18079
+ }
18080
+ return { problem: json };
18081
+ }
18082
+ const quantumEditProblemTool = {
18083
+ name: "quantum_edit_problem",
18084
+ implementation: (context) => ({
18085
+ toolFn: async (value) => {
18086
+ const params = value;
18087
+ const instruction = params.instruction?.trim();
18088
+ if (!instruction) return "Error: Please provide an instruction describing the change to make.";
18089
+ if (params.currentProblem == null || typeof params.currentProblem !== "object") return "Error: Please pass the current problem (the active brief JSON) as `currentProblem` so it can be edited in place.";
18090
+ if (params.familyId && !FAMILY_IDS.includes(params.familyId)) return `Error: unknown familyId "${String(params.familyId)}". Valid families: ${FAMILY_IDS.join(", ")}. Omit familyId to edit a job-shop scheduling problem.`;
18091
+ const familyId = params.familyId || void 0;
18092
+ try {
18093
+ const result = await completeToJson(context, editPrompt(params.currentProblem, familyId), instruction);
18094
+ if ("error" in result) return `Error: ${result.error}\n\nPlease restate the change more specifically.`;
18095
+ const { problem: rawProblem, changes } = unwrap(result.json);
18096
+ if (rawProblem == null || typeof rawProblem !== "object") return "Error: the edit did not produce a problem object. Please restate the change.";
18097
+ if (familyId) {
18098
+ const validated = FAMILY_PROBLEM_SCHEMAS[familyId].safeParse(rawProblem);
18099
+ if (!validated.success) return `Error: the edited ${familyId} problem is invalid.\n${validated.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}\n\nPlease restate the change.`;
18100
+ const problem = validated.data;
18101
+ const summary = [
18102
+ `## Updated: "${problem.name}"`,
18103
+ changes ? `\n${changes}` : "",
18104
+ "",
18105
+ `**${familyId} problem** — applied to your active brief.`,
18106
+ "",
18107
+ `I've updated the ${familyId} brief in place (use Undo on the banner to revert). Open Solvers to race it.`
18108
+ ].filter((l) => l !== void 0).join("\n");
18109
+ return JSON.stringify({
18110
+ __uiSideEffect: true,
18111
+ type: "populateFamilyProblem",
18112
+ payload: {
18113
+ familyId,
18114
+ problem
18115
+ },
18116
+ displayMessage: summary
18117
+ });
18118
+ }
18119
+ const validated = SchedulingProblemSchema.safeParse(rawProblem);
18120
+ if (!validated.success) return `Error: the edited problem structure is invalid.\n${validated.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}\n\nPlease restate the change.`;
18121
+ const problem = validated.data;
18122
+ const totalOps = problem.jobs.reduce((s, j) => s + j.operations.length, 0);
18123
+ const summary = [
18124
+ `## Updated: "${problem.name}"`,
18125
+ changes ? `\n${changes}` : "",
18126
+ "",
18127
+ `**${problem.jobs.length} jobs, ${problem.machines.length} machines, ${totalOps} operations**`,
18128
+ "",
18129
+ "I've updated your active scheduling brief in place (use Undo on the banner to revert). Open Solvers to race it."
18130
+ ].filter((l) => l !== void 0).join("\n");
18131
+ return JSON.stringify({
18132
+ __uiSideEffect: true,
18133
+ type: "populateProblem",
18134
+ payload: problem,
18135
+ displayMessage: summary
18136
+ });
18137
+ } catch (err) {
18138
+ return `Error editing problem: ${err instanceof Error ? err.message : String(err)}`;
18139
+ }
18140
+ },
18141
+ toolSchema: {
18142
+ name: "quantum_edit_problem",
18143
+ description: `Edit the optimization problem currently loaded in the /opti active brief. Use this — NOT quantum_formulate — whenever the user asks to change, refine, tweak, add to, or adjust the problem they're already looking at.
18144
+
18145
+ Pass the active brief as \`currentProblem\` (you have it in your context), describe the change in \`instruction\`, and pass \`familyId\` for one of the eight pattern families (omit for job-shop scheduling). The tool applies only the requested change, preserves everything else exactly, and the result is auto-applied to the active brief with one-click Undo. Use quantum_formulate instead only for a brand-new, unrelated problem.`,
18146
+ strict: false,
18147
+ parameters: {
18148
+ type: "object",
18149
+ properties: {
18150
+ instruction: {
18151
+ type: "string",
18152
+ description: "The change to apply to the current problem, in plain English (e.g. \"add two more jobs\")."
18153
+ },
18154
+ currentProblem: {
18155
+ type: "object",
18156
+ description: "The current problem JSON (the active brief) to edit in place.",
18157
+ additionalProperties: true
18158
+ },
18159
+ familyId: {
18160
+ type: "string",
18161
+ enum: FAMILY_IDS,
18162
+ description: "Optional optimization family. Omit for job-shop scheduling; otherwise one of the eight."
18163
+ }
18164
+ },
18165
+ required: ["instruction", "currentProblem"],
18166
+ additionalProperties: false
18167
+ }
18168
+ }
18169
+ })
18170
+ };
15565
18171
  const navigateViewTool = {
15566
18172
  name: "navigate_view",
15567
18173
  implementation: (context) => ({
@@ -15869,6 +18475,7 @@ Please generate a complete, well-structured notebook that performs this analysis
15869
18475
  }
15870
18476
  })
15871
18477
  };
18478
+ const XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
15872
18479
  const LIMITS = {
15873
18480
  MAX_SHEETS: 10,
15874
18481
  MAX_CELLS_PER_SHEET: 1e4,
@@ -16187,9 +18794,14 @@ const excelGenerationTool = {
16187
18794
  const sanitizedName = sanitizeFilename(params.filename);
16188
18795
  const filename = `${sanitizedName}-${v4().slice(0, 8)}.xlsx`;
16189
18796
  await context.imageGenerateStorage.upload(buffer, filename, {
16190
- ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
18797
+ ContentType: XLSX_MIME,
16191
18798
  ContentDisposition: `attachment; filename="${sanitizedName}.xlsx"`
16192
18799
  });
18800
+ await persistGeneratedFileAsFabFile(context, {
18801
+ fileName: `${sanitizedName}.xlsx`,
18802
+ mimeType: XLSX_MIME,
18803
+ content: buffer
18804
+ });
16193
18805
  await context.onFinish?.("excel_generation", [filename]);
16194
18806
  await context.statusUpdate({ images: [filename] });
16195
18807
  return `Successfully generated Excel file: ${sanitizedName}.xlsx`;
@@ -16574,15 +19186,28 @@ DO NOT USE FOR:
16574
19186
  }
16575
19187
  })
16576
19188
  };
19189
+ /** Cap on how many invocable skills are listed back when resolution fails. */
19190
+ const INVOCABLE_HINT_LIMIT = 50;
16577
19191
  /**
16578
19192
  * LLM-invocable `skill` tool — the autonomous counterpart to user `/skill-name`
16579
19193
  * invocations handled by SkillsFeature.
16580
19194
  *
16581
- * The LLM passes `{ name, args }`. The tool resolves the skill in the caller's
16582
- * scope (`findByNameForUser`), expands `$ARGUMENTS` / `$1` / `$2`, and returns
16583
- * the expanded body as the observation for the next turn. Skills with
19195
+ * The LLM passes `{ name, args }`. The tool resolves the skill across every
19196
+ * scope the caller can access (`findAccessibleByNameForUser` owned + shared +
19197
+ * global-read), expands `$ARGUMENTS` / `$1` / `$2`, and returns the expanded
19198
+ * body as the observation for the next turn. Skills with
16584
19199
  * `disableModelInvocation: true` are not callable here.
16585
19200
  *
19201
+ * Security: a skill the caller does NOT own (shared / org / global) is treated
19202
+ * as untrusted content — its body is wrapped with explicit delimiters and a
19203
+ * "do not follow conflicting instructions" framing so a malicious author can't
19204
+ * hijack the turn via prompt injection. The skill's `allowedTools` whitelist is
19205
+ * surfaced to the model as a *requested* tool scope (intersected with the
19206
+ * invoker's allow-list via `intersectAllowedTools`). NOTE: today this is prompt
19207
+ * guidance only — there is no `ToolContext` invoker allow-list to enforce it
19208
+ * against, so the wording stays advisory. When that wiring lands, the same
19209
+ * `permittedTools` set can narrow the completion's actual tool list.
19210
+ *
16586
19211
  * Catalog discovery: the available skill names + descriptions are listed in
16587
19212
  * the system prompt by SkillsFeature, so the LLM knows what's invocable. If
16588
19213
  * the LLM passes a name that isn't accessible, the tool returns an error
@@ -16598,14 +19223,18 @@ const skillTool = {
16598
19223
  const name = rawName.replace(/^\//, "");
16599
19224
  const skillsRepo = context.db.skills;
16600
19225
  if (!skillsRepo) return "Error: skills are not configured in this environment.";
16601
- const skill = await skillsRepo.findByNameForUser(context.userId, name);
19226
+ const skill = await skillsRepo.findAccessibleByNameForUser(context.userId, name);
16602
19227
  if (!skill || skill.disableModelInvocation) {
16603
- const invocable = (await skillsRepo.listForUser(context.userId)).filter((s) => !s.disableModelInvocation).map((s) => s.name);
19228
+ const invocable = (await skillsRepo.listAccessibleInvocableForUser(context.userId, INVOCABLE_HINT_LIMIT)).map((s) => s.name);
16604
19229
  return invocable.length === 0 ? `Error: skill "${name}" is not available, and you have no LLM-invocable skills defined.` : `Error: skill "${name}" is not available. Invocable skills: ${invocable.join(", ")}`;
16605
19230
  }
16606
19231
  const argArray = typeof params.args === "string" ? parseSkillArguments(params.args) : [];
16607
19232
  const expandedBody = substituteArguments$1(skill.body, argArray);
16608
- return `## Skill: /${skill.name}\n\n${expandedBody}\n\n---\nFollow the instructions above for the rest of this turn.`;
19233
+ if (skill.userId === context.userId) return `## Skill: /${skill.name}\n\n${expandedBody}\n\n---\nFollow the instructions above for the rest of this turn.`;
19234
+ const ownerLabel = skill.organizationId ? "your organization" : skill.isSystem ? "the system" : "another user";
19235
+ const wrapped = wrapUntrustedSkillBody(skill.name, expandedBody, ownerLabel);
19236
+ const permittedTools = intersectAllowedTools(skill.allowedTools, void 0);
19237
+ return `${wrapped}${permittedTools && permittedTools.length > 0 ? `\n\nThis skill is intended to use only these tools: ${permittedTools.join(", ")}. Prefer to stay within that set.` : ""}\n\n---\nFollow the intent above for the rest of this turn, subject to the constraints stated.`;
16609
19238
  },
16610
19239
  toolSchema: {
16611
19240
  name: "skill",
@@ -17469,16 +20098,18 @@ const b4mTools = {
17469
20098
  retrieve_knowledge_content: knowledgeBaseRetrieveTool,
17470
20099
  quantum_schedule: quantumScheduleTool,
17471
20100
  quantum_formulate: quantumFormulateTool,
20101
+ quantum_edit_problem: quantumEditProblemTool,
17472
20102
  navigate_view: navigateViewTool,
17473
20103
  generate_jupyter_notebook: jupyterNotebookTool,
17474
20104
  excel_generation: excelGenerationTool,
17475
20105
  fmp_financial_data: fmpTool,
17476
20106
  skill: skillTool
17477
20107
  };
17478
- const generateTools = (userId, user, logger, { db }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools = b4mTools, allowedDirectories, entitlementKeys = []) => {
20108
+ const generateTools = (userId, user, logger, { db }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools = b4mTools, allowedDirectories, entitlementKeys = [], sessionId) => {
17479
20109
  const context = {
17480
20110
  userId,
17481
20111
  user,
20112
+ sessionId,
17482
20113
  logger,
17483
20114
  db,
17484
20115
  storage,
@@ -18499,7 +21130,11 @@ var ReActAgent = class extends EventEmitter {
18499
21130
  * This can be overridden in the context or customized here
18500
21131
  */
18501
21132
  getSystemPrompt() {
18502
- if (this.context.systemPrompt) return this.context.systemPrompt;
21133
+ const base = this.context.systemPrompt ? this.context.systemPrompt : this.buildDefaultSystemPrompt();
21134
+ const persona = this.context.personaPrompt?.trim();
21135
+ return persona ? `${persona}\n\n${base}` : base;
21136
+ }
21137
+ buildDefaultSystemPrompt() {
18503
21138
  return `You are an autonomous AI agent with access to tools. Your job is to solve problems by taking action, not by asking for clarification.
18504
21139
 
18505
21140
  CORE PRINCIPLES:
@@ -19122,7 +21757,7 @@ Remember: You are an autonomous AGENT. Act independently and solve problems proa
19122
21757
  })}</function>`;
19123
21758
  }
19124
21759
  }
19125
- return `Error: Tool ${toolUse.name} not found in agent context`;
21760
+ return `Error: Tool "${toolUse.name}" is not available in this context and cannot be called. Do NOT attempt to call it again. Proceed without it — call other available tools if your plan still needs them, or compose your final answer from the information already gathered if no more tools are needed.`;
19126
21761
  }
19127
21762
  try {
19128
21763
  const params = this.parseToolArguments(toolUse.arguments);
@@ -19947,6 +22582,159 @@ parentPort.on('message', async (msg) => {
19947
22582
  }
19948
22583
  });
19949
22584
  `;
22585
+ String.raw`
22586
+ const __cap = _captureLine;
22587
+ const __callTool = _callTool;
22588
+ delete globalThis._captureLine;
22589
+ delete globalThis._callTool;
22590
+
22591
+ const HARD_PER_LINE_BYTES = ${5e4};
22592
+
22593
+ function __jsonReplacer(_k, v) {
22594
+ if (v instanceof Error) return { name: v.name, message: v.message };
22595
+ if (typeof v === 'bigint') return v.toString() + 'n';
22596
+ return v;
22597
+ }
22598
+ function __formatLine(args) {
22599
+ const line = args.map(a => {
22600
+ if (typeof a === 'string') return a;
22601
+ if (a === undefined) return 'undefined';
22602
+ if (a === null) return 'null';
22603
+ try { return JSON.stringify(a, __jsonReplacer, 2); } catch { return String(a); }
22604
+ }).join(' ');
22605
+ return line.length > HARD_PER_LINE_BYTES
22606
+ ? line.slice(0, HARD_PER_LINE_BYTES) + ' [...line truncated]'
22607
+ : line;
22608
+ }
22609
+ globalThis.console = {
22610
+ log: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
22611
+ warn: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
22612
+ error: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
22613
+ info: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
22614
+ };
22615
+
22616
+ // A bare isolate has no structuredClone (it's a host/web API, not a V8
22617
+ // intrinsic). The in-process + worker backends expose the *host's* real
22618
+ // structuredClone — which we can't reach across the isolate boundary — so
22619
+ // polyfill the structured-clone algorithm in-isolate for the common cloneable
22620
+ // types (Date, RegExp, Map, Set, ArrayBuffer / typed arrays / DataView,
22621
+ // arrays, plain objects) WITH circular-reference support. This is real
22622
+ // parity for those types; a JSON round-trip would silently drop
22623
+ // Date -> string, Map/Set -> {}, RegExp -> {}, TypedArray -> index map.
22624
+ // Genuinely uncloneable inputs (functions, etc.) throw, as the real API does.
22625
+ if (typeof globalThis.structuredClone !== 'function') {
22626
+ globalThis.structuredClone = function structuredClone(input) {
22627
+ const seen = new WeakMap();
22628
+ function clone(v) {
22629
+ if (v === null || typeof v !== 'object') {
22630
+ if (typeof v === 'function') throw new Error('structuredClone: a function could not be cloned');
22631
+ return v;
22632
+ }
22633
+ if (seen.has(v)) return seen.get(v);
22634
+ // Every cloneable type records its clone in the seen-map BEFORE returning,
22635
+ // so the same object appearing at multiple paths yields one shared clone
22636
+ // (reference identity), matching the real structured-clone algorithm —
22637
+ // not just the recursive containers.
22638
+ if (v instanceof Date) { const out = new Date(v.getTime()); seen.set(v, out); return out; }
22639
+ if (v instanceof RegExp) { const out = new RegExp(v.source, v.flags); seen.set(v, out); return out; }
22640
+ if (v instanceof ArrayBuffer) { const out = v.slice(0); seen.set(v, out); return out; }
22641
+ if (typeof DataView !== 'undefined' && v instanceof DataView) {
22642
+ const out = new DataView(clone(v.buffer), v.byteOffset, v.byteLength); seen.set(v, out); return out;
22643
+ }
22644
+ if (ArrayBuffer.isView(v)) { const out = new v.constructor(v); seen.set(v, out); return out; } // typed array — fresh buffer
22645
+ if (v instanceof Map) {
22646
+ const out = new Map(); seen.set(v, out);
22647
+ for (const [k, val] of v) out.set(clone(k), clone(val));
22648
+ return out;
22649
+ }
22650
+ if (v instanceof Set) {
22651
+ const out = new Set(); seen.set(v, out);
22652
+ for (const val of v) out.add(clone(val));
22653
+ return out;
22654
+ }
22655
+ if (Array.isArray(v)) {
22656
+ const out = []; seen.set(v, out);
22657
+ for (let i = 0; i < v.length; i++) out[i] = clone(v[i]);
22658
+ return out;
22659
+ }
22660
+ const out = {}; seen.set(v, out);
22661
+ for (const k of Object.keys(v)) out[k] = clone(v[k]);
22662
+ return out;
22663
+ }
22664
+ return clone(input);
22665
+ };
22666
+ }
22667
+
22668
+ // Neuter string-based code generation. Same posture as the worker /
22669
+ // in-process backends' codeGeneration:{strings:false} (Ken's P2 #2): the
22670
+ // LLM has no legitimate reason to generate second-order code that's
22671
+ // invisible in the logged 'code' parameter. isolated-vm exposes no V8-level
22672
+ // codegen toggle, so we block every reachable path to the intrinsic
22673
+ // Function constructors — not just the global bindings. The well-known
22674
+ // vm-escape vector \`(function(){}).constructor("…")()\` reaches the
22675
+ // intrinsic via the prototype chain, around \`globalThis.Function\`, so we
22676
+ // capture each function-type intrinsic BEFORE overriding the globals and
22677
+ // replace its \`prototype.constructor\` too. (Generated code would stay
22678
+ // trapped in the isolate regardless — this is auditability + worker parity,
22679
+ // not the core isolation guarantee, which the isolate itself provides.)
22680
+ const __RealFunction = (function () {}).constructor;
22681
+ const __AsyncFunction = (async function () {}).constructor;
22682
+ const __GeneratorFunction = (function* () {}).constructor;
22683
+ const __AsyncGeneratorFunction = (async function* () {}).constructor;
22684
+ const __blockCodegen = function () {
22685
+ throw new Error('code generation from strings (eval / Function) is disabled in the REPL sandbox');
22686
+ };
22687
+ for (const __Ctor of [__RealFunction, __AsyncFunction, __GeneratorFunction, __AsyncGeneratorFunction]) {
22688
+ try {
22689
+ Object.defineProperty(__Ctor.prototype, 'constructor', {
22690
+ value: __blockCodegen,
22691
+ writable: true,
22692
+ configurable: true,
22693
+ enumerable: false,
22694
+ });
22695
+ } catch (_e) {
22696
+ // best-effort; the global overrides below still cover the common paths
22697
+ }
22698
+ }
22699
+ globalThis.eval = __blockCodegen;
22700
+ globalThis.Function = __blockCodegen;
22701
+
22702
+ // Tool-stub registry. Each registered tool becomes a top-level async
22703
+ // function that round-trips through the host dispatcher and re-throws on
22704
+ // the { ok:false } envelope.
22705
+ globalThis.__registerTools = function (names) {
22706
+ for (const name of names) {
22707
+ globalThis[name] = async (...args) => {
22708
+ const envJson = await __callTool.apply(
22709
+ undefined,
22710
+ [name, JSON.stringify(args)],
22711
+ { arguments: { copy: true }, result: { promise: true, copy: true } }
22712
+ );
22713
+ const env = JSON.parse(envJson);
22714
+ if (!env.ok) throw new Error(env.error);
22715
+ return env.value;
22716
+ };
22717
+ }
22718
+ };
22719
+ `;
22720
+ z.object({
22721
+ reflection: z.string().min(1),
22722
+ summary: z.string().min(1),
22723
+ nextIntendedAction: z.string().min(1),
22724
+ scopeLocks: z.array(z.string()).default([]),
22725
+ drivesAfter: DriveVectorSchema
22726
+ });
22727
+ z.object({
22728
+ semanticMemory: z.array(z.object({
22729
+ id: z.string().min(1),
22730
+ fact: z.string().min(1),
22731
+ evidenceTier: EvidenceTierSchema,
22732
+ confidence: z.number().min(0).max(1).default(.5),
22733
+ sourceEpisodeIds: z.array(z.string()).default([]),
22734
+ lastAffirmedAt: z.string()
22735
+ })),
22736
+ openQuestions: z.array(z.string()).default([])
22737
+ });
19950
22738
  z.object({
19951
22739
  count: z.number().optional(),
19952
22740
  unlimitedUse: z.boolean().optional(),
@@ -20462,6 +23250,18 @@ z.object({
20462
23250
  id: z.string().min(1) });
20463
23251
  z.object({ organizationId: z.string() });
20464
23252
  z.object({ id: z.string() });
23253
+ const createPromptSchema = z.object({
23254
+ type: z.string(),
23255
+ name: z.string(),
23256
+ promptText: z.string(),
23257
+ tags: z.array(z.string())
23258
+ });
23259
+ z.object({ id: z.string() });
23260
+ z.object({ id: z.string() });
23261
+ z.object({ name: z.string() });
23262
+ z.object({ tags: z.array(z.string()) });
23263
+ z.object({ type: z.string() });
23264
+ createPromptSchema.partial().extend({ id: z.string() });
20465
23265
  z.object({ id: z.string() });
20466
23266
  z.object({ ids: z.array(z.string()).optional() });
20467
23267
  z.object({
@@ -20474,7 +23274,7 @@ z.object({
20474
23274
  systemPriority: z.number().min(0).max(999).optional(),
20475
23275
  sessionId: z.string().optional(),
20476
23276
  notes: z.string().optional(),
20477
- primaryTag: z.string().optional(),
23277
+ primaryTag: z.string().nullable().optional(),
20478
23278
  tags: z.array(z.object({
20479
23279
  name: z.string(),
20480
23280
  strength: z.number()
@@ -20490,33 +23290,6 @@ z.object({
20490
23290
  fabFileId: z.string(),
20491
23291
  chunkId: z.string()
20492
23292
  });
20493
- z.object({
20494
- fileName: z.string(),
20495
- mimeType: z.string(),
20496
- fileSize: z.number(),
20497
- type: z.enum(KnowledgeType),
20498
- content: z.union([z.string(), z.instanceof(Buffer)]).optional(),
20499
- organizationId: z.string().optional(),
20500
- /**
20501
- * Content type of the file
20502
- * @example 'text/markdown'
20503
- * @example 'application/pdf'
20504
- * @example 'application/octet-stream' for binary files
20505
- */
20506
- contentType: z.string().optional(),
20507
- public: z.boolean().optional(),
20508
- prefix: z.string().optional(),
20509
- system: z.boolean().optional(),
20510
- tags: z.array(z.object({
20511
- name: z.string(),
20512
- strength: z.number()
20513
- })).optional(),
20514
- systemPriority: z.number().optional(),
20515
- sessionId: z.string().optional(),
20516
- contentHash: z.string().optional(),
20517
- batchId: z.string().optional(),
20518
- relativePath: z.string().optional()
20519
- });
20520
23293
  z.object({ sessionId: z.string() });
20521
23294
  z.object({ questId: z.string() });
20522
23295
  z.object({ url: z.string().regex(/^(?!https?:\/\/(drive|docs)\.google\.com\/(?:file\/d\/|open\?id=|uc\?id=|document\/d\/|spreadsheets\/d\/|presentation\/d\/|forms\/d\/|drive\/folders\/)([a-zA-Z0-9_-]{10,})).+/) });
@@ -20559,6 +23332,7 @@ z.object({
20559
23332
  userGroups: z.array(z.string()).optional(),
20560
23333
  dataLakeTags: z.array(z.string()).optional(),
20561
23334
  dataLakeTagPrefixes: z.array(z.string()).optional(),
23335
+ scopedTagPrefixes: z.array(z.string()).optional(),
20562
23336
  excludeContent: z.coerce.boolean().optional()
20563
23337
  }).optional()
20564
23338
  });
@@ -21910,6 +24684,15 @@ z.object({
21910
24684
  isPersonal: z.boolean(),
21911
24685
  mementos: z.array(SingleMementoEvalSchema).optional()
21912
24686
  });
24687
+ new Set([{
24688
+ key: "internal",
24689
+ audienceType: "internal",
24690
+ label: "Internal"
24691
+ }, {
24692
+ key: "customer",
24693
+ audienceType: "customer",
24694
+ label: "Customer"
24695
+ }].map((v) => v.key));
21913
24696
  crypto_exports.safeCompareTokens;
21914
24697
  //#endregion
21915
24698
  //#region ../../b4m-core/services/dist/llm/tools/cliTools.mjs
@@ -25008,16 +27791,21 @@ function extractUsageInfo(parsed) {
25008
27791
  return {
25009
27792
  inputTokens: parsed.usage?.inputTokens,
25010
27793
  outputTokens: parsed.usage?.outputTokens,
27794
+ cacheReadInputTokens: parsed.usage?.cacheReadInputTokens,
27795
+ cacheCreationInputTokens: parsed.usage?.cacheCreationInputTokens,
25011
27796
  creditsUsed: parsed.credits?.used,
25012
27797
  usdCost: parsed.credits?.usdCost
25013
27798
  };
25014
27799
  }
25015
27800
  /**
25016
- * Accumulates streaming LLM response chunks (text, tool calls, thinking blocks, usage)
25017
- * and fires the completion callback once at the end.
27801
+ * Reduces a stream of {@link StreamEvent}s (text, tool calls, thinking blocks,
27802
+ * usage) into a single accumulated assistant turn, then fires the completion
27803
+ * callback once at the end.
25018
27804
  *
25019
- * Shared between ServerLlmBackend (SSE) and WebSocketLlmBackend (WebSocket frames)
25020
- * so accumulation logic lives in exactly one place.
27805
+ * Both backends — `ServerLlmBackend` (SSE) and `WebSocketLlmBackend` (WebSocket
27806
+ * frames) decode their wire payloads into the shared event union and feed
27807
+ * each event to {@link apply}, so accumulation logic lives in exactly one
27808
+ * place and is transport-agnostic.
25021
27809
  */
25022
27810
  var StreamAccumulator = class {
25023
27811
  constructor() {
@@ -25026,21 +27814,25 @@ var StreamAccumulator = class {
25026
27814
  this.thinkingBlocks = [];
25027
27815
  this.lastUsageInfo = {};
25028
27816
  }
25029
- onContent(text, usage, credits) {
25030
- this.accumulatedText += text;
25031
- if (usage || credits) this.lastUsageInfo = extractUsageInfo({
25032
- usage,
25033
- credits
25034
- });
25035
- }
25036
- onToolUse(text, tools, thinking, usage, credits) {
25037
- if (text) this.accumulatedText += text;
25038
- if (tools && tools.length > 0) this.toolsUsed = tools;
25039
- if (thinking && thinking.length > 0) this.thinkingBlocks = thinking;
25040
- if (usage || credits) this.lastUsageInfo = extractUsageInfo({
25041
- usage,
25042
- credits
25043
- });
27817
+ /**
27818
+ * Fold one streaming event into the accumulated turn. `error` events carry no
27819
+ * accumulable content backends surface them by rejecting — so they are a
27820
+ * no-op here.
27821
+ */
27822
+ apply(event) {
27823
+ switch (event.type) {
27824
+ case "content":
27825
+ this.accumulatedText += event.text ?? "";
27826
+ if (event.usage || event.credits) this.lastUsageInfo = extractUsageInfo(event);
27827
+ break;
27828
+ case "tool_use":
27829
+ if (event.text) this.accumulatedText += event.text;
27830
+ if (event.tools && event.tools.length > 0) this.toolsUsed = event.tools;
27831
+ if (event.thinking && event.thinking.length > 0) this.thinkingBlocks = event.thinking;
27832
+ if (event.usage || event.credits) this.lastUsageInfo = extractUsageInfo(event);
27833
+ break;
27834
+ case "error": break;
27835
+ }
25044
27836
  }
25045
27837
  /** True when neither text nor tools have been accumulated (stream produced nothing useful). */
25046
27838
  isEmpty() {
@@ -25073,6 +27865,93 @@ var StreamAccumulator = class {
25073
27865
  }
25074
27866
  };
25075
27867
  //#endregion
27868
+ //#region src/llm/streamEvents.ts
27869
+ /**
27870
+ * Unified streaming event protocol for CLI LLM backends.
27871
+ *
27872
+ * Both transports — `ServerLlmBackend` (SSE) and `WebSocketLlmBackend`
27873
+ * (HTTP POST + WebSocket frames) — decode their wire payloads into this single
27874
+ * typed union, then feed it to `StreamAccumulator`. This collapses the two
27875
+ * previously-duplicated, loosely-typed chunk shapes (`SSEEvent` in
27876
+ * ServerLlmBackend, the inline `chunk` type in WebSocketLlmBackend) into one
27877
+ * source of truth, and replaces the unchecked `as SSEEvent` cast on
27878
+ * server-provided data with a runtime-validated parse at the boundary.
27879
+ *
27880
+ * The vocabulary mirrors what the backends emit today (`content` / `tool_use` /
27881
+ * `error`) so the refactor changes no observable behavior. Finer-grained delta
27882
+ * events (text_delta / toolcall_delta / …) are intentionally out of scope — the
27883
+ * CLI accumulates a full turn before firing its callback, so per-delta events
27884
+ * would be a behavior change, not a refactor.
27885
+ */
27886
+ /**
27887
+ * Token usage counts carried alongside content/tool_use events. Shape mirrors
27888
+ * `SSEContentEvent.usage` in `@bike4mind/common` (`sseEvents.ts`) — the
27889
+ * authoritative wire contract — including the Anthropic cache-token deltas
27890
+ * (#8155) so they survive the boundary parse instead of being stripped.
27891
+ */
27892
+ const usageSchema = z.object({
27893
+ inputTokens: z.number().optional(),
27894
+ outputTokens: z.number().optional(),
27895
+ cacheReadInputTokens: z.number().optional(),
27896
+ cacheCreationInputTokens: z.number().optional()
27897
+ }).partial();
27898
+ /** Credit/cost accounting carried alongside content/tool_use events (SSE only). */
27899
+ const creditsSchema = z.object({
27900
+ used: z.number().optional(),
27901
+ usdCost: z.number().optional()
27902
+ }).partial();
27903
+ /**
27904
+ * A single tool call requested by the model. Shape mirrors the wire contract in
27905
+ * `@bike4mind/common` (`SSEContentEvent.tools` / `CompletionInfo.toolsUsed`):
27906
+ * the server sends `{ name, arguments?: string, id? }` — `arguments` is a raw
27907
+ * JSON string, NOT a parsed object. (The previous `input: Record<…>` shape was
27908
+ * a typed lie the old unchecked `as SSEEvent` cast hid; downstream consumers
27909
+ * read `tool.arguments`, e.g. `toolParallelizer.ts`.)
27910
+ */
27911
+ const toolUseSchema = z.object({
27912
+ name: z.string(),
27913
+ arguments: z.string().optional(),
27914
+ id: z.string().optional()
27915
+ });
27916
+ /**
27917
+ * Discriminated union of streaming events. `thinking` blocks are opaque
27918
+ * provider-shaped objects (Anthropic extended thinking) replayed verbatim into
27919
+ * the next request, so they stay `unknown[]` rather than being modeled.
27920
+ */
27921
+ const streamEventSchema = z.discriminatedUnion("type", [
27922
+ z.object({
27923
+ type: z.literal("content"),
27924
+ text: z.string().optional(),
27925
+ usage: usageSchema.optional(),
27926
+ credits: creditsSchema.optional()
27927
+ }),
27928
+ z.object({
27929
+ type: z.literal("tool_use"),
27930
+ text: z.string().optional(),
27931
+ tools: z.array(toolUseSchema).optional(),
27932
+ thinking: z.array(z.unknown()).optional(),
27933
+ usage: usageSchema.optional(),
27934
+ credits: creditsSchema.optional()
27935
+ }),
27936
+ z.object({
27937
+ type: z.literal("error"),
27938
+ message: z.string().optional()
27939
+ })
27940
+ ]);
27941
+ /**
27942
+ * Validate an already-JSON-decoded wire payload against the event union.
27943
+ *
27944
+ * Returns the typed event on success, or `null` when the payload does not match
27945
+ * a known event shape. A `null` result is treated as "skip this event" by the
27946
+ * backends — preserving the prior behavior where an unrecognized `type` simply
27947
+ * fell through unhandled. (Malformed JSON is still caught upstream at the
27948
+ * `JSON.parse` boundary.)
27949
+ */
27950
+ function parseStreamEvent(data) {
27951
+ const result = streamEventSchema.safeParse(data);
27952
+ return result.success ? result.data : null;
27953
+ }
27954
+ //#endregion
25076
27955
  //#region src/llm/ServerLlmBackend.ts
25077
27956
  /**
25078
27957
  * Connection-level failures that should be retried rather than surfaced to the
@@ -25247,30 +28126,29 @@ var ServerLlmBackend = class ServerLlmBackend {
25247
28126
  return;
25248
28127
  }
25249
28128
  try {
25250
- const parsed = JSON.parse(data);
25251
- if (parsed.type === "error") {
28129
+ const event = parseStreamEvent(JSON.parse(data));
28130
+ if (!event) return;
28131
+ if (event.type === "error") {
25252
28132
  receivedError = true;
25253
- streamLogger.onCriticalEvent(eventCount, "ERROR", parsed.message || "Server error");
25254
- reject(new Error(parsed.message || "Server error"));
28133
+ streamLogger.onCriticalEvent(eventCount, "ERROR", event.message || "Server error");
28134
+ reject(new Error(event.message || "Server error"));
25255
28135
  return;
25256
28136
  }
25257
- if (parsed.type === "content") {
25258
- const textChunk = parsed.text || "";
25259
- accumulator.onContent(textChunk, parsed.usage, parsed.credits);
25260
- streamLogger.onContent(eventCount, textChunk, accumulator.rawText);
25261
- } else if (parsed.type === "tool_use") {
25262
- streamLogger.onCriticalEvent(eventCount, "TOOL_USE", `tools: ${parsed.tools?.length}`);
25263
- if (parsed.tools && parsed.tools.length > 0) for (const tool of parsed.tools) {
28137
+ if (event.type === "content") {
28138
+ accumulator.apply(event);
28139
+ streamLogger.onContent(eventCount, event.text || "", accumulator.rawText);
28140
+ } else if (event.type === "tool_use") {
28141
+ streamLogger.onCriticalEvent(eventCount, "TOOL_USE", `tools: ${event.tools?.length}`);
28142
+ if (event.tools && event.tools.length > 0) for (const tool of event.tools) {
25264
28143
  logger.debug(`TOOL REQUEST: ${tool.name}`);
25265
28144
  try {
25266
- const paramsStr = JSON.stringify(tool.input);
25267
- logger.debug(` Params: ${paramsStr}`);
28145
+ logger.debug(` Params: ${tool.arguments ?? "{}"}`);
25268
28146
  } catch {
25269
28147
  logger.debug(` Params: [Unable to stringify]`);
25270
28148
  }
25271
28149
  }
25272
- accumulator.onToolUse(parsed.text || "", parsed.tools, parsed.thinking, parsed.usage, parsed.credits);
25273
- if (parsed.thinking && parsed.thinking.length > 0) streamLogger.onCriticalEvent(eventCount, "THINKING", `${parsed.thinking.length} thinking blocks`);
28150
+ accumulator.apply(event);
28151
+ if (event.thinking && event.thinking.length > 0) streamLogger.onCriticalEvent(eventCount, "THINKING", `${event.thinking.length} thinking blocks`);
25274
28152
  }
25275
28153
  } catch (parseError) {
25276
28154
  streamLogger.streamError(parseError);
@@ -25451,20 +28329,22 @@ var FallbackLlmBackend = class {
25451
28329
  async complete(model, messages, options, callback) {
25452
28330
  if (options.abortSignal?.aborted) return this.inner.complete(model, messages, options, callback);
25453
28331
  const modelsToTry = [model, ...this.fallbackModels.filter((m) => m !== model)];
25454
- let callbackInvoked = false;
25455
- const guardedCallback = async (text, completionInfo) => {
25456
- callbackInvoked = true;
25457
- return callback(text, completionInfo);
25458
- };
25459
28332
  let lastError;
25460
28333
  for (let i = 0; i < modelsToTry.length; i++) {
25461
28334
  const modelToTry = modelsToTry[i];
28335
+ const buffered = [];
28336
+ const bufferingCallback = async (text, completionInfo) => {
28337
+ buffered.push({
28338
+ text,
28339
+ completionInfo
28340
+ });
28341
+ };
25462
28342
  try {
25463
- await this.inner.complete(modelToTry, messages, options, guardedCallback);
28343
+ await this.inner.complete(modelToTry, messages, options, bufferingCallback);
28344
+ for (const delivery of buffered) await callback(delivery.text, delivery.completionInfo);
25464
28345
  return;
25465
28346
  } catch (error) {
25466
28347
  if (options.abortSignal?.aborted) throw error;
25467
- if (callbackInvoked) throw error;
25468
28348
  lastError = error instanceof Error ? error : new Error(String(error));
25469
28349
  const nextModel = modelsToTry[i + 1];
25470
28350
  if (nextModel) {
@@ -27371,12 +30251,14 @@ var WebSocketLlmBackend = class {
27371
30251
  eventCount++;
27372
30252
  const chunk = message.chunk;
27373
30253
  streamLogger.onEvent(eventCount, JSON.stringify(chunk));
27374
- if (chunk.type === "content") {
27375
- accumulator.onContent(chunk.text || "", chunk.usage);
27376
- streamLogger.onContent(eventCount, chunk.text || "", accumulator.rawText);
27377
- } else if (chunk.type === "tool_use") {
27378
- streamLogger.onCriticalEvent(eventCount, "TOOL_USE", `tools: ${chunk.tools?.length}`);
27379
- accumulator.onToolUse(chunk.text || "", chunk.tools, chunk.thinking, chunk.usage);
30254
+ const event = parseStreamEvent(chunk);
30255
+ if (!event) return;
30256
+ if (event.type === "content") {
30257
+ accumulator.apply(event);
30258
+ streamLogger.onContent(eventCount, event.text || "", accumulator.rawText);
30259
+ } else if (event.type === "tool_use") {
30260
+ streamLogger.onCriticalEvent(eventCount, "TOOL_USE", `tools: ${event.tools?.length}`);
30261
+ accumulator.apply(event);
27380
30262
  }
27381
30263
  } else if (action === "cli_completion_done") {
27382
30264
  streamLogger.streamComplete(accumulator.rawText);