@bike4mind/cli 0.18.1 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BackgroundAgentManager-BEPNKfmi.mjs → BackgroundAgentManager-H_MnlEXz.mjs} +597 -99
- package/dist/{ConfigStore-BbTg-UaE.mjs → ConfigStore-DL7p3ZiY.mjs} +148 -28
- package/dist/commands/apiCommand.mjs +1 -1
- package/dist/commands/doctorCommand.mjs +1 -1
- package/dist/commands/envCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +2 -2
- package/dist/commands/mcpCommand.mjs +1 -1
- package/dist/commands/updateCommand.mjs +1 -1
- package/dist/index.mjs +3 -3
- package/dist/{package-CSa_ad8r.mjs → package-B-ONmxJ7.mjs} +1 -1
- package/package.json +13 -13
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as PermissionDeniedError, $t as
|
|
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-
|
|
5
|
+
import { t as version } from "./package-B-ONmxJ7.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";
|
|
@@ -28,7 +28,7 @@ import { Logger, Logger as Logger$1 } from "@bike4mind/observability";
|
|
|
28
28
|
import { BEDROCK_EMBEDDING_MODEL_MAP, BaseStorage, BaseStorage as BaseStorage$1, BedrockEmbeddingService, ChunkSchema, EmbeddingFactory, EmbeddingModelProvider, EmbeddingService, OPENAI_EMBEDDING_MODEL_MAP, OpenAIEmbeddingService, S3Storage, SmartChunker, URL_REGEX, VOYAGEAI_EMBEDDING_MODEL_MAP, VoyageAIEmbeddingProvider, detectURLs, detectURLs as detectURLs$1, fetchAndParseURL, fetchAndParseURL as fetchAndParseURL$1, getProviderFromModel, hasURLs, hasURLs as hasURLs$1, isPrivateIP, isPrivateOrInternalHostname, urlExists, validateUrlForFetch } from "@bike4mind/fab-pipeline";
|
|
29
29
|
import * as dotenv from "dotenv";
|
|
30
30
|
import * as import__bike4mind_llm_adapters from "@bike4mind/llm-adapters";
|
|
31
|
-
import { OpenAIBackend, ensureToolPairingIntegrity } from "@bike4mind/llm-adapters";
|
|
31
|
+
import { OpenAIBackend, ensureToolPairingIntegrity, toProviderEndUserId } from "@bike4mind/llm-adapters";
|
|
32
32
|
import { fileTypeFromBuffer } from "file-type";
|
|
33
33
|
import invert from "lodash/invert.js";
|
|
34
34
|
import * as util from "node:util";
|
|
@@ -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
|
|
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
|
-
*
|
|
7888
|
-
*
|
|
7889
|
-
* 1. Log errors with structured context
|
|
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
|
-
*
|
|
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 {
|
|
7921
|
+
* import { registerProcessErrorHandlers } from '@bike4mind/utils';
|
|
7900
7922
|
*
|
|
7901
|
-
* // Call at
|
|
7902
|
-
*
|
|
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
|
|
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(
|
|
7936
|
+
log.warn(`[${context}] Network error (unhandled rejection)`, logEntry);
|
|
7919
7937
|
promise.catch(() => {});
|
|
7920
|
-
} else log.error(
|
|
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(
|
|
7925
|
-
else log.error(
|
|
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,
|
|
@@ -10738,11 +10767,12 @@ RULES:
|
|
|
10738
10767
|
5. Each machine can only process one operation at a time
|
|
10739
10768
|
6. Assign reasonable durations if not specified (use whole numbers)
|
|
10740
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."
|
|
10741
10771
|
|
|
10742
10772
|
RESPOND WITH ONLY A JSON OBJECT matching this schema:
|
|
10743
10773
|
{
|
|
10744
|
-
"name": "string - descriptive problem name",
|
|
10745
|
-
"description": "string -
|
|
10774
|
+
"name": "string - short descriptive problem name",
|
|
10775
|
+
"description": "string - one plain-English sentence summarizing the scenario (see rule 8)",
|
|
10746
10776
|
"jobs": [
|
|
10747
10777
|
{
|
|
10748
10778
|
"id": 0,
|
|
@@ -13491,7 +13521,7 @@ const FAMILY_SAMPLE_PROBLEMS = {
|
|
|
13491
13521
|
continuous: continuousSampleProblems
|
|
13492
13522
|
};
|
|
13493
13523
|
//#endregion
|
|
13494
|
-
//#region ../../b4m-core/services/dist/tools-
|
|
13524
|
+
//#region ../../b4m-core/services/dist/tools-BIrr-Tc6.mjs
|
|
13495
13525
|
const createFabFileSchema = z.object({
|
|
13496
13526
|
fileName: z.string(),
|
|
13497
13527
|
mimeType: z.string(),
|
|
@@ -13683,7 +13713,7 @@ async function performDeepResearch(context, params, config = {}) {
|
|
|
13683
13713
|
let analysisLlm = context.llm;
|
|
13684
13714
|
let analysisModel = config.model || ChatModels.GPT4_1;
|
|
13685
13715
|
if (config.apiKeys?.openai) try {
|
|
13686
|
-
analysisLlm = new OpenAIBackend(config.apiKeys.openai, context.logger);
|
|
13716
|
+
analysisLlm = new OpenAIBackend(config.apiKeys.openai, context.logger, toProviderEndUserId(context.userId));
|
|
13687
13717
|
analysisModel = ChatModels.GPT4_1;
|
|
13688
13718
|
log("🔬 Deep Research: Using GPT-4.1 for analysis");
|
|
13689
13719
|
} catch (error) {
|
|
@@ -13919,18 +13949,21 @@ async function getDynamicDataLakeAccess(context) {
|
|
|
13919
13949
|
const userTags = context.user.tags || [];
|
|
13920
13950
|
const entitlementKeys = context.entitlementKeys ?? [];
|
|
13921
13951
|
const organizationId = context.user.organizationId ? String(context.user.organizationId) : void 0;
|
|
13952
|
+
const userId = context.user.id ? String(context.user.id) : void 0;
|
|
13922
13953
|
let dynamicDataLakes;
|
|
13923
13954
|
if (context.db.dataLakes) try {
|
|
13924
|
-
dynamicDataLakes = (await context.db.dataLakes.findActiveByUserTagsAndEntitlements(userTags, entitlementKeys, organizationId)).map(toDataLakeConfig);
|
|
13955
|
+
dynamicDataLakes = (await context.db.dataLakes.findActiveByUserTagsAndEntitlements(userTags, entitlementKeys, organizationId, userId)).map(toDataLakeConfig);
|
|
13925
13956
|
} catch {}
|
|
13926
13957
|
const accessibleLakes = getAccessibleDataLakes(userTags, dynamicDataLakes, entitlementKeys);
|
|
13958
|
+
const dynamicIds = new Set((dynamicDataLakes ?? []).map((d) => d.id));
|
|
13927
13959
|
return {
|
|
13928
13960
|
dataLakeTags: accessibleLakes.map((dl) => dl.datalakeTag),
|
|
13929
|
-
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)
|
|
13930
13963
|
};
|
|
13931
13964
|
}
|
|
13932
13965
|
async function semanticDataLakeSearch(params, adapters) {
|
|
13933
|
-
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;
|
|
13934
13967
|
const empty = {
|
|
13935
13968
|
results: [],
|
|
13936
13969
|
totalChunksSearched: 0,
|
|
@@ -13964,6 +13997,7 @@ async function semanticDataLakeSearch(params, adapters) {
|
|
|
13964
13997
|
userGroups,
|
|
13965
13998
|
dataLakeTags,
|
|
13966
13999
|
dataLakeTagPrefixes,
|
|
14000
|
+
scopedTagPrefixes,
|
|
13967
14001
|
excludeContent: true
|
|
13968
14002
|
});
|
|
13969
14003
|
const fileIds = fileSearch.data.map((f) => f.id);
|
|
@@ -14876,7 +14910,7 @@ const currentDateTimeTool = {
|
|
|
14876
14910
|
},
|
|
14877
14911
|
toolSchema: {
|
|
14878
14912
|
name: "current_datetime",
|
|
14879
|
-
description: "Get the current date and time
|
|
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.",
|
|
14880
14914
|
parameters: {
|
|
14881
14915
|
type: "object",
|
|
14882
14916
|
properties: {
|
|
@@ -17295,7 +17329,7 @@ async function trySemanticKbSearch(context, query, tags, maxResults) {
|
|
|
17295
17329
|
const provider = getProviderFromModel(embeddingModel);
|
|
17296
17330
|
if (provider === "openai" && !apiKeyTable?.openai) return null;
|
|
17297
17331
|
if (provider === "voyageai" && !apiKeyTable?.voyageai) return null;
|
|
17298
|
-
const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
|
|
17332
|
+
const { dataLakeTags, dataLakeTagPrefixes, scopedTagPrefixes } = await getDynamicDataLakeAccess(context);
|
|
17299
17333
|
if (dataLakeTags.length === 0) return null;
|
|
17300
17334
|
const search = await semanticDataLakeSearch({
|
|
17301
17335
|
userId: context.userId,
|
|
@@ -17308,6 +17342,7 @@ async function trySemanticKbSearch(context, query, tags, maxResults) {
|
|
|
17308
17342
|
apiKeyTable,
|
|
17309
17343
|
dataLakeTags,
|
|
17310
17344
|
dataLakeTagPrefixes,
|
|
17345
|
+
scopedTagPrefixes,
|
|
17311
17346
|
logger: context.logger
|
|
17312
17347
|
}, { db: {
|
|
17313
17348
|
fabfiles: context.db.fabfiles,
|
|
@@ -17381,7 +17416,7 @@ const knowledgeBaseSearchTool = {
|
|
|
17381
17416
|
const semantic = await trySemanticKbSearch(context, query, tags, max_results);
|
|
17382
17417
|
if (semantic) return semantic;
|
|
17383
17418
|
try {
|
|
17384
|
-
const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
|
|
17419
|
+
const { dataLakeTags, dataLakeTagPrefixes, scopedTagPrefixes } = await getDynamicDataLakeAccess(context);
|
|
17385
17420
|
const searchResults = await context.db.fabfiles.search(context.userId, query, {
|
|
17386
17421
|
tags: tags || [],
|
|
17387
17422
|
type: file_type,
|
|
@@ -17398,6 +17433,7 @@ const knowledgeBaseSearchTool = {
|
|
|
17398
17433
|
userGroups: context.user.groups || [],
|
|
17399
17434
|
dataLakeTags,
|
|
17400
17435
|
dataLakeTagPrefixes,
|
|
17436
|
+
scopedTagPrefixes,
|
|
17401
17437
|
excludeContent: true
|
|
17402
17438
|
});
|
|
17403
17439
|
const queryTerms = Array.from(new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3)));
|
|
@@ -17545,7 +17581,7 @@ const knowledgeBaseRetrieveTool = {
|
|
|
17545
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.`;
|
|
17546
17582
|
}
|
|
17547
17583
|
if (files.length === 0 && (tags?.length || query)) {
|
|
17548
|
-
const { dataLakeTags, dataLakeTagPrefixes } = await getDynamicDataLakeAccess(context);
|
|
17584
|
+
const { dataLakeTags, dataLakeTagPrefixes, scopedTagPrefixes } = await getDynamicDataLakeAccess(context);
|
|
17549
17585
|
files = (await context.db.fabfiles.search(context.userId, query || "", {
|
|
17550
17586
|
tags: tags || [],
|
|
17551
17587
|
shared: false
|
|
@@ -17561,6 +17597,7 @@ const knowledgeBaseRetrieveTool = {
|
|
|
17561
17597
|
userGroups: context.user.groups || [],
|
|
17562
17598
|
dataLakeTags,
|
|
17563
17599
|
dataLakeTagPrefixes,
|
|
17600
|
+
scopedTagPrefixes,
|
|
17564
17601
|
excludeContent: true
|
|
17565
17602
|
})).data;
|
|
17566
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.`;
|
|
@@ -17664,6 +17701,13 @@ function formatResult$1(result) {
|
|
|
17664
17701
|
}
|
|
17665
17702
|
return lines.join("\n");
|
|
17666
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
|
+
*/
|
|
17667
17711
|
async function runQuantumSchedule(params) {
|
|
17668
17712
|
const parseResult = SchedulingProblemSchema.safeParse(params.problem);
|
|
17669
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 }] }`;
|
|
@@ -17709,7 +17753,12 @@ async function runQuantumSchedule(params) {
|
|
|
17709
17753
|
lines.push("### Errors");
|
|
17710
17754
|
errors.forEach((e) => lines.push(`- ${e}`));
|
|
17711
17755
|
}
|
|
17712
|
-
return
|
|
17756
|
+
return JSON.stringify({
|
|
17757
|
+
__uiSideEffect: true,
|
|
17758
|
+
type: "populateProblem",
|
|
17759
|
+
payload: problem,
|
|
17760
|
+
displayMessage: lines.join("\n")
|
|
17761
|
+
});
|
|
17713
17762
|
}
|
|
17714
17763
|
const quantumScheduleTool = {
|
|
17715
17764
|
name: "quantum_schedule",
|
|
@@ -17723,7 +17772,9 @@ Available solvers: ${allSolvers.map((s) => `${s.id} (${s.name})`).join(", ")}.
|
|
|
17723
17772
|
|
|
17724
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.
|
|
17725
17774
|
|
|
17726
|
-
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.`,
|
|
17727
17778
|
parameters: {
|
|
17728
17779
|
type: "object",
|
|
17729
17780
|
properties: {
|
|
@@ -17814,7 +17865,7 @@ The problem must define jobs (each with ordered operations) and machines. Each o
|
|
|
17814
17865
|
}
|
|
17815
17866
|
})
|
|
17816
17867
|
};
|
|
17817
|
-
const FAMILY_IDS = [
|
|
17868
|
+
const FAMILY_IDS$1 = [
|
|
17818
17869
|
"routing",
|
|
17819
17870
|
"packing",
|
|
17820
17871
|
"assignment",
|
|
@@ -17825,7 +17876,7 @@ const FAMILY_IDS = [
|
|
|
17825
17876
|
"continuous"
|
|
17826
17877
|
];
|
|
17827
17878
|
/** Run the model and pull a JSON object out of the response (handles code fences). */
|
|
17828
|
-
async function completeToJson(context, systemPrompt, userDescription) {
|
|
17879
|
+
async function completeToJson$1(context, systemPrompt, userDescription) {
|
|
17829
17880
|
let rawContent = "";
|
|
17830
17881
|
await context.llm.complete(context.model ?? "gpt-4", [{
|
|
17831
17882
|
role: "system",
|
|
@@ -17860,6 +17911,7 @@ Rules:
|
|
|
17860
17911
|
- "family" MUST be exactly "${familyId}".
|
|
17861
17912
|
- Keep it small and browser-solvable, at a scale similar to the example.
|
|
17862
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.
|
|
17863
17915
|
- Output ONLY the JSON object — no prose, no markdown fences.`;
|
|
17864
17916
|
}
|
|
17865
17917
|
const quantumFormulateTool = {
|
|
@@ -17869,11 +17921,11 @@ const quantumFormulateTool = {
|
|
|
17869
17921
|
const params = value;
|
|
17870
17922
|
const userDescription = params.description?.trim();
|
|
17871
17923
|
if (!userDescription) return "Error: Please provide a description of the optimization problem to formulate.";
|
|
17872
|
-
if (params.familyId && !FAMILY_IDS.includes(params.familyId)) return `Error: unknown familyId "${String(params.familyId)}". Valid families: ${FAMILY_IDS.join(", ")}. Omit familyId to formulate a job-shop scheduling problem.`;
|
|
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.`;
|
|
17873
17925
|
const familyId = params.familyId || void 0;
|
|
17874
17926
|
try {
|
|
17875
17927
|
if (familyId) {
|
|
17876
|
-
const result = await completeToJson(context, familyFormulationPrompt(familyId), userDescription);
|
|
17928
|
+
const result = await completeToJson$1(context, familyFormulationPrompt(familyId), userDescription);
|
|
17877
17929
|
if ("error" in result) return `Error: ${result.error}\n\nPlease rephrase with more specifics for a ${familyId} problem.`;
|
|
17878
17930
|
const validated = FAMILY_PROBLEM_SCHEMAS[familyId].safeParse(result.json);
|
|
17879
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.`;
|
|
@@ -17882,9 +17934,9 @@ const quantumFormulateTool = {
|
|
|
17882
17934
|
`## Formulated: "${problem.name}"`,
|
|
17883
17935
|
problem.description ? `\n${problem.description}` : "",
|
|
17884
17936
|
"",
|
|
17885
|
-
`**${familyId} problem** —
|
|
17937
|
+
`**${familyId} problem** — loaded into your active brief.`,
|
|
17886
17938
|
"",
|
|
17887
|
-
`
|
|
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.`
|
|
17888
17940
|
].filter((l) => l !== void 0).join("\n");
|
|
17889
17941
|
return JSON.stringify({
|
|
17890
17942
|
__uiSideEffect: true,
|
|
@@ -17896,7 +17948,7 @@ const quantumFormulateTool = {
|
|
|
17896
17948
|
displayMessage: summary
|
|
17897
17949
|
});
|
|
17898
17950
|
}
|
|
17899
|
-
const result = await completeToJson(context, PROBLEM_FORMULATION_PROMPT, userDescription);
|
|
17951
|
+
const result = await completeToJson$1(context, PROBLEM_FORMULATION_PROMPT, userDescription);
|
|
17900
17952
|
if ("error" in result) return `Error: ${result.error}\n\nPlease try rephrasing with specific jobs, machines, and processing times.`;
|
|
17901
17953
|
const validated = SchedulingProblemSchema.safeParse(result.json);
|
|
17902
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.`;
|
|
@@ -17920,7 +17972,7 @@ const quantumFormulateTool = {
|
|
|
17920
17972
|
"### Machines:",
|
|
17921
17973
|
...problem.machines.map((m) => `- ${m.name} (id: ${m.id})`),
|
|
17922
17974
|
"",
|
|
17923
|
-
"
|
|
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."
|
|
17924
17976
|
].filter((l) => l !== void 0).join("\n");
|
|
17925
17977
|
return JSON.stringify({
|
|
17926
17978
|
__uiSideEffect: true,
|
|
@@ -17952,7 +18004,7 @@ Describe the scenario in plain English — the entities, resources, costs/values
|
|
|
17952
18004
|
},
|
|
17953
18005
|
familyId: {
|
|
17954
18006
|
type: "string",
|
|
17955
|
-
enum: FAMILY_IDS,
|
|
18007
|
+
enum: FAMILY_IDS$1,
|
|
17956
18008
|
description: "Optional optimization family. Omit for job-shop scheduling; otherwise one of the eight families."
|
|
17957
18009
|
}
|
|
17958
18010
|
},
|
|
@@ -17962,6 +18014,160 @@ Describe the scenario in plain English — the entities, resources, costs/values
|
|
|
17962
18014
|
}
|
|
17963
18015
|
})
|
|
17964
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
|
+
};
|
|
17965
18171
|
const navigateViewTool = {
|
|
17966
18172
|
name: "navigate_view",
|
|
17967
18173
|
implementation: (context) => ({
|
|
@@ -18980,15 +19186,28 @@ DO NOT USE FOR:
|
|
|
18980
19186
|
}
|
|
18981
19187
|
})
|
|
18982
19188
|
};
|
|
19189
|
+
/** Cap on how many invocable skills are listed back when resolution fails. */
|
|
19190
|
+
const INVOCABLE_HINT_LIMIT = 50;
|
|
18983
19191
|
/**
|
|
18984
19192
|
* LLM-invocable `skill` tool — the autonomous counterpart to user `/skill-name`
|
|
18985
19193
|
* invocations handled by SkillsFeature.
|
|
18986
19194
|
*
|
|
18987
|
-
* The LLM passes `{ name, args }`. The tool resolves the skill
|
|
18988
|
-
* scope
|
|
18989
|
-
*
|
|
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
|
|
18990
19199
|
* `disableModelInvocation: true` are not callable here.
|
|
18991
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
|
+
*
|
|
18992
19211
|
* Catalog discovery: the available skill names + descriptions are listed in
|
|
18993
19212
|
* the system prompt by SkillsFeature, so the LLM knows what's invocable. If
|
|
18994
19213
|
* the LLM passes a name that isn't accessible, the tool returns an error
|
|
@@ -19004,14 +19223,18 @@ const skillTool = {
|
|
|
19004
19223
|
const name = rawName.replace(/^\//, "");
|
|
19005
19224
|
const skillsRepo = context.db.skills;
|
|
19006
19225
|
if (!skillsRepo) return "Error: skills are not configured in this environment.";
|
|
19007
|
-
const skill = await skillsRepo.
|
|
19226
|
+
const skill = await skillsRepo.findAccessibleByNameForUser(context.userId, name);
|
|
19008
19227
|
if (!skill || skill.disableModelInvocation) {
|
|
19009
|
-
const invocable = (await skillsRepo.
|
|
19228
|
+
const invocable = (await skillsRepo.listAccessibleInvocableForUser(context.userId, INVOCABLE_HINT_LIMIT)).map((s) => s.name);
|
|
19010
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(", ")}`;
|
|
19011
19230
|
}
|
|
19012
19231
|
const argArray = typeof params.args === "string" ? parseSkillArguments(params.args) : [];
|
|
19013
19232
|
const expandedBody = substituteArguments$1(skill.body, argArray);
|
|
19014
|
-
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.`;
|
|
19015
19238
|
},
|
|
19016
19239
|
toolSchema: {
|
|
19017
19240
|
name: "skill",
|
|
@@ -19875,6 +20098,7 @@ const b4mTools = {
|
|
|
19875
20098
|
retrieve_knowledge_content: knowledgeBaseRetrieveTool,
|
|
19876
20099
|
quantum_schedule: quantumScheduleTool,
|
|
19877
20100
|
quantum_formulate: quantumFormulateTool,
|
|
20101
|
+
quantum_edit_problem: quantumEditProblemTool,
|
|
19878
20102
|
navigate_view: navigateViewTool,
|
|
19879
20103
|
generate_jupyter_notebook: jupyterNotebookTool,
|
|
19880
20104
|
excel_generation: excelGenerationTool,
|
|
@@ -22358,6 +22582,159 @@ parentPort.on('message', async (msg) => {
|
|
|
22358
22582
|
}
|
|
22359
22583
|
});
|
|
22360
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
|
+
});
|
|
22361
22738
|
z.object({
|
|
22362
22739
|
count: z.number().optional(),
|
|
22363
22740
|
unlimitedUse: z.boolean().optional(),
|
|
@@ -22873,6 +23250,18 @@ z.object({
|
|
|
22873
23250
|
id: z.string().min(1) });
|
|
22874
23251
|
z.object({ organizationId: z.string() });
|
|
22875
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() });
|
|
22876
23265
|
z.object({ id: z.string() });
|
|
22877
23266
|
z.object({ ids: z.array(z.string()).optional() });
|
|
22878
23267
|
z.object({
|
|
@@ -22885,7 +23274,7 @@ z.object({
|
|
|
22885
23274
|
systemPriority: z.number().min(0).max(999).optional(),
|
|
22886
23275
|
sessionId: z.string().optional(),
|
|
22887
23276
|
notes: z.string().optional(),
|
|
22888
|
-
primaryTag: z.string().optional(),
|
|
23277
|
+
primaryTag: z.string().nullable().optional(),
|
|
22889
23278
|
tags: z.array(z.object({
|
|
22890
23279
|
name: z.string(),
|
|
22891
23280
|
strength: z.number()
|
|
@@ -22943,6 +23332,7 @@ z.object({
|
|
|
22943
23332
|
userGroups: z.array(z.string()).optional(),
|
|
22944
23333
|
dataLakeTags: z.array(z.string()).optional(),
|
|
22945
23334
|
dataLakeTagPrefixes: z.array(z.string()).optional(),
|
|
23335
|
+
scopedTagPrefixes: z.array(z.string()).optional(),
|
|
22946
23336
|
excludeContent: z.coerce.boolean().optional()
|
|
22947
23337
|
}).optional()
|
|
22948
23338
|
});
|
|
@@ -24294,6 +24684,15 @@ z.object({
|
|
|
24294
24684
|
isPersonal: z.boolean(),
|
|
24295
24685
|
mementos: z.array(SingleMementoEvalSchema).optional()
|
|
24296
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));
|
|
24297
24696
|
crypto_exports.safeCompareTokens;
|
|
24298
24697
|
//#endregion
|
|
24299
24698
|
//#region ../../b4m-core/services/dist/llm/tools/cliTools.mjs
|
|
@@ -27392,16 +27791,21 @@ function extractUsageInfo(parsed) {
|
|
|
27392
27791
|
return {
|
|
27393
27792
|
inputTokens: parsed.usage?.inputTokens,
|
|
27394
27793
|
outputTokens: parsed.usage?.outputTokens,
|
|
27794
|
+
cacheReadInputTokens: parsed.usage?.cacheReadInputTokens,
|
|
27795
|
+
cacheCreationInputTokens: parsed.usage?.cacheCreationInputTokens,
|
|
27395
27796
|
creditsUsed: parsed.credits?.used,
|
|
27396
27797
|
usdCost: parsed.credits?.usdCost
|
|
27397
27798
|
};
|
|
27398
27799
|
}
|
|
27399
27800
|
/**
|
|
27400
|
-
*
|
|
27401
|
-
*
|
|
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.
|
|
27402
27804
|
*
|
|
27403
|
-
*
|
|
27404
|
-
*
|
|
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.
|
|
27405
27809
|
*/
|
|
27406
27810
|
var StreamAccumulator = class {
|
|
27407
27811
|
constructor() {
|
|
@@ -27410,21 +27814,25 @@ var StreamAccumulator = class {
|
|
|
27410
27814
|
this.thinkingBlocks = [];
|
|
27411
27815
|
this.lastUsageInfo = {};
|
|
27412
27816
|
}
|
|
27413
|
-
|
|
27414
|
-
|
|
27415
|
-
|
|
27416
|
-
|
|
27417
|
-
|
|
27418
|
-
|
|
27419
|
-
|
|
27420
|
-
|
|
27421
|
-
|
|
27422
|
-
|
|
27423
|
-
|
|
27424
|
-
|
|
27425
|
-
|
|
27426
|
-
|
|
27427
|
-
|
|
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
|
+
}
|
|
27428
27836
|
}
|
|
27429
27837
|
/** True when neither text nor tools have been accumulated (stream produced nothing useful). */
|
|
27430
27838
|
isEmpty() {
|
|
@@ -27457,6 +27865,93 @@ var StreamAccumulator = class {
|
|
|
27457
27865
|
}
|
|
27458
27866
|
};
|
|
27459
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
|
|
27460
27955
|
//#region src/llm/ServerLlmBackend.ts
|
|
27461
27956
|
/**
|
|
27462
27957
|
* Connection-level failures that should be retried rather than surfaced to the
|
|
@@ -27631,30 +28126,29 @@ var ServerLlmBackend = class ServerLlmBackend {
|
|
|
27631
28126
|
return;
|
|
27632
28127
|
}
|
|
27633
28128
|
try {
|
|
27634
|
-
const
|
|
27635
|
-
if (
|
|
28129
|
+
const event = parseStreamEvent(JSON.parse(data));
|
|
28130
|
+
if (!event) return;
|
|
28131
|
+
if (event.type === "error") {
|
|
27636
28132
|
receivedError = true;
|
|
27637
|
-
streamLogger.onCriticalEvent(eventCount, "ERROR",
|
|
27638
|
-
reject(new Error(
|
|
28133
|
+
streamLogger.onCriticalEvent(eventCount, "ERROR", event.message || "Server error");
|
|
28134
|
+
reject(new Error(event.message || "Server error"));
|
|
27639
28135
|
return;
|
|
27640
28136
|
}
|
|
27641
|
-
if (
|
|
27642
|
-
|
|
27643
|
-
|
|
27644
|
-
|
|
27645
|
-
|
|
27646
|
-
|
|
27647
|
-
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) {
|
|
27648
28143
|
logger.debug(`TOOL REQUEST: ${tool.name}`);
|
|
27649
28144
|
try {
|
|
27650
|
-
|
|
27651
|
-
logger.debug(` Params: ${paramsStr}`);
|
|
28145
|
+
logger.debug(` Params: ${tool.arguments ?? "{}"}`);
|
|
27652
28146
|
} catch {
|
|
27653
28147
|
logger.debug(` Params: [Unable to stringify]`);
|
|
27654
28148
|
}
|
|
27655
28149
|
}
|
|
27656
|
-
accumulator.
|
|
27657
|
-
if (
|
|
28150
|
+
accumulator.apply(event);
|
|
28151
|
+
if (event.thinking && event.thinking.length > 0) streamLogger.onCriticalEvent(eventCount, "THINKING", `${event.thinking.length} thinking blocks`);
|
|
27658
28152
|
}
|
|
27659
28153
|
} catch (parseError) {
|
|
27660
28154
|
streamLogger.streamError(parseError);
|
|
@@ -27835,20 +28329,22 @@ var FallbackLlmBackend = class {
|
|
|
27835
28329
|
async complete(model, messages, options, callback) {
|
|
27836
28330
|
if (options.abortSignal?.aborted) return this.inner.complete(model, messages, options, callback);
|
|
27837
28331
|
const modelsToTry = [model, ...this.fallbackModels.filter((m) => m !== model)];
|
|
27838
|
-
let callbackInvoked = false;
|
|
27839
|
-
const guardedCallback = async (text, completionInfo) => {
|
|
27840
|
-
callbackInvoked = true;
|
|
27841
|
-
return callback(text, completionInfo);
|
|
27842
|
-
};
|
|
27843
28332
|
let lastError;
|
|
27844
28333
|
for (let i = 0; i < modelsToTry.length; i++) {
|
|
27845
28334
|
const modelToTry = modelsToTry[i];
|
|
28335
|
+
const buffered = [];
|
|
28336
|
+
const bufferingCallback = async (text, completionInfo) => {
|
|
28337
|
+
buffered.push({
|
|
28338
|
+
text,
|
|
28339
|
+
completionInfo
|
|
28340
|
+
});
|
|
28341
|
+
};
|
|
27846
28342
|
try {
|
|
27847
|
-
await this.inner.complete(modelToTry, messages, options,
|
|
28343
|
+
await this.inner.complete(modelToTry, messages, options, bufferingCallback);
|
|
28344
|
+
for (const delivery of buffered) await callback(delivery.text, delivery.completionInfo);
|
|
27848
28345
|
return;
|
|
27849
28346
|
} catch (error) {
|
|
27850
28347
|
if (options.abortSignal?.aborted) throw error;
|
|
27851
|
-
if (callbackInvoked) throw error;
|
|
27852
28348
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
27853
28349
|
const nextModel = modelsToTry[i + 1];
|
|
27854
28350
|
if (nextModel) {
|
|
@@ -29755,12 +30251,14 @@ var WebSocketLlmBackend = class {
|
|
|
29755
30251
|
eventCount++;
|
|
29756
30252
|
const chunk = message.chunk;
|
|
29757
30253
|
streamLogger.onEvent(eventCount, JSON.stringify(chunk));
|
|
29758
|
-
|
|
29759
|
-
|
|
29760
|
-
|
|
29761
|
-
|
|
29762
|
-
streamLogger.
|
|
29763
|
-
|
|
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);
|
|
29764
30262
|
}
|
|
29765
30263
|
} else if (action === "cli_completion_done") {
|
|
29766
30264
|
streamLogger.streamComplete(accumulator.rawText);
|