@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.
Files changed (70) hide show
  1. package/dist/channelCustomer.d.ts +99 -0
  2. package/dist/channelCustomer.d.ts.map +1 -0
  3. package/dist/channelCustomer.js +533 -0
  4. package/dist/channelDebounce.d.ts +7 -0
  5. package/dist/channelDebounce.d.ts.map +1 -1
  6. package/dist/chunkOverlap.d.ts +2 -0
  7. package/dist/chunkOverlap.d.ts.map +1 -0
  8. package/dist/chunkOverlap.js +9 -0
  9. package/dist/chunking.d.ts.map +1 -1
  10. package/dist/chunking.js +24 -4
  11. package/dist/citations.d.ts +28 -0
  12. package/dist/citations.d.ts.map +1 -0
  13. package/dist/citations.js +111 -0
  14. package/dist/constants.d.ts +26 -0
  15. package/dist/constants.d.ts.map +1 -1
  16. package/dist/constants.js +108 -10
  17. package/dist/index.d.ts +263 -31
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +70 -0
  20. package/dist/jsonSchemaToZodShape.d.ts +5 -1
  21. package/dist/jsonSchemaToZodShape.d.ts.map +1 -1
  22. package/dist/jsonSchemaToZodShape.js +21 -9
  23. package/dist/pagination.d.ts +47 -0
  24. package/dist/pagination.d.ts.map +1 -0
  25. package/dist/pagination.js +160 -0
  26. package/dist/processQueueBatch.d.ts +6 -1
  27. package/dist/processQueueBatch.d.ts.map +1 -1
  28. package/dist/processQueueBatch.js +23 -10
  29. package/dist/queueFailure.d.ts +14 -0
  30. package/dist/queueFailure.d.ts.map +1 -0
  31. package/dist/queueFailure.js +43 -0
  32. package/dist/resourceRerank.d.ts.map +1 -1
  33. package/dist/resourceRerank.js +3 -1
  34. package/dist/schema.d.ts +90 -26
  35. package/dist/schema.d.ts.map +1 -1
  36. package/dist/schema.js +87 -6
  37. package/dist/ssrf.d.ts +1 -0
  38. package/dist/ssrf.d.ts.map +1 -1
  39. package/dist/ssrf.js +48 -12
  40. package/dist/toolConfirmation.d.ts +48 -0
  41. package/dist/toolConfirmation.d.ts.map +1 -1
  42. package/dist/toolConfirmation.js +75 -1
  43. package/dist/toolCount.d.ts +13 -0
  44. package/dist/toolCount.d.ts.map +1 -0
  45. package/dist/toolCount.js +45 -0
  46. package/dist/toolLint.d.ts +1 -1
  47. package/dist/toolLint.d.ts.map +1 -1
  48. package/dist/toolLint.js +37 -3
  49. package/dist/untrustedContent.d.ts +135 -0
  50. package/dist/untrustedContent.d.ts.map +1 -0
  51. package/dist/untrustedContent.js +402 -0
  52. package/package.json +2 -1
  53. package/src/channelCustomer.ts +604 -0
  54. package/src/channelDebounce.ts +17 -0
  55. package/src/chunkOverlap.ts +6 -0
  56. package/src/chunking.ts +25 -4
  57. package/src/citations.ts +127 -0
  58. package/src/constants.ts +112 -10
  59. package/src/index.ts +152 -2
  60. package/src/jsonSchemaToZodShape.ts +31 -13
  61. package/src/pagination.ts +193 -0
  62. package/src/processQueueBatch.ts +42 -12
  63. package/src/queueFailure.ts +63 -0
  64. package/src/resourceRerank.ts +5 -1
  65. package/src/schema.ts +100 -4
  66. package/src/ssrf.ts +40 -11
  67. package/src/toolConfirmation.ts +123 -0
  68. package/src/toolCount.ts +53 -0
  69. package/src/toolLint.ts +46 -3
  70. package/src/untrustedContent.ts +476 -0
@@ -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, and the model is told to ask the user and call
812
- // again with a token that binds the second call to these exact arguments.
813
- // How long that token stays good, and the argument that carries it.
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 token; after the user says yes, call again with the ' +
819
- 'same arguments and that token here.';
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); recommended scopes only enrich names/titles and
845
- // degrade gracefully; bot events are what the webhook subscribes to.
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://docs.ganju.ai';
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,