@bike4mind/cli 0.13.0 → 0.15.0

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,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { $ as ProjectEvents, A as GenerateImageToolCallSchema, At as dayjsConfig_default, B as InviteEvents, Bt as sanitizeTelemetryError, C as ElabsEvents, Ct as UnauthorizedError, D as ForbiddenError, Dt as VideoModels, E as FileEvents, Et as VideoGenerationUsageTransaction, F as ImageEditUsageTransaction, Ft as isGPTImage2Model, G as ModalEvents, Gt as buildRateLimitLogEntry, H as KnowledgeType, Ht as settingsMap, I as ImageGenerationUsageTransaction, It as isGPTImageModel, J as OpenAIEmbeddingModel, Jt as parseRateLimitHeaders, K as ModelBackend, Kt as extractSnippetMeta, L as ImageModels, Lt as isZodError, M as GenericCreditDeductTransaction, Mt as getDataLakeTags, N as HTTPError, Nt as getMcpProviderMetadata, O as FriendshipEvents, Ot as XAI_IMAGE_MODELS, P as HttpStatus, Pt as getViewById, Q as ProfileEvents, R as InboxEvents, Rt as obfuscateApiKey, S as DashboardParamsSchema, St as UiNavigationEvents, T as FeedbackEvents, Tt as VIDEO_SIZE_CONSTRAINTS, U as LLMEvents, V as InviteType, Vt as secureParameters, W as MiscEvents, X as Permission, Y as OpenAIImageGenerationInput, Yt as CollectionType, Z as PermissionDeniedError, _ as ChatCompletionCreateInputSchema, _t as TaskScheduleHandler, a as ALERT_THRESHOLDS, at as ReceivedCreditTransaction, b as CompletionApiUsageTransaction, bt as ToolUsageTransaction, c as ApiKeyScope, ct as ResearchModeParamsSchema, d as ArtifactTypeSchema, dt as ResearchTaskType, et as PromptIntentSchema, f as AuthEvents, ft as SessionEvents, gt as TagType, h as BadRequestError, ht as SupportedFabFileMimeTypes, it as RealtimeVoiceUsageTransaction, j as GenericCreditAddTransaction, jt as getAccessibleDataLakes, k as GEMINI_IMAGE_MODELS, kt as b4mLLMTools, l as ApiKeyType, lt as ResearchTaskExecutionType, m as BFL_SAFETY_TOLERANCE, mt as SubscriptionCreditTransaction, n as logger, nt as PurchaseTransaction, o as AiEvents, ot as RechartsChartTypeList, p as BFL_IMAGE_MODELS, pt as SpeechToTextUsageTransaction, q as NotFoundError, qt as isNearLimit, rt as QuestMasterParamsSchema, s as ApiKeyEvents, st as RegInviteEvents, t as ConfigStore, tt as PromptMetaZodSchema, u as AppFileEvents, ut as ResearchTaskPeriodicFrequencyType, v as ChatModels, vt as TextGenerationUsageTransaction, w as FavoriteDocumentType, wt as UnprocessableEntityError, x as CorruptedFileError, xt as TransferCreditTransaction, y as ClaudeArtifactMimeTypes, yt as TooManyRequestsError, z as InternalServerError, zt as resolveNavigationIntents } from "./ConfigStore-C3tokQej.mjs";
2
+ import { $ as ProjectEvents, A as GenerateImageToolCallSchema, At as dayjsConfig_default, B as InviteEvents, Bt as resolveNavigationIntents, C as ElabsEvents, Ct as UnauthorizedError, D as ForbiddenError, Dt as VideoModels, E as FileEvents, Et as VideoGenerationUsageTransaction, F as ImageEditUsageTransaction, Ft as isGPTImage2Model, G as ModalEvents, H as KnowledgeType, Ht as secureParameters, I as ImageGenerationUsageTransaction, It as isGPTImageModel, J as OpenAIEmbeddingModel, Jt as isNearLimit, K as ModelBackend, Kt as buildRateLimitLogEntry, L as ImageModels, Lt as isSupportedEmbeddingModel, M as GenericCreditDeductTransaction, Mt as getDataLakeTags, N as HTTPError, Nt as getMcpProviderMetadata, O as FriendshipEvents, Ot as XAI_IMAGE_MODELS, P as HttpStatus, Pt as getViewById, Q as ProfileEvents, R as InboxEvents, Rt as isZodError, S as DashboardParamsSchema, St as UiNavigationEvents, T as FeedbackEvents, Tt as VIDEO_SIZE_CONSTRAINTS, U as LLMEvents, Ut as settingsMap, V as InviteType, Vt as sanitizeTelemetryError, W as MiscEvents, X as Permission, Xt as CollectionType, Y as OpenAIImageGenerationInput, Yt as parseRateLimitHeaders, Z as PermissionDeniedError, _ as ChatCompletionCreateInputSchema, _t as TaskScheduleHandler, a as ALERT_THRESHOLDS, at as ReceivedCreditTransaction, b as CompletionApiUsageTransaction, bt as ToolUsageTransaction, c as ApiKeyScope, ct as ResearchModeParamsSchema, d as ArtifactTypeSchema, dt as ResearchTaskType, et as PromptIntentSchema, f as AuthEvents, ft as SessionEvents, gt as TagType, h as BadRequestError, ht as SupportedFabFileMimeTypes, it as RealtimeVoiceUsageTransaction, j as GenericCreditAddTransaction, jt as getAccessibleDataLakes, k as GEMINI_IMAGE_MODELS, kt as b4mLLMTools, l as ApiKeyType, lt as ResearchTaskExecutionType, m as BFL_SAFETY_TOLERANCE, mt as SubscriptionCreditTransaction, n as logger, nt as PurchaseTransaction, o as AiEvents, ot as RechartsChartTypeList, p as BFL_IMAGE_MODELS, pt as SpeechToTextUsageTransaction, q as NotFoundError, qt as extractSnippetMeta, rt as QuestMasterParamsSchema, s as ApiKeyEvents, st as RegInviteEvents, t as ConfigStore, tt as PromptMetaZodSchema, u as AppFileEvents, ut as ResearchTaskPeriodicFrequencyType, v as ChatModels, vt as TextGenerationUsageTransaction, w as FavoriteDocumentType, wt as UnprocessableEntityError, x as CorruptedFileError, xt as TransferCreditTransaction, y as ClaudeArtifactMimeTypes, yt as TooManyRequestsError, z as InternalServerError, zt as obfuscateApiKey } from "./ConfigStore-HRgwfPBk.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-PpNti-tY.mjs";
4
4
  import { n as isPathAllowed, t as assertPathAllowed } from "./pathValidation-D8tjkQXE-1HwvsuYT.mjs";
5
- import { t as version } from "./package-DNcd24qN.mjs";
5
+ import { t as version } from "./package-CaPvuP1F.mjs";
6
6
  import { execFile, execFileSync, spawn } from "child_process";
7
- import crypto, { createHash, randomBytes } from "crypto";
7
+ import crypto, { createHash, randomBytes, randomUUID } from "crypto";
8
8
  import { existsSync, promises, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "fs";
9
9
  import os, { homedir } from "os";
10
10
  import path, { dirname, join } from "path";
@@ -559,7 +559,8 @@ const COMMANDS = [
559
559
  },
560
560
  {
561
561
  name: "handoff",
562
- description: "Show or generate the session handoff for cross-session continuity (alias for /workflow handoff)"
562
+ description: "Show or generate the session handoff for cross-session continuity. Use --local for an LLM-free snapshot (works when rate-limited or offline). Alias for /workflow handoff.",
563
+ args: "[generate|--local]"
563
564
  }
564
565
  ];
565
566
  /**
@@ -2061,8 +2062,9 @@ var ReActAgent = class extends EventEmitter {
2061
2062
  cacheConversationHistory: false,
2062
2063
  cacheTTL: "5m"
2063
2064
  } : void 0;
2065
+ const iterationIndex = iterations - 1;
2064
2066
  await this.context.llm.complete(this.context.model, messages, {
2065
- stream: false,
2067
+ stream: true,
2066
2068
  tools: this.context.tools,
2067
2069
  maxTokens,
2068
2070
  temperature,
@@ -2072,7 +2074,13 @@ var ReActAgent = class extends EventEmitter {
2072
2074
  thinking: this.context.thinking,
2073
2075
  cacheStrategy
2074
2076
  }, async (texts, completionInfo) => {
2075
- for (const text of texts) if (text) currentText += text;
2077
+ for (const text of texts) if (text) {
2078
+ currentText += text;
2079
+ this.emit("text_delta", {
2080
+ delta: text,
2081
+ iteration: iterationIndex
2082
+ });
2083
+ }
2076
2084
  if (completionInfo) {
2077
2085
  const inputTokens = completionInfo.inputTokens || 0;
2078
2086
  const outputTokens = completionInfo.outputTokens || 0;
@@ -2529,8 +2537,9 @@ Remember: You are an autonomous AGENT. Act independently and solve problems proa
2529
2537
  cacheConversationHistory: false,
2530
2538
  cacheTTL: "5m"
2531
2539
  } : void 0;
2540
+ const iterationIndex = this.iterations - 1;
2532
2541
  await this.context.llm.complete(this.context.model, this.messages, {
2533
- stream: false,
2542
+ stream: true,
2534
2543
  tools: this.context.tools,
2535
2544
  maxTokens,
2536
2545
  temperature,
@@ -2540,7 +2549,13 @@ Remember: You are an autonomous AGENT. Act independently and solve problems proa
2540
2549
  thinking: this.context.thinking,
2541
2550
  cacheStrategy
2542
2551
  }, async (texts, completionInfo) => {
2543
- for (const text of texts) if (text) currentText += text;
2552
+ for (const text of texts) if (text) {
2553
+ currentText += text;
2554
+ this.emit("text_delta", {
2555
+ delta: text,
2556
+ iteration: iterationIndex
2557
+ });
2558
+ }
2544
2559
  if (completionInfo) {
2545
2560
  const inputTokens = completionInfo.inputTokens || 0;
2546
2561
  const outputTokens = completionInfo.outputTokens || 0;
@@ -3156,6 +3171,398 @@ function buildPipelineResult(taskResults, options = {}) {
3156
3171
  summary: summaryParts.join("\n")
3157
3172
  };
3158
3173
  }
3174
+ /**
3175
+ * Drives are bounded scalars in [0, 1] that decay over time and are satisfied
3176
+ * by certain action classes. They give the agent a *direction* between
3177
+ * explicit prompts — the "Sims needs system" applied to autonomous agents.
3178
+ *
3179
+ * At policy time, the current drive vector is summarized in natural language
3180
+ * and injected into the orient prompt (e.g., "you are feeling curious,
3181
+ * somewhat bored, slightly anxious about progress").
3182
+ *
3183
+ * Each named drive captures one motivational axis:
3184
+ *
3185
+ * - `curiosity`: satisfied by encountering novelty/surprise; decays when
3186
+ * observations are repetitive.
3187
+ * - `progress`: satisfied by measurable goal-state change; decays when
3188
+ * wake cycles produce no advancement.
3189
+ * - `social`: satisfied by human interaction; decays when the agent runs
3190
+ * without external input.
3191
+ * - `novelty`: satisfied by producing a falsifiable, original hypothesis
3192
+ * (distinct from curiosity, which is satisfied by intake). Decays as the
3193
+ * corpus of read material grows without ideation.
3194
+ * - `caution`: rises with budget burn or repeated failure; biases the
3195
+ * policy step toward cheaper / lower-tier actions.
3196
+ * - `aesthetic`: satisfied by polish/refinement actions. Tunable for
3197
+ * game-design-style work where craft matters.
3198
+ */
3199
+ const DriveVectorSchema = z.object({
3200
+ curiosity: z.number().min(0).max(1),
3201
+ progress: z.number().min(0).max(1),
3202
+ social: z.number().min(0).max(1),
3203
+ novelty: z.number().min(0).max(1),
3204
+ caution: z.number().min(0).max(1),
3205
+ aesthetic: z.number().min(0).max(1)
3206
+ });
3207
+ /**
3208
+ * Evidence tier classifies how strong the support is for a claim or finding.
3209
+ *
3210
+ * Lifted directly from the patterns evolved in
3211
+ * `~/Desktop/quantum-work/q-paper-neutron-scattering/`, where the claims
3212
+ * ledger distinguished "engineering evidence" from "paper-facing evidence".
3213
+ *
3214
+ * This is the most important schema-level invariant inherited from the
3215
+ * working paper-reproduction agent: every long-horizon agent must be able
3216
+ * to distinguish *"I made this work in my sandbox"* from *"this passes the
3217
+ * external bar"*. Drives and budgets behave differently at each tier:
3218
+ * exploration is cheap at low tiers and expensive at high tiers.
3219
+ *
3220
+ * - `engineering-proxy`: works on a small/synthetic proxy of the real
3221
+ * problem. Cheapest to produce, weakest claim.
3222
+ * - `engineering-scaled`: works at production-relevant scale, but still
3223
+ * inside the agent's own sandbox. No external validation.
3224
+ * - `external-facing`: passes an externally-defined bar (target metric,
3225
+ * reference dataset, paper claim). Still agent-graded.
3226
+ * - `human-reviewed`: an external human reviewer has signed off. Highest
3227
+ * tier; required before any public artifact ships.
3228
+ */
3229
+ const EvidenceTierSchema = z.enum([
3230
+ "engineering-proxy",
3231
+ "engineering-scaled",
3232
+ "external-facing",
3233
+ "human-reviewed"
3234
+ ]);
3235
+ /**
3236
+ * Default charter size budget in bytes. 8KB honors the Ember scarcity insight:
3237
+ * a hard cap forces the agent to *curate* rather than accumulate, and curation
3238
+ * is the mechanism by which identity and taste emerge.
3239
+ *
3240
+ * Tunable per agent; production research agents may need more, but the cap
3241
+ * itself is load-bearing.
3242
+ */
3243
+ const DEFAULT_CHARTER_SIZE_BUDGET_BYTES = 8 * 1024;
3244
+ /**
3245
+ * Identity is the slow-changing core of the charter. Once set, these fields
3246
+ * rarely change — the agent's name and instantiation moment are stable
3247
+ * anchors across the inevitable identity discontinuities (deploys, model
3248
+ * swaps, context overflows).
3249
+ */
3250
+ const CharterIdentitySchema = z.object({
3251
+ /** Stable agent id (the load-bearing key across all storage). */
3252
+ agentId: z.string().min(1),
3253
+ /**
3254
+ * The user who owns this agent. Tool execution runs as this user — their
3255
+ * storage, billing, and permissions scope the agent's actions. Long-horizon
3256
+ * agents are headless but always answer to an owner.
3257
+ */
3258
+ ownerUserId: z.string().min(1),
3259
+ /**
3260
+ * MISSION LINKAGE: when set, this charter is a Mission of an existing B4M
3261
+ * Agent (the AgentModel id). The mission inherits the agent's persona
3262
+ * (system prompt) and tool policy at act time; `agentId` above remains the
3263
+ * mission's own unique key, so one B4M agent can run many missions.
3264
+ * Absent = a standalone deep agent (the original mode).
3265
+ */
3266
+ linkedAgentId: z.string().min(1).optional(),
3267
+ /** Human-readable name. Public; appears in logs and dashboards. */
3268
+ name: z.string().min(1),
3269
+ /** Role / archetype, e.g. "paper-repro", "game-designer", "researcher". */
3270
+ role: z.string().min(1),
3271
+ /** ISO-8601 timestamp of first wake. */
3272
+ instantiatedAt: z.string().datetime(),
3273
+ /** Charter schema version, for migrations. */
3274
+ schemaVersion: z.literal(1)
3275
+ });
3276
+ /**
3277
+ * The goal is what the agent is pursuing. `successCriteria` should be
3278
+ * concrete enough that the reflect step can decide whether progress was made.
3279
+ * `deadlineKind` is intentionally a soft category rather than a wall-clock
3280
+ * date — long-horizon research has no real deadline; game prototypes do.
3281
+ */
3282
+ const CharterGoalSchema = z.object({
3283
+ description: z.string().min(1),
3284
+ successCriteria: z.array(z.string()).default([]),
3285
+ deadlineKind: z.enum([
3286
+ "none",
3287
+ "soft",
3288
+ "hard"
3289
+ ]).default("none"),
3290
+ /** ISO-8601; only meaningful (and only allowed) when deadlineKind !== 'none'. */
3291
+ deadlineAt: z.string().datetime().optional()
3292
+ }).refine((goal) => goal.deadlineKind !== "none" || goal.deadlineAt === void 0, {
3293
+ message: "deadlineAt requires deadlineKind to be 'soft' or 'hard'",
3294
+ path: ["deadlineAt"]
3295
+ });
3296
+ const SubgoalStatusSchema = z.enum([
3297
+ "planned",
3298
+ "active",
3299
+ "blocked",
3300
+ "completed",
3301
+ "abandoned"
3302
+ ]);
3303
+ const SubgoalSchema = z.object({
3304
+ id: z.string().min(1),
3305
+ description: z.string().min(1),
3306
+ status: SubgoalStatusSchema.default("planned"),
3307
+ /** Higher = more important. Used by the policy step to rank. */
3308
+ priority: z.number().int().min(0).max(100).default(50),
3309
+ /** Tier required for this subgoal to be considered "done". */
3310
+ targetTier: EvidenceTierSchema.default("engineering-scaled"),
3311
+ /** IDs of subgoals that must complete before this one is unblocked. */
3312
+ dependsOn: z.array(z.string()).default([])
3313
+ });
3314
+ /**
3315
+ * A semantic memory entry is a single distilled fact the agent has chosen
3316
+ * to preserve across wake cycles. Provenance-typed via `evidenceTier`.
3317
+ *
3318
+ * `sourceEpisodeIds` lets the agent (and humans) trace any claim back to
3319
+ * the wake cycles in which it was formed — the audit trail that makes
3320
+ * adversarial review tractable.
3321
+ */
3322
+ const SemanticMemoryEntrySchema = z.object({
3323
+ id: z.string().min(1),
3324
+ fact: z.string().min(1),
3325
+ evidenceTier: EvidenceTierSchema,
3326
+ /** Subjective confidence in [0, 1]. Self-reported by the agent. */
3327
+ confidence: z.number().min(0).max(1).default(.5),
3328
+ sourceEpisodeIds: z.array(z.string()).default([]),
3329
+ /** ISO-8601 when this entry was last reaffirmed during grooming. */
3330
+ lastAffirmedAt: z.string().datetime()
3331
+ });
3332
+ z.object({
3333
+ identity: CharterIdentitySchema,
3334
+ goal: CharterGoalSchema,
3335
+ /** Current drive vector (decayed at wake time before policy step). */
3336
+ drives: DriveVectorSchema,
3337
+ subgoals: z.array(SubgoalSchema).default([]),
3338
+ semanticMemory: z.array(SemanticMemoryEntrySchema).default([]),
3339
+ /**
3340
+ * The tier the agent is currently operating at. Tier-gated progression
3341
+ * (Tier 0 charter → Tier N envelope) is inherited from q-paper's tier
3342
+ * system. Drives and budgets behave differently per tier.
3343
+ */
3344
+ currentTier: EvidenceTierSchema.default("engineering-proxy"),
3345
+ /** Open questions the agent wants to resolve. Free-form. */
3346
+ openQuestions: z.array(z.string()).default([]),
3347
+ /** Active blockers (mirrored from the workflow blocker system if used). */
3348
+ blockers: z.array(z.string()).default([]),
3349
+ /**
3350
+ * The B4M session acting as this charter's mission log — wake summaries and
3351
+ * deliverables land there as chat history. Created lazily on first bridge.
3352
+ */
3353
+ sessionId: z.string().min(1).optional(),
3354
+ /** Size budget in bytes. Grooming is triggered when exceeded. */
3355
+ sizeBudgetBytes: z.number().int().positive().default(DEFAULT_CHARTER_SIZE_BUDGET_BYTES),
3356
+ /** Monotonic version counter, bumped on every successful groom/update. */
3357
+ version: z.number().int().nonnegative().default(0),
3358
+ /** ISO-8601 of last groom (compaction). */
3359
+ groomedAt: z.string().datetime().optional(),
3360
+ /** ISO-8601 of last update (any field). */
3361
+ updatedAt: z.string().datetime()
3362
+ });
3363
+ z.object({
3364
+ agentId: z.string().min(1),
3365
+ /** Monotonic counter, bumped on every wake cycle. */
3366
+ wakeCount: z.number().int().nonnegative(),
3367
+ /** ISO-8601 of the most recent wake. */
3368
+ lastWakeAt: z.string().datetime(),
3369
+ /**
3370
+ * One-paragraph summary of what was done in the last wake cycle.
3371
+ * The reflect step writes this. Short enough to fit comfortably in any
3372
+ * subsequent orient prompt.
3373
+ */
3374
+ lastActionSummary: z.string().default(""),
3375
+ /**
3376
+ * What the agent intends to do on the next wake. Written by the reflect
3377
+ * step. The orient step uses it as a strong prior but is free to override
3378
+ * if drives or new observations dictate.
3379
+ */
3380
+ nextIntendedAction: z.string().default(""),
3381
+ /**
3382
+ * Hint from the agent about how soon it should wake again, in
3383
+ * milliseconds. The scheduler may honor or override based on drive state,
3384
+ * cost budget, and external triggers.
3385
+ *
3386
+ * - Hot loop (active debugging): minutes
3387
+ * - Normal research cadence: hours
3388
+ * - Waiting on external process (training, build): much longer
3389
+ */
3390
+ nextWakeIntervalMs: z.number().int().positive().optional(),
3391
+ /**
3392
+ * Active blockers, in human-readable form. Mirrors the workflow blocker
3393
+ * system but local to the agent's working surface.
3394
+ */
3395
+ openBlockers: z.array(z.string()).default([]),
3396
+ /**
3397
+ * The id of the most recent episode record. Lets the next wake load the
3398
+ * tail of episodic memory without scanning.
3399
+ */
3400
+ lastEpisodeId: z.string().optional(),
3401
+ /** ISO-8601 of last update. */
3402
+ updatedAt: z.string().datetime()
3403
+ });
3404
+ /**
3405
+ * Deep Agent Episode — the per-wake-cycle structured record.
3406
+ *
3407
+ * One Episode is written per wake cycle. Episodes are append-only and
3408
+ * unbounded; they are the agent's raw experience log. Periodically the
3409
+ * grooming process consolidates episodes into Charter semantic memory,
3410
+ * compressing many concrete experiences into fewer reusable facts.
3411
+ *
3412
+ * Key q-paper-neutron-scattering pattern: every Episode carries explicit
3413
+ * `scopeLocks` — what the agent *did NOT do* in this wake. This is the
3414
+ * agentic equivalent of Postel's principle (be conservative in what you
3415
+ * claim to have done) and is what makes adversarial review tractable.
3416
+ */
3417
+ /**
3418
+ * The policy decision made by the orient step at the start of a wake.
3419
+ *
3420
+ * The policy step is a cheap LLM call: given charter + recent episodes
3421
+ * + current drives, what action class maximizes expected drive
3422
+ * satisfaction subject to the goal and tier? Its output is captured
3423
+ * here for later analysis of decision quality.
3424
+ */
3425
+ const PolicyDecisionSchema = z.object({
3426
+ /**
3427
+ * Named action class (matches a key in the agent's toolbelt profile).
3428
+ * Examples: "read_paper", "run_experiment", "ideate_hypothesis",
3429
+ * "request_review", "consolidate_memory".
3430
+ */
3431
+ actionKind: z.string().min(1),
3432
+ /** Natural-language justification for the choice. */
3433
+ rationale: z.string().min(1),
3434
+ /**
3435
+ * The drive deltas the policy expects this action to produce.
3436
+ * Compared against actual deltas at reflect time to calibrate
3437
+ * future policy decisions.
3438
+ */
3439
+ expectedDriveDelta: z.record(z.string(), z.number()).default({})
3440
+ });
3441
+ /**
3442
+ * A single tool/action invocation within a wake cycle.
3443
+ *
3444
+ * One Episode may contain many ActionsTaken — the ReAct loop iterates
3445
+ * within a wake, calling tools, observing, deciding. Each individual
3446
+ * tool call is one ActionTaken record.
3447
+ */
3448
+ const ActionTakenSchema = z.object({
3449
+ /** Tool or sub-action name. */
3450
+ tool: z.string().min(1),
3451
+ /** Arbitrary structured input. Serialized at persist time. */
3452
+ input: z.unknown(),
3453
+ /** Whether the action completed without throwing. */
3454
+ succeeded: z.boolean(),
3455
+ /** Optional duration in ms — useful for budget accounting. */
3456
+ durationMs: z.number().int().min(0).optional()
3457
+ });
3458
+ /**
3459
+ * An observation returned by the world to the agent.
3460
+ *
3461
+ * Observations are deliberately separated from ActionsTaken because
3462
+ * the same action may yield multiple observations (e.g. a shell command
3463
+ * with stdout and stderr) and because some observations are unsolicited
3464
+ * (e.g. an external review arrives between wakes).
3465
+ */
3466
+ const ObservationSchema = z.object({
3467
+ /** Brief label for what kind of observation this is. */
3468
+ kind: z.string().min(1),
3469
+ /** Natural-language summary of what was observed. */
3470
+ summary: z.string().min(1),
3471
+ /** Optional pointer to a fuller artifact (file path, URL, episode id). */
3472
+ artifactRef: z.string().optional()
3473
+ });
3474
+ /**
3475
+ * A proposed change to the Charter, emitted by the reflect step.
3476
+ *
3477
+ * CharterDiff is intentionally narrow — we capture *intent to change*,
3478
+ * not the resulting Charter. The Charter Repository applies the diff
3479
+ * and increments the revision counter. This gives us a clean audit
3480
+ * trail of identity drift over time.
3481
+ */
3482
+ const CharterDiffSchema = z.object({
3483
+ /** Semantic memory entries to add (ids must be fresh). */
3484
+ addedSemanticMemory: z.array(z.string()).default([]),
3485
+ /** Semantic memory entry ids to remove. */
3486
+ removedSemanticMemoryIds: z.array(z.string()).default([]),
3487
+ /** Subgoal ids whose status changed; details captured in reflection. */
3488
+ subgoalStatusChanges: z.array(z.string()).default([]),
3489
+ /** Free-form prose describing the full diff for human review. */
3490
+ summary: z.string().min(1)
3491
+ });
3492
+ z.object({
3493
+ /** Stable identifier (ULID or UUID). */
3494
+ id: z.string().min(1),
3495
+ /** Pointer back to the owning agent. */
3496
+ agentId: z.string().min(1),
3497
+ /** ISO timestamp of wake. */
3498
+ wakeAt: z.string().datetime(),
3499
+ /** Drives at start of wake. */
3500
+ drivesBefore: DriveVectorSchema,
3501
+ /** Output of the orient step. */
3502
+ policyDecision: PolicyDecisionSchema,
3503
+ /** Tool invocations that occurred during the act step. */
3504
+ actionsTaken: z.array(ActionTakenSchema).default([]),
3505
+ /** Observations gathered during the act step. */
3506
+ observations: z.array(ObservationSchema).default([]),
3507
+ /**
3508
+ * Natural-language reflection from the reflect step.
3509
+ * Answers: what just happened? what did I learn? what should change?
3510
+ */
3511
+ reflection: z.string().min(1),
3512
+ /** Proposed Charter changes, applied by the repository. */
3513
+ charterDiff: CharterDiffSchema,
3514
+ /** Drives at end of wake (after applyDelta from observations). */
3515
+ drivesAfter: DriveVectorSchema,
3516
+ /**
3517
+ * SCOPE LOCKS — the q-paper invariant.
3518
+ *
3519
+ * Explicit enumeration of what was NOT done in this wake. Required
3520
+ * for any tier-advancing work; optional but encouraged for routine
3521
+ * work. Examples from q-paper-neutron-scattering:
3522
+ * "did NOT generate exact Lee 2026 target states"
3523
+ * "did NOT touch Q-Work"
3524
+ * "did NOT change evidence labels"
3525
+ *
3526
+ * Scope locks are what make adversarial reviewer subagents tractable:
3527
+ * the reviewer doesn't have to guess what to check against, the actor
3528
+ * told them upfront.
3529
+ */
3530
+ scopeLocks: z.array(z.string()).default([]),
3531
+ /**
3532
+ * Evidence tier this Episode's work was operating at.
3533
+ * Reviewer routing depends on this — engineering-proxy work can be
3534
+ * self-reviewed; external-facing work requires an adversarial reviewer
3535
+ * subagent; human-reviewed work requires a `request_review_gate` action.
3536
+ */
3537
+ evidenceTier: EvidenceTierSchema,
3538
+ /** Token spend during this wake (input + output, all model calls). */
3539
+ tokensSpent: z.number().int().min(0).default(0),
3540
+ /** Cost in USD during this wake. */
3541
+ costUsd: z.number().min(0).default(0),
3542
+ /**
3543
+ * Optional pointer to a reviewer Episode that audited this one.
3544
+ * Set after an adversarial reviewer subagent has completed its pass.
3545
+ */
3546
+ reviewedByEpisodeId: z.string().optional()
3547
+ });
3548
+ z.object({
3549
+ /**
3550
+ * - approved: claims hold up; tierGranted may certify tier advancement
3551
+ * - needs-changes: salvageable, but issues must be addressed first
3552
+ * - rejected: claims refuted or unsupported
3553
+ */
3554
+ verdict: z.enum([
3555
+ "approved",
3556
+ "needs-changes",
3557
+ "rejected"
3558
+ ]),
3559
+ /** Specific, checkable problems found (empty when approved clean). */
3560
+ issues: z.array(z.string()).default([]),
3561
+ /** Highest evidence tier the reviewer certifies for this work. */
3562
+ tierGranted: EvidenceTierSchema.optional(),
3563
+ /** One-paragraph justification of the verdict. */
3564
+ summary: z.string().min(1)
3565
+ });
3159
3566
  String.raw`
3160
3567
  const { parentPort } = require('node:worker_threads');
3161
3568
  const vm = require('node:vm');
@@ -3954,6 +4361,7 @@ function mapMimeTypeToArtifactType(mimeType) {
3954
4361
  case ClaudeArtifactMimeTypes.CODE: return "code";
3955
4362
  case ClaudeArtifactMimeTypes.MARKDOWN: return "code";
3956
4363
  case ClaudeArtifactMimeTypes.LATTICE: return "lattice";
4364
+ case ClaudeArtifactMimeTypes.BLOG_DRAFT: return "blog-draft";
3957
4365
  default:
3958
4366
  if (mimeType.includes("javascript") || mimeType.includes("jsx")) return "react";
3959
4367
  if (mimeType.includes("html")) return "html";
@@ -7323,6 +7731,20 @@ var AIImageService = class {
7323
7731
  }
7324
7732
  };
7325
7733
  /**
7734
+ * AWS Lambda hard limit for synchronous (RequestResponse) invocation payloads.
7735
+ * @see https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html
7736
+ */
7737
+ const LAMBDA_SYNC_PAYLOAD_LIMIT_BYTES = 6291456;
7738
+ /**
7739
+ * Max raw image size we allow into the synchronous ImageProcessor invocation.
7740
+ * base64 inflates bytes by ~4/3, and the JSON envelope adds a small constant,
7741
+ * so a raw image > ~4.5 MB produces a payload over the 6 MB Lambda limit.
7742
+ * We guard at 4.4 MiB to leave margin for the JSON wrapper and key names.
7743
+ * Use binary MiB (matches how currentSizeMB is computed) so the constant,
7744
+ * the user-facing message, and the PR description all agree on "4.4MB".
7745
+ */
7746
+ const MAX_RAW_IMAGE_BYTES = 4.4 * 1024 * 1024;
7747
+ /**
7326
7748
  * Invokes the image processor Lambda to convert and resize images
7327
7749
  * This is a serverless alternative to using sharp directly
7328
7750
  *
@@ -7341,6 +7763,11 @@ async function invokeImageProcessor(imageBuffer, lambdaFunctionName, maxSizeMB =
7341
7763
  }
7342
7764
  console.log(`[ImageProcessorUtils] Processing needed - isPng: ${isPng}, needsResize: ${currentSizeMB > maxSizeMB}`);
7343
7765
  if (!lambdaFunctionName) throw new Error("ImageProcessor Lambda function name is required. Please pass the Lambda function name as an argument.");
7766
+ if (imageBuffer.length > MAX_RAW_IMAGE_BYTES) {
7767
+ const projectedPayloadMB = (imageBuffer.length * 4 / 3 / (1024 * 1024)).toFixed(2);
7768
+ const maxRawMB = (MAX_RAW_IMAGE_BYTES / (1024 * 1024)).toFixed(1);
7769
+ throw new Error(`Image too large (${currentSizeMB.toFixed(2)}MB). Images sent for editing must be under ${maxRawMB}MB (encoding would produce a ~${projectedPayloadMB}MB request, exceeding the ${(LAMBDA_SYNC_PAYLOAD_LIMIT_BYTES / (1024 * 1024)).toFixed(0)}MB limit). Please resize the image and try again.`);
7770
+ }
7344
7771
  const lambdaClient = new LambdaClient({});
7345
7772
  const request = {
7346
7773
  imageBuffer: imageBuffer.toString("base64"),
@@ -8559,6 +8986,24 @@ function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logg
8559
8986
  "claude-opus-4-6"
8560
8987
  ],
8561
8988
  "gemini-1.5-flash": ["claude-haiku-4-5-20251001", "gpt-4o-mini"],
8989
+ "claude-fable-5": [
8990
+ "claude-opus-4-8",
8991
+ "claude-opus-4-7",
8992
+ "claude-opus-4-6",
8993
+ "claude-sonnet-4-6",
8994
+ "gpt-5"
8995
+ ],
8996
+ "claude-opus-4-8": [
8997
+ "claude-opus-4-7",
8998
+ "claude-opus-4-6",
8999
+ "claude-sonnet-4-6",
9000
+ "gpt-5"
9001
+ ],
9002
+ "claude-opus-4-7": [
9003
+ "claude-opus-4-6",
9004
+ "claude-sonnet-4-6",
9005
+ "gpt-5"
9006
+ ],
8562
9007
  "claude-opus-4-5-20251101": [
8563
9008
  "claude-sonnet-4-6",
8564
9009
  "claude-sonnet-4-5-20250929",
@@ -11881,11 +12326,11 @@ function tryDecodeSample(problem, opVarMap, stateIdx) {
11881
12326
  */
11882
12327
  const MAX_QUBITS = 16;
11883
12328
  /** QAOA layers (p). p=1 is standard for NISQ-era problems. */
11884
- const LAYERS = 1;
12329
+ const LAYERS$1 = 1;
11885
12330
  /** Grid steps per axis during parameter search (gammaSteps × betaSteps evaluations). */
11886
- const GRID_STEPS = 8;
12331
+ const GRID_STEPS$1 = 8;
11887
12332
  /** Bitstring samples drawn from the final optimised circuit. */
11888
- const FINAL_SHOTS = 512;
12333
+ const FINAL_SHOTS$1 = 512;
11889
12334
  /** All available solvers */
11890
12335
  const allSolvers = [
11891
12336
  greedySolver,
@@ -11924,7 +12369,7 @@ const allSolvers = [
11924
12369
  schedule: greedyResult.schedule,
11925
12370
  elapsedMs: elapsed(),
11926
12371
  metadata: {
11927
- layers: LAYERS,
12372
+ layers: LAYERS$1,
11928
12373
  shots: 0,
11929
12374
  gamma: 0,
11930
12375
  beta: 0,
@@ -11937,14 +12382,14 @@ const allSolvers = [
11937
12382
  const backend = new LocalSimBackend();
11938
12383
  progress(20);
11939
12384
  const { gamma, beta } = await gridSearchOptimize(numQubits, ising, backend, {
11940
- gammaSteps: GRID_STEPS,
11941
- betaSteps: GRID_STEPS,
11942
- p: LAYERS
12385
+ gammaSteps: GRID_STEPS$1,
12386
+ betaSteps: GRID_STEPS$1,
12387
+ p: LAYERS$1
11943
12388
  });
11944
12389
  progress(75);
11945
- const gates = buildQAOACircuit(numQubits, ising, [gamma], [beta], LAYERS);
12390
+ const gates = buildQAOACircuit(numQubits, ising, [gamma], [beta], LAYERS$1);
11946
12391
  const { probabilities } = await backend.run(numQubits, gates);
11947
- const samples = sampleFromProbabilities(probabilities, FINAL_SHOTS);
12392
+ const samples = sampleFromProbabilities(probabilities, FINAL_SHOTS$1);
11948
12393
  progress(90);
11949
12394
  const candidates = [decodeFromArgmax(problem, encoded.variables, probabilities, greedyResult.schedule), decodeSamples(problem, encoded.variables, samples)].filter((d) => d !== null);
11950
12395
  const bestQuantum = candidates.length > 0 ? candidates.reduce((a, b) => b.makespan < a.makespan ? b : a) : null;
@@ -11958,8 +12403,8 @@ const allSolvers = [
11958
12403
  schedule: winner.schedule,
11959
12404
  elapsedMs: elapsed(),
11960
12405
  metadata: {
11961
- layers: LAYERS,
11962
- shots: FINAL_SHOTS,
12406
+ layers: LAYERS$1,
12407
+ shots: FINAL_SHOTS$1,
11963
12408
  gamma,
11964
12409
  beta,
11965
12410
  rawMakespan: bestQuantum?.makespan ?? greedyResult.makespan,
@@ -12020,12 +12465,12 @@ RESPOND WITH ONLY A JSON OBJECT matching this schema:
12020
12465
 
12021
12466
  No markdown, no explanation, no code blocks — just the raw JSON object.`;
12022
12467
  [
12023
- " 0 2 4 6 8 10 12",
12024
- " ├──┼──┼──┼──┼──┼──┤",
12025
- "A [■■■■■][ ░░░░░░ ]",
12026
- "B [░░░░░][ ■■■■■ ]",
12027
- "C [■■■] [░░░░░░]",
12028
- " ──── time ────────►"
12468
+ " 0 3 6 9 12",
12469
+ " ├───┼───┼───┼───┤",
12470
+ "M1 ████▓▓▓▓░░░",
12471
+ "M2 ░░░░████ ▓▓▓▓",
12472
+ "M3 ▓▓▓░░░░ ████",
12473
+ " └──── time ─────▶"
12029
12474
  ].join("\n"), [
12030
12475
  "J1: [M-A 3t]->[M-B 2t]->[M-C 4t]",
12031
12476
  "J2: [M-B 2t]->[M-C 3t]->[M-A 2t]",
@@ -12057,15 +12502,16 @@ No markdown, no explanation, no code blocks — just the raw JSON object.`;
12057
12502
  " J4 === (arrived t=5)",
12058
12503
  " Must reschedule as jobs arrive!"
12059
12504
  ].join("\n"), [
12060
- " A",
12061
- " /|\\",
12062
- " 5/ | \\3",
12063
- " / | \\",
12064
- " B---+---C",
12065
- " \\ | /",
12066
- " 7\\ | /2",
12067
- " \\|/",
12068
- " D"
12505
+ " A",
12506
+ " ╱ ╲",
12507
+ " 8╱ ╲5",
12508
+ " ╱ ╲",
12509
+ " D● ●B",
12510
+ " ╲ ╱",
12511
+ " 7╲ ╱2",
12512
+ " ╲ ╱",
12513
+ " ●C",
12514
+ " tour A→B→C→D→A = 22"
12069
12515
  ].join("\n"), [
12070
12516
  "Start -> A -> B -> C -> D -> Start",
12071
12517
  "",
@@ -12085,13 +12531,12 @@ No markdown, no explanation, no code blocks — just the raw JSON object.`;
12085
12531
  "| | |",
12086
12532
  "B --6-- C --2-- D"
12087
12533
  ].join("\n"), [
12088
- "┌──────────────────┐",
12089
- " ████ ░░░░ ██████ │ 92%",
12090
- " ████ ░░░░ ██████ │",
12091
- "├──────────────────┤",
12092
- " ████████ ░░░░░░░ │ 68%",
12093
- " ████████ ░░░░░░░ │",
12094
- "└──────────────────┘"
12534
+ " bin 1 bin 2",
12535
+ " ┌────────┐ ┌────────┐",
12536
+ " │███▓▓░░░│ │█████ │",
12537
+ " │███▓▓░░▒│ │▒▒▒ │",
12538
+ " └────────┘ └────────┘",
12539
+ " 94% full 61% full"
12095
12540
  ].join("\n"), [
12096
12541
  "Items: [3] [5] [2] [4] [6]",
12097
12542
  "",
@@ -12110,12 +12555,13 @@ No markdown, no explanation, no code blocks — just the raw JSON object.`;
12110
12555
  " waste",
12111
12556
  "Objective: Minimize total waste"
12112
12557
  ].join("\n"), [
12113
- " Agents Tasks",
12114
- " [A] ────── [1]",
12115
- " [B] ──┐",
12116
- " └── [2]",
12117
- " [C] ────── [3]",
12118
- " [D] ────── [4]"
12558
+ " agents tasks",
12559
+ " [A]─────────(1)",
12560
+ " [B]────┐",
12561
+ " [C]────┼────(2)",
12562
+ " [D]──┐ └────(3)",
12563
+ " └──────(4)",
12564
+ " min Σ cost ▼"
12119
12565
  ].join("\n"), [
12120
12566
  "Workers Jobs",
12121
12567
  " Alice ---> Design cost: 3",
@@ -12133,12 +12579,12 @@ No markdown, no explanation, no code blocks — just the raw JSON object.`;
12133
12579
  "Agent C (cap 12): [T6:6][T7:4]",
12134
12580
  "Maximize quality within capacity"
12135
12581
  ].join("\n"), [
12136
- " a-b-c | d-e",
12137
- " | | | | |",
12138
- " f-g | h-i",
12139
- " -------+-------",
12140
- " Group A | Group B",
12141
- " max cut"
12582
+ " a───b ╌╌┆╌╌ d",
12583
+ " ╲ │ ┆ │",
12584
+ " c───e ╌╌┆╌╌ f",
12585
+ "",
12586
+ " group A ┆ group B",
12587
+ " maximize the cut"
12142
12588
  ].join("\n"), [
12143
12589
  "Group 0 | Group 1",
12144
12590
  " a--b | d--e",
@@ -12157,12 +12603,43 @@ No markdown, no explanation, no code blocks — just the raw JSON object.`;
12157
12603
  " sparse",
12158
12604
  "Find the natural communities"
12159
12605
  ].join("\n"), [
12160
- " [■] [·] [■] [·]",
12161
- " [·] [■] [·] [■]",
12162
- " [■] [·] [·] [·]",
12163
- " ───────────────",
12164
- " ■ = selected 5/12",
12165
- " score: 847 / 1000"
12606
+ " ┌──┬──╥──┬──┐",
12607
+ " │▒▒│▒▒║░░│░░│",
12608
+ " ├──┼──╫──┼──┤",
12609
+ " │▒▒│▒▒║░░│░░│",
12610
+ " └──┴──╨──┴──┘",
12611
+ " rank A rank B",
12612
+ " minimize the seam ▲"
12613
+ ].join("\n"), [
12614
+ " o─o─o ║ o─o─o",
12615
+ " │ │ │ ║ │ │ │",
12616
+ " o─o─o ║ o─o─o",
12617
+ " n/2 ║ n/2",
12618
+ " cut = xᵀLx ▲ minimize"
12619
+ ].join("\n"), [
12620
+ " [ A A B B C C ]",
12621
+ " [ A A B B C C ]",
12622
+ " [ A A B B C C ]",
12623
+ " k=3 · each n/3",
12624
+ " seams pay, balance holds"
12625
+ ].join("\n"), [
12626
+ " mesh ──▶ ranks",
12627
+ " ▒▒▒░░░ halo ↕ exchanged",
12628
+ " ▒▒▒░░░ every timestep",
12629
+ " cut faces = network tax"
12630
+ ].join("\n"), [
12631
+ " ┌────┬────┐",
12632
+ " │blk1│blk2│←wires that",
12633
+ " ├────┼────┤ cross cost",
12634
+ " │blk3│blk4│ timing",
12635
+ " └────┴────┘"
12636
+ ].join("\n"), [
12637
+ " ▣ ▢ ▣ ▢ ▣ ▢",
12638
+ " ▢ ▣ ▢ ▢ ▣ ▢",
12639
+ " ▣ ▢ ▢ ▢ ▢ ▣",
12640
+ " ─────────────",
12641
+ " ▣ picked 7/18",
12642
+ " value 847 ▲ max"
12166
12643
  ].join("\n"), [
12167
12644
  "Return ^",
12168
12645
  " | * efficient frontier",
@@ -12181,13 +12658,13 @@ No markdown, no explanation, no code blocks — just the raw JSON object.`;
12181
12658
  "S4={5,6,7} S5={1,6}",
12182
12659
  "Solution: S1 + S3 + S4 (covers all)"
12183
12660
  ].join("\n"), [
12184
- " P │\\ /",
12185
- " \\ / Supply",
12186
- " \\/",
12187
- " /\\ ← equilibrium",
12188
- " │ / \\",
12189
- " │/ \\ Demand",
12190
- " └──────── Q"
12661
+ " P demand╲ ╱supply",
12662
+ " ",
12663
+ " ⊗ p*",
12664
+ " ",
12665
+ " │ ╱ ",
12666
+ " └─────────┴────── Q",
12667
+ " q*"
12191
12668
  ].join("\n"), [
12192
12669
  "Buyers: B1=$50 B2=$40 B3=$30",
12193
12670
  "Sellers: S1=$20 S2=$35 S3=$45",
@@ -12206,13 +12683,13 @@ No markdown, no explanation, no code blocks — just the raw JSON object.`;
12206
12683
  " $20 40 $800",
12207
12684
  " $25 15 $375"
12208
12685
  ].join("\n"), [
12209
- " f(x)",
12210
- "╱╲",
12211
- " │ ╱ ╲ ╱╲",
12212
- " │╱ ╲ ╱ ",
12213
- " ╳ ╲",
12214
- " min",
12215
- " └───────────── x"
12686
+ " f(x)",
12687
+ " │╲ ╱╲",
12688
+ " ╱ ╲",
12689
+ " ╲ ╱ ",
12690
+ " ╲╱",
12691
+ " local min",
12692
+ " └──────────────── x"
12216
12693
  ].join("\n"), [
12217
12694
  "Param A: [0.1, 0.2, 0.3, 0.4]",
12218
12695
  "Param B: [10, 20, 30, 40, 50]",
@@ -12232,9 +12709,388 @@ No markdown, no explanation, no code blocks — just the raw JSON object.`;
12232
12709
  " \\/ <- fit here",
12233
12710
  "Minimize: sum(error^2)"
12234
12711
  ].join("\n");
12712
+ /** Build a jittered grid mesh: cells at (col,row), edges between face-sharing neighbors. */
12713
+ function gridMesh(cols, rows, cellName, weightAt, jitter) {
12714
+ const nodes = [];
12715
+ const padX = 100 / (cols + 1);
12716
+ const padY = 100 / (rows + 1);
12717
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
12718
+ const i = r * cols + c;
12719
+ const { dx, dy } = jitter(i);
12720
+ nodes.push({
12721
+ id: i,
12722
+ name: cellName(i),
12723
+ x: Math.round(padX * (c + 1) + dx),
12724
+ y: Math.round(padY * (r + 1) + dy)
12725
+ });
12726
+ }
12727
+ const edges = [];
12728
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
12729
+ const i = r * cols + c;
12730
+ if (c + 1 < cols) edges.push({
12731
+ a: i,
12732
+ b: i + 1,
12733
+ w: weightAt(i, i + 1)
12734
+ });
12735
+ if (r + 1 < rows) edges.push({
12736
+ a: i,
12737
+ b: i + cols,
12738
+ w: weightAt(i, i + cols)
12739
+ });
12740
+ }
12741
+ return {
12742
+ nodes,
12743
+ edges
12744
+ };
12745
+ }
12746
+ const wobble = (i) => i * 7919 % 11 / 10 - .5;
12747
+ const plate = gridMesh(4, 3, (i) => `cell ${i}`, (a, b) => 1 + (a * 31 + b * 17) % 4, (i) => ({
12748
+ dx: wobble(i) * 6,
12749
+ dy: wobble(i + 3) * 6
12750
+ }));
12751
+ const reservoir = gridMesh(6, 3, (i) => `block ${i}`, (a, b) => 1 + (a * 13 + b * 29) % 5, (i) => ({
12752
+ dx: wobble(i + 1) * 8,
12753
+ dy: wobble(i + 5) * 8
12754
+ }));
12755
+ const blade = gridMesh(6, 4, (i) => `elem ${i}`, (a, b) => 1 + (a * 23 + b * 19) % 4, (i) => ({
12756
+ dx: wobble(i + 2) * 7,
12757
+ dy: wobble(i + 7) * 7
12758
+ }));
12759
+ plate.nodes, plate.edges, reservoir.nodes, reservoir.edges, blade.nodes, blade.edges;
12760
+ var QuboBuilder = class {
12761
+ size;
12762
+ q = /* @__PURE__ */ new Map();
12763
+ constructor(size) {
12764
+ this.size = size;
12765
+ }
12766
+ add(i, j, v) {
12767
+ const [a, b] = i <= j ? [i, j] : [j, i];
12768
+ const key = `${a},${b}`;
12769
+ this.q.set(key, (this.q.get(key) ?? 0) + v);
12770
+ }
12771
+ entries() {
12772
+ return [...this.q.entries()].map(([key, v]) => {
12773
+ const [i, j] = key.split(",").map(Number);
12774
+ return [
12775
+ i,
12776
+ j,
12777
+ v
12778
+ ];
12779
+ });
12780
+ }
12781
+ };
12782
+ /** Add a one-hot penalty P·(Σx − 1)² over the given variable indices. */
12783
+ function oneHot(b, vars, P) {
12784
+ for (const i of vars) b.add(i, i, -P);
12785
+ for (let a = 0; a < vars.length; a++) for (let c = a + 1; c < vars.length; c++) b.add(vars[a], vars[c], 2 * P);
12786
+ }
12787
+ function schedulingToy() {
12788
+ const b = new QuboBuilder(12);
12789
+ const v = (op, t) => op * 4 + t;
12790
+ for (let op = 0; op < 3; op++) {
12791
+ oneHot(b, [
12792
+ 0,
12793
+ 1,
12794
+ 2,
12795
+ 3
12796
+ ].map((t) => v(op, t)), 4);
12797
+ for (let t = 0; t < 4; t++) b.add(v(op, t), v(op, t), t);
12798
+ }
12799
+ for (let t = 0; t < 4; t++) b.add(v(0, t), v(1, t), 6);
12800
+ return {
12801
+ familyId: "scheduling",
12802
+ size: 12,
12803
+ entries: b.entries(),
12804
+ variableGloss: "x[i,t] = 1 ⇔ operation i starts at time t",
12805
+ constraintGloss: "one start per op · no two ops share a machine-slot"
12806
+ };
12807
+ }
12808
+ function routingToy() {
12809
+ const b = new QuboBuilder(16);
12810
+ const v = (city, pos) => city * 4 + pos;
12811
+ const D = [
12812
+ [
12813
+ 0,
12814
+ 2,
12815
+ 3,
12816
+ 2
12817
+ ],
12818
+ [
12819
+ 2,
12820
+ 0,
12821
+ 2,
12822
+ 3
12823
+ ],
12824
+ [
12825
+ 3,
12826
+ 2,
12827
+ 0,
12828
+ 2
12829
+ ],
12830
+ [
12831
+ 2,
12832
+ 3,
12833
+ 2,
12834
+ 0
12835
+ ]
12836
+ ];
12837
+ for (let c = 0; c < 4; c++) oneHot(b, [
12838
+ 0,
12839
+ 1,
12840
+ 2,
12841
+ 3
12842
+ ].map((p) => v(c, p)), 5);
12843
+ for (let p = 0; p < 4; p++) oneHot(b, [
12844
+ 0,
12845
+ 1,
12846
+ 2,
12847
+ 3
12848
+ ].map((c) => v(c, p)), 5);
12849
+ for (let p = 0; p < 4; p++) {
12850
+ const next = (p + 1) % 4;
12851
+ for (let c1 = 0; c1 < 4; c1++) for (let c2 = 0; c2 < 4; c2++) if (c1 !== c2) b.add(v(c1, p), v(c2, next), D[c1][c2]);
12852
+ }
12853
+ return {
12854
+ familyId: "routing",
12855
+ size: 16,
12856
+ entries: b.entries(),
12857
+ variableGloss: "x[c,p] = 1 ⇔ city c is visited at tour position p",
12858
+ constraintGloss: "each city once · each position once · pay the leg distance"
12859
+ };
12860
+ }
12861
+ function packingToy() {
12862
+ const b = new QuboBuilder(12);
12863
+ const v = (item, bin) => item * 3 + bin;
12864
+ const sizes = [
12865
+ 3,
12866
+ 2,
12867
+ 2,
12868
+ 1
12869
+ ];
12870
+ for (let i = 0; i < 4; i++) oneHot(b, [
12871
+ 0,
12872
+ 1,
12873
+ 2
12874
+ ].map((bin) => v(i, bin)), 5);
12875
+ for (let bin = 0; bin < 3; bin++) {
12876
+ for (let i = 0; i < 4; i++) for (let j = i + 1; j < 4; j++) if (sizes[i] + sizes[j] > 4) b.add(v(i, bin), v(j, bin), 3);
12877
+ b.add(v(0, bin), v(0, bin), bin);
12878
+ }
12879
+ return {
12880
+ familyId: "packing",
12881
+ size: 12,
12882
+ entries: b.entries(),
12883
+ variableGloss: "x[i,b] = 1 ⇔ item i rides in bin b",
12884
+ constraintGloss: "each item in one bin · oversized pairs repel each other"
12885
+ };
12886
+ }
12887
+ function assignmentToy() {
12888
+ const b = new QuboBuilder(16);
12889
+ const v = (agent, task) => agent * 4 + task;
12890
+ const C = [
12891
+ [
12892
+ 1,
12893
+ 4,
12894
+ 3,
12895
+ 2
12896
+ ],
12897
+ [
12898
+ 3,
12899
+ 1,
12900
+ 4,
12901
+ 2
12902
+ ],
12903
+ [
12904
+ 2,
12905
+ 3,
12906
+ 1,
12907
+ 4
12908
+ ],
12909
+ [
12910
+ 4,
12911
+ 2,
12912
+ 3,
12913
+ 1
12914
+ ]
12915
+ ];
12916
+ for (let a = 0; a < 4; a++) oneHot(b, [
12917
+ 0,
12918
+ 1,
12919
+ 2,
12920
+ 3
12921
+ ].map((t) => v(a, t)), 5);
12922
+ for (let t = 0; t < 4; t++) oneHot(b, [
12923
+ 0,
12924
+ 1,
12925
+ 2,
12926
+ 3
12927
+ ].map((a) => v(a, t)), 5);
12928
+ for (let a = 0; a < 4; a++) for (let t = 0; t < 4; t++) b.add(v(a, t), v(a, t), C[a][t]);
12929
+ return {
12930
+ familyId: "assignment",
12931
+ size: 16,
12932
+ entries: b.entries(),
12933
+ variableGloss: "x[a,t] = 1 ⇔ agent a takes task t",
12934
+ constraintGloss: "one task per agent · one agent per task · pay the mismatch"
12935
+ };
12936
+ }
12937
+ function networkToy() {
12938
+ const b = new QuboBuilder(10);
12939
+ for (const [i, j] of [
12940
+ [0, 1],
12941
+ [0, 4],
12942
+ [1, 2],
12943
+ [1, 4],
12944
+ [2, 5],
12945
+ [3, 4],
12946
+ [3, 7],
12947
+ [4, 5],
12948
+ [4, 8],
12949
+ [5, 6],
12950
+ [5, 9],
12951
+ [6, 9],
12952
+ [7, 8],
12953
+ [8, 9]
12954
+ ]) {
12955
+ b.add(i, i, -1);
12956
+ b.add(j, j, -1);
12957
+ b.add(i, j, 2);
12958
+ }
12959
+ return {
12960
+ familyId: "network",
12961
+ size: 10,
12962
+ entries: b.entries(),
12963
+ variableGloss: "x[i] = 1 ⇔ node i goes to side B",
12964
+ constraintGloss: "no constraints — max cut IS the raw QUBO"
12965
+ };
12966
+ }
12967
+ function partitioningToy() {
12968
+ const b = new QuboBuilder(9);
12969
+ const edges = [];
12970
+ for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) {
12971
+ const i = r * 3 + c;
12972
+ if (c < 2) edges.push([i, i + 1]);
12973
+ if (r < 2) edges.push([i, i + 3]);
12974
+ }
12975
+ for (const [i, j] of edges) {
12976
+ b.add(i, i, 1);
12977
+ b.add(j, j, 1);
12978
+ b.add(i, j, -2);
12979
+ }
12980
+ const Pb = 1.5;
12981
+ const m = 4.5;
12982
+ for (let i = 0; i < 9; i++) {
12983
+ b.add(i, i, Pb * (1 - 2 * m));
12984
+ for (let j = i + 1; j < 9; j++) b.add(i, j, 2 * Pb);
12985
+ }
12986
+ return {
12987
+ familyId: "partitioning",
12988
+ size: 9,
12989
+ entries: b.entries(),
12990
+ variableGloss: "x[i] = 1 ⇔ mesh cell i goes to rank B",
12991
+ constraintGloss: "minimize the cut — literally xᵀLx, the graph Laplacian · balance is a spring on Σx"
12992
+ };
12993
+ }
12994
+ function selectionToy() {
12995
+ const b = new QuboBuilder(10);
12996
+ const value = [
12997
+ 6,
12998
+ 5,
12999
+ 8,
13000
+ 3,
13001
+ 7,
13002
+ 4,
13003
+ 6,
13004
+ 2,
13005
+ 5,
13006
+ 4
13007
+ ];
13008
+ const cost = [
13009
+ 3,
13010
+ 2,
13011
+ 4,
13012
+ 1,
13013
+ 3,
13014
+ 2,
13015
+ 3,
13016
+ 1,
13017
+ 2,
13018
+ 2
13019
+ ];
13020
+ const B = 10;
13021
+ const P = 1.2;
13022
+ for (let i = 0; i < 10; i++) {
13023
+ b.add(i, i, -value[i] + P * (cost[i] * cost[i] - 2 * B * cost[i]));
13024
+ for (let j = i + 1; j < 10; j++) b.add(i, j, P * 2 * cost[i] * cost[j]);
13025
+ }
13026
+ return {
13027
+ familyId: "selection",
13028
+ size: 10,
13029
+ entries: b.entries(),
13030
+ variableGloss: "x[i] = 1 ⇔ item i makes the portfolio",
13031
+ constraintGloss: "budget enforced as a quadratic spring around B"
13032
+ };
13033
+ }
13034
+ function economicToy() {
13035
+ const b = new QuboBuilder(10);
13036
+ const price = [
13037
+ 8,
13038
+ 5,
13039
+ 9,
13040
+ 4,
13041
+ 7,
13042
+ 6,
13043
+ 5,
13044
+ 3,
13045
+ 6,
13046
+ 4
13047
+ ];
13048
+ const conflicts = [
13049
+ [0, 1],
13050
+ [0, 2],
13051
+ [1, 3],
13052
+ [2, 4],
13053
+ [2, 5],
13054
+ [3, 5],
13055
+ [4, 6],
13056
+ [5, 7],
13057
+ [6, 8],
13058
+ [7, 9],
13059
+ [8, 9]
13060
+ ];
13061
+ for (let i = 0; i < 10; i++) b.add(i, i, -price[i]);
13062
+ for (const [i, j] of conflicts) b.add(i, j, 12);
13063
+ return {
13064
+ familyId: "economic",
13065
+ size: 10,
13066
+ entries: b.entries(),
13067
+ variableGloss: "x[i] = 1 ⇔ bid i wins its bundle",
13068
+ constraintGloss: "bids sharing a good repel — one sale per asset"
13069
+ };
13070
+ }
13071
+ function continuousToy() {
13072
+ const b = new QuboBuilder(8);
13073
+ const target = 9;
13074
+ for (let p = 0; p < 2; p++) {
13075
+ const base = p * 4;
13076
+ for (let j = 0; j < 4; j++) {
13077
+ const wj = 2 ** j;
13078
+ b.add(base + j, base + j, wj * wj - 2 * target * wj);
13079
+ for (let k = j + 1; k < 4; k++) b.add(base + j, base + k, 2 * wj * 2 ** k);
13080
+ }
13081
+ }
13082
+ return {
13083
+ familyId: "continuous",
13084
+ size: 8,
13085
+ entries: b.entries(),
13086
+ variableGloss: "x = Σ 2ᵏ·bₖ — a dial spelled in bits",
13087
+ constraintGloss: "(x − x*)² expands into pairwise bit couplings"
13088
+ };
13089
+ }
13090
+ schedulingToy(), routingToy(), packingToy(), assignmentToy(), networkToy(), partitioningToy(), selectionToy(), economicToy(), continuousToy();
12235
13091
  //#endregion
12236
- //#region ../../b4m-core/services/dist/tools-CtLkSQLQ.mjs
12237
- async function performDeepResearch(context, params, config) {
13092
+ //#region ../../b4m-core/services/dist/tools-4APomBDv.mjs
13093
+ async function performDeepResearch(context, params, config = {}) {
12238
13094
  const maxDepth = config.maxDepth || 7;
12239
13095
  const duration = config.duration || 4.5;
12240
13096
  const startTime = Date.now();
@@ -12562,7 +13418,7 @@ const deepResearchTool = {
12562
13418
  toolFn: async (value) => {
12563
13419
  const params = value;
12564
13420
  await context.onStart?.("deep_research", params);
12565
- const result = await performDeepResearch(context, { topic: params.topic }, config);
13421
+ const result = await performDeepResearch(context, { topic: params.topic }, config ?? {});
12566
13422
  return JSON.stringify(result);
12567
13423
  },
12568
13424
  toolSchema: {
@@ -12603,6 +13459,72 @@ async function getDynamicDataLakeAccess(context) {
12603
13459
  dataLakeTagPrefixes: accessibleLakes.map((dl) => dl.fileTagPrefix)
12604
13460
  };
12605
13461
  }
13462
+ async function semanticDataLakeSearch(params, adapters) {
13463
+ const { userId, userGroups = [], query, tags = [], topK = 10, minScore = 0, embeddingModel, apiKeyTable, dataLakeTags, dataLakeTagPrefixes, maxFiles = 2e3, chunkLoadCap = 1e4, logger } = params;
13464
+ const empty = {
13465
+ results: [],
13466
+ totalChunksSearched: 0,
13467
+ filesInScope: 0,
13468
+ embeddingModel
13469
+ };
13470
+ if (!query.trim() || dataLakeTags.length === 0) return empty;
13471
+ const provider = getProviderFromModel(embeddingModel);
13472
+ const embeddingConfig = {};
13473
+ if (provider === "openai") {
13474
+ if (!apiKeyTable?.openai) throw new Error("OpenAI API key required for semantic search but not found.");
13475
+ embeddingConfig.openaiApiKey = apiKeyTable.openai;
13476
+ } else if (provider === "voyageai") {
13477
+ if (!apiKeyTable?.voyageai) throw new Error("VoyageAI API key required for semantic search but not found.");
13478
+ embeddingConfig.voyageApiKey = apiKeyTable.voyageai;
13479
+ }
13480
+ const queryEmbedding = await new EmbeddingFactory(embeddingConfig).createEmbeddingService(embeddingModel).generateEmbedding(query);
13481
+ const queryDim = queryEmbedding.length;
13482
+ const fileSearch = await adapters.db.fabfiles.search(userId, "", {
13483
+ tags,
13484
+ shared: false
13485
+ }, {
13486
+ page: 1,
13487
+ limit: maxFiles
13488
+ }, {
13489
+ by: "fileName",
13490
+ direction: "asc"
13491
+ }, {
13492
+ textSearch: false,
13493
+ includeShared: true,
13494
+ userGroups,
13495
+ dataLakeTags,
13496
+ dataLakeTagPrefixes,
13497
+ excludeContent: true
13498
+ });
13499
+ const fileIds = fileSearch.data.map((f) => f.id);
13500
+ if (fileIds.length === 0) return empty;
13501
+ const fileById = new Map(fileSearch.data.map((f) => [f.id, f]));
13502
+ const chunks = await adapters.db.fabfilechunks.findVectorsByFabFileIds(fileIds, chunkLoadCap);
13503
+ const scored = [];
13504
+ for (const chunk of chunks) {
13505
+ if (!chunk.vector || chunk.vector.length !== queryDim) continue;
13506
+ const score = computeCosineSimilarity(queryEmbedding, chunk.vector);
13507
+ if (score < minScore) continue;
13508
+ const file = fileById.get(chunk.fabFileId);
13509
+ if (!file) continue;
13510
+ scored.push({
13511
+ chunkId: chunk.id,
13512
+ fileId: chunk.fabFileId,
13513
+ fileName: file.fileName,
13514
+ fileTags: file.tags?.map((t) => t.name) ?? [],
13515
+ chunkText: chunk.text ?? "",
13516
+ score
13517
+ });
13518
+ }
13519
+ scored.sort((a, b) => b.score - a.score);
13520
+ logger?.debug?.(`[semanticDataLakeSearch] ${fileIds.length} files, ${chunks.length} chunks → ${scored.length} above min ${minScore}, top score ${scored[0]?.score?.toFixed(3) ?? "n/a"}`);
13521
+ return {
13522
+ results: scored.slice(0, topK),
13523
+ totalChunksSearched: chunks.length,
13524
+ filesInScope: fileIds.length,
13525
+ embeddingModel
13526
+ };
13527
+ }
12606
13528
  const diceRoll = async (parameters) => {
12607
13529
  if (!parameters?.sides || !parameters?.times) throw new Error("Tool dice roll: Missing required parameters");
12608
13530
  return sum(times(parameters.times, () => random(1, parameters.sides))).toString();
@@ -14349,6 +15271,49 @@ function parseTransformationResult(llmResponse) {
14349
15271
  throw new Error("Failed to parse transformation result from LLM");
14350
15272
  }
14351
15273
  }
15274
+ /**
15275
+ * Sanitize a title for safe, display-clean embedding in the <artifact title="…">
15276
+ * attribute. The pristine title lives in the JSON body (which is what the preview
15277
+ * card renders); this attribute is only used as a label/list value and for id
15278
+ * resolution. So we strip the parse-breaking characters rather than HTML-entity-
15279
+ * encode them — entity encoding renders as "&amp;"/"&lt;" gibberish wherever
15280
+ * `metadata.title` is shown verbatim (knowledge viewer list, etc.). See #8905 review.
15281
+ *
15282
+ * - newlines/tabs → space: the attribute regexes use `.*?`, which won't cross newlines.
15283
+ * - strip <,>: keep the tag/attribute matchers ([^>]) from breaking.
15284
+ * - straight quotes → typographic quotes: the value matcher is [^"'], so BOTH a "
15285
+ * and a ' (e.g. the apostrophe in "Can't") would terminate it early and truncate
15286
+ * the title. Curly quotes (’ ” “) aren't in that class, so they're parse-safe and
15287
+ * still read naturally.
15288
+ * `&` is left as-is: it doesn't break the regexes and React renders it correctly.
15289
+ */
15290
+ function sanitizeArtifactTitle(title) {
15291
+ return title.replace(/[\r\n\t]+/g, " ").replace(/[<>]/g, "").replace(/'/g, "’").replace(/"/g, "”").replace(/\s+/g, " ").trim();
15292
+ }
15293
+ /**
15294
+ * Wrap a drafted blog result in an <artifact> tag so it is surfaced as a
15295
+ * first-class artifact (streamed into the reply AND persisted via the
15296
+ * sharedToolBuilder tool_result extractor). See #8904.
15297
+ *
15298
+ * Two embedding concerns are handled here:
15299
+ * - Title goes in a tag attribute → sanitized via sanitizeArtifactTitle (parse-safe,
15300
+ * display-clean; the real title is preserved untouched in the JSON body).
15301
+ * - Blog prose can legitimately contain the literal "</artifact>" sequence, which
15302
+ * would truncate the non-greedy artifact-body regex. We escape it as "<\/artifact>";
15303
+ * JSON.parse treats "\/" as "/" and restores the original losslessly on the client.
15304
+ */
15305
+ function wrapDraftAsArtifact(result, identifier) {
15306
+ const artifactTitle = sanitizeArtifactTitle(result.title);
15307
+ const artifactBody = JSON.stringify(result, null, 2).replace(/<\/artifact>/gi, "<\\/artifact>");
15308
+ return `✨ Blog draft created successfully!
15309
+
15310
+ <artifact identifier="${identifier}" type="${ClaudeArtifactMimeTypes.BLOG_DRAFT}" title="${artifactTitle}">
15311
+ ${artifactBody}
15312
+ </artifact>
15313
+
15314
+ 📋 The preview card above is ready for you to review and edit before publishing.
15315
+ `;
15316
+ }
14352
15317
  const blogDraftTool = {
14353
15318
  name: "blog_draft",
14354
15319
  implementation: (context) => ({
@@ -14390,14 +15355,7 @@ const blogDraftTool = {
14390
15355
  contentLength: result.content.length,
14391
15356
  tagsCount: result.suggestedTags.length
14392
15357
  });
14393
- return `✨ Blog draft created successfully!
14394
-
14395
- \`\`\`json
14396
- ${JSON.stringify(result, null, 2)}
14397
- \`\`\`
14398
-
14399
- 📋 The preview card will appear below for you to review and edit before publishing.
14400
- `;
15358
+ return wrapDraftAsArtifact(result, `blog-draft-${randomUUID()}`);
14401
15359
  } catch (error) {
14402
15360
  logger.error("Blog draft creation failed:", error);
14403
15361
  throw error;
@@ -15707,6 +16665,95 @@ const planetVisibilityTool = {
15707
16665
  }
15708
16666
  })
15709
16667
  };
16668
+ const CHUNK_TEXT_CAP = 1200;
16669
+ /** Clean "[Category] 01 Some Name.md" → "Some Name" for display. */
16670
+ function prettyFileName(fn) {
16671
+ return fn.replace(/\.[a-z0-9]+$/i, "").replace(/^\[[^\]]*\]\s*/, "").replace(/^\d+[\s._-]*/, "").replace(/[-_]+/g, " ").trim();
16672
+ }
16673
+ /** Format semantic passages WITH their content so the model can answer without retrieving. */
16674
+ function formatSemanticResults(results) {
16675
+ const blocks = results.map((r, i) => {
16676
+ const text = r.chunkText.trim();
16677
+ const clipped = text.length > CHUNK_TEXT_CAP ? `${text.slice(0, CHUNK_TEXT_CAP)}…` : text;
16678
+ return `${i + 1}. **${prettyFileName(r.fileName)}** (relevance ${r.score.toFixed(2)})\n${clipped}`;
16679
+ });
16680
+ return `Found ${results.length} relevant passage(s) in the knowledge base — the content is included below, so answer directly and only call retrieve_knowledge_content if you need MORE detail from a specific file:\n\n` + blocks.join("\n\n---\n\n");
16681
+ }
16682
+ /**
16683
+ * Semantic-first KB search: embed the query and cosine-rank against the pre-computed chunk
16684
+ * vectors (tag-independent, ranks by meaning), returning the matching passage TEXT inline so
16685
+ * the model answers without a search→retrieve-N loop. Returns null to fall through to the
16686
+ * keyword path when embedding deps are unavailable or nothing matches.
16687
+ */
16688
+ async function trySemanticKbSearch(context, query, tags, maxResults) {
16689
+ const chunkRepo = context.db.fabfilechunks;
16690
+ const adminSettings = context.db.adminSettings;
16691
+ const apiKeys = context.db.apiKeys;
16692
+ if (!context.db.fabfiles || !chunkRepo?.findVectorsByFabFileIds || !adminSettings || !apiKeys) return null;
16693
+ try {
16694
+ const modelRaw = await adminSettings.getSettingsValue("defaultEmbeddingModel");
16695
+ if (!modelRaw || !isSupportedEmbeddingModel(modelRaw)) return null;
16696
+ const embeddingModel = modelRaw;
16697
+ const apiKeyTable = await (0, apiKeyService_exports.getEffectiveLLMApiKeys)(context.userId, {
16698
+ db: {
16699
+ apiKeys,
16700
+ adminSettings
16701
+ },
16702
+ getSettingsByNames
16703
+ }, { logger: context.logger });
16704
+ const provider = getProviderFromModel(embeddingModel);
16705
+ if (provider === "openai" && !apiKeyTable?.openai) return null;
16706
+ if (provider === "voyageai" && !apiKeyTable?.voyageai) return null;
16707
+ const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
16708
+ if (dataLakeTags.length === 0) return null;
16709
+ const search = await semanticDataLakeSearch({
16710
+ userId: context.userId,
16711
+ userGroups: context.user.groups ?? [],
16712
+ query,
16713
+ tags,
16714
+ topK: Math.max(maxResults, 6),
16715
+ minScore: 0,
16716
+ embeddingModel,
16717
+ apiKeyTable,
16718
+ dataLakeTags,
16719
+ dataLakeTagPrefixes,
16720
+ logger: context.logger
16721
+ }, { db: {
16722
+ fabfiles: context.db.fabfiles,
16723
+ fabfilechunks: chunkRepo
16724
+ } });
16725
+ if (search.results.length === 0) return null;
16726
+ const ranked = search.results.slice(0, maxResults);
16727
+ const seenFile = /* @__PURE__ */ new Set();
16728
+ const citables = [];
16729
+ for (const r of ranked) {
16730
+ if (seenFile.has(r.fileId)) continue;
16731
+ seenFile.add(r.fileId);
16732
+ citables.push({
16733
+ id: r.fileId,
16734
+ type: "document",
16735
+ title: r.fileName,
16736
+ url: `/opti?mode=datalake&article=${r.fileId}`,
16737
+ description: r.fileTags.filter((t) => !t.startsWith("datalake:")).slice(0, 4).join(", ") || void 0,
16738
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
16739
+ status: "complete",
16740
+ metadata: {
16741
+ sourceSystem: "knowledge_base",
16742
+ tags: r.fileTags,
16743
+ relevanceScore: r.score
16744
+ }
16745
+ });
16746
+ }
16747
+ const names = citables.slice(0, 3).map((c) => prettyFileName(c.title));
16748
+ const more = citables.length > 3 ? ` +${citables.length - 3} more` : "";
16749
+ await context.statusUpdate({ promptMeta: { citables } }, `📄 Found ${citables.length} relevant doc(s) in the data lake: ${names.join(", ")}${more}`);
16750
+ context.logger.log(`📚 [semantic] returning ${ranked.length}/${search.results.length} passages from ${citables.length} files (top score ${search.results[0].score.toFixed(3)})`);
16751
+ return formatSemanticResults(ranked);
16752
+ } catch (err) {
16753
+ context.logger.warn("📚 [semantic] KB search failed, falling back to keyword:", err);
16754
+ return null;
16755
+ }
16756
+ }
15710
16757
  /**
15711
16758
  * Formats fab file search results for LLM consumption
15712
16759
  */
@@ -15722,160 +16769,35 @@ function formatSearchResults(files) {
15722
16769
  }
15723
16770
  const knowledgeBaseSearchTool = {
15724
16771
  name: "search_knowledge_base",
15725
- implementation: (context) => ({
15726
- toolFn: async (value) => {
15727
- const params = value;
15728
- await context.onStart?.("search_knowledge_base", params);
15729
- const { query, tags, file_type, max_results = 5 } = params;
15730
- context.logger.log("📚 Knowledge Base Search: userId:", context.userId, "query:", query, "tags:", tags);
15731
- if (!context.db.fabfiles) {
15732
- context.logger.error("❌ Knowledge Base Search: fabfiles repository not available");
15733
- return "Knowledge base search is not available at this time.";
15734
- }
15735
- try {
15736
- const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
15737
- const searchResults = await context.db.fabfiles.search(context.userId, query, {
15738
- tags: tags || [],
15739
- type: file_type,
15740
- shared: false
15741
- }, {
15742
- page: 1,
15743
- limit: Math.min(max_results, 10)
15744
- }, {
15745
- by: "fileName",
15746
- direction: "asc"
15747
- }, {
15748
- textSearch: true,
15749
- includeShared: true,
15750
- userGroups: context.user.groups || [],
15751
- dataLakeTags,
15752
- dataLakeTagPrefixes,
15753
- excludeContent: true
15754
- });
15755
- context.logger.log("📚 Knowledge Base Search: Found", searchResults.data.length, "of", searchResults.total, "results. Files:", searchResults.data.map((f) => f.fileName));
15756
- if (searchResults.data.length > 0) {
15757
- const citables = searchResults.data.map((file, index) => {
15758
- const fileTags = (file.tags?.map((t) => t.name) || []).filter((t) => !t.startsWith("datalake:")).slice(0, 4).join(", ");
15759
- return {
15760
- id: file.id,
15761
- type: "document",
15762
- title: file.fileName,
15763
- url: `/opti?mode=datalake&article=${file.id}`,
15764
- description: fileTags || void 0,
15765
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
15766
- status: "complete",
15767
- metadata: {
15768
- sourceSystem: "knowledge_base",
15769
- tags: file.tags?.map((t) => t.name) || [],
15770
- relevanceScore: 1 - index * .1
15771
- }
15772
- };
15773
- });
15774
- await context.statusUpdate({ promptMeta: { citables } }, "Knowledge base search results");
15775
- context.logger.log(`📚 Knowledge Base Search: Stored ${citables.length} citables`);
16772
+ implementation: (context) => {
16773
+ let searchCallCount = 0;
16774
+ const MAX_SEARCHES = 3;
16775
+ return {
16776
+ toolFn: async (value) => {
16777
+ const params = value;
16778
+ await context.onStart?.("search_knowledge_base", params);
16779
+ const { query, tags, file_type, max_results = 5 } = params;
16780
+ searchCallCount++;
16781
+ if (searchCallCount > MAX_SEARCHES) {
16782
+ context.logger.log(`📚 Knowledge Base Search: call #${searchCallCount} — capped, instructing model to answer`);
16783
+ return `You have already run ${searchCallCount - 1} knowledge-base searches; the relevant passages are in the conversation above. STOP searching and compose your complete answer NOW from those results. Do NOT call search_knowledge_base or retrieve_knowledge_content again unless a specific named fact is genuinely missing.`;
15776
16784
  }
15777
- return formatSearchResults(searchResults.data);
15778
- } catch (error) {
15779
- context.logger.error("❌ Knowledge Base Search: Error during search:", error);
15780
- return "An error occurred while searching your knowledge base. Please try again.";
15781
- }
15782
- },
15783
- toolSchema: {
15784
- name: "search_knowledge_base",
15785
- description: "Search the user's uploaded knowledge base (fab files). Searches across file names, tags, and notes for broad recall. Returns relevant documents from the user's own files, organization-shared files, and files explicitly shared with them. Use this tool when the user asks about their own documents, uploaded files, or organization knowledge.",
15786
- parameters: {
15787
- type: "object",
15788
- properties: {
15789
- query: {
15790
- type: "string",
15791
- description: "The search query to find relevant documents. Matches against file names, tags, and notes."
15792
- },
15793
- tags: {
15794
- type: "array",
15795
- items: { type: "string" },
15796
- description: "Optional: filter results by tag names. Supports partial matching. For optimization docs, use tags like \"opti:family:scheduling\", \"opti:QUBO\", \"opti:solver:highs\". For IonQ sales intelligence, use tags like \"ionq:vertical:pharma\", \"ionq:competitor:ibm\", \"ionq:type:product-specs\", \"ionq:stage:discovery\", \"ionq:offering:forte\". Any matching tag qualifies the file."
15797
- },
15798
- file_type: {
15799
- type: "string",
15800
- enum: [
15801
- "pdf",
15802
- "text",
15803
- "image",
15804
- "excel",
15805
- "word",
15806
- "json",
15807
- "csv",
15808
- "markdown",
15809
- "code",
15810
- "url"
15811
- ],
15812
- description: "Optional: filter results by file type"
15813
- },
15814
- max_results: {
15815
- type: "number",
15816
- description: "Maximum number of results to return (default: 5, max: 10)",
15817
- minimum: 1,
15818
- maximum: 10
15819
- }
15820
- },
15821
- required: ["query"]
15822
- }
15823
- }
15824
- })
15825
- };
15826
- const DEFAULT_MAX_CHARS = 8e3;
15827
- const ABSOLUTE_MAX_CHARS = 16e3;
15828
- const knowledgeBaseRetrieveTool = {
15829
- name: "retrieve_knowledge_content",
15830
- implementation: (context) => ({
15831
- toolFn: async (value) => {
15832
- const params = value;
15833
- await context.onStart?.("retrieve_knowledge_content", params);
15834
- const { file_id, tags, query, max_chars } = params;
15835
- const charBudget = Math.min(max_chars ?? DEFAULT_MAX_CHARS, ABSOLUTE_MAX_CHARS);
15836
- context.logger.log("📖 Knowledge Retrieve: params", {
15837
- file_id,
15838
- tags,
15839
- query,
15840
- max_chars: charBudget
15841
- });
15842
- if (!file_id && !tags?.length && !query) return "Error: You must provide at least one of file_id, tags, or query.";
15843
- if (!context.db.fabfiles) {
15844
- context.logger.error("❌ Knowledge Retrieve: fabfiles repository not available");
15845
- return "Knowledge base retrieval is not available at this time.";
15846
- }
15847
- if (!context.db.fabfilechunks) {
15848
- context.logger.error("❌ Knowledge Retrieve: fabfilechunks repository not available");
15849
- return "Knowledge base retrieval is not available at this time (chunk reader unavailable).";
15850
- }
15851
- try {
15852
- let files = [];
15853
- if (file_id) {
15854
- const ownedFile = await context.db.fabfiles.findByIdAndUserId(file_id, context.userId);
15855
- if (ownedFile) files = [ownedFile];
15856
- else {
15857
- const sharedFile = await context.db.fabfiles.findById(file_id);
15858
- if (sharedFile && !sharedFile.deletedAt && !sharedFile.archivedAt) {
15859
- const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
15860
- const fileTags = sharedFile.tags?.map((t) => t.name) || [];
15861
- const hasMetaTagAccess = dataLakeTags.some((dlt) => fileTags.includes(dlt));
15862
- const hasPrefixAccess = dataLakeTagPrefixes.some((p) => fileTags.some((t) => t.startsWith(p)));
15863
- const hasShareAccess = sharedFile.users?.some((u) => u.userId === context.userId && u.permissions?.some((p) => p === "read" || p === "write"));
15864
- const userGroups = context.user.groups || [];
15865
- const hasGroupAccess = userGroups.length > 0 && sharedFile.groups?.some((g) => userGroups.includes(g.groupId) && g.permissions?.some((p) => p === "read" || p === "write"));
15866
- if (hasMetaTagAccess || hasPrefixAccess || hasShareAccess || hasGroupAccess) files = [sharedFile];
15867
- }
15868
- }
15869
- 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.`;
16785
+ context.logger.log("📚 Knowledge Base Search: userId:", context.userId, "query:", query, "tags:", tags);
16786
+ if (!context.db.fabfiles) {
16787
+ context.logger.error("❌ Knowledge Base Search: fabfiles repository not available");
16788
+ return "Knowledge base search is not available at this time.";
15870
16789
  }
15871
- if (files.length === 0 && (tags?.length || query)) {
16790
+ const semantic = await trySemanticKbSearch(context, query, tags, max_results);
16791
+ if (semantic) return semantic;
16792
+ try {
15872
16793
  const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
15873
- files = (await context.db.fabfiles.search(context.userId, query || "", {
16794
+ const searchResults = await context.db.fabfiles.search(context.userId, query, {
15874
16795
  tags: tags || [],
16796
+ type: file_type,
15875
16797
  shared: false
15876
16798
  }, {
15877
16799
  page: 1,
15878
- limit: 5
16800
+ limit: dataLakeTags.length > 0 ? 200 : 50
15879
16801
  }, {
15880
16802
  by: "fileName",
15881
16803
  direction: "asc"
@@ -15886,87 +16808,251 @@ const knowledgeBaseRetrieveTool = {
15886
16808
  dataLakeTags,
15887
16809
  dataLakeTagPrefixes,
15888
16810
  excludeContent: true
15889
- })).data;
15890
- 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.`;
15891
- }
15892
- let totalCharsUsed = 0;
15893
- const sections = [];
15894
- const retrievedFiles = [];
15895
- for (const file of files) {
15896
- if (totalCharsUsed >= charBudget) break;
15897
- const chunks = await context.db.fabfilechunks.findByFabFileId(file.id);
15898
- if (chunks.length === 0) {
15899
- context.logger.log(`📖 Knowledge Retrieve: No chunks for file ${file.fileName} (${file.id})`);
15900
- continue;
15901
- }
15902
- const fullText = chunks.map((c) => c.text).join("\n");
15903
- const remainingBudget = charBudget - totalCharsUsed;
15904
- const truncated = fullText.length > remainingBudget;
15905
- const content = truncated ? fullText.slice(0, remainingBudget) : fullText;
15906
- const fileTags = file.tags?.map((t) => t.name).join(", ") || "none";
15907
- const charLabel = truncated ? `${content.length} (truncated from ${fullText.length})` : `${content.length}`;
15908
- sections.push(`### ${file.fileName} (ID: ${file.id})\nTags: ${fileTags}\nChunks: ${chunks.length} | Characters: ${charLabel}\n---\n` + content);
15909
- totalCharsUsed += content.length;
15910
- retrievedFiles.push(file);
16811
+ });
16812
+ const queryTerms = Array.from(new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3)));
16813
+ const scoreFile = (file) => {
16814
+ const hay = `${file.fileName} ${(file.tags?.map((t) => t.name) || []).join(" ")} ${file.notes || ""}`.toLowerCase();
16815
+ return queryTerms.reduce((n, term) => hay.includes(term) ? n + 1 : n, 0);
16816
+ };
16817
+ const seen = /* @__PURE__ */ new Set();
16818
+ const rankedResults = searchResults.data.filter((f) => {
16819
+ const key = (f.fileName || f.id || "").toLowerCase();
16820
+ if (seen.has(key)) return false;
16821
+ seen.add(key);
16822
+ return true;
16823
+ }).map((f) => ({
16824
+ f,
16825
+ score: scoreFile(f)
16826
+ })).sort((a, b) => b.score - a.score || a.f.fileName.localeCompare(b.f.fileName)).slice(0, max_results).map((r) => r.f);
16827
+ context.logger.log("📚 Knowledge Base Search: Found", rankedResults.length, "of", searchResults.total, "results (deduped + relevance-ranked). Files:", rankedResults.map((f) => f.fileName));
16828
+ if (rankedResults.length > 0) {
16829
+ const citables = rankedResults.map((file, index) => {
16830
+ const fileTags = (file.tags?.map((t) => t.name) || []).filter((t) => !t.startsWith("datalake:")).slice(0, 4).join(", ");
16831
+ return {
16832
+ id: file.id,
16833
+ type: "document",
16834
+ title: file.fileName,
16835
+ url: `/opti?mode=datalake&article=${file.id}`,
16836
+ description: fileTags || void 0,
16837
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
16838
+ status: "complete",
16839
+ metadata: {
16840
+ sourceSystem: "knowledge_base",
16841
+ tags: file.tags?.map((t) => t.name) || [],
16842
+ relevanceScore: 1 - index * .1
16843
+ }
16844
+ };
16845
+ });
16846
+ const prettyName = (fn) => fn.replace(/\.[a-z0-9]+$/i, "").replace(/^\[[^\]]*\]\s*/, "").replace(/^\d+[\s._-]*/, "").replace(/[-_]+/g, " ").trim();
16847
+ const names = rankedResults.slice(0, 3).map((f) => prettyName(f.fileName));
16848
+ const more = rankedResults.length > 3 ? ` +${rankedResults.length - 3} more` : "";
16849
+ const foundStatus = `📄 Found ${rankedResults.length} in the data lake: ${names.join(", ")}${more}`;
16850
+ await context.statusUpdate({ promptMeta: { citables } }, foundStatus);
16851
+ context.logger.log(`📚 Knowledge Base Search: Stored ${citables.length} citables`);
16852
+ } else await context.statusUpdate({}, `📭 No data-lake matches for “${query.length > 50 ? query.slice(0, 49) + "…" : query}” — broadening…`);
16853
+ return formatSearchResults(rankedResults);
16854
+ } catch (error) {
16855
+ context.logger.error("❌ Knowledge Base Search: Error during search:", error);
16856
+ return "An error occurred while searching your knowledge base. Please try again.";
15911
16857
  }
15912
- if (retrievedFiles.length === 0) return "Found matching documents but they have no indexed content. The files may not have been processed yet.";
15913
- const citables = retrievedFiles.map((file, index) => {
15914
- const fileTags = (file.tags?.map((t) => t.name) || []).filter((t) => !t.startsWith("datalake:")).slice(0, 4).join(", ");
15915
- return {
15916
- id: file.id,
15917
- type: "document",
15918
- title: file.fileName,
15919
- url: `/opti?mode=datalake&article=${file.id}`,
15920
- description: fileTags || void 0,
15921
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
15922
- status: "complete",
15923
- metadata: {
15924
- sourceSystem: "knowledge_base",
15925
- tags: file.tags?.map((t) => t.name) || [],
15926
- relevanceScore: 1 - index * .1
16858
+ },
16859
+ toolSchema: {
16860
+ name: "search_knowledge_base",
16861
+ description: "Semantic search over the user's knowledge base. Ranks documents by MEANING (embeddings) and returns the most relevant passage CONTENT inline — so you can usually answer directly from the results without any further calls. Use a clear natural-language query describing what you need; you do NOT need to know exact tags. Make ONE good search per distinct topic, then compose your answer.",
16862
+ parameters: {
16863
+ type: "object",
16864
+ properties: {
16865
+ query: {
16866
+ type: "string",
16867
+ description: "Natural-language description of what you need (e.g. \"IonQ Aria Forte Tempo product specs, #AQ, gate fidelity, use cases\"). Ranked by semantic similarity — be descriptive."
16868
+ },
16869
+ tags: {
16870
+ type: "array",
16871
+ items: { type: "string" },
16872
+ description: "OPTIONAL narrowing filter — semantic ranking already finds the right docs, so usually omit this. If you do filter, use a real tag (matching is partial + case-insensitive), e.g. \"ionq:vertical:pharma\" or \"ionq:type:product-spec\"."
16873
+ },
16874
+ file_type: {
16875
+ type: "string",
16876
+ enum: [
16877
+ "pdf",
16878
+ "text",
16879
+ "image",
16880
+ "excel",
16881
+ "word",
16882
+ "json",
16883
+ "csv",
16884
+ "markdown",
16885
+ "code",
16886
+ "url"
16887
+ ],
16888
+ description: "Optional: filter results by file type"
16889
+ },
16890
+ max_results: {
16891
+ type: "number",
16892
+ description: "Maximum number of results to return (default: 5, max: 10)",
16893
+ minimum: 1,
16894
+ maximum: 10
15927
16895
  }
15928
- };
16896
+ },
16897
+ required: ["query"]
16898
+ }
16899
+ }
16900
+ };
16901
+ }
16902
+ };
16903
+ const DEFAULT_MAX_CHARS = 8e3;
16904
+ const ABSOLUTE_MAX_CHARS = 16e3;
16905
+ const knowledgeBaseRetrieveTool = {
16906
+ name: "retrieve_knowledge_content",
16907
+ implementation: (context) => {
16908
+ let retrieveCallCount = 0;
16909
+ const MAX_RETRIEVES = 2;
16910
+ return {
16911
+ toolFn: async (value) => {
16912
+ const params = value;
16913
+ await context.onStart?.("retrieve_knowledge_content", params);
16914
+ const { file_id, tags, query, max_chars } = params;
16915
+ const charBudget = Math.min(max_chars ?? DEFAULT_MAX_CHARS, ABSOLUTE_MAX_CHARS);
16916
+ retrieveCallCount++;
16917
+ if (retrieveCallCount > MAX_RETRIEVES) {
16918
+ context.logger.log(`📖 Knowledge Retrieve: call #${retrieveCallCount} — capped, instructing model to answer`);
16919
+ return `You have already retrieved ${retrieveCallCount - 1} documents and the content is in the conversation above. STOP retrieving and compose your complete answer NOW from what you have.`;
16920
+ }
16921
+ context.logger.log("📖 Knowledge Retrieve: params", {
16922
+ file_id,
16923
+ tags,
16924
+ query,
16925
+ max_chars: charBudget
15929
16926
  });
15930
- if (citables.length > 0) {
15931
- await context.statusUpdate({ promptMeta: { citables } }, "Knowledge base content retrieved");
15932
- context.logger.log(`📖 Knowledge Retrieve: Stored ${citables.length} citables`);
16927
+ if (!file_id && !tags?.length && !query) return "Error: You must provide at least one of file_id, tags, or query.";
16928
+ if (!context.db.fabfiles) {
16929
+ context.logger.error("❌ Knowledge Retrieve: fabfiles repository not available");
16930
+ return "Knowledge base retrieval is not available at this time.";
16931
+ }
16932
+ if (!context.db.fabfilechunks) {
16933
+ context.logger.error("❌ Knowledge Retrieve: fabfilechunks repository not available");
16934
+ return "Knowledge base retrieval is not available at this time (chunk reader unavailable).";
15933
16935
  }
15934
- return `Retrieved content from ${retrievedFiles.length} of ${files.length} document(s):\n
16936
+ try {
16937
+ let files = [];
16938
+ if (file_id) {
16939
+ const ownedFile = await context.db.fabfiles.findByIdAndUserId(file_id, context.userId);
16940
+ if (ownedFile) files = [ownedFile];
16941
+ else {
16942
+ const sharedFile = await context.db.fabfiles.findById(file_id);
16943
+ if (sharedFile && !sharedFile.deletedAt && !sharedFile.archivedAt) {
16944
+ const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
16945
+ const fileTags = sharedFile.tags?.map((t) => t.name) || [];
16946
+ const hasMetaTagAccess = dataLakeTags.some((dlt) => fileTags.includes(dlt));
16947
+ const hasPrefixAccess = dataLakeTagPrefixes.some((p) => fileTags.some((t) => t.startsWith(p)));
16948
+ const hasShareAccess = sharedFile.users?.some((u) => u.userId === context.userId && u.permissions?.some((p) => p === "read" || p === "write"));
16949
+ const userGroups = context.user.groups || [];
16950
+ const hasGroupAccess = userGroups.length > 0 && sharedFile.groups?.some((g) => userGroups.includes(g.groupId) && g.permissions?.some((p) => p === "read" || p === "write"));
16951
+ if (hasMetaTagAccess || hasPrefixAccess || hasShareAccess || hasGroupAccess) files = [sharedFile];
16952
+ }
16953
+ }
16954
+ 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.`;
16955
+ }
16956
+ if (files.length === 0 && (tags?.length || query)) {
16957
+ const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
16958
+ files = (await context.db.fabfiles.search(context.userId, query || "", {
16959
+ tags: tags || [],
16960
+ shared: false
16961
+ }, {
16962
+ page: 1,
16963
+ limit: 5
16964
+ }, {
16965
+ by: "fileName",
16966
+ direction: "asc"
16967
+ }, {
16968
+ textSearch: true,
16969
+ includeShared: true,
16970
+ userGroups: context.user.groups || [],
16971
+ dataLakeTags,
16972
+ dataLakeTagPrefixes,
16973
+ excludeContent: true
16974
+ })).data;
16975
+ 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.`;
16976
+ }
16977
+ let totalCharsUsed = 0;
16978
+ const sections = [];
16979
+ const retrievedFiles = [];
16980
+ for (const file of files) {
16981
+ if (totalCharsUsed >= charBudget) break;
16982
+ const chunks = await context.db.fabfilechunks.findByFabFileId(file.id);
16983
+ if (chunks.length === 0) {
16984
+ context.logger.log(`📖 Knowledge Retrieve: No chunks for file ${file.fileName} (${file.id})`);
16985
+ continue;
16986
+ }
16987
+ const fullText = chunks.map((c) => c.text).join("\n");
16988
+ const remainingBudget = charBudget - totalCharsUsed;
16989
+ const truncated = fullText.length > remainingBudget;
16990
+ const content = truncated ? fullText.slice(0, remainingBudget) : fullText;
16991
+ const fileTags = file.tags?.map((t) => t.name).join(", ") || "none";
16992
+ const charLabel = truncated ? `${content.length} (truncated from ${fullText.length})` : `${content.length}`;
16993
+ sections.push(`### ${file.fileName} (ID: ${file.id})\nTags: ${fileTags}\nChunks: ${chunks.length} | Characters: ${charLabel}\n---\n` + content);
16994
+ totalCharsUsed += content.length;
16995
+ retrievedFiles.push(file);
16996
+ }
16997
+ if (retrievedFiles.length === 0) return "Found matching documents but they have no indexed content. The files may not have been processed yet.";
16998
+ const citables = retrievedFiles.map((file, index) => {
16999
+ const fileTags = (file.tags?.map((t) => t.name) || []).filter((t) => !t.startsWith("datalake:")).slice(0, 4).join(", ");
17000
+ return {
17001
+ id: file.id,
17002
+ type: "document",
17003
+ title: file.fileName,
17004
+ url: `/opti?mode=datalake&article=${file.id}`,
17005
+ description: fileTags || void 0,
17006
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
17007
+ status: "complete",
17008
+ metadata: {
17009
+ sourceSystem: "knowledge_base",
17010
+ tags: file.tags?.map((t) => t.name) || [],
17011
+ relevanceScore: 1 - index * .1
17012
+ }
17013
+ };
17014
+ });
17015
+ if (citables.length > 0) {
17016
+ await context.statusUpdate({ promptMeta: { citables } }, "Knowledge base content retrieved");
17017
+ context.logger.log(`📖 Knowledge Retrieve: Stored ${citables.length} citables`);
17018
+ }
17019
+ return `Retrieved content from ${retrievedFiles.length} of ${files.length} document(s):\n
15935
17020
  ` + sections.join("\n\n---\n\n");
15936
- } catch (error) {
15937
- context.logger.error("❌ Knowledge Retrieve: Error during retrieval:", error);
15938
- return "An error occurred while retrieving document content. Please try again.";
15939
- }
15940
- },
15941
- toolSchema: {
15942
- name: "retrieve_knowledge_content",
15943
- description: "Read the actual text content of knowledge base documents. Use this after search_knowledge_base to read documents by file ID, or provide tags/query to find and read documents in one step. Returns the full text content (up to the character budget) for grounding your responses in the user's curated knowledge.",
15944
- parameters: {
15945
- type: "object",
15946
- properties: {
15947
- file_id: {
15948
- type: "string",
15949
- description: "The file ID to retrieve (from search_knowledge_base results). Most efficient for single-document retrieval."
15950
- },
15951
- tags: {
15952
- type: "array",
15953
- items: { type: "string" },
15954
- description: "Filter documents by tags. For optimization docs, use tags like \"opti:family:scheduling\", \"opti:solver:highs\". For IonQ sales intelligence, use tags like \"ionq:vertical:pharma\", \"ionq:competitor:ibm\", \"ionq:type:product-specs\", \"ionq:offering:forte\"."
15955
- },
15956
- query: {
15957
- type: "string",
15958
- description: "Search query to find documents. Can be combined with tags for more targeted retrieval."
15959
- },
15960
- max_chars: {
15961
- type: "number",
15962
- description: "Maximum characters of content to return (default: 8000, max: 16000). Lower values for quick lookups, higher for detailed reading.",
15963
- minimum: 500,
15964
- maximum: 16e3
17021
+ } catch (error) {
17022
+ context.logger.error("❌ Knowledge Retrieve: Error during retrieval:", error);
17023
+ return "An error occurred while retrieving document content. Please try again.";
17024
+ }
17025
+ },
17026
+ toolSchema: {
17027
+ name: "retrieve_knowledge_content",
17028
+ description: "Read the actual text content of knowledge base documents. Use this after search_knowledge_base to read documents by file ID, or provide tags/query to find and read documents in one step. Returns the full text content (up to the character budget) for grounding your responses in the user's curated knowledge.",
17029
+ parameters: {
17030
+ type: "object",
17031
+ properties: {
17032
+ file_id: {
17033
+ type: "string",
17034
+ description: "The file ID to retrieve (from search_knowledge_base results). Most efficient for single-document retrieval."
17035
+ },
17036
+ tags: {
17037
+ type: "array",
17038
+ items: { type: "string" },
17039
+ description: "OPTIONAL tag filter (usually unnecessary search_knowledge_base already returns the content you need). If used, real examples: \"ionq:vertical:pharma\", \"ionq:competitor:ibm\", \"ionq:type:product-spec\"."
17040
+ },
17041
+ query: {
17042
+ type: "string",
17043
+ description: "Search query to find documents. Can be combined with tags for more targeted retrieval."
17044
+ },
17045
+ max_chars: {
17046
+ type: "number",
17047
+ description: "Maximum characters of content to return (default: 8000, max: 16000). Lower values for quick lookups, higher for detailed reading.",
17048
+ minimum: 500,
17049
+ maximum: 16e3
17050
+ }
15965
17051
  }
15966
17052
  }
15967
17053
  }
15968
- }
15969
- })
17054
+ };
17055
+ }
15970
17056
  };
15971
17057
  function formatResult$1(result) {
15972
17058
  const lines = [
@@ -18848,6 +19934,13 @@ z.object({
18848
19934
  knowledgeIds: z.array(z.string()).optional(),
18849
19935
  artifactIds: z.array(z.string()).optional(),
18850
19936
  agentIds: z.array(z.string()).optional(),
19937
+ systemPromptText: z.string().optional(),
19938
+ surface: z.string().optional(),
19939
+ enabledTools: z.array(z.string()).optional(),
19940
+ disabledTools: z.array(z.string()).optional(),
19941
+ forceKnowledgeRetrieval: z.boolean().optional(),
19942
+ retrievalTags: z.array(z.string()).optional(),
19943
+ temperature: z.number().optional(),
18851
19944
  tags: z.array(z.object({
18852
19945
  name: z.string(),
18853
19946
  strength: z.number()
@@ -18857,7 +19950,8 @@ z.object({
18857
19950
  clonedSourceId: z.string().optional().nullable(),
18858
19951
  forkedSourceId: z.string().optional().nullable(),
18859
19952
  projectId: z.string().optional(),
18860
- lastUsedModel: z.string().optional().nullable()
19953
+ lastUsedModel: z.string().optional().nullable(),
19954
+ optiHashi: z.boolean().optional()
18861
19955
  });
18862
19956
  z.object({ id: z.string() });
18863
19957
  z.object({ id: z.string() });
@@ -23995,6 +25089,7 @@ var ServerLlmBackend = class ServerLlmBackend {
23995
25089
  let eventCount = 0;
23996
25090
  const accumulator = new StreamAccumulator();
23997
25091
  let receivedDone = false;
25092
+ let receivedError = false;
23998
25093
  const parser = createParser({ onEvent: (event) => {
23999
25094
  eventCount++;
24000
25095
  streamLogger.onEvent(eventCount, event.data || "");
@@ -24015,6 +25110,7 @@ var ServerLlmBackend = class ServerLlmBackend {
24015
25110
  try {
24016
25111
  const parsed = JSON.parse(data);
24017
25112
  if (parsed.type === "error") {
25113
+ receivedError = true;
24018
25114
  streamLogger.onCriticalEvent(eventCount, "ERROR", parsed.message || "Server error");
24019
25115
  reject(new Error(parsed.message || "Server error"));
24020
25116
  return;
@@ -24061,6 +25157,10 @@ var ServerLlmBackend = class ServerLlmBackend {
24061
25157
  parser.feed(chunk.toString());
24062
25158
  });
24063
25159
  response.data.on("end", () => {
25160
+ if (receivedError) {
25161
+ logger.debug("[ServerLlmBackend] Stream ended after server-sent error event");
25162
+ return;
25163
+ }
24064
25164
  if (!receivedDone) {
24065
25165
  logger.warn(`[ServerLlmBackend] Stream ended without [DONE] signal. Accumulated text: ${accumulator.accumulatedLength} chars, tools: ${accumulator.toolCount}`);
24066
25166
  if (!accumulator.isEmpty()) {