@ganju/utils 0.0.9 → 0.0.11
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/channelDebounce.d.ts +11 -0
- package/dist/channelDebounce.d.ts.map +1 -1
- package/dist/channelDebounce.js +12 -1
- package/dist/chunking.d.ts +17 -3
- package/dist/chunking.d.ts.map +1 -1
- package/dist/chunking.js +125 -19
- package/dist/constants.d.ts +30 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +136 -5
- package/dist/index.d.ts +207 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/resourceRerank.d.ts +20 -0
- package/dist/resourceRerank.d.ts.map +1 -0
- package/dist/resourceRerank.js +37 -0
- package/dist/schema.d.ts +106 -24
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +45 -4
- package/dist/syncableResource.d.ts +14 -0
- package/dist/syncableResource.d.ts.map +1 -0
- package/dist/syncableResource.js +43 -0
- package/dist/toolCatalog.d.ts +70 -1
- package/dist/toolCatalog.d.ts.map +1 -1
- package/dist/toolCatalog.js +151 -63
- package/dist/toolConfirmation.d.ts +47 -0
- package/dist/toolConfirmation.d.ts.map +1 -0
- package/dist/toolConfirmation.js +217 -0
- package/package.json +1 -1
- package/src/channelDebounce.ts +36 -0
- package/src/chunking.ts +166 -23
- package/src/constants.ts +149 -5
- package/src/index.ts +51 -1
- package/src/resourceRerank.ts +54 -0
- package/src/schema.ts +58 -6
- package/src/syncableResource.ts +68 -0
- package/src/toolCatalog.ts +170 -63
- package/src/toolConfirmation.ts +261 -0
package/src/constants.ts
CHANGED
|
@@ -278,6 +278,43 @@ const RESOURCE_SOURCE_TYPES = [
|
|
|
278
278
|
RESOURCE_SOURCE_TYPE_CUSTOM_CODE
|
|
279
279
|
];
|
|
280
280
|
|
|
281
|
+
// How often a source that lives somewhere else — a crawled website, a Drive or
|
|
282
|
+
// OneDrive folder, a file imported from either — is refreshed from it. Every
|
|
283
|
+
// source is stored with an interval, weekly unless its owner changes it; the
|
|
284
|
+
// hourly sync job only acts on it for a plan that includes automatic sync, so a
|
|
285
|
+
// Free org's sources start syncing the moment it upgrades.
|
|
286
|
+
const RESOURCE_SYNC_INTERVAL_OFF = 'off' as 'off';
|
|
287
|
+
const RESOURCE_SYNC_INTERVAL_DAILY = 'daily' as 'daily';
|
|
288
|
+
const RESOURCE_SYNC_INTERVAL_WEEKLY = 'weekly' as 'weekly';
|
|
289
|
+
const RESOURCE_SYNC_INTERVALS = [
|
|
290
|
+
RESOURCE_SYNC_INTERVAL_OFF,
|
|
291
|
+
RESOURCE_SYNC_INTERVAL_DAILY,
|
|
292
|
+
RESOURCE_SYNC_INTERVAL_WEEKLY
|
|
293
|
+
] as const;
|
|
294
|
+
const RESOURCE_SYNC_INTERVAL_DEFAULT = RESOURCE_SYNC_INTERVAL_WEEKLY;
|
|
295
|
+
const RESOURCE_SYNC_INTERVAL_MS = {
|
|
296
|
+
[RESOURCE_SYNC_INTERVAL_DAILY]: 24 * 60 * 60 * 1000,
|
|
297
|
+
[RESOURCE_SYNC_INTERVAL_WEEKLY]: 7 * 24 * 60 * 60 * 1000
|
|
298
|
+
} as const;
|
|
299
|
+
// Sources started per hourly run. A sync is cheap to start and expensive to
|
|
300
|
+
// run — a website re-crawls every page — so the job works through a backlog
|
|
301
|
+
// over several hours rather than launching all of it at once.
|
|
302
|
+
const RESOURCE_SYNC_BATCH_SIZE = 50;
|
|
303
|
+
// How soon a source can be synced by hand again. Manual sync is open to every
|
|
304
|
+
// plan, and a website sync re-crawls every page, so it can't be a button to
|
|
305
|
+
// press in a loop.
|
|
306
|
+
const RESOURCE_SYNC_MANUAL_COOLDOWN_MS = 5 * 60 * 1000;
|
|
307
|
+
// How long a sync may stay PENDING before it counts as stuck rather than
|
|
308
|
+
// running. A lost queue message or a job that exhausted its retries never
|
|
309
|
+
// completes its root, and a root that stays PENDING would otherwise be skipped
|
|
310
|
+
// by the hourly job and refused by "Sync now" forever. Twelve hours outlasts
|
|
311
|
+
// any realistic wait: the crawl-page queue runs 5 at a time for every org, and
|
|
312
|
+
// one hourly run can queue 50 sites — thousands of pages, hours of backlog. A
|
|
313
|
+
// shorter window restarts syncs that are merely queued, re-fetching every page
|
|
314
|
+
// into the same backlog; a longer one leaves a stuck daily source missing more
|
|
315
|
+
// than one sync.
|
|
316
|
+
const RESOURCE_SYNC_STALE_MS = 12 * 60 * 60 * 1000;
|
|
317
|
+
|
|
281
318
|
const CRAWL_RENDERER_CHEERIO = 'cheerio' as 'cheerio';
|
|
282
319
|
const CRAWL_RENDERER_PLAYWRIGHT = 'playwright' as 'playwright';
|
|
283
320
|
const CRAWL_RENDERERS = [CRAWL_RENDERER_CHEERIO, CRAWL_RENDERER_PLAYWRIGHT];
|
|
@@ -731,6 +768,49 @@ const CHANNEL_DEBOUNCE_JOIN = '\n';
|
|
|
731
768
|
const CHANNEL_DEBOUNCE_RETRY_MS = 5000;
|
|
732
769
|
const CHANNEL_DEBOUNCE_MAX_ATTEMPTS = 3;
|
|
733
770
|
|
|
771
|
+
// Confirming sensitive actions, for organizations that turn it on. A channel
|
|
772
|
+
// bot that wants to run a tool annotated `destructiveHint` asks first, and the
|
|
773
|
+
// call runs only if the participant's next message is a plain yes.
|
|
774
|
+
//
|
|
775
|
+
// How long the question stays open. An answer after that is read as a new
|
|
776
|
+
// message, never as consent to something asked long ago.
|
|
777
|
+
const TOOL_CONFIRMATION_TTL_MS = 30 * 60 * 1000;
|
|
778
|
+
// The tool result the model gets instead of running the call. It is what makes
|
|
779
|
+
// the model write the question — in the participant's language, naming what
|
|
780
|
+
// the call would do — rather than claiming the action happened.
|
|
781
|
+
// The call's exact arguments are listed under the model's question by the
|
|
782
|
+
// runner, so the model only has to say what it is about to do.
|
|
783
|
+
const TOOL_CONFIRMATION_REQUIRED_RESULT =
|
|
784
|
+
'Not run yet: this action needs the user to confirm it first. In one or ' +
|
|
785
|
+
'two sentences, tell them what it will do (who it goes to, what will be ' +
|
|
786
|
+
'changed or deleted) and ask them to answer yes to go ahead or no to ' +
|
|
787
|
+
'cancel — in their own language, never quoting English words at someone ' +
|
|
788
|
+
'writing in another one. The exact details are listed for them under your ' +
|
|
789
|
+
'message. It runs only if their next message is yes. Do not say it was done.';
|
|
790
|
+
// Told to the model on every turn of an organization that confirms: the
|
|
791
|
+
// asking is done for it, so it calls the tool when asked instead of asking
|
|
792
|
+
// first itself — and calls every action a request needs in one step, since
|
|
793
|
+
// the turn stops at the first held call and each later one would be a
|
|
794
|
+
// question of its own.
|
|
795
|
+
const TOOL_CONFIRMATION_SYSTEM_NOTE =
|
|
796
|
+
'Actions that send, change or delete something are confirmed with the ' +
|
|
797
|
+
'user by the system before they run. Ask for any detail you are missing, ' +
|
|
798
|
+
'but once you have what the action needs, call the tool — do not ask the ' +
|
|
799
|
+
'user to confirm it yourself; the system asks them and runs it on their ' +
|
|
800
|
+
'yes. When a request needs several such actions (say, delete an event ' +
|
|
801
|
+
'and create its replacement), call them all in the same step, so the ' +
|
|
802
|
+
'user confirms them together once.';
|
|
803
|
+
// The system-prompt addition for the step that writes the question, with the
|
|
804
|
+
// held actions' names in place of {{actions}}.
|
|
805
|
+
const TOOL_CONFIRMATION_ASK_INSTRUCTION =
|
|
806
|
+
'IMPORTANT: these actions have NOT been done: {{actions}}. They are ' +
|
|
807
|
+
'waiting for the user to confirm them. Your reply must ask the user to ' +
|
|
808
|
+
'confirm, and must not say or suggest that they were done, nor describe ' +
|
|
809
|
+
'or make up any result from them.';
|
|
810
|
+
// The question when the model wrote none, above the runner's own summary.
|
|
811
|
+
const TOOL_CONFIRMATION_FALLBACK_QUESTION =
|
|
812
|
+
'Before I do this, please confirm. Reply yes to go ahead, or no to cancel.';
|
|
813
|
+
|
|
734
814
|
const TELEGRAM_SECRET_HEADER = 'x-telegram-bot-api-secret-token';
|
|
735
815
|
const TELEGRAM_API_BASE = 'https://api.telegram.org';
|
|
736
816
|
const TELEGRAM_MESSAGE_LIMIT = 3500;
|
|
@@ -790,8 +870,12 @@ const DISCORD_TIMESTAMP_HEADER = 'x-signature-timestamp';
|
|
|
790
870
|
// Interaction request types and response types (Discord API).
|
|
791
871
|
const DISCORD_INTERACTION_TYPE_PING = 1;
|
|
792
872
|
const DISCORD_INTERACTION_TYPE_APPLICATION_COMMAND = 2;
|
|
873
|
+
// A tap on a message button — the Yes/No under a confirmation question.
|
|
874
|
+
const DISCORD_INTERACTION_TYPE_MESSAGE_COMPONENT = 3;
|
|
793
875
|
const DISCORD_INTERACTION_RESPONSE_PONG = 1;
|
|
794
876
|
const DISCORD_INTERACTION_RESPONSE_DEFERRED = 5;
|
|
877
|
+
// Edits the message the tapped button is on, in the same response.
|
|
878
|
+
const DISCORD_INTERACTION_RESPONSE_UPDATE_MESSAGE = 7;
|
|
795
879
|
// Channel types we map to conversation scopes: DM (1) / group DM (3) = private;
|
|
796
880
|
// everything else (guild text channels, threads) = channel.
|
|
797
881
|
const DISCORD_CHANNEL_TYPE_DM = 1;
|
|
@@ -820,6 +904,8 @@ const WHATSAPP_API_BASE = 'https://graph.facebook.com';
|
|
|
820
904
|
const WHATSAPP_API_VERSION = 'v25.0';
|
|
821
905
|
// A WhatsApp text message body caps at 4096 chars; we chunk a bit under that.
|
|
822
906
|
const WHATSAPP_MESSAGE_LIMIT = 4000;
|
|
907
|
+
// The body of an interactive (reply-button) message holds at most this much.
|
|
908
|
+
const WHATSAPP_INTERACTIVE_BODY_LIMIT = 1024;
|
|
823
909
|
const WHATSAPP_SIGNATURE_HEADER = 'x-hub-signature-256';
|
|
824
910
|
const WHATSAPP_SIGNATURE_PREFIX = 'sha256=';
|
|
825
911
|
// GET-handshake query params + the expected hub.mode value.
|
|
@@ -850,6 +936,25 @@ const CHUNK_TARGET_CHARS = 2000;
|
|
|
850
936
|
const CHUNK_OVERLAP_CHARS = 200;
|
|
851
937
|
const EMBED_BATCH_SIZE = 96;
|
|
852
938
|
|
|
939
|
+
// Hybrid resource search. Each ranker contributes its top candidates, and the
|
|
940
|
+
// two lists are merged with reciprocal rank fusion: score = sum of 1/(k + rank).
|
|
941
|
+
// Fusion reads ranks, never raw scores, so a cosine distance and a ts_rank never
|
|
942
|
+
// have to be put on one scale. k = 60 is the constant from the original RRF
|
|
943
|
+
// paper; it keeps one list's #1 from outweighing a chunk both lists agree on.
|
|
944
|
+
const RESOURCE_SEARCH_CANDIDATES = 50;
|
|
945
|
+
const RESOURCE_SEARCH_RRF_K = 60;
|
|
946
|
+
// A query word found in more than 1 of every 10 chunks of the artifact is
|
|
947
|
+
// dropped from the lexical query — never below 5 chunks, so a small artifact
|
|
948
|
+
// keeps its words.
|
|
949
|
+
const RESOURCE_SEARCH_COMMON_TERM_DIVISOR = 10;
|
|
950
|
+
const RESOURCE_SEARCH_COMMON_TERM_MIN = 5;
|
|
951
|
+
// Second stage, on wherever the Worker has an AI binding: the top fused
|
|
952
|
+
// candidates are re-scored by a cross-encoder, which reads query and chunk
|
|
953
|
+
// together instead of comparing two independently computed vectors. One model
|
|
954
|
+
// call per search; a failure falls back to the fused order.
|
|
955
|
+
const RESOURCE_SEARCH_RERANK_CANDIDATES = 30;
|
|
956
|
+
const RESOURCE_SEARCH_RERANK_MODEL = '@cf/baai/bge-reranker-base';
|
|
957
|
+
|
|
853
958
|
const RESOURCE_HANDLER_SLEEP_AFTER = '10m';
|
|
854
959
|
|
|
855
960
|
const DOCS_URL = 'https://docs.ganju.ai';
|
|
@@ -1207,8 +1312,7 @@ const MCP_PROXY_TOOL_NAME_MAX = 64;
|
|
|
1207
1312
|
//
|
|
1208
1313
|
// The row's config holds only `activeVersionId` plus the egress/limit settings;
|
|
1209
1314
|
// names and schemas live on the version. That's what keeps MCP boot off the
|
|
1210
|
-
// dispatcher — a slow or broken script must never break tools/list.
|
|
1211
|
-
// docs/CUSTOM_TOOLS.md ("Boot contract").
|
|
1315
|
+
// dispatcher — a slow or broken script must never break tools/list.
|
|
1212
1316
|
const TOOL_DEFINITION_KEY_CUSTOM_CODE = 'custom-code';
|
|
1213
1317
|
|
|
1214
1318
|
// A version is uploaded as `draft`, becomes `published` when it is made active,
|
|
@@ -1996,6 +2100,12 @@ interface PlanLimits {
|
|
|
1996
2100
|
// against). Not used for blocking.
|
|
1997
2101
|
includedMessages: number;
|
|
1998
2102
|
includedEmbeddedBytes: number;
|
|
2103
|
+
// The intervals the hourly job will sync a source on. Empty = no automatic
|
|
2104
|
+
// sync: the org can still sync a source by hand. Refreshing costs crawl or
|
|
2105
|
+
// Drive fetches plus embeddings for whatever changed, all on our side.
|
|
2106
|
+
autoSyncIntervals: ReadonlyArray<
|
|
2107
|
+
typeof RESOURCE_SYNC_INTERVAL_DAILY | typeof RESOURCE_SYNC_INTERVAL_WEEKLY
|
|
2108
|
+
>;
|
|
1999
2109
|
}
|
|
2000
2110
|
|
|
2001
2111
|
const PLAN_LIMITS: Record<
|
|
@@ -2035,7 +2145,8 @@ const PLAN_LIMITS: Record<
|
|
|
2035
2145
|
includedToolCalls: 0,
|
|
2036
2146
|
toolCallHardCap: 10_000,
|
|
2037
2147
|
includedMessages: 100,
|
|
2038
|
-
includedEmbeddedBytes: 5 * MB
|
|
2148
|
+
includedEmbeddedBytes: 5 * MB,
|
|
2149
|
+
autoSyncIntervals: []
|
|
2039
2150
|
},
|
|
2040
2151
|
PRO: {
|
|
2041
2152
|
maxProjects: null,
|
|
@@ -2058,7 +2169,11 @@ const PLAN_LIMITS: Record<
|
|
|
2058
2169
|
includedToolCalls: PRICING_INCLUDED_TOOL_CALLS,
|
|
2059
2170
|
toolCallHardCap: PRICING_TOOL_CALL_HARD_CAP,
|
|
2060
2171
|
includedMessages: PRICING_INCLUDED_MESSAGES,
|
|
2061
|
-
includedEmbeddedBytes: PRICING_INCLUDED_EMBEDDED_GB * GB
|
|
2172
|
+
includedEmbeddedBytes: PRICING_INCLUDED_EMBEDDED_GB * GB,
|
|
2173
|
+
autoSyncIntervals: [
|
|
2174
|
+
RESOURCE_SYNC_INTERVAL_DAILY,
|
|
2175
|
+
RESOURCE_SYNC_INTERVAL_WEEKLY
|
|
2176
|
+
]
|
|
2062
2177
|
},
|
|
2063
2178
|
ENTERPRISE: {
|
|
2064
2179
|
maxProjects: null,
|
|
@@ -2081,7 +2196,11 @@ const PLAN_LIMITS: Record<
|
|
|
2081
2196
|
// contract; don't make one plan's abuse limit another plan's product limit.
|
|
2082
2197
|
toolCallHardCap: null,
|
|
2083
2198
|
includedMessages: PRICING_INCLUDED_MESSAGES,
|
|
2084
|
-
includedEmbeddedBytes: PRICING_INCLUDED_EMBEDDED_GB * GB
|
|
2199
|
+
includedEmbeddedBytes: PRICING_INCLUDED_EMBEDDED_GB * GB,
|
|
2200
|
+
autoSyncIntervals: [
|
|
2201
|
+
RESOURCE_SYNC_INTERVAL_DAILY,
|
|
2202
|
+
RESOURCE_SYNC_INTERVAL_WEEKLY
|
|
2203
|
+
]
|
|
2085
2204
|
}
|
|
2086
2205
|
};
|
|
2087
2206
|
|
|
@@ -2100,6 +2219,7 @@ const PLAN_FEATURE_RAW_STORAGE = 'rawStorage' as 'rawStorage';
|
|
|
2100
2219
|
const PLAN_FEATURE_EMBEDDED_STORAGE = 'embeddedStorage' as 'embeddedStorage';
|
|
2101
2220
|
const PLAN_FEATURE_MESSAGE = 'message' as 'message';
|
|
2102
2221
|
const PLAN_FEATURE_TOOL_CALL = 'toolCall' as 'toolCall';
|
|
2222
|
+
const PLAN_FEATURE_RESOURCE_SYNC = 'resourceSync' as 'resourceSync';
|
|
2103
2223
|
|
|
2104
2224
|
// Stable code returned on a quota block so clients can branch on it (402).
|
|
2105
2225
|
const PLAN_LIMIT_ERROR_CODE = 'PLAN_LIMIT_EXCEEDED';
|
|
@@ -2282,6 +2402,7 @@ export const constants = {
|
|
|
2282
2402
|
PLAN_FEATURE_EMBEDDED_STORAGE,
|
|
2283
2403
|
PLAN_FEATURE_MESSAGE,
|
|
2284
2404
|
PLAN_FEATURE_TOOL_CALL,
|
|
2405
|
+
PLAN_FEATURE_RESOURCE_SYNC,
|
|
2285
2406
|
PLAN_LIMIT_ERROR_CODE,
|
|
2286
2407
|
BILLING_METER_MESSAGES,
|
|
2287
2408
|
BILLING_METER_SHARED_MESSAGES,
|
|
@@ -2476,6 +2597,15 @@ export const constants = {
|
|
|
2476
2597
|
RESOURCE_SOURCE_TYPE_ONE_DRIVE_FOLDER,
|
|
2477
2598
|
RESOURCE_SOURCE_TYPE_CUSTOM_CODE,
|
|
2478
2599
|
RESOURCE_SOURCE_TYPES,
|
|
2600
|
+
RESOURCE_SYNC_INTERVAL_OFF,
|
|
2601
|
+
RESOURCE_SYNC_INTERVAL_DAILY,
|
|
2602
|
+
RESOURCE_SYNC_INTERVAL_WEEKLY,
|
|
2603
|
+
RESOURCE_SYNC_INTERVALS,
|
|
2604
|
+
RESOURCE_SYNC_INTERVAL_DEFAULT,
|
|
2605
|
+
RESOURCE_SYNC_INTERVAL_MS,
|
|
2606
|
+
RESOURCE_SYNC_BATCH_SIZE,
|
|
2607
|
+
RESOURCE_SYNC_MANUAL_COOLDOWN_MS,
|
|
2608
|
+
RESOURCE_SYNC_STALE_MS,
|
|
2479
2609
|
CRAWL_RENDERER_CHEERIO,
|
|
2480
2610
|
CRAWL_RENDERER_PLAYWRIGHT,
|
|
2481
2611
|
CRAWL_RENDERERS,
|
|
@@ -2534,6 +2664,11 @@ export const constants = {
|
|
|
2534
2664
|
SHARED_KEY_HISTORY_LIMIT,
|
|
2535
2665
|
SHARED_KEY_MAX_TOOL_LOOPS,
|
|
2536
2666
|
CHANNEL_MAX_TOOLS,
|
|
2667
|
+
TOOL_CONFIRMATION_TTL_MS,
|
|
2668
|
+
TOOL_CONFIRMATION_REQUIRED_RESULT,
|
|
2669
|
+
TOOL_CONFIRMATION_FALLBACK_QUESTION,
|
|
2670
|
+
TOOL_CONFIRMATION_ASK_INSTRUCTION,
|
|
2671
|
+
TOOL_CONFIRMATION_SYSTEM_NOTE,
|
|
2537
2672
|
CHANNEL_DEBOUNCE_DEFAULT_MS,
|
|
2538
2673
|
CHANNEL_DEBOUNCE_MIN_MS,
|
|
2539
2674
|
CHANNEL_DEBOUNCE_MAX_MS,
|
|
@@ -2566,8 +2701,10 @@ export const constants = {
|
|
|
2566
2701
|
DISCORD_TIMESTAMP_HEADER,
|
|
2567
2702
|
DISCORD_INTERACTION_TYPE_PING,
|
|
2568
2703
|
DISCORD_INTERACTION_TYPE_APPLICATION_COMMAND,
|
|
2704
|
+
DISCORD_INTERACTION_TYPE_MESSAGE_COMPONENT,
|
|
2569
2705
|
DISCORD_INTERACTION_RESPONSE_PONG,
|
|
2570
2706
|
DISCORD_INTERACTION_RESPONSE_DEFERRED,
|
|
2707
|
+
DISCORD_INTERACTION_RESPONSE_UPDATE_MESSAGE,
|
|
2571
2708
|
DISCORD_CHANNEL_TYPE_DM,
|
|
2572
2709
|
DISCORD_CHANNEL_TYPE_GROUP_DM,
|
|
2573
2710
|
DISCORD_GATEWAY_OP_DISPATCH,
|
|
@@ -2582,6 +2719,7 @@ export const constants = {
|
|
|
2582
2719
|
WHATSAPP_API_BASE,
|
|
2583
2720
|
WHATSAPP_API_VERSION,
|
|
2584
2721
|
WHATSAPP_MESSAGE_LIMIT,
|
|
2722
|
+
WHATSAPP_INTERACTIVE_BODY_LIMIT,
|
|
2585
2723
|
WHATSAPP_SIGNATURE_HEADER,
|
|
2586
2724
|
WHATSAPP_SIGNATURE_PREFIX,
|
|
2587
2725
|
WHATSAPP_HUB_MODE_PARAM,
|
|
@@ -2597,6 +2735,12 @@ export const constants = {
|
|
|
2597
2735
|
CHUNK_TARGET_CHARS,
|
|
2598
2736
|
CHUNK_OVERLAP_CHARS,
|
|
2599
2737
|
EMBED_BATCH_SIZE,
|
|
2738
|
+
RESOURCE_SEARCH_CANDIDATES,
|
|
2739
|
+
RESOURCE_SEARCH_RRF_K,
|
|
2740
|
+
RESOURCE_SEARCH_COMMON_TERM_DIVISOR,
|
|
2741
|
+
RESOURCE_SEARCH_COMMON_TERM_MIN,
|
|
2742
|
+
RESOURCE_SEARCH_RERANK_CANDIDATES,
|
|
2743
|
+
RESOURCE_SEARCH_RERANK_MODEL,
|
|
2600
2744
|
RESOURCE_HANDLER_SLEEP_AFTER,
|
|
2601
2745
|
DOCS_URL,
|
|
2602
2746
|
BASE64_DATA_URI_RE,
|
package/src/index.ts
CHANGED
|
@@ -117,6 +117,15 @@ import type { OAuthProviderConfig } from './oauthProviders';
|
|
|
117
117
|
import { resolveAttachment } from './attachment';
|
|
118
118
|
import { isExposedResource } from './exposedResource';
|
|
119
119
|
import type { ExposableResource } from './exposedResource';
|
|
120
|
+
import {
|
|
121
|
+
isSyncableResource,
|
|
122
|
+
isResourceSyncInProgress,
|
|
123
|
+
resourceSyncProvider
|
|
124
|
+
} from './syncableResource';
|
|
125
|
+
import type {
|
|
126
|
+
SyncableResource,
|
|
127
|
+
ResourceSyncProvider
|
|
128
|
+
} from './syncableResource';
|
|
120
129
|
import { describeVendorError } from './vendorError';
|
|
121
130
|
import { withDeadline, isDeadlineError } from './deadline';
|
|
122
131
|
import {
|
|
@@ -164,6 +173,7 @@ import type {
|
|
|
164
173
|
CatalogGroup,
|
|
165
174
|
CatalogTool,
|
|
166
175
|
CatalogToolDescriptor,
|
|
176
|
+
ToolAnnotations,
|
|
167
177
|
ToolGroupKey,
|
|
168
178
|
ToolKey
|
|
169
179
|
} from './toolCatalog';
|
|
@@ -186,6 +196,8 @@ import {
|
|
|
186
196
|
import type { Separator, ChunkMetadata, PreparedChunk } from './chunking';
|
|
187
197
|
import { isEmbeddableMimeType } from './embeddable';
|
|
188
198
|
import { l2Normalize } from './l2Normalize';
|
|
199
|
+
import { createResourceReranker } from './resourceRerank';
|
|
200
|
+
import type { RerankAiBinding } from './resourceRerank';
|
|
189
201
|
import { sleep, isRateLimitError, withRateLimitRetry } from './retry';
|
|
190
202
|
import type { RateLimitRetryOptions } from './retry';
|
|
191
203
|
import { processQueueBatch } from './processQueueBatch';
|
|
@@ -204,13 +216,30 @@ import type { ChannelNotifier, ToolStatusEvent } from './channelNotifier';
|
|
|
204
216
|
import {
|
|
205
217
|
resolveDebounceMs,
|
|
206
218
|
channelBufferKey,
|
|
207
|
-
joinBufferedMessages
|
|
219
|
+
joinBufferedMessages,
|
|
220
|
+
batchCommand
|
|
208
221
|
} from './channelDebounce';
|
|
209
222
|
import type {
|
|
210
223
|
BufferedChannelMessage,
|
|
224
|
+
BufferedChannelCommand,
|
|
211
225
|
ChannelBufferEnvelope,
|
|
212
226
|
ChannelBufferFlush
|
|
213
227
|
} from './channelDebounce';
|
|
228
|
+
import {
|
|
229
|
+
isConfirmationReply,
|
|
230
|
+
confirmationButtonLabels,
|
|
231
|
+
confirmationButtonId,
|
|
232
|
+
parseConfirmationButton,
|
|
233
|
+
languageFromPhoneNumber,
|
|
234
|
+
formatConfirmationSummary,
|
|
235
|
+
annotationsForEffect,
|
|
236
|
+
TOOL_EFFECTS
|
|
237
|
+
} from './toolConfirmation';
|
|
238
|
+
import type {
|
|
239
|
+
PendingToolConfirmation,
|
|
240
|
+
PendingToolConfirmations,
|
|
241
|
+
ToolEffect
|
|
242
|
+
} from './toolConfirmation';
|
|
214
243
|
import {
|
|
215
244
|
isResourceSourceEnabled,
|
|
216
245
|
isDownloadableSource,
|
|
@@ -291,6 +320,9 @@ export const utils = {
|
|
|
291
320
|
oauthProviders,
|
|
292
321
|
resolveAttachment,
|
|
293
322
|
isExposedResource,
|
|
323
|
+
isSyncableResource,
|
|
324
|
+
isResourceSyncInProgress,
|
|
325
|
+
resourceSyncProvider,
|
|
294
326
|
describeVendorError,
|
|
295
327
|
withDeadline,
|
|
296
328
|
isDeadlineError,
|
|
@@ -337,6 +369,7 @@ export const utils = {
|
|
|
337
369
|
prepareChunks,
|
|
338
370
|
isEmbeddableMimeType,
|
|
339
371
|
l2Normalize,
|
|
372
|
+
createResourceReranker,
|
|
340
373
|
sleep,
|
|
341
374
|
isRateLimitError,
|
|
342
375
|
withRateLimitRetry,
|
|
@@ -344,7 +377,16 @@ export const utils = {
|
|
|
344
377
|
getToolStatusMessage,
|
|
345
378
|
resolveDebounceMs,
|
|
346
379
|
channelBufferKey,
|
|
380
|
+
batchCommand,
|
|
347
381
|
joinBufferedMessages,
|
|
382
|
+
isConfirmationReply,
|
|
383
|
+
confirmationButtonLabels,
|
|
384
|
+
confirmationButtonId,
|
|
385
|
+
parseConfirmationButton,
|
|
386
|
+
languageFromPhoneNumber,
|
|
387
|
+
formatConfirmationSummary,
|
|
388
|
+
annotationsForEffect,
|
|
389
|
+
TOOL_EFFECTS,
|
|
348
390
|
isResourceSourceEnabled,
|
|
349
391
|
isDownloadableSource,
|
|
350
392
|
safeHostname,
|
|
@@ -373,6 +415,10 @@ export type {
|
|
|
373
415
|
CatalogGroup,
|
|
374
416
|
CatalogTool,
|
|
375
417
|
CatalogToolDescriptor,
|
|
418
|
+
ToolAnnotations,
|
|
419
|
+
PendingToolConfirmation,
|
|
420
|
+
PendingToolConfirmations,
|
|
421
|
+
ToolEffect,
|
|
376
422
|
ToolGroupKey,
|
|
377
423
|
ToolKey,
|
|
378
424
|
CalendarConfigField,
|
|
@@ -407,12 +453,14 @@ export type {
|
|
|
407
453
|
ExtractedDocumentMetadata,
|
|
408
454
|
ExtractedDocumentSource,
|
|
409
455
|
RateLimitRetryOptions,
|
|
456
|
+
RerankAiBinding,
|
|
410
457
|
QueueBatchLike,
|
|
411
458
|
QueueMessageLike,
|
|
412
459
|
ProcessQueueBatchHandlers,
|
|
413
460
|
ChannelNotifier,
|
|
414
461
|
ToolStatusEvent,
|
|
415
462
|
BufferedChannelMessage,
|
|
463
|
+
BufferedChannelCommand,
|
|
416
464
|
ChannelBufferEnvelope,
|
|
417
465
|
ChannelBufferFlush,
|
|
418
466
|
Source,
|
|
@@ -438,6 +486,8 @@ export type {
|
|
|
438
486
|
ResolvedAttachment,
|
|
439
487
|
ResolveAttachmentResult,
|
|
440
488
|
ExposableResource,
|
|
489
|
+
SyncableResource,
|
|
490
|
+
ResourceSyncProvider,
|
|
441
491
|
PlanLimitDetails,
|
|
442
492
|
PlanLimits
|
|
443
493
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { constants } from './constants';
|
|
2
|
+
import type { EnvSource } from './getEnv';
|
|
3
|
+
|
|
4
|
+
// The slice of the Workers AI binding the reranker calls. Declared here rather
|
|
5
|
+
// than imported so this package doesn't take a dependency on workers-types.
|
|
6
|
+
export interface RerankAiBinding {
|
|
7
|
+
run(
|
|
8
|
+
model: string,
|
|
9
|
+
input: { query: string; contexts: Array<{ text: string }>; top_k?: number }
|
|
10
|
+
): Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type RerankResponse = {
|
|
14
|
+
response?: Array<{ id?: number; score?: number }>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The cross-encoder second stage for resource search, or undefined when the
|
|
19
|
+
* Worker has no `AI` binding — local dev without Workers AI keeps plain fusion.
|
|
20
|
+
*
|
|
21
|
+
* Returns one score per document, in input order. A response that doesn't
|
|
22
|
+
* cover every document returns null, which keeps the fused order — a partial
|
|
23
|
+
* ranking would silently drop the chunks it left out.
|
|
24
|
+
*/
|
|
25
|
+
export const createResourceReranker = (
|
|
26
|
+
source: EnvSource
|
|
27
|
+
):
|
|
28
|
+
| ((query: string, documents: string[]) => Promise<number[] | null>)
|
|
29
|
+
| undefined => {
|
|
30
|
+
const ai = (source.env as { AI?: RerankAiBinding } | undefined)?.AI;
|
|
31
|
+
if (!ai) return undefined;
|
|
32
|
+
|
|
33
|
+
return async (query, documents) => {
|
|
34
|
+
const result = (await ai.run(constants.RESOURCE_SEARCH_RERANK_MODEL, {
|
|
35
|
+
query,
|
|
36
|
+
contexts: documents.map(text => ({ text })),
|
|
37
|
+
top_k: documents.length
|
|
38
|
+
})) as RerankResponse;
|
|
39
|
+
|
|
40
|
+
const scores: Array<number | undefined> = new Array(documents.length);
|
|
41
|
+
for (const item of result?.response ?? []) {
|
|
42
|
+
if (
|
|
43
|
+
typeof item.id === 'number' &&
|
|
44
|
+
typeof item.score === 'number' &&
|
|
45
|
+
item.id >= 0 &&
|
|
46
|
+
item.id < documents.length
|
|
47
|
+
) {
|
|
48
|
+
scores[item.id] = item.score;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (scores.some(score => score === undefined)) return null;
|
|
52
|
+
return scores as number[];
|
|
53
|
+
};
|
|
54
|
+
};
|
package/src/schema.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { isAllowedLlmBaseUrl, requiresLlmBaseUrl } from './llmBaseUrl';
|
|
|
5
5
|
import { isReservedSlug, isValidSlugFormat } from './slug';
|
|
6
6
|
import { isReservedToolName } from './reservedToolName';
|
|
7
7
|
import { slugifyTitle } from './slugifyTitle';
|
|
8
|
+
import { TOOL_EFFECTS } from './toolConfirmation';
|
|
8
9
|
|
|
9
10
|
// A prompt title becomes a slash command; it must not collide with a command
|
|
10
11
|
// the channel runner handles itself (e.g. `/link`).
|
|
@@ -43,11 +44,20 @@ const ORGANIZATION_CREATE_VIEW = z.object({
|
|
|
43
44
|
projectDescription: z.string().max(500)
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
// Each setting is saved on its own — the name from its form, the switches the
|
|
48
|
+
// moment they're flipped — so every field is optional, but not all at once.
|
|
49
|
+
const ORGANIZATION_UPDATE = z
|
|
50
|
+
.object({
|
|
51
|
+
id: z.uuid(),
|
|
52
|
+
userId: z.uuid(),
|
|
53
|
+
name: z.string().min(3).max(100).optional(),
|
|
54
|
+
requireToolConfirmation: z.boolean().optional()
|
|
55
|
+
})
|
|
56
|
+
.refine(
|
|
57
|
+
value =>
|
|
58
|
+
value.name !== undefined || value.requireToolConfirmation !== undefined,
|
|
59
|
+
{ message: 'Nothing to update' }
|
|
60
|
+
);
|
|
51
61
|
|
|
52
62
|
const ORGANIZATION_GET = z.object({
|
|
53
63
|
id: z.uuid(),
|
|
@@ -851,6 +861,12 @@ const HTTP_ENDPOINT_CONFIG = z
|
|
|
851
861
|
contentType: constants.HTTP_ENDPOINT_RESPONSE_CONTENT_TYPE_AUTO,
|
|
852
862
|
maxBytes: constants.HTTP_ENDPOINT_DEFAULT_MAX_BYTES
|
|
853
863
|
}),
|
|
864
|
+
// What the endpoint does, as its owner says (`read`, `write` or
|
|
865
|
+
// `sensitive`). Absent means the method decides: GET and HEAD read,
|
|
866
|
+
// anything else is sensitive — right for most, but a POST that only
|
|
867
|
+
// searches needs its owner to say so, or a confirming channel asks
|
|
868
|
+
// before every search.
|
|
869
|
+
effect: z.enum(TOOL_EFFECTS).optional(),
|
|
854
870
|
// Auth — credentials referenced by id, never inlined.
|
|
855
871
|
auth: HTTP_ENDPOINT_AUTH.default({
|
|
856
872
|
kind: constants.HTTP_ENDPOINT_AUTH_KIND_NONE
|
|
@@ -1056,6 +1072,19 @@ const CUSTOM_CODE_CONFIG_WRITE = CUSTOM_CODE_CONFIG.superRefine((cfg, ctx) => {
|
|
|
1056
1072
|
// that version registering, and the owner would see tools quietly disappear with
|
|
1057
1073
|
// only a log line to explain it. Reservation belongs to the manifest below,
|
|
1058
1074
|
// which is the write path.
|
|
1075
|
+
// MCP tool annotations as a tool's author declares them — every hint optional,
|
|
1076
|
+
// with the spec's own defaults for what is left out. The channel runner reads
|
|
1077
|
+
// them to decide what to confirm, so a tool that doesn't say it is read-only
|
|
1078
|
+
// is treated as one that may change things.
|
|
1079
|
+
const TOOL_ANNOTATIONS = z
|
|
1080
|
+
.object({
|
|
1081
|
+
readOnlyHint: z.boolean().optional(),
|
|
1082
|
+
destructiveHint: z.boolean().optional(),
|
|
1083
|
+
idempotentHint: z.boolean().optional(),
|
|
1084
|
+
openWorldHint: z.boolean().optional()
|
|
1085
|
+
})
|
|
1086
|
+
.strict();
|
|
1087
|
+
|
|
1059
1088
|
const CUSTOM_CODE_TOOL = z.object({
|
|
1060
1089
|
name: z
|
|
1061
1090
|
.string()
|
|
@@ -1068,7 +1097,8 @@ const CUSTOM_CODE_TOOL = z.object({
|
|
|
1068
1097
|
title: z.string().min(1).max(200).optional(),
|
|
1069
1098
|
description: z.string().max(2000).optional(),
|
|
1070
1099
|
inputSchema: SCHEMA_DEFINITION.default({ type: 'object', properties: {} }),
|
|
1071
|
-
outputSchema: SCHEMA_DEFINITION.optional()
|
|
1100
|
+
outputSchema: SCHEMA_DEFINITION.optional(),
|
|
1101
|
+
annotations: TOOL_ANNOTATIONS.optional()
|
|
1072
1102
|
});
|
|
1073
1103
|
|
|
1074
1104
|
// The manifest a `ganju deploy` (or the dashboard) uploads alongside a bundle.
|
|
@@ -1449,6 +1479,21 @@ const ARTIFACT_UPDATE_RESOURCE_SHOW_SOURCE = z.object({
|
|
|
1449
1479
|
organizationId: z.uuid()
|
|
1450
1480
|
});
|
|
1451
1481
|
|
|
1482
|
+
const ARTIFACT_UPDATE_RESOURCE_SYNC_INTERVAL = z.object({
|
|
1483
|
+
resourceId: z.uuid(),
|
|
1484
|
+
syncInterval: z.enum(constants.RESOURCE_SYNC_INTERVALS),
|
|
1485
|
+
projectId: z.uuid(),
|
|
1486
|
+
userId: z.uuid(),
|
|
1487
|
+
organizationId: z.uuid()
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
const ARTIFACT_SYNC_RESOURCE = z.object({
|
|
1491
|
+
resourceId: z.uuid(),
|
|
1492
|
+
projectId: z.uuid(),
|
|
1493
|
+
userId: z.uuid(),
|
|
1494
|
+
organizationId: z.uuid()
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1452
1497
|
const ARTIFACT_GET = z.object({
|
|
1453
1498
|
projectId: z.uuid(),
|
|
1454
1499
|
userId: z.uuid(),
|
|
@@ -1625,12 +1670,15 @@ export const Schema = {
|
|
|
1625
1670
|
CHANNEL_LIST_CONVERSATIONS,
|
|
1626
1671
|
CHANNEL_LIST_MESSAGES,
|
|
1627
1672
|
ARTIFACT_UPDATE_RESOURCE_SHOW_SOURCE,
|
|
1673
|
+
ARTIFACT_UPDATE_RESOURCE_SYNC_INTERVAL,
|
|
1674
|
+
ARTIFACT_SYNC_RESOURCE,
|
|
1628
1675
|
HTTP_ENDPOINT_CONFIG,
|
|
1629
1676
|
HTTP_ENDPOINT_CONFIG_WRITE,
|
|
1630
1677
|
MCP_PROXY_CONFIG,
|
|
1631
1678
|
CUSTOM_CODE_CONFIG,
|
|
1632
1679
|
CUSTOM_CODE_CONFIG_WRITE,
|
|
1633
1680
|
CUSTOM_CODE_TOOL,
|
|
1681
|
+
TOOL_ANNOTATIONS,
|
|
1634
1682
|
CUSTOM_CODE_MANIFEST,
|
|
1635
1683
|
ARTIFACT_CUSTOM_CODE_CREATE_VERSION,
|
|
1636
1684
|
ARTIFACT_CUSTOM_CODE_UPLOAD_BUNDLE,
|
|
@@ -1690,6 +1738,10 @@ export interface McpProxyDiscoveredTool {
|
|
|
1690
1738
|
title?: string;
|
|
1691
1739
|
description?: string;
|
|
1692
1740
|
inputSchema: unknown;
|
|
1741
|
+
// The remote's own annotations, passed through at registration. Absent on
|
|
1742
|
+
// discoveries made before they were recorded — the tool then counts as one
|
|
1743
|
+
// that may change things until the server is discovered again.
|
|
1744
|
+
annotations?: z.infer<typeof TOOL_ANNOTATIONS>;
|
|
1693
1745
|
}
|
|
1694
1746
|
|
|
1695
1747
|
// One remote resource discovered from a proxied MCP server. Identified by uri;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { constants } from './constants';
|
|
2
|
+
|
|
3
|
+
// Which resources are sync roots: the rows a sync starts from and records its
|
|
4
|
+
// schedule on.
|
|
5
|
+
//
|
|
6
|
+
// - a WEBSITE seed (the row with no parent) — a sync re-crawls the site;
|
|
7
|
+
// - a Drive/OneDrive folder — a sync walks it for new, changed and removed
|
|
8
|
+
// files;
|
|
9
|
+
// - a Drive/OneDrive file imported on its own — a sync re-fetches it when it
|
|
10
|
+
// changed.
|
|
11
|
+
//
|
|
12
|
+
// A page under a website or a file under a folder is not a root: its parent's
|
|
13
|
+
// sync covers it. An uploaded file has nothing to sync from.
|
|
14
|
+
export interface SyncableResource {
|
|
15
|
+
sourceType?: string | null;
|
|
16
|
+
parentResourceId?: string | null;
|
|
17
|
+
metadata?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ResourceSyncProvider = 'website' | 'google-drive' | 'one-drive';
|
|
21
|
+
|
|
22
|
+
export const resourceSyncProvider = (
|
|
23
|
+
resource: SyncableResource
|
|
24
|
+
): ResourceSyncProvider | null => {
|
|
25
|
+
if (resource.parentResourceId) return null;
|
|
26
|
+
switch (resource.sourceType) {
|
|
27
|
+
case constants.RESOURCE_SOURCE_TYPE_WEBSITE:
|
|
28
|
+
return 'website';
|
|
29
|
+
case constants.RESOURCE_SOURCE_TYPE_GOOGLE_DRIVE_FOLDER:
|
|
30
|
+
return 'google-drive';
|
|
31
|
+
case constants.RESOURCE_SOURCE_TYPE_ONE_DRIVE_FOLDER:
|
|
32
|
+
return 'one-drive';
|
|
33
|
+
case constants.RESOURCE_SOURCE_TYPE_FILE: {
|
|
34
|
+
const meta =
|
|
35
|
+
resource.metadata && typeof resource.metadata === 'object'
|
|
36
|
+
? (resource.metadata as Record<string, unknown>)
|
|
37
|
+
: null;
|
|
38
|
+
if (typeof meta?.driveFileId === 'string') return 'google-drive';
|
|
39
|
+
if (typeof meta?.oneDriveItemId === 'string') return 'one-drive';
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
default:
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const isSyncableResource = (resource: SyncableResource): boolean =>
|
|
48
|
+
resourceSyncProvider(resource) !== null;
|
|
49
|
+
|
|
50
|
+
// Whether a sync root is still genuinely syncing: PENDING, and started within
|
|
51
|
+
// RESOURCE_SYNC_STALE_MS. Past that, a PENDING root is stuck — a job was lost
|
|
52
|
+
// or gave up — and may be synced again, which re-queues whatever it left
|
|
53
|
+
// unfinished. A root that never synced counts from its creation, so a first
|
|
54
|
+
// import that stalled recovers too.
|
|
55
|
+
export const isResourceSyncInProgress = (
|
|
56
|
+
resource: {
|
|
57
|
+
status: string;
|
|
58
|
+
syncStartedAt?: Date | string | null;
|
|
59
|
+
createdAt: Date | string;
|
|
60
|
+
},
|
|
61
|
+
now: number = Date.now()
|
|
62
|
+
): boolean => {
|
|
63
|
+
if (resource.status !== constants.STATUS_PENDING) return false;
|
|
64
|
+
const startedAt = new Date(
|
|
65
|
+
resource.syncStartedAt ?? resource.createdAt
|
|
66
|
+
).getTime();
|
|
67
|
+
return now - startedAt < constants.RESOURCE_SYNC_STALE_MS;
|
|
68
|
+
};
|