@ganju/utils 0.0.13 → 0.0.15
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/channelCustomer.d.ts +99 -0
- package/dist/channelCustomer.d.ts.map +1 -0
- package/dist/channelCustomer.js +533 -0
- package/dist/channelDebounce.d.ts +7 -0
- package/dist/channelDebounce.d.ts.map +1 -1
- package/dist/chunkOverlap.d.ts +2 -0
- package/dist/chunkOverlap.d.ts.map +1 -0
- package/dist/chunkOverlap.js +9 -0
- package/dist/chunking.d.ts.map +1 -1
- package/dist/chunking.js +24 -4
- package/dist/citations.d.ts +28 -0
- package/dist/citations.d.ts.map +1 -0
- package/dist/citations.js +111 -0
- package/dist/constants.d.ts +26 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +108 -10
- package/dist/index.d.ts +263 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +70 -0
- package/dist/jsonSchemaToZodShape.d.ts +5 -1
- package/dist/jsonSchemaToZodShape.d.ts.map +1 -1
- package/dist/jsonSchemaToZodShape.js +21 -9
- package/dist/pagination.d.ts +47 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +160 -0
- package/dist/processQueueBatch.d.ts +6 -1
- package/dist/processQueueBatch.d.ts.map +1 -1
- package/dist/processQueueBatch.js +23 -10
- package/dist/queueFailure.d.ts +14 -0
- package/dist/queueFailure.d.ts.map +1 -0
- package/dist/queueFailure.js +43 -0
- package/dist/resourceRerank.d.ts.map +1 -1
- package/dist/resourceRerank.js +3 -1
- package/dist/schema.d.ts +90 -26
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +87 -6
- package/dist/ssrf.d.ts +1 -0
- package/dist/ssrf.d.ts.map +1 -1
- package/dist/ssrf.js +48 -12
- package/dist/toolConfirmation.d.ts +48 -0
- package/dist/toolConfirmation.d.ts.map +1 -1
- package/dist/toolConfirmation.js +75 -1
- package/dist/toolCount.d.ts +13 -0
- package/dist/toolCount.d.ts.map +1 -0
- package/dist/toolCount.js +45 -0
- package/dist/toolLint.d.ts +1 -1
- package/dist/toolLint.d.ts.map +1 -1
- package/dist/toolLint.js +37 -3
- package/dist/untrustedContent.d.ts +135 -0
- package/dist/untrustedContent.d.ts.map +1 -0
- package/dist/untrustedContent.js +402 -0
- package/package.json +2 -1
- package/src/channelCustomer.ts +604 -0
- package/src/channelDebounce.ts +17 -0
- package/src/chunkOverlap.ts +6 -0
- package/src/chunking.ts +25 -4
- package/src/citations.ts +127 -0
- package/src/constants.ts +112 -10
- package/src/index.ts +152 -2
- package/src/jsonSchemaToZodShape.ts +31 -13
- package/src/pagination.ts +193 -0
- package/src/processQueueBatch.ts +42 -12
- package/src/queueFailure.ts +63 -0
- package/src/resourceRerank.ts +5 -1
- package/src/schema.ts +100 -4
- package/src/ssrf.ts +40 -11
- package/src/toolConfirmation.ts +123 -0
- package/src/toolCount.ts +53 -0
- package/src/toolLint.ts +46 -3
- package/src/untrustedContent.ts +476 -0
package/src/citations.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Source } from './sources';
|
|
2
|
+
|
|
3
|
+
// "p. 125", "pp. 123–126", "page 12", "págs. 4-5", "páginas 3 a 5". An
|
|
4
|
+
// abbreviation counts only with its dot and a number has to stand alone, so
|
|
5
|
+
// "p2p", "mp3" and "pp 2026" are not page references. The lookarounds, not \b,
|
|
6
|
+
// mark the edges: \b sees "á" as a word break.
|
|
7
|
+
const PAGE_REFERENCE =
|
|
8
|
+
/(?<![\p{L}\p{N}])(?:(?:pp?|p[aá]gs?)\.|pages?|p[aá]ginas?)\s*(\d{1,5})(?![\p{L}\p{N}])(?:(?:\s*[-–—]\s*|\s+(?:to|a)\s+)(\d{1,5})(?![\p{L}\p{N}]))?/giu;
|
|
9
|
+
const MAX_PAGE_RANGE = 50;
|
|
10
|
+
|
|
11
|
+
// Lowercased words only, so "ETag header — HTTP | MDN" in an answer matches the
|
|
12
|
+
// title "ETag header - HTTP | MDN", and "mml-book.pdf" matches "mml-book".
|
|
13
|
+
const normalize = (text: string): string =>
|
|
14
|
+
` ${text
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.normalize('NFKD')
|
|
17
|
+
.replace(/[̀-ͯ]/g, '')
|
|
18
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
19
|
+
.trim()} `;
|
|
20
|
+
|
|
21
|
+
// The ways an answer refers to a source: its whole title, the title without a
|
|
22
|
+
// parenthetical or a site suffix ("Northwind Handbook (draft)", "ETag header
|
|
23
|
+
// - HTTP | MDN"), its file name with and without the extension, and its URL.
|
|
24
|
+
const namesFor = (source: Source): string[] => {
|
|
25
|
+
const names = [source.title, source.title.replace(/\s*\([^)]*\)\s*/g, ' ')];
|
|
26
|
+
const lead = source.title.split(/\s+[|\-–—:]\s+/)[0];
|
|
27
|
+
// A one-word lead ("Introduction", "Pricing") names half the corpus.
|
|
28
|
+
if (lead.trim().split(/\s+/).length >= 2) names.push(lead);
|
|
29
|
+
if (source.fileName) {
|
|
30
|
+
names.push(source.fileName, source.fileName.replace(/\.[a-z0-9]+$/i, ''));
|
|
31
|
+
}
|
|
32
|
+
return names.map(normalize).filter(name => name.trim().length >= 4);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// The characters that can carry a URL on past the point we matched. Brackets,
|
|
36
|
+
// quotes and whitespace end it — that is how a URL sits inside Markdown links,
|
|
37
|
+
// parentheses and prose.
|
|
38
|
+
const URL_CONTINUATION = /^[^\s<>()[\]{}"'`|]*/;
|
|
39
|
+
// Punctuation that closes a sentence around a URL rather than belonging to it.
|
|
40
|
+
const TRAILING_PUNCTUATION = /[.,;:!?*_]+$/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether the answer cites this exact URL, not merely one that starts with it:
|
|
44
|
+
* on a crawled docs site the index page is a prefix of every page beneath it,
|
|
45
|
+
* so a plain substring test kept "/docs" and the site root in the footer of an
|
|
46
|
+
* answer that cited "/docs/api". A trailing slash and a #fragment still count
|
|
47
|
+
* as the same page.
|
|
48
|
+
*/
|
|
49
|
+
export const citesUrl = (answer: string, url: string): boolean => {
|
|
50
|
+
const target = url.replace(/\/+$/, '');
|
|
51
|
+
if (!target) return false;
|
|
52
|
+
for (
|
|
53
|
+
let at = answer.indexOf(target);
|
|
54
|
+
at !== -1;
|
|
55
|
+
at = answer.indexOf(target, at + 1)
|
|
56
|
+
) {
|
|
57
|
+
const rest = answer
|
|
58
|
+
.slice(at + target.length)
|
|
59
|
+
.match(URL_CONTINUATION)![0]
|
|
60
|
+
.replace(TRAILING_PUNCTUATION, '');
|
|
61
|
+
if (/^\/*(#.*)?$/.test(rest)) return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Every page number the answer cites, ranges expanded. */
|
|
67
|
+
export const citedPages = (answer: string): Set<number> => {
|
|
68
|
+
const pages = new Set<number>();
|
|
69
|
+
for (const match of answer.matchAll(PAGE_REFERENCE)) {
|
|
70
|
+
const from = Number(match[1]);
|
|
71
|
+
const to = match[2] ? Number(match[2]) : from;
|
|
72
|
+
if (to < from || to - from > MAX_PAGE_RANGE) {
|
|
73
|
+
pages.add(from);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (let page = from; page <= to; page++) pages.add(page);
|
|
77
|
+
}
|
|
78
|
+
return pages;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Narrow a turn's sources to the ones its answer actually cites.
|
|
83
|
+
*
|
|
84
|
+
* Every search hit used to become a footer line, so an answer drawn from one
|
|
85
|
+
* handbook section was followed by eight unrelated pages the search had also
|
|
86
|
+
* returned. A search hit is kept when the answer names its resource — by title,
|
|
87
|
+
* file name or URL — and, for a paged file, only the pages the answer cites
|
|
88
|
+
* (or the best-ranked one, when it cites none). Resources the model read whole
|
|
89
|
+
* are always kept: reading one is itself the citation.
|
|
90
|
+
*
|
|
91
|
+
* An answer that names nothing gets no search sources at all. The model is told
|
|
92
|
+
* to cite what it uses, so a reply that names nothing is a greeting or "I
|
|
93
|
+
* couldn't find that" — and a footer under it would claim sources it never
|
|
94
|
+
* used, just because search runs on every message.
|
|
95
|
+
*/
|
|
96
|
+
export const selectCitedSources = (
|
|
97
|
+
sources: Source[],
|
|
98
|
+
answerText: string
|
|
99
|
+
): Source[] => {
|
|
100
|
+
const searchEntries = sources.filter(s => s.score !== undefined);
|
|
101
|
+
const readEntries = sources.filter(s => s.score === undefined);
|
|
102
|
+
if (searchEntries.length === 0) return sources;
|
|
103
|
+
|
|
104
|
+
const answer = normalize(answerText);
|
|
105
|
+
const pages = citedPages(answerText);
|
|
106
|
+
const citedResources = new Set(
|
|
107
|
+
searchEntries
|
|
108
|
+
.filter(
|
|
109
|
+
s =>
|
|
110
|
+
citesUrl(answerText, s.uri) ||
|
|
111
|
+
namesFor(s).some(name => answer.includes(name))
|
|
112
|
+
)
|
|
113
|
+
.map(s => s.resourceId)
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const kept: Source[] = [];
|
|
117
|
+
for (const resourceId of citedResources) {
|
|
118
|
+
const entries = searchEntries.filter(s => s.resourceId === resourceId);
|
|
119
|
+
const onCitedPage = entries.filter(
|
|
120
|
+
s => s.pageNumber !== undefined && pages.has(s.pageNumber)
|
|
121
|
+
);
|
|
122
|
+
kept.push(...(onCitedPage.length > 0 ? onCitedPage : entries.slice(0, 1)));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const cited = searchEntries.filter(s => kept.includes(s));
|
|
126
|
+
return [...cited, ...readEntries];
|
|
127
|
+
};
|
package/src/constants.ts
CHANGED
|
@@ -324,6 +324,9 @@ const CRAWL_MAX_PAGES_LIMIT = 1000;
|
|
|
324
324
|
const CRAWL_DEFAULT_MAX_DEPTH = 3;
|
|
325
325
|
const CRAWL_MAX_DEPTH_LIMIT = 10;
|
|
326
326
|
const CRAWL_PAGE_FETCH_TIMEOUT_MS = 30000;
|
|
327
|
+
// Redirects one crawl fetch may follow. Each hop is screened on its own, so a
|
|
328
|
+
// public URL can't bounce the crawler onto a private address.
|
|
329
|
+
const CRAWL_MAX_REDIRECTS = 5;
|
|
327
330
|
const CRAWL_USER_AGENT =
|
|
328
331
|
'Mozilla/5.0 (compatible; GanjuCrawler/1.0; +https://ganju.ai)';
|
|
329
332
|
const CRAWL_PAGE_QUEUE_BATCH_SIZE = 100;
|
|
@@ -597,6 +600,11 @@ const RATE_LIMIT_BACKOFF_SECONDS = 60;
|
|
|
597
600
|
// / Container reset) so the retried job lands after the object has recovered.
|
|
598
601
|
const TRANSIENT_BACKOFF_SECONDS = 5;
|
|
599
602
|
|
|
603
|
+
// Retries every resource queue consumer is configured with (`max_retries` in
|
|
604
|
+
// the API worker's wrangler config). A job's last delivery is attempt
|
|
605
|
+
// QUEUE_MAX_RETRIES + 1; only then is its work marked failed.
|
|
606
|
+
const QUEUE_MAX_RETRIES = 3;
|
|
607
|
+
|
|
600
608
|
const CHANNEL_PLATFORM_TELEGRAM = 'telegram' as 'telegram';
|
|
601
609
|
const CHANNEL_PLATFORM_SLACK = 'slack' as 'slack';
|
|
602
610
|
const CHANNEL_PLATFORM_WHATSAPP = 'whatsapp' as 'whatsapp';
|
|
@@ -808,15 +816,39 @@ const TOOL_CONFIRMATION_ASK_INSTRUCTION =
|
|
|
808
816
|
'confirm, and must not say or suggest that they were done, nor describe ' +
|
|
809
817
|
'or make up any result from them.';
|
|
810
818
|
// For MCP clients, whose chat the server can't see: a sensitive call without a
|
|
811
|
-
// valid confirmation isn't run
|
|
812
|
-
// again with a token that binds the second call to these exact
|
|
813
|
-
//
|
|
819
|
+
// valid confirmation isn't run. Ordinarily the model is told to ask the user
|
|
820
|
+
// and call again with a token that binds the second call to these exact
|
|
821
|
+
// arguments; once the session has read outside content, the user approves it
|
|
822
|
+
// on a dashboard page instead, and the second call carries that approval's
|
|
823
|
+
// id. How long either stays good, and the argument that carries it.
|
|
814
824
|
const MCP_TOOL_CONFIRMATION_TTL_MS = 10 * 60 * 1000;
|
|
815
825
|
const MCP_TOOL_CONFIRMATION_ARG = 'confirmation';
|
|
816
826
|
const MCP_TOOL_CONFIRMATION_ARG_DESCRIPTION =
|
|
817
827
|
"Leave out on the first call. This action needs the user's yes: the " +
|
|
818
|
-
'first call returns a
|
|
819
|
-
'same arguments and that
|
|
828
|
+
'first call returns a code and says how to get that yes; once the user ' +
|
|
829
|
+
'has given it, call again with the same arguments and that code here.';
|
|
830
|
+
// The same argument where only some calls ask: those made after the session
|
|
831
|
+
// read outside content.
|
|
832
|
+
const MCP_TOOL_CONFIRMATION_ARG_DESCRIPTION_CONDITIONAL =
|
|
833
|
+
"Leave out on the first call. If this action needs the user's yes, the " +
|
|
834
|
+
'first call returns a code instead of running and says how to get that ' +
|
|
835
|
+
'yes; once the user has given it, call again with the same arguments and ' +
|
|
836
|
+
'that code here.';
|
|
837
|
+
// The dashboard page where a signed-in user approves or denies one such call,
|
|
838
|
+
// as `${TOOL_APPROVAL_PATH}/<approval id>`, and the two answers it sends.
|
|
839
|
+
const TOOL_APPROVAL_PATH = '/tool-approval';
|
|
840
|
+
const TOOL_APPROVAL_DECISION_APPROVE = 'approve';
|
|
841
|
+
const TOOL_APPROVAL_DECISION_DENY = 'deny';
|
|
842
|
+
const TOOL_APPROVAL_DECISIONS = [
|
|
843
|
+
TOOL_APPROVAL_DECISION_APPROVE,
|
|
844
|
+
TOOL_APPROVAL_DECISION_DENY
|
|
845
|
+
] as const;
|
|
846
|
+
// Where an approval stands, as the page shows it.
|
|
847
|
+
const TOOL_APPROVAL_STATUS_PENDING = 'pending';
|
|
848
|
+
const TOOL_APPROVAL_STATUS_APPROVED = 'approved';
|
|
849
|
+
const TOOL_APPROVAL_STATUS_DENIED = 'denied';
|
|
850
|
+
const TOOL_APPROVAL_STATUS_USED = 'used';
|
|
851
|
+
const TOOL_APPROVAL_STATUS_EXPIRED = 'expired';
|
|
820
852
|
// The question when the model wrote none, above the runner's own summary.
|
|
821
853
|
const TOOL_CONFIRMATION_FALLBACK_QUESTION =
|
|
822
854
|
'Before I do this, please confirm. Reply yes to go ahead, or no to cancel.';
|
|
@@ -841,13 +873,15 @@ const SLACK_MAX_SOURCE_BUTTONS = 10;
|
|
|
841
873
|
|
|
842
874
|
// The Slack app configuration Ganju needs, surfaced in the UI when connecting a
|
|
843
875
|
// Slack channel. Required scopes back the runner's calls (mentions, DMs,
|
|
844
|
-
// posting, file upload
|
|
845
|
-
//
|
|
876
|
+
// posting, file upload, downloading a voice clip to transcribe it);
|
|
877
|
+
// recommended scopes only enrich names/titles and degrade gracefully; bot
|
|
878
|
+
// events are what the webhook subscribes to.
|
|
846
879
|
const SLACK_REQUIRED_SCOPES = [
|
|
847
880
|
'app_mentions:read',
|
|
848
881
|
'im:history',
|
|
849
882
|
'chat:write',
|
|
850
|
-
'files:write'
|
|
883
|
+
'files:write',
|
|
884
|
+
'files:read'
|
|
851
885
|
];
|
|
852
886
|
const SLACK_RECOMMENDED_SCOPES = ['users:read', 'channels:read', 'groups:read'];
|
|
853
887
|
const SLACK_BOT_EVENTS = ['app_mention', 'message.im'];
|
|
@@ -964,10 +998,23 @@ const RESOURCE_SEARCH_COMMON_TERM_MIN = 5;
|
|
|
964
998
|
// call per search; a failure falls back to the fused order.
|
|
965
999
|
const RESOURCE_SEARCH_RERANK_CANDIDATES = 30;
|
|
966
1000
|
const RESOURCE_SEARCH_RERANK_MODEL = '@cf/baai/bge-reranker-base';
|
|
1001
|
+
// An identifier in the query (a code mixing letters and digits, a snake_case
|
|
1002
|
+
// name, or a number, dotted or hyphenated name typed on its own) found in at
|
|
1003
|
+
// most this many documents is what the user is after: its chunks go first,
|
|
1004
|
+
// whatever the reranker thinks. A cross-encoder reads "86400" or "PGRST116" as noise and
|
|
1005
|
+
// would bury the one chunk that contains it under pages that merely look
|
|
1006
|
+
// related. Past EXACT_MAX_CHUNKS matching chunks the identifier is common
|
|
1007
|
+
// enough that the usual ranking is left to decide, and the scan stops there.
|
|
1008
|
+
const RESOURCE_SEARCH_EXACT_MAX_RESOURCES = 5;
|
|
1009
|
+
const RESOURCE_SEARCH_EXACT_MAX_CHUNKS = 30;
|
|
1010
|
+
// An identifier the index never saw is looked for as plain text, which reads
|
|
1011
|
+
// every chunk of the artifact when nothing matches. Past this many chunks that
|
|
1012
|
+
// costs more than the rare hit is worth, so a large artifact skips it.
|
|
1013
|
+
const RESOURCE_SEARCH_SUBSTRING_MAX_CHUNKS = 20000;
|
|
967
1014
|
|
|
968
1015
|
const RESOURCE_HANDLER_SLEEP_AFTER = '10m';
|
|
969
1016
|
|
|
970
|
-
const DOCS_URL = 'https://
|
|
1017
|
+
const DOCS_URL = 'https://ganju.ai/docs/';
|
|
971
1018
|
|
|
972
1019
|
const BASE64_DATA_URI_RE =
|
|
973
1020
|
/data:image\/[a-zA-Z0-9+\-.]+;base64,[A-Za-z0-9+/=\s]+/g;
|
|
@@ -983,6 +1030,12 @@ const CHUNK_SEPARATORS: Array<{ split: string | RegExp; join: string }> = [
|
|
|
983
1030
|
|
|
984
1031
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
985
1032
|
|
|
1033
|
+
// One page of `list-resources`. A knowledge base with a crawled site runs to
|
|
1034
|
+
// thousands of resources; listed whole, that is one result of several hundred
|
|
1035
|
+
// KB in the model's context.
|
|
1036
|
+
const LIST_RESOURCES_DEFAULT_LIMIT = 100;
|
|
1037
|
+
const LIST_RESOURCES_MAX_LIMIT = 500;
|
|
1038
|
+
|
|
986
1039
|
const RESOURCE_TOOL_KEY_LIST_RESOURCES = 'list-resources';
|
|
987
1040
|
const RESOURCE_TOOL_KEY_SEARCH_RESOURCES = 'search-resources';
|
|
988
1041
|
const RESOURCE_TOOL_KEY_READ_RESOURCE = 'read-resource';
|
|
@@ -1514,7 +1567,9 @@ const RESERVED_TOOL_NAMES = [
|
|
|
1514
1567
|
TOOL_DEFINITION_KEY_HTTP_ENDPOINT,
|
|
1515
1568
|
TOOL_DEFINITION_KEY_MCP_PROXY,
|
|
1516
1569
|
TOOL_DEFINITION_KEY_CUSTOM_CODE,
|
|
1517
|
-
CUSTOM_CODE_HEALTH_TOOL
|
|
1570
|
+
CUSTOM_CODE_HEALTH_TOOL,
|
|
1571
|
+
// The channel runner's own memory tool (PARTICIPANT_MEMORY_TOOL_NAME).
|
|
1572
|
+
'remember-customer'
|
|
1518
1573
|
];
|
|
1519
1574
|
|
|
1520
1575
|
// Shared by both write paths so there is one string to translate. It has to
|
|
@@ -1781,6 +1836,10 @@ const MCP_SESSION_HEADER = 'mcp-session-id';
|
|
|
1781
1836
|
const MCP_INTERNAL_HEADER = 'x-ganju-internal-secret';
|
|
1782
1837
|
const MCP_CHANNEL_ID_HEADER = 'x-ganju-channel-id';
|
|
1783
1838
|
const MCP_CHANNEL_PLATFORM_HEADER = 'x-ganju-channel-platform';
|
|
1839
|
+
// The conversation a channel turn belongs to. The MCP worker files that turn's
|
|
1840
|
+
// calls under one session per conversation, so a call can be traced back to
|
|
1841
|
+
// the chat it ran in.
|
|
1842
|
+
const MCP_CHANNEL_CONVERSATION_HEADER = 'x-ganju-channel-conversation-id';
|
|
1784
1843
|
const MCP_CHANNEL_CLIENT_USER_AGENT = 'ganju-channel/0.0.1';
|
|
1785
1844
|
const JWKS_KV_KEY = 'jwks:v1';
|
|
1786
1845
|
const JWKS_TTL_SECONDS = 600;
|
|
@@ -1882,6 +1941,23 @@ const ACCESS_TOKEN_SCOPE_MESSAGE =
|
|
|
1882
1941
|
const CUSTOM_CODE_LOGS_DEFAULT_LIMIT = 20;
|
|
1883
1942
|
const CUSTOM_CODE_LOGS_MAX_LIMIT = 100;
|
|
1884
1943
|
|
|
1944
|
+
// Tool observability on the project home. The windows match the activity
|
|
1945
|
+
// chart's, and none reaches past `RETENTION_DAYS.mcpRequest`.
|
|
1946
|
+
const OBSERVABILITY_RANGE_DAYS = [7, 30, 90];
|
|
1947
|
+
// An enabled tool with no call in this many days is offered for disabling.
|
|
1948
|
+
const OBSERVABILITY_UNUSED_DAYS = 30;
|
|
1949
|
+
// Two calls in one session this close together are one attempt followed by
|
|
1950
|
+
// the next: a retry when it's the same tool, a switch when it's another tool
|
|
1951
|
+
// after an error.
|
|
1952
|
+
const OBSERVABILITY_FOLLOW_UP_SECONDS = 120;
|
|
1953
|
+
// How a call rejected by its tool's input schema reads in `error_message`.
|
|
1954
|
+
const OBSERVABILITY_SCHEMA_REJECTION_PREFIX = 'Input validation error';
|
|
1955
|
+
const OBSERVABILITY_TOP_ERRORS = 8;
|
|
1956
|
+
const OBSERVABILITY_CALLS_DEFAULT_LIMIT = 25;
|
|
1957
|
+
const OBSERVABILITY_CALLS_MAX_LIMIT = 100;
|
|
1958
|
+
// A session timeline is read whole up to this many calls, newest kept.
|
|
1959
|
+
const OBSERVABILITY_SESSION_MAX_CALLS = 500;
|
|
1960
|
+
|
|
1885
1961
|
const BOT_GRANT_TYPE = 'urn:ganju:bot-on-behalf-of';
|
|
1886
1962
|
const EXTERNAL_LINK_VERIFICATION_PREFIX = 'external_link:';
|
|
1887
1963
|
const EXTERNAL_LINK_TTL_SECONDS = 600;
|
|
@@ -2624,6 +2700,7 @@ export const constants = {
|
|
|
2624
2700
|
CRAWL_DEFAULT_MAX_DEPTH,
|
|
2625
2701
|
CRAWL_MAX_DEPTH_LIMIT,
|
|
2626
2702
|
CRAWL_PAGE_FETCH_TIMEOUT_MS,
|
|
2703
|
+
CRAWL_MAX_REDIRECTS,
|
|
2627
2704
|
CRAWL_PAGE_QUEUE_BATCH_SIZE,
|
|
2628
2705
|
CRAWL_USER_AGENT,
|
|
2629
2706
|
RESOURCE_ICON_THEME_DARK,
|
|
@@ -2641,6 +2718,7 @@ export const constants = {
|
|
|
2641
2718
|
TELEGRAM_MAX_FILE_BYTES,
|
|
2642
2719
|
RATE_LIMIT_BACKOFF_SECONDS,
|
|
2643
2720
|
TRANSIENT_BACKOFF_SECONDS,
|
|
2721
|
+
QUEUE_MAX_RETRIES,
|
|
2644
2722
|
ENCODINGS,
|
|
2645
2723
|
ENCODING_UTF8,
|
|
2646
2724
|
CHANNEL_PLATFORM_TELEGRAM,
|
|
@@ -2682,6 +2760,16 @@ export const constants = {
|
|
|
2682
2760
|
MCP_TOOL_CONFIRMATION_TTL_MS,
|
|
2683
2761
|
MCP_TOOL_CONFIRMATION_ARG,
|
|
2684
2762
|
MCP_TOOL_CONFIRMATION_ARG_DESCRIPTION,
|
|
2763
|
+
MCP_TOOL_CONFIRMATION_ARG_DESCRIPTION_CONDITIONAL,
|
|
2764
|
+
TOOL_APPROVAL_PATH,
|
|
2765
|
+
TOOL_APPROVAL_DECISION_APPROVE,
|
|
2766
|
+
TOOL_APPROVAL_DECISION_DENY,
|
|
2767
|
+
TOOL_APPROVAL_DECISIONS,
|
|
2768
|
+
TOOL_APPROVAL_STATUS_PENDING,
|
|
2769
|
+
TOOL_APPROVAL_STATUS_APPROVED,
|
|
2770
|
+
TOOL_APPROVAL_STATUS_DENIED,
|
|
2771
|
+
TOOL_APPROVAL_STATUS_USED,
|
|
2772
|
+
TOOL_APPROVAL_STATUS_EXPIRED,
|
|
2685
2773
|
CHANNEL_DEBOUNCE_DEFAULT_MS,
|
|
2686
2774
|
CHANNEL_DEBOUNCE_MIN_MS,
|
|
2687
2775
|
CHANNEL_DEBOUNCE_MAX_MS,
|
|
@@ -2754,6 +2842,9 @@ export const constants = {
|
|
|
2754
2842
|
RESOURCE_SEARCH_COMMON_TERM_MIN,
|
|
2755
2843
|
RESOURCE_SEARCH_RERANK_CANDIDATES,
|
|
2756
2844
|
RESOURCE_SEARCH_RERANK_MODEL,
|
|
2845
|
+
RESOURCE_SEARCH_EXACT_MAX_RESOURCES,
|
|
2846
|
+
RESOURCE_SEARCH_EXACT_MAX_CHUNKS,
|
|
2847
|
+
RESOURCE_SEARCH_SUBSTRING_MAX_CHUNKS,
|
|
2757
2848
|
RESOURCE_HANDLER_SLEEP_AFTER,
|
|
2758
2849
|
DOCS_URL,
|
|
2759
2850
|
BASE64_DATA_URI_RE,
|
|
@@ -2761,6 +2852,8 @@ export const constants = {
|
|
|
2761
2852
|
CHUNK_SEPARATORS,
|
|
2762
2853
|
DEFAULT_MAX_TOKENS,
|
|
2763
2854
|
RESOURCE_TOOL_KEY_LIST_RESOURCES,
|
|
2855
|
+
LIST_RESOURCES_DEFAULT_LIMIT,
|
|
2856
|
+
LIST_RESOURCES_MAX_LIMIT,
|
|
2764
2857
|
RESOURCE_TOOL_KEY_SEARCH_RESOURCES,
|
|
2765
2858
|
RESOURCE_TOOL_KEY_READ_RESOURCE,
|
|
2766
2859
|
RESOURCE_TOOL_KEY_SEND_RESOURCE,
|
|
@@ -2934,6 +3027,7 @@ export const constants = {
|
|
|
2934
3027
|
MCP_INTERNAL_HEADER,
|
|
2935
3028
|
MCP_CHANNEL_ID_HEADER,
|
|
2936
3029
|
MCP_CHANNEL_PLATFORM_HEADER,
|
|
3030
|
+
MCP_CHANNEL_CONVERSATION_HEADER,
|
|
2937
3031
|
MCP_CHANNEL_CLIENT_USER_AGENT,
|
|
2938
3032
|
MCP_SESSION_HEADER,
|
|
2939
3033
|
MCP_REQUEST_METHOD_INITIALIZE,
|
|
@@ -2971,6 +3065,14 @@ export const constants = {
|
|
|
2971
3065
|
ACCESS_TOKEN_SCOPE_MESSAGE,
|
|
2972
3066
|
CUSTOM_CODE_LOGS_DEFAULT_LIMIT,
|
|
2973
3067
|
CUSTOM_CODE_LOGS_MAX_LIMIT,
|
|
3068
|
+
OBSERVABILITY_RANGE_DAYS,
|
|
3069
|
+
OBSERVABILITY_UNUSED_DAYS,
|
|
3070
|
+
OBSERVABILITY_FOLLOW_UP_SECONDS,
|
|
3071
|
+
OBSERVABILITY_SCHEMA_REJECTION_PREFIX,
|
|
3072
|
+
OBSERVABILITY_TOP_ERRORS,
|
|
3073
|
+
OBSERVABILITY_CALLS_DEFAULT_LIMIT,
|
|
3074
|
+
OBSERVABILITY_CALLS_MAX_LIMIT,
|
|
3075
|
+
OBSERVABILITY_SESSION_MAX_CALLS,
|
|
2974
3076
|
BOT_GRANT_TYPE,
|
|
2975
3077
|
EXTERNAL_LINK_VERIFICATION_PREFIX,
|
|
2976
3078
|
EXTERNAL_LINK_TTL_SECONDS,
|
package/src/index.ts
CHANGED
|
@@ -98,6 +98,14 @@ import {
|
|
|
98
98
|
} from './jsonSchemaToZodShape';
|
|
99
99
|
import type { SchemaViolation } from './jsonSchemaToZodShape';
|
|
100
100
|
import { buildProxyToolName } from './mcpProxy';
|
|
101
|
+
import { countInstallTools } from './toolCount';
|
|
102
|
+
import {
|
|
103
|
+
capText,
|
|
104
|
+
findNextCursor,
|
|
105
|
+
paginate,
|
|
106
|
+
InvalidCursorError,
|
|
107
|
+
listResourcesPage
|
|
108
|
+
} from './pagination';
|
|
101
109
|
import {
|
|
102
110
|
mintCustomCodeToken,
|
|
103
111
|
verifyCustomCodeToken,
|
|
@@ -142,7 +150,7 @@ import type {
|
|
|
142
150
|
ResolvedAttachment,
|
|
143
151
|
ResolveAttachmentResult
|
|
144
152
|
} from './attachment';
|
|
145
|
-
import { ipv4InPrivateRange, isBlockedHost } from './ssrf';
|
|
153
|
+
import { ipv4InPrivateRange, isBlockedHost, isBlockedUrl } from './ssrf';
|
|
146
154
|
import {
|
|
147
155
|
requiresLlmBaseUrl,
|
|
148
156
|
isAllowedLlmBaseUrl,
|
|
@@ -201,8 +209,10 @@ import type { RerankAiBinding } from './resourceRerank';
|
|
|
201
209
|
import { sleep, isRateLimitError, withRateLimitRetry } from './retry';
|
|
202
210
|
import type { RateLimitRetryOptions } from './retry';
|
|
203
211
|
import { processQueueBatch } from './processQueueBatch';
|
|
212
|
+
import { PermanentJobError, isPermanentJobError } from './queueFailure';
|
|
204
213
|
import type {
|
|
205
214
|
QueueBatchLike,
|
|
215
|
+
QueueFailureContext,
|
|
206
216
|
QueueMessageLike,
|
|
207
217
|
ProcessQueueBatchHandlers
|
|
208
218
|
} from './processQueueBatch';
|
|
@@ -222,6 +232,7 @@ import {
|
|
|
222
232
|
import type {
|
|
223
233
|
BufferedChannelMessage,
|
|
224
234
|
BufferedChannelCommand,
|
|
235
|
+
BufferedVoiceNote,
|
|
225
236
|
ChannelBufferEnvelope,
|
|
226
237
|
ChannelBufferFlush
|
|
227
238
|
} from './channelDebounce';
|
|
@@ -235,13 +246,79 @@ import {
|
|
|
235
246
|
annotationsForEffect,
|
|
236
247
|
TOOL_EFFECTS,
|
|
237
248
|
isSensitiveTool,
|
|
249
|
+
proxiedToolAnnotations,
|
|
250
|
+
proxiedToolTrust,
|
|
251
|
+
isSensitiveProxiedTool,
|
|
252
|
+
keepProxiedToolMarks,
|
|
253
|
+
toolApprovalStatus,
|
|
254
|
+
toolApprovalUrl,
|
|
238
255
|
stableJson
|
|
239
256
|
} from './toolConfirmation';
|
|
240
257
|
import type {
|
|
241
258
|
PendingToolConfirmation,
|
|
242
259
|
PendingToolConfirmations,
|
|
243
|
-
ToolEffect
|
|
260
|
+
ToolEffect,
|
|
261
|
+
ToolApprovalStatus,
|
|
262
|
+
ProxiedToolTrust
|
|
244
263
|
} from './toolConfirmation';
|
|
264
|
+
import {
|
|
265
|
+
wrapUntrustedContent,
|
|
266
|
+
unwrapUntrustedContent,
|
|
267
|
+
isUntrustedToolResult,
|
|
268
|
+
findInstructionLikeText,
|
|
269
|
+
UNTRUSTED_CONTENT_TAG,
|
|
270
|
+
UNTRUSTED_CONTENT_META_KEY,
|
|
271
|
+
UNTRUSTED_CONTENT_NOTE,
|
|
272
|
+
INSTRUCTION_WARNING_METADATA_KEY,
|
|
273
|
+
untrustedToolResult,
|
|
274
|
+
ownWritingToolResult,
|
|
275
|
+
labelOwnToolResult,
|
|
276
|
+
labelProxiedToolResult,
|
|
277
|
+
untrustedErrorResult,
|
|
278
|
+
readsLikeInstructions,
|
|
279
|
+
labelResourceContents
|
|
280
|
+
} from './untrustedContent';
|
|
281
|
+
import type {
|
|
282
|
+
InstructionWarning,
|
|
283
|
+
LabelledToolResult
|
|
284
|
+
} from './untrustedContent';
|
|
285
|
+
import {
|
|
286
|
+
PARTICIPANT_DAILY_LIMIT_DEFAULT,
|
|
287
|
+
PARTICIPANT_DAILY_LIMIT_MAX,
|
|
288
|
+
PARTICIPANT_DAILY_LIMIT_OFF,
|
|
289
|
+
PARTICIPANT_DAILY_LIMIT_WINDOW_MS,
|
|
290
|
+
PARTICIPANT_LIMIT_METADATA_KEY,
|
|
291
|
+
resolveParticipantDailyLimit,
|
|
292
|
+
resolveParticipantMemoryEnabled,
|
|
293
|
+
VOICE_NOTE_MAX_BYTES,
|
|
294
|
+
VOICE_NOTE_MAX_SECONDS,
|
|
295
|
+
audioDurationSeconds,
|
|
296
|
+
oggDurationSeconds,
|
|
297
|
+
VOICE_NOTE_TRANSCRIPTION_PROMPT,
|
|
298
|
+
VOICE_NOTE_METADATA_KEY,
|
|
299
|
+
VOICE_NOTE_UNREADABLE_METADATA_KEY,
|
|
300
|
+
PARTICIPANT_MEMORY_TOOL_NAME,
|
|
301
|
+
PARTICIPANT_MEMORY_MAX_FACTS,
|
|
302
|
+
PARTICIPANT_MEMORY_TOOL_DESCRIPTION,
|
|
303
|
+
PARTICIPANT_MEMORY_TOOL_SCHEMA,
|
|
304
|
+
PARTICIPANT_MEMORY_GROUP_NOTE,
|
|
305
|
+
readParticipantMemory,
|
|
306
|
+
applyParticipantMemoryUpdate,
|
|
307
|
+
redactParticipantMemoryArgs,
|
|
308
|
+
redactParticipantMemoryConfirmation,
|
|
309
|
+
participantMemoryUpdateReadsLikeInstructions,
|
|
310
|
+
CONVERSATION_UNTRUSTED_WINDOW_MS,
|
|
311
|
+
CONVERSATION_UNTRUSTED_READ_AT_KEY,
|
|
312
|
+
conversationReadUntrustedRecently,
|
|
313
|
+
formatParticipantMemoryPrompt,
|
|
314
|
+
participantLimitNotice,
|
|
315
|
+
voiceNoteUnreadableNotice,
|
|
316
|
+
voiceNoteTooLongNotice
|
|
317
|
+
} from './channelCustomer';
|
|
318
|
+
import type {
|
|
319
|
+
ParticipantMemory,
|
|
320
|
+
ParticipantMemoryUpdate
|
|
321
|
+
} from './channelCustomer';
|
|
245
322
|
import {
|
|
246
323
|
lintTools,
|
|
247
324
|
TOOL_LINT_RULES,
|
|
@@ -263,6 +340,7 @@ import {
|
|
|
263
340
|
formatSourcesAsButtons
|
|
264
341
|
} from './sources';
|
|
265
342
|
import type { Source, ResourceUrlContext, SourceButton } from './sources';
|
|
343
|
+
import { citesUrl, citedPages, selectCitedSources } from './citations';
|
|
266
344
|
import {
|
|
267
345
|
refreshOAuthToken,
|
|
268
346
|
OAuthReauthRequiredError,
|
|
@@ -323,6 +401,12 @@ export const utils = {
|
|
|
323
401
|
jsonSchemaToZodShape,
|
|
324
402
|
validateAgainstJsonSchema,
|
|
325
403
|
buildProxyToolName,
|
|
404
|
+
countInstallTools,
|
|
405
|
+
capText,
|
|
406
|
+
findNextCursor,
|
|
407
|
+
paginate,
|
|
408
|
+
InvalidCursorError,
|
|
409
|
+
listResourcesPage,
|
|
326
410
|
mintCustomCodeToken,
|
|
327
411
|
verifyCustomCodeToken,
|
|
328
412
|
customCodeScriptName,
|
|
@@ -347,6 +431,7 @@ export const utils = {
|
|
|
347
431
|
validateProjectPath,
|
|
348
432
|
projectPathIssue,
|
|
349
433
|
ipv4InPrivateRange,
|
|
434
|
+
isBlockedUrl,
|
|
350
435
|
isBlockedHost,
|
|
351
436
|
requiresLlmBaseUrl,
|
|
352
437
|
isAllowedLlmBaseUrl,
|
|
@@ -388,6 +473,8 @@ export const utils = {
|
|
|
388
473
|
isRateLimitError,
|
|
389
474
|
withRateLimitRetry,
|
|
390
475
|
processQueueBatch,
|
|
476
|
+
PermanentJobError,
|
|
477
|
+
isPermanentJobError,
|
|
391
478
|
getToolStatusMessage,
|
|
392
479
|
resolveDebounceMs,
|
|
393
480
|
channelBufferKey,
|
|
@@ -402,7 +489,59 @@ export const utils = {
|
|
|
402
489
|
annotationsForEffect,
|
|
403
490
|
TOOL_EFFECTS,
|
|
404
491
|
isSensitiveTool,
|
|
492
|
+
proxiedToolAnnotations,
|
|
493
|
+
proxiedToolTrust,
|
|
494
|
+
isSensitiveProxiedTool,
|
|
495
|
+
keepProxiedToolMarks,
|
|
496
|
+
toolApprovalStatus,
|
|
497
|
+
toolApprovalUrl,
|
|
405
498
|
stableJson,
|
|
499
|
+
wrapUntrustedContent,
|
|
500
|
+
unwrapUntrustedContent,
|
|
501
|
+
isUntrustedToolResult,
|
|
502
|
+
findInstructionLikeText,
|
|
503
|
+
UNTRUSTED_CONTENT_TAG,
|
|
504
|
+
UNTRUSTED_CONTENT_META_KEY,
|
|
505
|
+
UNTRUSTED_CONTENT_NOTE,
|
|
506
|
+
INSTRUCTION_WARNING_METADATA_KEY,
|
|
507
|
+
untrustedToolResult,
|
|
508
|
+
ownWritingToolResult,
|
|
509
|
+
labelOwnToolResult,
|
|
510
|
+
labelProxiedToolResult,
|
|
511
|
+
untrustedErrorResult,
|
|
512
|
+
readsLikeInstructions,
|
|
513
|
+
labelResourceContents,
|
|
514
|
+
PARTICIPANT_DAILY_LIMIT_DEFAULT,
|
|
515
|
+
PARTICIPANT_DAILY_LIMIT_MAX,
|
|
516
|
+
PARTICIPANT_DAILY_LIMIT_OFF,
|
|
517
|
+
PARTICIPANT_DAILY_LIMIT_WINDOW_MS,
|
|
518
|
+
PARTICIPANT_LIMIT_METADATA_KEY,
|
|
519
|
+
resolveParticipantDailyLimit,
|
|
520
|
+
resolveParticipantMemoryEnabled,
|
|
521
|
+
VOICE_NOTE_MAX_BYTES,
|
|
522
|
+
VOICE_NOTE_MAX_SECONDS,
|
|
523
|
+
audioDurationSeconds,
|
|
524
|
+
oggDurationSeconds,
|
|
525
|
+
VOICE_NOTE_TRANSCRIPTION_PROMPT,
|
|
526
|
+
VOICE_NOTE_METADATA_KEY,
|
|
527
|
+
VOICE_NOTE_UNREADABLE_METADATA_KEY,
|
|
528
|
+
PARTICIPANT_MEMORY_TOOL_NAME,
|
|
529
|
+
PARTICIPANT_MEMORY_MAX_FACTS,
|
|
530
|
+
PARTICIPANT_MEMORY_TOOL_DESCRIPTION,
|
|
531
|
+
PARTICIPANT_MEMORY_TOOL_SCHEMA,
|
|
532
|
+
PARTICIPANT_MEMORY_GROUP_NOTE,
|
|
533
|
+
readParticipantMemory,
|
|
534
|
+
applyParticipantMemoryUpdate,
|
|
535
|
+
redactParticipantMemoryArgs,
|
|
536
|
+
redactParticipantMemoryConfirmation,
|
|
537
|
+
participantMemoryUpdateReadsLikeInstructions,
|
|
538
|
+
CONVERSATION_UNTRUSTED_WINDOW_MS,
|
|
539
|
+
CONVERSATION_UNTRUSTED_READ_AT_KEY,
|
|
540
|
+
conversationReadUntrustedRecently,
|
|
541
|
+
formatParticipantMemoryPrompt,
|
|
542
|
+
participantLimitNotice,
|
|
543
|
+
voiceNoteUnreadableNotice,
|
|
544
|
+
voiceNoteTooLongNotice,
|
|
406
545
|
lintTools,
|
|
407
546
|
TOOL_LINT_RULES,
|
|
408
547
|
TOOL_LINT_MAX_TOOLS,
|
|
@@ -413,6 +552,9 @@ export const utils = {
|
|
|
413
552
|
buildResourceDownloadUrl,
|
|
414
553
|
formatSourcesAsMarkdown,
|
|
415
554
|
formatSourcesAsButtons,
|
|
555
|
+
citesUrl,
|
|
556
|
+
citedPages,
|
|
557
|
+
selectCitedSources,
|
|
416
558
|
refreshOAuthToken,
|
|
417
559
|
OAuthReauthRequiredError,
|
|
418
560
|
buildReauthMetadata,
|
|
@@ -429,6 +571,10 @@ export const utils = {
|
|
|
429
571
|
};
|
|
430
572
|
|
|
431
573
|
export type {
|
|
574
|
+
ParticipantMemory,
|
|
575
|
+
ParticipantMemoryUpdate,
|
|
576
|
+
InstructionWarning,
|
|
577
|
+
LabelledToolResult,
|
|
432
578
|
MintedAccessToken,
|
|
433
579
|
CustomCodeProject,
|
|
434
580
|
ProjectPathIssue,
|
|
@@ -439,6 +585,8 @@ export type {
|
|
|
439
585
|
PendingToolConfirmation,
|
|
440
586
|
PendingToolConfirmations,
|
|
441
587
|
ToolEffect,
|
|
588
|
+
ToolApprovalStatus,
|
|
589
|
+
ProxiedToolTrust,
|
|
442
590
|
LintableTool,
|
|
443
591
|
ToolLintFinding,
|
|
444
592
|
ToolLintOptions,
|
|
@@ -479,12 +627,14 @@ export type {
|
|
|
479
627
|
RateLimitRetryOptions,
|
|
480
628
|
RerankAiBinding,
|
|
481
629
|
QueueBatchLike,
|
|
630
|
+
QueueFailureContext,
|
|
482
631
|
QueueMessageLike,
|
|
483
632
|
ProcessQueueBatchHandlers,
|
|
484
633
|
ChannelNotifier,
|
|
485
634
|
ToolStatusEvent,
|
|
486
635
|
BufferedChannelMessage,
|
|
487
636
|
BufferedChannelCommand,
|
|
637
|
+
BufferedVoiceNote,
|
|
488
638
|
ChannelBufferEnvelope,
|
|
489
639
|
ChannelBufferFlush,
|
|
490
640
|
Source,
|