@bike4mind/cli 1.2.0 → 2.0.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.
- package/README.md +3 -1
- package/dist/{AgentHistoryStore-B2NEOvSW.mjs → AgentHistoryStore-DIMzvf-y.mjs} +460 -274
- package/dist/{ApiClient-D0fQ2FT6.mjs → ApiClient-BTI0-m0g.mjs} +44 -5
- package/dist/{ConfigStore-qV7NrCgZ.mjs → ConfigStore-DIs-K9oD.mjs} +3205 -1078
- package/dist/{buildAgent-C-C-VGff.mjs → buildAgent-DOvEp92a.mjs} +115 -10
- package/dist/commands/acpCommand.mjs +5 -4
- 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 +4 -3
- package/dist/commands/mcpCommand.mjs +3 -3
- package/dist/commands/pluginCommand.mjs +1 -1
- package/dist/commands/updateCommand.mjs +1 -1
- package/dist/index.mjs +522 -34
- package/dist/{package-CGZIoxcs.mjs → package-Dl_F2Fah.mjs} +1 -1
- package/dist/{serve-CPXqcEZr.mjs → serve-Y2zMbfGs.mjs} +237 -73
- package/package.json +11 -11
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { F as NotFoundError, G as WORK_ITEM_STATUSES, I as OllamaEmbeddingModel, L as OpenAIEmbeddingModel, N as McpServerName, O as ChatModels, Q as googleMapsSearchUrl, T as BedrockEmbeddingModel, U as SupportedFabFileMimeTypes, V as SEARCH_RESULT_CARDS_LANGUAGE, W as VoyageAIEmbeddingModel, X as getMcpProviderMetadata, a as htmlErrorTitle, at as secureParameters, bt as APP_NAME, c as loadContextFiles, d as isReadOnlyTool, i as isValidSessionId, it as obfuscateApiKey, j as LOCATION_MAP_LANGUAGE, l as canTrustTool, mt as withRetry, n as logger, nt as isRetryableError, o as htmlFirstH1, ot as signImageUrl, r as SESSION_ID_PATTERN, st as splitEquals, tt as isPlaceholderImageSigningSecret, u as getToolCategory, w as ApiKeyType, xt as WEBSITE_URL, z as PermissionDeniedError } from "./ConfigStore-DIs-K9oD.mjs";
|
|
3
3
|
import { n as isPathAllowed, t as assertPathAllowed } from "./pathValidation-BRqf4HFX-CHwtwp3O.mjs";
|
|
4
4
|
import { n as isTerminalShellStatus, t as getShellSessionManager } from "./ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs";
|
|
5
5
|
import { execFile, execFileSync, spawn } from "child_process";
|
|
@@ -864,7 +864,7 @@ const getSerperKey = async (adapters) => {
|
|
|
864
864
|
};
|
|
865
865
|
const getSearxngUrl = async (adapters) => {
|
|
866
866
|
const { db } = adapters;
|
|
867
|
-
return (await db.adminSettings.findBySettingName("SearxngUrl"))?.settingValue?.trim() ||
|
|
867
|
+
return (await db.adminSettings.findBySettingName("SearxngUrl"))?.settingValue?.trim() || process.env.SEARXNG_BASE_URL?.trim() || null;
|
|
868
868
|
};
|
|
869
869
|
const getWebSearchProviderSetting = async (adapters) => {
|
|
870
870
|
const { db } = adapters;
|
|
@@ -982,7 +982,10 @@ const setApiKey = async (userId, parameters, { db }) => {
|
|
|
982
982
|
if (!apiKey) throw new NotFoundError("API key not found");
|
|
983
983
|
await db.apiKeys.updateAllByUserIdAndType(userId, type, { isActive: false });
|
|
984
984
|
apiKey.isActive = true;
|
|
985
|
-
await db.apiKeys.update(
|
|
985
|
+
await db.apiKeys.update({
|
|
986
|
+
id: apiKey.id,
|
|
987
|
+
isActive: true
|
|
988
|
+
});
|
|
986
989
|
return apiKey;
|
|
987
990
|
};
|
|
988
991
|
const listOwnApiKeys = async (userId, adapters, options = {}) => {
|
|
@@ -1629,6 +1632,7 @@ new https.Agent({
|
|
|
1629
1632
|
lookup: ssrfSafeLookup,
|
|
1630
1633
|
keepAlive: false
|
|
1631
1634
|
});
|
|
1635
|
+
`${APP_NAME || "App"}`, WEBSITE_URL && `${WEBSITE_URL}`;
|
|
1632
1636
|
`${"a, span, em, strong, b, i, u, code, kbd, samp, var, sub, sup, small, abbr, cite, q, time, mark, s, del, ins, bdi, bdo, wbr, ruby, rt, rp".split(", ").join("):not(")}`;
|
|
1633
1637
|
[
|
|
1634
1638
|
"[aria-hidden=\"true\"]",
|
|
@@ -2206,7 +2210,39 @@ const webFetchTool = {
|
|
|
2206
2210
|
})
|
|
2207
2211
|
};
|
|
2208
2212
|
//#endregion
|
|
2209
|
-
//#region ../../b4m-core/services/dist/websearch-
|
|
2213
|
+
//#region ../../b4m-core/services/dist/websearch-ClW051tj.mjs
|
|
2214
|
+
/**
|
|
2215
|
+
* Per-process cache for web-search results. Keyed on normalized query + result count, with a
|
|
2216
|
+
* short TTL so repeated/similar searches within a turn or across back-to-back turns reuse the
|
|
2217
|
+
* same provider response. The cache lives in the Fargate container's memory and is never shared
|
|
2218
|
+
* across containers or persisted.
|
|
2219
|
+
*/
|
|
2220
|
+
const SEARCH_CACHE_TTL_MS = 3e5;
|
|
2221
|
+
const SEARCH_CACHE_MAX_ENTRIES = 200;
|
|
2222
|
+
const searchCache = /* @__PURE__ */ new Map();
|
|
2223
|
+
function searchCacheKey(query, numResults, placeSearch) {
|
|
2224
|
+
return `${query.trim().toLowerCase()}::${numResults}${placeSearch ? "::places" : ""}`;
|
|
2225
|
+
}
|
|
2226
|
+
function searchCacheGet(query, numResults, placeSearch) {
|
|
2227
|
+
const key = searchCacheKey(query, numResults, placeSearch);
|
|
2228
|
+
const entry = searchCache.get(key);
|
|
2229
|
+
if (!entry) return void 0;
|
|
2230
|
+
if (Date.now() > entry.expiresAt) {
|
|
2231
|
+
searchCache.delete(key);
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
return entry.results;
|
|
2235
|
+
}
|
|
2236
|
+
function searchCacheSet(query, numResults, results, placeSearch) {
|
|
2237
|
+
if (searchCache.size >= SEARCH_CACHE_MAX_ENTRIES) {
|
|
2238
|
+
const firstKey = searchCache.keys().next().value;
|
|
2239
|
+
if (firstKey !== void 0) searchCache.delete(firstKey);
|
|
2240
|
+
}
|
|
2241
|
+
searchCache.set(searchCacheKey(query, numResults, placeSearch), {
|
|
2242
|
+
results,
|
|
2243
|
+
expiresAt: Date.now() + SEARCH_CACHE_TTL_MS
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2210
2246
|
/**
|
|
2211
2247
|
* The coarse recency bucket both providers speak, as the smallest one containing `recencyDays`.
|
|
2212
2248
|
* Null when there is no constraint, or when the window is wider than the widest bucket - a
|
|
@@ -2228,10 +2264,19 @@ const SERPAPI_QDR = {
|
|
|
2228
2264
|
year: "qdr:y"
|
|
2229
2265
|
};
|
|
2230
2266
|
const DEFAULT_NUM_RESULTS = 3;
|
|
2231
|
-
const SEARCH_TIMEOUT_MS =
|
|
2232
|
-
const SERPAPI_ATTEMPT_TIMEOUT_MS =
|
|
2267
|
+
const SEARCH_TIMEOUT_MS = 1e4;
|
|
2268
|
+
const SERPAPI_ATTEMPT_TIMEOUT_MS = 1e4;
|
|
2233
2269
|
const SERPAPI_MAX_ATTEMPTS = 2;
|
|
2234
2270
|
const SERPAPI_RETRY_DELAY_MS = 500;
|
|
2271
|
+
/**
|
|
2272
|
+
* How long the lead provider gets before the backup is started alongside it (sooner if the lead
|
|
2273
|
+
* fails or comes back empty first). Just past SerpAPI's normal response time in production: answers come back in about
|
|
2274
|
+
* 1s typically and 3s at the 90th percentile, while a stalled request usually never answers.
|
|
2275
|
+
*/
|
|
2276
|
+
const WEB_SEARCH_HEDGE_DELAY_MS = 3e3;
|
|
2277
|
+
const SERPAPI_ORGANIC_MS = 20500;
|
|
2278
|
+
const HEDGED_BACKUP_MS = WEB_SEARCH_HEDGE_DELAY_MS + Math.max(SEARCH_TIMEOUT_MS, 1 * SERPAPI_ATTEMPT_TIMEOUT_MS);
|
|
2279
|
+
Math.max(SERPAPI_ORGANIC_MS, HEDGED_BACKUP_MS) + SEARCH_TIMEOUT_MS;
|
|
2235
2280
|
const MAX_IMAGES_PER_RESULT = 4;
|
|
2236
2281
|
const DEFAULT_IMAGE_RESULTS = 12;
|
|
2237
2282
|
const DEFAULT_PLACE_RESULTS = 10;
|
|
@@ -2319,6 +2364,11 @@ function indexImagesByLink(inlineImages, shoppingResults) {
|
|
|
2319
2364
|
function isAbortError$1(error) {
|
|
2320
2365
|
return typeof error === "object" && error !== null && error.name === "AbortError";
|
|
2321
2366
|
}
|
|
2367
|
+
function setSerpApiGeoTargeting(searchParams, options) {
|
|
2368
|
+
if (options?.locationInQuery) return;
|
|
2369
|
+
searchParams.set("location", "United States");
|
|
2370
|
+
searchParams.set("gl", "us");
|
|
2371
|
+
}
|
|
2322
2372
|
const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2323
2373
|
/**
|
|
2324
2374
|
* One SerpAPI organic-search attempt, bounded by SERPAPI_ATTEMPT_TIMEOUT_MS. Never throws -
|
|
@@ -2413,21 +2463,21 @@ async function serpApiSearch(adapters, query, num_results, options) {
|
|
|
2413
2463
|
engine: "google",
|
|
2414
2464
|
api_key: apiKey,
|
|
2415
2465
|
q: query,
|
|
2416
|
-
location: "United States",
|
|
2417
2466
|
google_domain: "google.com",
|
|
2418
|
-
gl: "us",
|
|
2419
2467
|
hl: "en",
|
|
2420
2468
|
num: (num_results || DEFAULT_NUM_RESULTS).toString()
|
|
2421
2469
|
});
|
|
2470
|
+
setSerpApiGeoTargeting(searchParams, options);
|
|
2422
2471
|
const bucket = recencyBucket(options?.recencyDays);
|
|
2423
2472
|
if (bucket) searchParams.set("tbs", SERPAPI_QDR[bucket]);
|
|
2424
2473
|
url.search = searchParams.toString();
|
|
2474
|
+
const maxAttempts = Math.min(Math.max(1, options?.maxAttempts ?? SERPAPI_MAX_ATTEMPTS), SERPAPI_MAX_ATTEMPTS);
|
|
2425
2475
|
const failures = [];
|
|
2426
|
-
for (let attempt = 1; attempt <=
|
|
2476
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
2427
2477
|
const outcome = await attemptSerpApiRequest(url, attempt);
|
|
2428
2478
|
if (outcome.ok) return outcome.data;
|
|
2429
2479
|
failures.push(outcome);
|
|
2430
|
-
if (!outcome.retryable || attempt ===
|
|
2480
|
+
if (!outcome.retryable || attempt === maxAttempts) break;
|
|
2431
2481
|
Logger.globalInstance.log("📡 WebSearch Tool: retrying SerpAPI after transient failure", {
|
|
2432
2482
|
attempt,
|
|
2433
2483
|
status: outcome.status,
|
|
@@ -2442,20 +2492,20 @@ async function serpApiSearch(adapters, query, num_results, options) {
|
|
|
2442
2492
|
* `include_images` on a visual query. Failures resolve to [] - missing pictures degrade the reply
|
|
2443
2493
|
* to prose, they never fail the search.
|
|
2444
2494
|
*/
|
|
2445
|
-
async function serpApiImageSearch(adapters, query, limit) {
|
|
2495
|
+
async function serpApiImageSearch(adapters, query, limit, options) {
|
|
2446
2496
|
const apiKey = await (0, apiKeyService_exports.getSerperKey)(adapters);
|
|
2447
2497
|
if (!apiKey) return [];
|
|
2448
2498
|
const url = new URL("https://serpapi.com/search");
|
|
2449
|
-
|
|
2499
|
+
const searchParams = new URLSearchParams({
|
|
2450
2500
|
engine: "google_images",
|
|
2451
2501
|
api_key: apiKey,
|
|
2452
2502
|
q: query,
|
|
2453
|
-
location: "United States",
|
|
2454
2503
|
google_domain: "google.com",
|
|
2455
|
-
gl: "us",
|
|
2456
2504
|
hl: "en",
|
|
2457
2505
|
safe: "active"
|
|
2458
|
-
})
|
|
2506
|
+
});
|
|
2507
|
+
setSerpApiGeoTargeting(searchParams, options);
|
|
2508
|
+
url.search = searchParams.toString();
|
|
2459
2509
|
const controller = new AbortController();
|
|
2460
2510
|
const timeoutId = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS);
|
|
2461
2511
|
try {
|
|
@@ -2555,7 +2605,7 @@ function safeHost(url) {
|
|
|
2555
2605
|
function createSerpApiProvider(adapters) {
|
|
2556
2606
|
return {
|
|
2557
2607
|
name: "serpapi",
|
|
2558
|
-
searchImages: (query, limit) => serpApiImageSearch(adapters, query, limit ?? DEFAULT_IMAGE_RESULTS),
|
|
2608
|
+
searchImages: (query, limit, options) => serpApiImageSearch(adapters, query, limit ?? DEFAULT_IMAGE_RESULTS, options),
|
|
2559
2609
|
searchPlaces: (query, limit) => serpApiPlaceSearch(adapters, query, limit ?? DEFAULT_PLACE_RESULTS),
|
|
2560
2610
|
async search(query, numResults, options) {
|
|
2561
2611
|
const data = await serpApiSearch(adapters, query, numResults, options);
|
|
@@ -2696,36 +2746,49 @@ function createSearxngProvider(baseUrl) {
|
|
|
2696
2746
|
status: response.status,
|
|
2697
2747
|
statusText: response.statusText
|
|
2698
2748
|
});
|
|
2749
|
+
if (options?.throwOnError) throw new Error(`SearXNG error: HTTP ${response.status}`);
|
|
2699
2750
|
return [];
|
|
2700
2751
|
}
|
|
2701
2752
|
return parseSearxngResults(await response.json(), limit);
|
|
2702
2753
|
} catch (error) {
|
|
2703
2754
|
Logger.globalInstance.error("❌ WebSearch Tool: SearXNG request failed:", error);
|
|
2704
|
-
return [];
|
|
2755
|
+
if (!options?.throwOnError) return [];
|
|
2756
|
+
if (isAbortError$1(error)) throw new Error(`Web search timed out: SearXNG did not respond within ${SEARCH_TIMEOUT_MS / 1e3}s`);
|
|
2757
|
+
throw error;
|
|
2705
2758
|
} finally {
|
|
2706
2759
|
clearTimeout(timeoutId);
|
|
2707
2760
|
}
|
|
2708
2761
|
}
|
|
2709
2762
|
};
|
|
2710
2763
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
const choice = await (0, apiKeyService_exports.getWebSearchProviderSetting)(adapters) ?? "auto";
|
|
2720
|
-
const searxngUrl = await (0, apiKeyService_exports.getSearxngUrl)(adapters);
|
|
2721
|
-
const serperKey = await (0, apiKeyService_exports.getSerperKey)(adapters);
|
|
2764
|
+
async function readProviderSettings(adapters) {
|
|
2765
|
+
return {
|
|
2766
|
+
choice: await (0, apiKeyService_exports.getWebSearchProviderSetting)(adapters) ?? "auto",
|
|
2767
|
+
searxngUrl: await (0, apiKeyService_exports.getSearxngUrl)(adapters),
|
|
2768
|
+
serperKey: await (0, apiKeyService_exports.getSerperKey)(adapters)
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
function pickPrimaryProvider(adapters, { choice, searxngUrl, serperKey }) {
|
|
2722
2772
|
if (choice === "searxng") return searxngUrl ? createSearxngProvider(searxngUrl) : null;
|
|
2723
2773
|
if (choice === "serpapi") return serperKey ? createSerpApiProvider(adapters) : null;
|
|
2724
|
-
if (searxngUrl) return createSearxngProvider(searxngUrl);
|
|
2725
2774
|
if (serperKey) return createSerpApiProvider(adapters);
|
|
2775
|
+
if (searxngUrl) return createSearxngProvider(searxngUrl);
|
|
2726
2776
|
return null;
|
|
2727
2777
|
}
|
|
2728
2778
|
/**
|
|
2779
|
+
* Returns [lead, backup] for searchWithHedge. The lead is exactly what
|
|
2780
|
+
* resolveWebSearchProvider picks: the admin's WebSearchProvider choice, or under 'auto' SerpAPI
|
|
2781
|
+
* when a key is set, else SearXNG. The backup is the other provider when it is configured too. An
|
|
2782
|
+
* unconfigured explicit choice yields no lead at all; it is never quietly replaced by the other.
|
|
2783
|
+
*/
|
|
2784
|
+
async function resolveWebSearchProviders(adapters) {
|
|
2785
|
+
const settings = await readProviderSettings(adapters);
|
|
2786
|
+
const lead = pickPrimaryProvider(adapters, settings);
|
|
2787
|
+
if (!lead) return [null, null];
|
|
2788
|
+
if (lead.name === "serpapi") return [lead, settings.searxngUrl ? createSearxngProvider(settings.searxngUrl) : null];
|
|
2789
|
+
return [lead, settings.serperKey ? createSerpApiProvider(adapters) : null];
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2729
2792
|
* Teaches the `b4m_cards` fence, appended to a web_search result ONLY when that search actually
|
|
2730
2793
|
* returned images (see shouldIncludeImages). Delivering it with the results rather than in the system
|
|
2731
2794
|
* prompt costs nothing on the turns that will never use it, and arrives exactly when it is actionable.
|
|
@@ -2864,9 +2927,73 @@ function placeCitable(place, imageUrlSigningSecret, canSignImages) {
|
|
|
2864
2927
|
};
|
|
2865
2928
|
}
|
|
2866
2929
|
const WEB_SEARCH_NOT_CONFIGURED_MSG = "Web search is not configured: an administrator needs to set a Serper API key or a local SearXNG URL in Admin > API Keys. No search was performed.";
|
|
2867
|
-
|
|
2930
|
+
var EmptySearchAnswer = class extends Error {};
|
|
2931
|
+
const errorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
|
|
2932
|
+
/**
|
|
2933
|
+
* Organic search as a hedged request: the lead starts at once, and the backup starts alongside it
|
|
2934
|
+
* once the lead has been silent for WEB_SEARCH_HEDGE_DELAY_MS, or as soon as the lead fails. The
|
|
2935
|
+
* first non-empty answer wins; the loser runs on to its own timeout and is ignored. An empty lead
|
|
2936
|
+
* answer also starts the backup, since a throttled SearXNG answers 200 with no results. With no
|
|
2937
|
+
* backup the lead keeps its own failure mode (SearXNG fail-soft, SerpAPI throws). Returns the
|
|
2938
|
+
* provider that answered so follow-up calls (images, places) go to a live provider. With a backup,
|
|
2939
|
+
* results are empty when neither provider has any (both empty, or one empty and the other failed),
|
|
2940
|
+
* and it throws only when both failed.
|
|
2941
|
+
*/
|
|
2942
|
+
async function searchWithHedge(lead, backup, query, numResults, searchOptions) {
|
|
2943
|
+
if (!backup) return {
|
|
2944
|
+
results: await lead.search(query, numResults, searchOptions),
|
|
2945
|
+
servedBy: lead
|
|
2946
|
+
};
|
|
2947
|
+
const fromLead = lead.search(query, numResults, {
|
|
2948
|
+
...searchOptions,
|
|
2949
|
+
throwOnError: true
|
|
2950
|
+
});
|
|
2951
|
+
const fromBackup = new Promise((start) => {
|
|
2952
|
+
const timer = setTimeout(() => start("slow"), WEB_SEARCH_HEDGE_DELAY_MS);
|
|
2953
|
+
fromLead.then((results) => {
|
|
2954
|
+
clearTimeout(timer);
|
|
2955
|
+
if (results.length === 0) start("empty");
|
|
2956
|
+
}, () => {
|
|
2957
|
+
clearTimeout(timer);
|
|
2958
|
+
start("failed");
|
|
2959
|
+
});
|
|
2960
|
+
}).then((reason) => {
|
|
2961
|
+
Logger.globalInstance.log("WebSearch Tool: starting backup provider", {
|
|
2962
|
+
lead: lead.name,
|
|
2963
|
+
backup: backup.name,
|
|
2964
|
+
reason
|
|
2965
|
+
});
|
|
2966
|
+
return backup.search(query, numResults, {
|
|
2967
|
+
...searchOptions,
|
|
2968
|
+
throwOnError: true,
|
|
2969
|
+
maxAttempts: 1
|
|
2970
|
+
});
|
|
2971
|
+
});
|
|
2972
|
+
const nonEmpty = (answer, provider) => answer.then((results) => {
|
|
2973
|
+
if (results.length === 0) throw new EmptySearchAnswer();
|
|
2974
|
+
return {
|
|
2975
|
+
results,
|
|
2976
|
+
servedBy: provider
|
|
2977
|
+
};
|
|
2978
|
+
});
|
|
2979
|
+
try {
|
|
2980
|
+
return await Promise.any([nonEmpty(fromLead, lead), nonEmpty(fromBackup, backup)]);
|
|
2981
|
+
} catch (error) {
|
|
2982
|
+
const [leadError, backupError] = error instanceof AggregateError ? error.errors : [error, error];
|
|
2983
|
+
if (leadError instanceof EmptySearchAnswer) return {
|
|
2984
|
+
results: [],
|
|
2985
|
+
servedBy: lead
|
|
2986
|
+
};
|
|
2987
|
+
if (backupError instanceof EmptySearchAnswer) return {
|
|
2988
|
+
results: [],
|
|
2989
|
+
servedBy: backup
|
|
2990
|
+
};
|
|
2991
|
+
throw new Error(`Web search failed on both providers. ${lead.name}: ${errorMessage$1(leadError)}; ${backup.name}: ${errorMessage$1(backupError)}`);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
async function performWebSearch(adapters, params, imageUrlSigningSecret = "", resolvedProviders) {
|
|
2868
2995
|
Logger.globalInstance.log("🔍 WebSearch Tool: Starting search for query:", params.query);
|
|
2869
|
-
const provider = await
|
|
2996
|
+
const [provider, backup] = await (resolvedProviders ?? resolveWebSearchProviders(adapters));
|
|
2870
2997
|
if (!provider) {
|
|
2871
2998
|
Logger.globalInstance.error("❌ WebSearch Tool: No web-search provider configured. Skipping search.");
|
|
2872
2999
|
return {
|
|
@@ -2875,21 +3002,33 @@ async function performWebSearch(adapters, params, imageUrlSigningSecret = "") {
|
|
|
2875
3002
|
};
|
|
2876
3003
|
}
|
|
2877
3004
|
try {
|
|
2878
|
-
const
|
|
2879
|
-
|
|
3005
|
+
const searchOptions = params.include_places ? { locationInQuery: true } : void 0;
|
|
3006
|
+
const numResults = params.num_results ?? 3;
|
|
3007
|
+
const isPlaceSearch = !!params.include_places;
|
|
3008
|
+
const cached = searchCacheGet(params.query, numResults, isPlaceSearch);
|
|
3009
|
+
let results;
|
|
3010
|
+
let servedBy = provider;
|
|
3011
|
+
if (cached) {
|
|
3012
|
+
Logger.globalInstance.log("WebSearch Tool: cache hit", { query: params.query });
|
|
3013
|
+
results = cached;
|
|
3014
|
+
} else {
|
|
3015
|
+
({results, servedBy} = await searchWithHedge(provider, backup, params.query, numResults, searchOptions));
|
|
3016
|
+
if (results.length > 0) searchCacheSet(params.query, numResults, results, isPlaceSearch);
|
|
3017
|
+
}
|
|
3018
|
+
Logger.globalInstance.log(`📊 WebSearch Tool: ${servedBy.name} found ${results.length} results${cached ? " (cached)" : ""}`);
|
|
2880
3019
|
const canSignImages = !isPlaceholderImageSigningSecret(imageUrlSigningSecret);
|
|
2881
3020
|
const wantsImages = !!params.include_images && canSignImages;
|
|
2882
3021
|
const anchorQuery = params.include_places ? params.anchor_location?.trim() : void 0;
|
|
2883
3022
|
const [imageResults, placeResults, anchorResults] = await Promise.all([
|
|
2884
|
-
wantsImages ?
|
|
2885
|
-
params.include_places ?
|
|
2886
|
-
anchorQuery ?
|
|
3023
|
+
wantsImages ? servedBy.searchImages?.(params.query, void 0, searchOptions) : void 0,
|
|
3024
|
+
params.include_places ? servedBy.searchPlaces?.(params.query) : void 0,
|
|
3025
|
+
anchorQuery ? servedBy.searchPlaces?.(anchorQuery, 1) : void 0
|
|
2887
3026
|
]).then((all) => all.map((result) => result ?? []));
|
|
2888
3027
|
const anchor = anchorResults[0];
|
|
2889
3028
|
const places = placeResults.filter((place) => place.id !== anchor?.id);
|
|
2890
3029
|
const withPlaces = shouldIncludePlaces(places, params.include_places);
|
|
2891
|
-
if (placeResults.length) Logger.globalInstance.log(`WebSearch Tool: ${
|
|
2892
|
-
if (imageResults.length) Logger.globalInstance.log(`🖼️ WebSearch Tool: ${
|
|
3030
|
+
if (placeResults.length) Logger.globalInstance.log(`WebSearch Tool: ${servedBy.name} found ${placeResults.length} places`);
|
|
3031
|
+
if (imageResults.length) Logger.globalInstance.log(`🖼️ WebSearch Tool: ${servedBy.name} found ${imageResults.length} images`);
|
|
2893
3032
|
const withImages = shouldIncludeImages(results, wantsImages, imageResults);
|
|
2894
3033
|
const citables = results.map((result, index) => ({
|
|
2895
3034
|
id: result.url,
|
|
@@ -2927,50 +3066,66 @@ async function performWebSearch(adapters, params, imageUrlSigningSecret = "") {
|
|
|
2927
3066
|
}
|
|
2928
3067
|
const webSearchTool = {
|
|
2929
3068
|
name: "web_search",
|
|
2930
|
-
implementation: (context,
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3069
|
+
implementation: (context, config) => {
|
|
3070
|
+
let searchCallCount = 0;
|
|
3071
|
+
let providersPromise;
|
|
3072
|
+
return {
|
|
3073
|
+
toolFn: async (value) => {
|
|
3074
|
+
const params = value;
|
|
3075
|
+
const callNumber = ++searchCallCount;
|
|
3076
|
+
await context.onStart?.("web_search", params);
|
|
3077
|
+
providersPromise ??= resolveWebSearchProviders({ db: context.db }).catch((error) => {
|
|
3078
|
+
providersPromise = void 0;
|
|
3079
|
+
throw error;
|
|
3080
|
+
});
|
|
3081
|
+
const startedAt = Date.now();
|
|
3082
|
+
const { formattedResults, citables } = await performWebSearch({ db: context.db }, params, config?.imageUrlSigningSecret, providersPromise);
|
|
3083
|
+
context.logger.log("🔍 WebSearch Tool: search timing", {
|
|
3084
|
+
callNumber,
|
|
3085
|
+
query: params.query,
|
|
3086
|
+
durationMs: Date.now() - startedAt,
|
|
3087
|
+
resultCount: citables.length
|
|
3088
|
+
});
|
|
3089
|
+
if (citables.length > 0) {
|
|
3090
|
+
await context.statusUpdate({ promptMeta: { citables } }, "Web search complete");
|
|
3091
|
+
Logger.globalInstance.log(`📚 WebSearch Tool: Stored ${citables.length} citables`);
|
|
3092
|
+
}
|
|
3093
|
+
return formattedResults;
|
|
3094
|
+
},
|
|
3095
|
+
toolSchema: {
|
|
3096
|
+
name: "web_search",
|
|
3097
|
+
description: "Search the web using Google Search API to FIND pages about a topic. Use this when you need to find URLs or search for information. DO NOT use this if the user provides a specific URL - use web_fetch instead to read the full content.",
|
|
3098
|
+
parameters: {
|
|
3099
|
+
type: "object",
|
|
3100
|
+
properties: {
|
|
3101
|
+
query: {
|
|
3102
|
+
type: "string",
|
|
3103
|
+
description: "The search query to look up"
|
|
3104
|
+
},
|
|
3105
|
+
num_results: {
|
|
3106
|
+
type: "number",
|
|
3107
|
+
description: "Number of results to return (default: 3, max: 10)",
|
|
3108
|
+
minimum: 1,
|
|
3109
|
+
maximum: 10
|
|
3110
|
+
},
|
|
3111
|
+
include_images: {
|
|
3112
|
+
type: "boolean",
|
|
3113
|
+
description: "Set true whenever the answer is about things worth SEEING - products, watches, gear, places, buildings, plants, animals, people, cars, art, food, anything with a look. Decide this yourself from the subject matter: the user will NOT ask for pictures, and an answer that describes a physical object without showing it is a worse answer. Adds a set of attributed images so you can illustrate your reply with a b4m_cards block. Leave unset only for genuinely non-visual questions - code, math, definitions, policy - where images would be junk tokens."
|
|
3114
|
+
},
|
|
3115
|
+
include_places: {
|
|
3116
|
+
type: "boolean",
|
|
3117
|
+
description: "Set true when the answer is a set of PLACES the user would want to see on a map - restaurants, bars, cafes, shops, hotels, attractions, things to do in or near somewhere. Decide this yourself: the user will not ask for a map. Adds places with map locations so you can show them with a b4m_map block. Leave unset for anything that is not about physical locations."
|
|
3118
|
+
},
|
|
3119
|
+
anchor_location: {
|
|
3120
|
+
type: "string",
|
|
3121
|
+
description: "With include_places: the reference place the user named that results should be near, as a searchable name with its city, e.g. \"citizenM Copenhagen Radhuspladsen\". Shown distinctly on the map. Omit when the user named none."
|
|
3122
|
+
}
|
|
2964
3123
|
},
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
description: "With include_places: the reference place the user named that results should be near, as a searchable name with its city, e.g. \"citizenM Copenhagen Radhuspladsen\". Shown distinctly on the map. Omit when the user named none."
|
|
2968
|
-
}
|
|
2969
|
-
},
|
|
2970
|
-
required: ["query"]
|
|
3124
|
+
required: ["query"]
|
|
3125
|
+
}
|
|
2971
3126
|
}
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
2974
3129
|
};
|
|
2975
3130
|
//#endregion
|
|
2976
3131
|
//#region ../../b4m-core/services/dist/weather-jxxOpyX5.mjs
|
|
@@ -3086,81 +3241,32 @@ function normalizeId(value) {
|
|
|
3086
3241
|
return typeof value === "number" && Number.isFinite(value) ? String(value) : void 0;
|
|
3087
3242
|
}
|
|
3088
3243
|
//#endregion
|
|
3089
|
-
//#region ../../b4m-core/services/dist/latticeModelService-
|
|
3090
|
-
const WHITESPACE$1 = /\s/;
|
|
3091
|
-
const EQUALS_OPS = [
|
|
3092
|
-
"=",
|
|
3093
|
-
"equals",
|
|
3094
|
-
"equal"
|
|
3095
|
-
];
|
|
3096
|
-
function isLineTerminator(s, i) {
|
|
3097
|
-
const c = s.charCodeAt(i);
|
|
3098
|
-
return c === 10 || c === 13 || c === 8232 || c === 8233;
|
|
3099
|
-
}
|
|
3100
|
-
function indexText(s) {
|
|
3101
|
-
const n = s.length;
|
|
3102
|
-
const wsEnd = new Int32Array(n + 1);
|
|
3103
|
-
const lineEnd = new Int32Array(n + 1);
|
|
3104
|
-
wsEnd[n] = n;
|
|
3105
|
-
lineEnd[n] = n;
|
|
3106
|
-
let lastLineTerminator = -1;
|
|
3107
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
3108
|
-
wsEnd[i] = WHITESPACE$1.test(s[i]) ? wsEnd[i + 1] : i;
|
|
3109
|
-
const lt = isLineTerminator(s, i);
|
|
3110
|
-
lineEnd[i] = lt ? i : lineEnd[i + 1];
|
|
3111
|
-
if (lt && lastLineTerminator < 0) lastLineTerminator = i;
|
|
3112
|
-
}
|
|
3113
|
-
return {
|
|
3114
|
-
s,
|
|
3115
|
-
n,
|
|
3116
|
-
wsEnd,
|
|
3117
|
-
lineEnd,
|
|
3118
|
-
lastLineTerminator
|
|
3119
|
-
};
|
|
3120
|
-
}
|
|
3121
|
-
/** Case-insensitive match of a lowercase ASCII `word` at i, as the `i` flag compares ASCII letters. */
|
|
3122
|
-
function wordAt(s, i, word) {
|
|
3123
|
-
if (i + word.length > s.length) return false;
|
|
3124
|
-
for (let j = 0; j < word.length; j++) {
|
|
3125
|
-
let c = s.charCodeAt(i + j);
|
|
3126
|
-
if (c >= 65 && c <= 90) c += 32;
|
|
3127
|
-
if (c !== word.charCodeAt(j)) return false;
|
|
3128
|
-
}
|
|
3129
|
-
return true;
|
|
3130
|
-
}
|
|
3131
|
-
/** `/^(.+?)\s*(?:=|equals?)\s*(.+)$/i` -> [output, expression]. */
|
|
3132
|
-
function splitEquals(s) {
|
|
3133
|
-
const ix = indexText(s);
|
|
3134
|
-
const limit = Math.min(ix.lineEnd[0], ix.n - 1);
|
|
3135
|
-
let lastK = -1;
|
|
3136
|
-
for (let g = 1; g <= limit; g++) {
|
|
3137
|
-
const k = ix.wsEnd[g];
|
|
3138
|
-
if (k === lastK) continue;
|
|
3139
|
-
lastK = k;
|
|
3140
|
-
for (const op of EQUALS_OPS) {
|
|
3141
|
-
if (!wordAt(s, k, op)) continue;
|
|
3142
|
-
const p = k + op.length;
|
|
3143
|
-
const w = ix.wsEnd[p];
|
|
3144
|
-
let rest = -1;
|
|
3145
|
-
if (w < ix.n) rest = ix.lastLineTerminator < w ? w : -1;
|
|
3146
|
-
else if (ix.n - 1 >= p && !isLineTerminator(s, ix.n - 1)) rest = ix.n - 1;
|
|
3147
|
-
if (rest >= 0) return [s.slice(0, g), s.slice(rest)];
|
|
3148
|
-
}
|
|
3149
|
-
}
|
|
3150
|
-
return null;
|
|
3151
|
-
}
|
|
3244
|
+
//#region ../../b4m-core/services/dist/latticeModelService-DTKpo_hV.mjs
|
|
3152
3245
|
/**
|
|
3153
3246
|
* The model's creator, compared through `normalizeId` on both sides.
|
|
3154
3247
|
*
|
|
3155
|
-
* This is the WRITE gate (via `getModelForWrite`) and the owner arm of the read gate
|
|
3156
|
-
* subagent Lattice tools in `llm/tools/implementation/lattice` import it so the two surfaces
|
|
3157
|
-
* cannot drift apart again.
|
|
3248
|
+
* This is the WRITE gate (via `getModelForWrite`) and the owner arm of the read gate.
|
|
3158
3249
|
*/
|
|
3159
3250
|
function isModelOwner(model, user) {
|
|
3160
3251
|
const ownerId = normalizeId(model.userId);
|
|
3161
3252
|
return ownerId !== void 0 && ownerId === normalizeId(user.id);
|
|
3162
3253
|
}
|
|
3163
3254
|
/**
|
|
3255
|
+
* READ gate: the creator, or a member of the organization the model was created in.
|
|
3256
|
+
*
|
|
3257
|
+
* Both org ids go through `normalizeId` because they are NOT the same runtime type:
|
|
3258
|
+
* `LatticeModel.organizationId` is a String schema path while `req.user.organizationId` is an
|
|
3259
|
+
* ObjectId on the hydrated user document, so a raw `===` was always false and silently collapsed
|
|
3260
|
+
* org sharing to owner-only everywhere.
|
|
3261
|
+
*
|
|
3262
|
+
* Write authority stays narrower on purpose - see `getModelForWrite`.
|
|
3263
|
+
*/
|
|
3264
|
+
function canReadModel(model, user) {
|
|
3265
|
+
if (isModelOwner(model, user)) return true;
|
|
3266
|
+
const modelOrgId = normalizeId(model.organizationId);
|
|
3267
|
+
return modelOrgId !== void 0 && modelOrgId === normalizeId(user.organizationId);
|
|
3268
|
+
}
|
|
3269
|
+
/**
|
|
3164
3270
|
* The document every newly created model starts from: ownership, org membership, session/project
|
|
3165
3271
|
* scoping, and the empty three-layer skeleton.
|
|
3166
3272
|
*
|
|
@@ -3205,8 +3311,36 @@ function buildNewModel(user, options) {
|
|
|
3205
3311
|
updatedAt: now
|
|
3206
3312
|
};
|
|
3207
3313
|
}
|
|
3314
|
+
/**
|
|
3315
|
+
* Get a model by ID with authorization check
|
|
3316
|
+
*/
|
|
3317
|
+
async function getModel(user, modelId, deps) {
|
|
3318
|
+
const model = await deps.db.latticeModels.findById(modelId);
|
|
3319
|
+
if (!model) return null;
|
|
3320
|
+
return canReadModel(model, user) ? model : null;
|
|
3321
|
+
}
|
|
3322
|
+
/**
|
|
3323
|
+
* Get a model the caller may MUTATE, or null when they may not.
|
|
3324
|
+
*
|
|
3325
|
+
* Ownership only, and deliberately NARROWER than `getModel`. `getModel` is the READ gate and admits
|
|
3326
|
+
* same-org members by design (a model carries its creator's `organizationId`), but read access is
|
|
3327
|
+
* not write access - and every mutator took its authorization from it, so a colleague could rename,
|
|
3328
|
+
* re-value, re-rule or delete a model they did not create. Callers that only read keep using `getModel`: `listModels`,
|
|
3329
|
+
* `searchModels`, `hydrateModel` (computes derived values and stamps `lastComputedAt`), and
|
|
3330
|
+
* `duplicateModel` (reads one model and creates a NEW one owned by the caller).
|
|
3331
|
+
*
|
|
3332
|
+
* Lattice models carry no share/grant field, so the creator is the only principal that can hold
|
|
3333
|
+
* write authority today. If an explicit write grant is ever added, this is the one place to admit
|
|
3334
|
+
* it - keeping the check here rather than inlined in six mutators is what makes that a one-line
|
|
3335
|
+
* change instead of a six-site audit.
|
|
3336
|
+
*/
|
|
3337
|
+
async function getModelForWrite(user, modelId, deps) {
|
|
3338
|
+
const model = await getModel(user, modelId, deps);
|
|
3339
|
+
if (!model) return null;
|
|
3340
|
+
return isModelOwner(model, user) ? model : null;
|
|
3341
|
+
}
|
|
3208
3342
|
//#endregion
|
|
3209
|
-
//#region ../../b4m-core/services/dist/toolGenerators-
|
|
3343
|
+
//#region ../../b4m-core/services/dist/toolGenerators-KiYiCRXT.mjs
|
|
3210
3344
|
const diceRoll = async (parameters) => {
|
|
3211
3345
|
if (!parameters?.sides || !parameters?.times) throw new Error("Tool dice roll: Missing required parameters");
|
|
3212
3346
|
return sum(times(parameters.times, () => random(1, parameters.sides))).toString();
|
|
@@ -3936,7 +4070,7 @@ const askUserQuestionTool = {
|
|
|
3936
4070
|
* re-export them without pulling the full tool graph. `index.ts` re-exports them
|
|
3937
4071
|
* so the server barrel's public API is unchanged.
|
|
3938
4072
|
*/
|
|
3939
|
-
const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, sessionLakeScopeExplicit, sessionPreauthorizedLakeIds, questId, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
|
|
4073
|
+
const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, sessionReaderConsentDatalakeTags, sessionLakeScopeExplicit, sessionPreauthorizedLakeIds, organizationId, apiKeyId, questId, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
|
|
3940
4074
|
const context = {
|
|
3941
4075
|
userId,
|
|
3942
4076
|
user,
|
|
@@ -3960,17 +4094,26 @@ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inl
|
|
|
3960
4094
|
fullyInlinedAttachmentIds,
|
|
3961
4095
|
suppressLakeArms,
|
|
3962
4096
|
sessionRetrievalTags,
|
|
4097
|
+
sessionReaderConsentDatalakeTags,
|
|
3963
4098
|
sessionLakeScopeExplicit,
|
|
3964
4099
|
sessionPreauthorizedLakeIds,
|
|
4100
|
+
organizationId,
|
|
4101
|
+
apiKeyId,
|
|
3965
4102
|
codeMinifier,
|
|
3966
4103
|
availableModels,
|
|
3967
4104
|
onToolLlmUsage,
|
|
3968
4105
|
getAbortSignal
|
|
3969
4106
|
};
|
|
3970
|
-
return Object.entries(tools).reduce((acc, [key, tool]) =>
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
4107
|
+
return Object.entries(tools).reduce((acc, [key, tool]) => {
|
|
4108
|
+
const built = tool.implementation(context, config[key]);
|
|
4109
|
+
return {
|
|
4110
|
+
...acc,
|
|
4111
|
+
[key]: tool.artifactType ? {
|
|
4112
|
+
...built,
|
|
4113
|
+
artifactType: tool.artifactType
|
|
4114
|
+
} : built
|
|
4115
|
+
};
|
|
4116
|
+
}, {});
|
|
3974
4117
|
};
|
|
3975
4118
|
/**
|
|
3976
4119
|
* Normalize MCP tool parameters for OpenAI compatibility.
|
|
@@ -5494,34 +5637,28 @@ const latticeAddEntityTool = {
|
|
|
5494
5637
|
updatedAt: /* @__PURE__ */ new Date()
|
|
5495
5638
|
};
|
|
5496
5639
|
if (context.db.latticeModels && modelId && isObjectIdShaped$1(modelId)) try {
|
|
5497
|
-
const model = await
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
id: modelId,
|
|
5504
|
-
data: model.data,
|
|
5505
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
5506
|
-
});
|
|
5507
|
-
context.logger.info(`[Lattice] Added entity ${entityId} to model ${modelId}`);
|
|
5508
|
-
} else if (model) {
|
|
5509
|
-
context.logger.warn(`[Lattice] Access denied: caller does not own model ${modelId}`);
|
|
5510
|
-
return JSON.stringify({
|
|
5511
|
-
success: false,
|
|
5512
|
-
action: "ADD_ENTITY",
|
|
5513
|
-
modelId,
|
|
5514
|
-
error: `Access denied: you do not have permission to modify model ${modelId}`
|
|
5515
|
-
});
|
|
5516
|
-
} else {
|
|
5517
|
-
context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
|
|
5640
|
+
const model = await getModelForWrite({
|
|
5641
|
+
id: context.userId,
|
|
5642
|
+
organizationId: context.user?.organizationId
|
|
5643
|
+
}, modelId, { db: { latticeModels: context.db.latticeModels } });
|
|
5644
|
+
if (!model) {
|
|
5645
|
+
context.logger.warn(`[Lattice] Model ${modelId} not found or access denied for user ${context.userId}`);
|
|
5518
5646
|
return JSON.stringify({
|
|
5519
5647
|
success: false,
|
|
5520
5648
|
action: "ADD_ENTITY",
|
|
5521
5649
|
modelId,
|
|
5522
|
-
error: `Model ${modelId} not found`
|
|
5650
|
+
error: `Model ${modelId} not found or you do not have permission to modify it`
|
|
5523
5651
|
});
|
|
5524
5652
|
}
|
|
5653
|
+
const existingIndex = model.data.entities.findIndex((e) => e.id === entityId);
|
|
5654
|
+
if (existingIndex >= 0) model.data.entities[existingIndex] = entityData;
|
|
5655
|
+
else model.data.entities.push(entityData);
|
|
5656
|
+
await context.db.latticeModels.update({
|
|
5657
|
+
id: modelId,
|
|
5658
|
+
data: model.data,
|
|
5659
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
5660
|
+
});
|
|
5661
|
+
context.logger.info(`[Lattice] Added entity ${entityId} to model ${modelId}`);
|
|
5525
5662
|
} catch (error) {
|
|
5526
5663
|
context.logger.error(`[Lattice] Failed to persist entity to database:`, error);
|
|
5527
5664
|
return JSON.stringify({
|
|
@@ -5644,51 +5781,45 @@ const latticeSetValueTool = {
|
|
|
5644
5781
|
else if (rawValue.toLowerCase() === "false") value = false;
|
|
5645
5782
|
const entityId = toEntityId(entityName);
|
|
5646
5783
|
if (context.db.latticeModels && modelId && isObjectIdShaped$1(modelId)) try {
|
|
5647
|
-
const model = await
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
success: false,
|
|
5654
|
-
action: "SET_VALUE",
|
|
5655
|
-
modelId,
|
|
5656
|
-
error: entityNotFoundError(model.data.entities, entityName, attributeKey)
|
|
5657
|
-
});
|
|
5658
|
-
}
|
|
5659
|
-
const attrIndex = entity.attributes.findIndex((a) => a.key === attributeKey);
|
|
5660
|
-
const attributeData = {
|
|
5661
|
-
key: attributeKey,
|
|
5662
|
-
value,
|
|
5663
|
-
dataType: typeof value === "number" ? "number" : typeof value === "boolean" ? "boolean" : "string",
|
|
5664
|
-
isComputed: false
|
|
5665
|
-
};
|
|
5666
|
-
if (attrIndex >= 0) entity.attributes[attrIndex] = attributeData;
|
|
5667
|
-
else entity.attributes.push(attributeData);
|
|
5668
|
-
entity.updatedAt = /* @__PURE__ */ new Date();
|
|
5669
|
-
await context.db.latticeModels.update({
|
|
5670
|
-
id: modelId,
|
|
5671
|
-
data: model.data,
|
|
5672
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
5673
|
-
});
|
|
5674
|
-
context.logger.info(`[Lattice] Set ${entityId}.${attributeKey} = ${value} in model ${modelId}`);
|
|
5675
|
-
} else if (model) {
|
|
5676
|
-
context.logger.warn(`[Lattice] Access denied: caller does not own model ${modelId}`);
|
|
5784
|
+
const model = await getModelForWrite({
|
|
5785
|
+
id: context.userId,
|
|
5786
|
+
organizationId: context.user?.organizationId
|
|
5787
|
+
}, modelId, { db: { latticeModels: context.db.latticeModels } });
|
|
5788
|
+
if (!model) {
|
|
5789
|
+
context.logger.warn(`[Lattice] Model ${modelId} not found or access denied for user ${context.userId}`);
|
|
5677
5790
|
return JSON.stringify({
|
|
5678
5791
|
success: false,
|
|
5679
5792
|
action: "SET_VALUE",
|
|
5680
5793
|
modelId,
|
|
5681
|
-
error: `
|
|
5794
|
+
error: `Model ${modelId} not found or you do not have permission to modify it`
|
|
5682
5795
|
});
|
|
5683
|
-
}
|
|
5684
|
-
|
|
5796
|
+
}
|
|
5797
|
+
const entity = model.data.entities.find((e) => e.id === entityId || toEntityId(e.name) === entityId);
|
|
5798
|
+
if (!entity) {
|
|
5799
|
+
context.logger.warn(`[Lattice] Entity ${entityName} not found in model ${modelId}`);
|
|
5685
5800
|
return JSON.stringify({
|
|
5686
5801
|
success: false,
|
|
5687
5802
|
action: "SET_VALUE",
|
|
5688
5803
|
modelId,
|
|
5689
|
-
error:
|
|
5804
|
+
error: entityNotFoundError(model.data.entities, entityName, attributeKey)
|
|
5690
5805
|
});
|
|
5691
5806
|
}
|
|
5807
|
+
const attrIndex = entity.attributes.findIndex((a) => a.key === attributeKey);
|
|
5808
|
+
const attributeData = {
|
|
5809
|
+
key: attributeKey,
|
|
5810
|
+
value,
|
|
5811
|
+
dataType: typeof value === "number" ? "number" : typeof value === "boolean" ? "boolean" : "string",
|
|
5812
|
+
isComputed: false
|
|
5813
|
+
};
|
|
5814
|
+
if (attrIndex >= 0) entity.attributes[attrIndex] = attributeData;
|
|
5815
|
+
else entity.attributes.push(attributeData);
|
|
5816
|
+
entity.updatedAt = /* @__PURE__ */ new Date();
|
|
5817
|
+
await context.db.latticeModels.update({
|
|
5818
|
+
id: modelId,
|
|
5819
|
+
data: model.data,
|
|
5820
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
5821
|
+
});
|
|
5822
|
+
context.logger.info(`[Lattice] Set ${entityId}.${attributeKey} = ${value} in model ${modelId}`);
|
|
5692
5823
|
} catch (error) {
|
|
5693
5824
|
context.logger.error(`[Lattice] Failed to persist value to database:`, error);
|
|
5694
5825
|
return JSON.stringify({
|
|
@@ -5795,61 +5926,55 @@ const latticeCreateRuleTool = {
|
|
|
5795
5926
|
const outputEntityId = parsedRule.outputEntity.toLowerCase().replace(/\s+/g, "_");
|
|
5796
5927
|
let entityCreatedMessage = "";
|
|
5797
5928
|
if (context.db.latticeModels && modelId && isObjectIdShaped$1(modelId)) try {
|
|
5798
|
-
const model = await
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
type: "line_item",
|
|
5805
|
-
name: parsedRule.outputEntity,
|
|
5806
|
-
displayName: parsedRule.outputEntity,
|
|
5807
|
-
attributes: [{
|
|
5808
|
-
key: "value",
|
|
5809
|
-
value: 0,
|
|
5810
|
-
dataType: "currency",
|
|
5811
|
-
isComputed: true
|
|
5812
|
-
}, {
|
|
5813
|
-
key: "category",
|
|
5814
|
-
value: "Computed",
|
|
5815
|
-
dataType: "string",
|
|
5816
|
-
isComputed: false
|
|
5817
|
-
}],
|
|
5818
|
-
metadata: { isComputed: true },
|
|
5819
|
-
createdAt: now,
|
|
5820
|
-
updatedAt: now
|
|
5821
|
-
};
|
|
5822
|
-
model.data.entities.push(newEntity);
|
|
5823
|
-
entityCreatedMessage = ` Created output entity "${parsedRule.outputEntity}".`;
|
|
5824
|
-
context.logger.info(`[Lattice] Auto-created output entity ${outputEntityId} for rule ${ruleId}`);
|
|
5825
|
-
}
|
|
5826
|
-
const existingIndex = model.rules.rules.findIndex((r) => r.id === ruleId || r.name === name);
|
|
5827
|
-
if (existingIndex >= 0) model.rules.rules[existingIndex] = ruleData;
|
|
5828
|
-
else model.rules.rules.push(ruleData);
|
|
5829
|
-
await context.db.latticeModels.update({
|
|
5830
|
-
id: modelId,
|
|
5831
|
-
data: model.data,
|
|
5832
|
-
rules: model.rules,
|
|
5833
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
5834
|
-
});
|
|
5835
|
-
context.logger.info(`[Lattice] Created rule ${ruleId} in model ${modelId}`);
|
|
5836
|
-
} else if (model) {
|
|
5837
|
-
context.logger.warn(`[Lattice] Access denied: caller does not own model ${modelId}`);
|
|
5838
|
-
return JSON.stringify({
|
|
5839
|
-
success: false,
|
|
5840
|
-
action: "CREATE_RULE",
|
|
5841
|
-
modelId,
|
|
5842
|
-
error: `Access denied: you do not have permission to modify model ${modelId}`
|
|
5843
|
-
});
|
|
5844
|
-
} else {
|
|
5845
|
-
context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
|
|
5929
|
+
const model = await getModelForWrite({
|
|
5930
|
+
id: context.userId,
|
|
5931
|
+
organizationId: context.user?.organizationId
|
|
5932
|
+
}, modelId, { db: { latticeModels: context.db.latticeModels } });
|
|
5933
|
+
if (!model) {
|
|
5934
|
+
context.logger.warn(`[Lattice] Model ${modelId} not found or access denied for user ${context.userId}`);
|
|
5846
5935
|
return JSON.stringify({
|
|
5847
5936
|
success: false,
|
|
5848
5937
|
action: "CREATE_RULE",
|
|
5849
5938
|
modelId,
|
|
5850
|
-
error: `Model ${modelId} not found`
|
|
5939
|
+
error: `Model ${modelId} not found or you do not have permission to modify it`
|
|
5851
5940
|
});
|
|
5852
5941
|
}
|
|
5942
|
+
if (!model.data.entities.some((e) => e.id === outputEntityId || e.name.toLowerCase() === parsedRule.outputEntity.toLowerCase()) && parsedRule.outputEntity !== "unknown") {
|
|
5943
|
+
const now = /* @__PURE__ */ new Date();
|
|
5944
|
+
const newEntity = {
|
|
5945
|
+
id: outputEntityId,
|
|
5946
|
+
type: "line_item",
|
|
5947
|
+
name: parsedRule.outputEntity,
|
|
5948
|
+
displayName: parsedRule.outputEntity,
|
|
5949
|
+
attributes: [{
|
|
5950
|
+
key: "value",
|
|
5951
|
+
value: 0,
|
|
5952
|
+
dataType: "currency",
|
|
5953
|
+
isComputed: true
|
|
5954
|
+
}, {
|
|
5955
|
+
key: "category",
|
|
5956
|
+
value: "Computed",
|
|
5957
|
+
dataType: "string",
|
|
5958
|
+
isComputed: false
|
|
5959
|
+
}],
|
|
5960
|
+
metadata: { isComputed: true },
|
|
5961
|
+
createdAt: now,
|
|
5962
|
+
updatedAt: now
|
|
5963
|
+
};
|
|
5964
|
+
model.data.entities.push(newEntity);
|
|
5965
|
+
entityCreatedMessage = ` Created output entity "${parsedRule.outputEntity}".`;
|
|
5966
|
+
context.logger.info(`[Lattice] Auto-created output entity ${outputEntityId} for rule ${ruleId}`);
|
|
5967
|
+
}
|
|
5968
|
+
const existingIndex = model.rules.rules.findIndex((r) => r.id === ruleId || r.name === name);
|
|
5969
|
+
if (existingIndex >= 0) model.rules.rules[existingIndex] = ruleData;
|
|
5970
|
+
else model.rules.rules.push(ruleData);
|
|
5971
|
+
await context.db.latticeModels.update({
|
|
5972
|
+
id: modelId,
|
|
5973
|
+
data: model.data,
|
|
5974
|
+
rules: model.rules,
|
|
5975
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
5976
|
+
});
|
|
5977
|
+
context.logger.info(`[Lattice] Created rule ${ruleId} in model ${modelId}`);
|
|
5853
5978
|
} catch (error) {
|
|
5854
5979
|
context.logger.error(`[Lattice] Failed to persist rule to database:`, error);
|
|
5855
5980
|
return JSON.stringify({
|
|
@@ -6041,7 +6166,8 @@ This tool traces the calculation chain from the target value back through all it
|
|
|
6041
6166
|
* This is a simplified implementation - production would use LLM structured output
|
|
6042
6167
|
*/
|
|
6043
6168
|
function parseFormula(formula) {
|
|
6044
|
-
const
|
|
6169
|
+
const normalized = formula.toLowerCase().trim();
|
|
6170
|
+
const equalsMatch = splitEquals(normalized);
|
|
6045
6171
|
if (equalsMatch) {
|
|
6046
6172
|
const [output, expression] = equalsMatch;
|
|
6047
6173
|
if (expression.includes("+") || expression.includes("plus")) return {
|
|
@@ -9681,6 +9807,13 @@ var CliLogger = class extends Logger {
|
|
|
9681
9807
|
debug(...args) {}
|
|
9682
9808
|
log(...args) {}
|
|
9683
9809
|
};
|
|
9810
|
+
const FILE_WRITE_SUCCESS = /^File (edited|created|overwritten) successfully: /;
|
|
9811
|
+
const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set(["edit_local_file", "create_file"]);
|
|
9812
|
+
function notifyFileChanged(agentContext, toolName, args, result) {
|
|
9813
|
+
if (!agentContext.onFileChanged || !FILE_WRITE_TOOLS.has(toolName)) return;
|
|
9814
|
+
if (typeof args.path !== "string" || !FILE_WRITE_SUCCESS.test(result)) return;
|
|
9815
|
+
agentContext.onFileChanged(args.path);
|
|
9816
|
+
}
|
|
9684
9817
|
/**
|
|
9685
9818
|
* Wrap a tool with permission checking, server routing, and observation tracking.
|
|
9686
9819
|
*/
|
|
@@ -9727,8 +9860,8 @@ function wrapToolWithPermission(tool, permissionManager, showPermissionPrompt, a
|
|
|
9727
9860
|
} else if (decision.type === "unsandboxed") sandboxOrchestrator.recordUnsandboxed();
|
|
9728
9861
|
}
|
|
9729
9862
|
const effectiveArgs = isSandboxed ? sandboxedArgs : args;
|
|
9730
|
-
const { gateSnapshot: _modelSuppliedGateSnapshot, ...
|
|
9731
|
-
let execArgs =
|
|
9863
|
+
const { gateSnapshot: _modelSuppliedGateSnapshot, confirmedFuzzyHash: _modelSuppliedFuzzyHash, ...effectiveArgsSansGateFields } = effectiveArgs ?? {};
|
|
9864
|
+
let execArgs = effectiveArgsSansGateFields;
|
|
9732
9865
|
const sandboxCleanupPaths = isSandboxed ? sandboxedArgs?._sandboxCleanup : void 0;
|
|
9733
9866
|
/**
|
|
9734
9867
|
* Shared execution flow: run tool, capture violations, offer retry on
|
|
@@ -9750,6 +9883,7 @@ function wrapToolWithPermission(tool, permissionManager, showPermissionPrompt, a
|
|
|
9750
9883
|
toolName,
|
|
9751
9884
|
result
|
|
9752
9885
|
});
|
|
9886
|
+
notifyFileChanged(agentContext, toolName, execArgs, result);
|
|
9753
9887
|
getProcessHooks()?.firePostToolUse(toolName);
|
|
9754
9888
|
return result;
|
|
9755
9889
|
}
|
|
@@ -10767,6 +10901,25 @@ function appendWorkflowReminder(messages, provider) {
|
|
|
10767
10901
|
content: `${WORKFLOW_REMINDER_MARKER}\n${body}`
|
|
10768
10902
|
});
|
|
10769
10903
|
}
|
|
10904
|
+
const TOOL_RESULTS_NUDGE = "Based on the tool results above, please provide a complete answer. If I asked for multiple things, make sure to address all of them.";
|
|
10905
|
+
/**
|
|
10906
|
+
* The end-of-iteration nudge, with any drained host feedback folded in. Folding
|
|
10907
|
+
* (rather than pushing a second plain-string user message) keeps exactly one
|
|
10908
|
+
* iteration boundary for findIterationBoundary's trimming heuristic.
|
|
10909
|
+
*/
|
|
10910
|
+
function buildToolResultsNudge(feedback) {
|
|
10911
|
+
return feedback ? `${TOOL_RESULTS_NUDGE}\n\n${feedback}` : TOOL_RESULTS_NUDGE;
|
|
10912
|
+
}
|
|
10913
|
+
/** Calls the host's feedback drain. A failing drain is logged and treated as empty - it must never fail the run. */
|
|
10914
|
+
async function drainHostFeedback(drain, phase, logger) {
|
|
10915
|
+
if (!drain) return null;
|
|
10916
|
+
try {
|
|
10917
|
+
return (await drain(phase))?.trim() || null;
|
|
10918
|
+
} catch (error) {
|
|
10919
|
+
logger.warn(`[ReActAgent] drainFeedback(${phase}) failed; continuing without feedback:`, error);
|
|
10920
|
+
return null;
|
|
10921
|
+
}
|
|
10922
|
+
}
|
|
10770
10923
|
/**
|
|
10771
10924
|
* Build the per-iteration prompt-cache strategy. History caching anchors on the
|
|
10772
10925
|
* last message so the cached prefix grows each turn (standard Anthropic
|
|
@@ -11146,7 +11299,9 @@ var ReActAgent = class extends EventEmitter {
|
|
|
11146
11299
|
}
|
|
11147
11300
|
throw completionError;
|
|
11148
11301
|
}
|
|
11149
|
-
|
|
11302
|
+
const finalFeedback = !hadToolCalls && currentText.trim() && iterations < maxIterations ? await drainHostFeedback(options.drainFeedback, "final", this.context.logger) : null;
|
|
11303
|
+
if (finalFeedback) this.reopenAnswerWithFeedback(messages, currentText.trim(), finalFeedback);
|
|
11304
|
+
else if (!hadToolCalls && currentText.trim()) {
|
|
11150
11305
|
finalAnswer = currentText.trim();
|
|
11151
11306
|
const iterInputTokens = this.totalInputTokens - preCallTotalInputTokens;
|
|
11152
11307
|
const iterOutputTokens = this.totalOutputTokens - preCallTotalOutputTokens;
|
|
@@ -11194,10 +11349,13 @@ var ReActAgent = class extends EventEmitter {
|
|
|
11194
11349
|
});
|
|
11195
11350
|
}
|
|
11196
11351
|
this.iterationConfidences = [];
|
|
11197
|
-
if (!iterationComplete && hadToolCalls)
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11352
|
+
if (!iterationComplete && hadToolCalls) {
|
|
11353
|
+
const feedback = await drainHostFeedback(options.drainFeedback, "turn", this.context.logger);
|
|
11354
|
+
messages.push({
|
|
11355
|
+
role: "user",
|
|
11356
|
+
content: buildToolResultsNudge(feedback)
|
|
11357
|
+
});
|
|
11358
|
+
}
|
|
11201
11359
|
if (maxTotalTokens !== void 0 && this.totalTokens >= maxTotalTokens && !iterationComplete) {
|
|
11202
11360
|
reachedMaxTotalTokens = true;
|
|
11203
11361
|
finalAnswer = currentText.trim() || `I stopped after reaching the cumulative token ceiling (${this.totalTokens}/${maxTotalTokens}) without arriving at a final answer.`;
|
|
@@ -11311,6 +11469,29 @@ BEHAVIOR GUIDELINES:
|
|
|
11311
11469
|
Remember: You are an autonomous AGENT. Act independently and solve problems proactively.`;
|
|
11312
11470
|
}
|
|
11313
11471
|
/**
|
|
11472
|
+
* Demote a would-be final answer to a thought and hand the model drained host
|
|
11473
|
+
* feedback (see AgentRunOptions.drainFeedback) as the next user message, so
|
|
11474
|
+
* the loop runs another iteration instead of ending. The user message is a
|
|
11475
|
+
* plain string and so is a normal iteration boundary for trimming.
|
|
11476
|
+
*/
|
|
11477
|
+
reopenAnswerWithFeedback(messages, answer, feedback) {
|
|
11478
|
+
const thoughtStep = {
|
|
11479
|
+
type: "thought",
|
|
11480
|
+
content: answer,
|
|
11481
|
+
metadata: { timestamp: Date.now() }
|
|
11482
|
+
};
|
|
11483
|
+
this.steps.push(thoughtStep);
|
|
11484
|
+
this.emit("thought", thoughtStep);
|
|
11485
|
+
messages.push({
|
|
11486
|
+
role: "assistant",
|
|
11487
|
+
content: answer
|
|
11488
|
+
}, {
|
|
11489
|
+
role: "user",
|
|
11490
|
+
content: feedback
|
|
11491
|
+
});
|
|
11492
|
+
return thoughtStep;
|
|
11493
|
+
}
|
|
11494
|
+
/**
|
|
11314
11495
|
* Add an observation step (used by tool wrappers to record tool results).
|
|
11315
11496
|
* Kept for backwards compatibility but no longer used. Observations are now
|
|
11316
11497
|
* queued and drained in the completion callback for correct ordering.
|
|
@@ -11689,7 +11870,9 @@ Remember: You are an autonomous AGENT. Act independently and solve problems proa
|
|
|
11689
11870
|
}
|
|
11690
11871
|
}
|
|
11691
11872
|
});
|
|
11692
|
-
|
|
11873
|
+
const finalFeedback = !hadToolCalls && currentText.trim() && this.iterations < maxIterations ? await drainHostFeedback(options.drainFeedback, "final", this.context.logger) : null;
|
|
11874
|
+
if (finalFeedback) iterationSteps.push(this.reopenAnswerWithFeedback(this.messages, currentText.trim(), finalFeedback));
|
|
11875
|
+
else if (!hadToolCalls && currentText.trim()) {
|
|
11693
11876
|
finalAnswer = currentText.trim();
|
|
11694
11877
|
const iterInputTokens = this.totalInputTokens - iterStartInputTokens;
|
|
11695
11878
|
const iterOutputTokens = this.totalOutputTokens - iterStartOutputTokens;
|
|
@@ -11746,10 +11929,13 @@ Remember: You are an autonomous AGENT. Act independently and solve problems proa
|
|
|
11746
11929
|
});
|
|
11747
11930
|
}
|
|
11748
11931
|
this.iterationConfidences = [];
|
|
11749
|
-
if (!iterationComplete && hadToolCalls)
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11932
|
+
if (!iterationComplete && hadToolCalls) {
|
|
11933
|
+
const feedback = await drainHostFeedback(options.drainFeedback, "turn", this.context.logger);
|
|
11934
|
+
this.messages.push({
|
|
11935
|
+
role: "user",
|
|
11936
|
+
content: buildToolResultsNudge(feedback)
|
|
11937
|
+
});
|
|
11938
|
+
}
|
|
11753
11939
|
if (!iterationComplete && maxTotalTokens !== void 0 && this.totalTokens >= maxTotalTokens) {
|
|
11754
11940
|
finalAnswer = currentText.trim() || `I stopped after reaching the cumulative token ceiling (${this.totalTokens}/${maxTotalTokens}) without arriving at a final answer.`;
|
|
11755
11941
|
const ceilingStep = {
|
|
@@ -16069,10 +16255,10 @@ var ServerLlmBackend = class {
|
|
|
16069
16255
|
} else if (typeof stream === "string") responseText = stream;
|
|
16070
16256
|
logger.debug(`[ServerLlmBackend] Response preview: ${responseText.substring(0, 200)}`);
|
|
16071
16257
|
if (responseText.includes("<!DOCTYPE") || responseText.includes("<html")) {
|
|
16072
|
-
const
|
|
16073
|
-
const
|
|
16074
|
-
if (
|
|
16075
|
-
else if (
|
|
16258
|
+
const title = htmlErrorTitle(responseText);
|
|
16259
|
+
const h1 = htmlFirstH1(responseText);
|
|
16260
|
+
if (title !== null) errorDetails = title;
|
|
16261
|
+
else if (h1 !== null) errorDetails = h1.trim();
|
|
16076
16262
|
} else if (responseText) errorDetails = responseText.substring(0, 100).trim();
|
|
16077
16263
|
} catch (extractError) {
|
|
16078
16264
|
logger.error("[ServerLlmBackend] Error extracting response:", extractError);
|