@chatpanel/events 0.102.0 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/adapters.js +1 -83
  2. package/adaptive-tool-policy.js +1 -45
  3. package/agent.js +2 -255
  4. package/attribution.js +1 -132
  5. package/backup-envelope.js +1 -221
  6. package/board-tool.js +2 -165
  7. package/budget.js +1 -116
  8. package/capability.js +1 -126
  9. package/citations.js +5 -127
  10. package/client-prefs.js +1 -148
  11. package/context-attachments.js +8 -144
  12. package/cowriter-router.js +1 -83
  13. package/cowriter-writer.js +19 -193
  14. package/cowriter.js +1 -190
  15. package/curate.js +3 -509
  16. package/distance.js +1 -124
  17. package/engine.js +1 -131
  18. package/entitlement.js +1 -332
  19. package/entity.js +1 -354
  20. package/event.js +1 -175
  21. package/extraction.js +4 -293
  22. package/failover.js +1 -93
  23. package/find-tool.js +1 -66
  24. package/flowchart.js +2 -644
  25. package/gate.js +1 -74
  26. package/harness.js +4 -101
  27. package/index.js +1 -345
  28. package/invariants.js +1 -174
  29. package/job.js +3 -147
  30. package/kernel.js +1 -255
  31. package/knowledge-derive.js +1 -267
  32. package/knowledge.js +3 -230
  33. package/library.js +11 -280
  34. package/loop.js +1 -132
  35. package/manifest.js +1 -107
  36. package/markdown-authoring.js +11 -288
  37. package/markdown-render.js +0 -0
  38. package/mcp-client.js +8 -358
  39. package/mcp-dispatch.js +1 -52
  40. package/mcp-errors.js +1 -106
  41. package/mcp-manager.js +1 -127
  42. package/media-transcript.js +8 -531
  43. package/meeting-analyzers.js +1 -83
  44. package/meeting-insights.js +7 -164
  45. package/meeting-shape.js +1 -282
  46. package/meeting-text.js +5 -94
  47. package/memory.js +2 -638
  48. package/model-candidates.js +1 -358
  49. package/model-health.js +1 -182
  50. package/model-ledger.js +1 -228
  51. package/model-picker.js +1 -188
  52. package/note-actions.js +15 -246
  53. package/note-graph.js +1 -158
  54. package/note-links.js +1 -68
  55. package/note-mentions.js +8 -114
  56. package/note-plan.js +9 -107
  57. package/note-research.js +1 -135
  58. package/observability.js +1 -119
  59. package/omni.js +1 -125
  60. package/order.js +1 -78
  61. package/package.json +4 -5
  62. package/pdf-layout.js +7 -253
  63. package/policy.js +1 -0
  64. package/project-run.js +20 -444
  65. package/project.js +1 -170
  66. package/promotion.js +1 -171
  67. package/queue.js +1 -98
  68. package/reach.js +1 -23
  69. package/recipe-tool.js +2 -161
  70. package/recipe.js +1 -264
  71. package/record-list.js +2 -110
  72. package/recruit.js +3 -456
  73. package/redaction-tokens.js +1 -61
  74. package/ref.js +1 -55
  75. package/registry.js +1 -240
  76. package/route-graph.js +1 -115
  77. package/route-strategies.js +2 -232
  78. package/router.js +2 -880
  79. package/rrf.js +1 -89
  80. package/rules.js +1 -142
  81. package/schedule.js +1 -663
  82. package/scm-connection.js +1 -180
  83. package/scopes.js +1 -12
  84. package/scorecard.js +1 -335
  85. package/search-engines.js +1 -81
  86. package/skill-manifest.js +1 -330
  87. package/skill-scan.js +3 -209
  88. package/skill-sources.js +1 -162
  89. package/skill-vars.js +2 -252
  90. package/slash-commands.js +1 -158
  91. package/source-gate.js +1 -92
  92. package/sources-retrieval.js +5 -186
  93. package/sources.js +1 -273
  94. package/store.js +1 -171
  95. package/structured.js +4 -901
  96. package/subject-kinds.js +1 -5
  97. package/subject-name.js +1 -96
  98. package/sync-plan.js +1 -213
  99. package/synthesis.js +3 -123
  100. package/tags.js +1 -203
  101. package/team-board.js +6 -337
  102. package/team-cache.js +2 -63
  103. package/team-observe.js +1 -118
  104. package/team-org.js +1 -442
  105. package/team-plan.js +4 -131
  106. package/team-record.js +1 -225
  107. package/team-run.js +9 -652
  108. package/team-subtask.js +2 -191
  109. package/team-task.js +2 -124
  110. package/team-tool.js +2 -196
  111. package/team-trail.js +2 -73
  112. package/team-worklog.js +2 -149
  113. package/team.js +1 -302
  114. package/text-search.js +1 -155
  115. package/theme.js +17 -154
  116. package/titles.js +4 -285
  117. package/tool-discovery.js +1 -87
  118. package/tool-dispatch.js +5 -230
  119. package/tool-groups.js +1 -81
  120. package/tool-hints.js +10 -185
  121. package/tool-loop-guard.js +4 -182
  122. package/tool-need.js +2 -124
  123. package/tool-result.js +2 -438
  124. package/tool-round.js +0 -0
  125. package/tool-schema.js +1 -155
  126. package/tool-traits.js +1 -182
  127. package/toolset.js +2 -88
  128. package/trajectory.js +1 -530
  129. package/turn-loop.js +4 -440
  130. package/upcast.js +1 -37
  131. package/vault.js +2 -250
  132. package/view.js +1 -88
  133. package/voice-intents.js +2 -1488
  134. package/voice-speaker.js +1 -98
  135. package/weather-tool.js +1 -65
  136. package/weather.js +2 -211
  137. package/web-search-tool.js +14 -102
  138. package/web-search.js +1 -160
  139. package/widget.js +1 -153
package/tool-result.js CHANGED
@@ -1,438 +1,2 @@
1
- // The response shield a tool result too big to read is stored, previewed and paged,
2
- // instead of being poured into the model's context whole.
3
- //
4
- // Every tool result used to go to the model in full. The only cap anywhere was the
5
- // 4,000-character slice the ACTIVITY LOG shows a person — the model got everything. One
6
- // `search_issues` returning three hundred kilobytes of JSON therefore cost the whole turn:
7
- // the request blew the context window, or it fit and every later turn re-read it.
8
- //
9
- // What is borrowed here is not truncation, which is easy, but the shape of the truncated
10
- // reply: it TEACHES the model how to get the rest. A preview goes back, plus what the
11
- // full result looks like (an array of 240 objects with these keys; 128,400 characters of
12
- // text) and a ready-to-send `get_result` call that pages, filters or projects the stored
13
- // copy. A cut with nothing after it is a result the model cannot recover from; a cut with
14
- // a retrieval note is one more tool call away from whatever it needed.
15
- //
16
- // Shared because the extension, the desktop and the gateway each feed tool results to a
17
- // model, and three limits would be three different answers to "what did the model see".
18
- // Pure: no storage binding, no clock, no id generator of its own — the host injects
19
- // `now`/`newId` (matching loop.js) and keeps the store wherever it keeps its turn state.
20
-
21
- export const RESULT_TOOL_NAME = 'get_result';
22
-
23
- export const DEFAULT_SHIELD = Object.freeze({
24
- // Characters of result text a model receives before the shield engages. Above the
25
- // page-read default (40,000) on purpose: a page the user asked to summarise must arrive
26
- // whole, and the shield is for the results nobody asked to be that large.
27
- maxChars: 48_000,
28
- maxArrayItems: 50,
29
- maxStringChars: 4_000,
30
- maxKeys: 60,
31
- maxDepth: 8,
32
- });
33
-
34
- export const DEFAULT_STORE = Object.freeze({
35
- maxEntries: 100,
36
- // Bytes across all stored results; oldest evicted first. A handful of very large
37
- // results must not grow memory without bound.
38
- maxBytes: 64 * 1024 * 1024,
39
- });
40
-
41
- const PAGE_LIMIT_DEFAULT = 50;
42
- const PAGE_LIMIT_MAX = 200;
43
- const TEXT_LIMIT_DEFAULT = 8_000;
44
- const TEXT_LIMIT_MAX = 32_000;
45
- const SEARCH_MATCHES_MAX = 20;
46
- const SEARCH_WINDOW = 160;
47
-
48
- const defaultId = () => `r_${Math.random().toString(36).slice(2, 8)}${Date.now().toString(36).slice(-3)}`;
49
-
50
- const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
51
-
52
- function byteLength(value) {
53
- const s = typeof value === 'string' ? value : JSON.stringify(value);
54
- return s == null ? 0 : s.length;
55
- }
56
-
57
- /** Strict JSON, but only a container counts — a bare number or string is not "structured". */
58
- function parseStructured(text) {
59
- const t = String(text || '').trim();
60
- if (!t || (t[0] !== '{' && t[0] !== '[')) return undefined;
61
- try {
62
- const v = JSON.parse(t);
63
- return v && typeof v === 'object' ? v : undefined;
64
- } catch {
65
- return undefined;
66
- }
67
- }
68
-
69
- /**
70
- * The store: full results, keyed by ref, owned by whoever produced them.
71
- *
72
- * `owner` scopes retrieval — a gateway serving several sessions must not let one page
73
- * another's results. `undefined` means unowned (a single-user client) and is readable
74
- * by anyone; a store that receives owners must be asked with owners.
75
- */
76
- export function createResultStore({ maxEntries = DEFAULT_STORE.maxEntries, maxBytes = DEFAULT_STORE.maxBytes, now = () => Date.now(), newId = defaultId } = {}) {
77
- const entries = new Map(); // ref -> { ref, tool, owner, createdAt, bytes, value }
78
- let bytes = 0;
79
-
80
- const drop = (ref) => {
81
- const e = entries.get(ref);
82
- if (!e) return false;
83
- entries.delete(ref);
84
- bytes -= e.bytes;
85
- return true;
86
- };
87
- const evict = () => {
88
- while (entries.size && (entries.size > maxEntries || bytes > maxBytes)) {
89
- drop(entries.keys().next().value);
90
- }
91
- };
92
-
93
- return {
94
- put({ tool = '', value, owner } = {}) {
95
- let ref = newId();
96
- while (entries.has(ref)) ref = newId();
97
- const e = { ref, tool: String(tool || ''), owner, createdAt: now(), bytes: byteLength(value), value };
98
- entries.set(ref, e);
99
- bytes += e.bytes;
100
- evict();
101
- return ref;
102
- },
103
- get(ref, owner) {
104
- const e = entries.get(String(ref || ''));
105
- if (!e) return null;
106
- if (e.owner !== undefined && e.owner !== owner) return null;
107
- return e;
108
- },
109
- drop,
110
- clear() { entries.clear(); bytes = 0; },
111
- get size() { return entries.size; },
112
- get bytes() { return bytes; },
113
- };
114
- }
115
-
116
- // ── shape ────────────────────────────────────────────────────────────────────────────
117
-
118
- /**
119
- * What a stored value LOOKS like, in a sentence's worth of facts — enough for the model to
120
- * choose between paging, searching, projecting and descending without seeing it all.
121
- */
122
- export function describeShape(value, { maxKeys = 20, maxPaths = 8 } = {}) {
123
- if (typeof value === 'string') return { kind: 'text', chars: value.length };
124
- if (Array.isArray(value)) {
125
- const keys = new Set();
126
- let objects = 0;
127
- for (const item of value.slice(0, 50)) {
128
- if (!isRecord(item)) continue;
129
- objects += 1;
130
- for (const k of Object.keys(item)) keys.add(k);
131
- }
132
- const itemKind = objects === Math.min(value.length, 50) && value.length ? 'object' : value.length ? typeof value[0] : 'empty';
133
- return { kind: 'array', items: value.length, itemKind, keys: [...keys].slice(0, maxKeys), moreKeys: Math.max(0, keys.size - maxKeys) };
134
- }
135
- if (isRecord(value)) {
136
- const keys = Object.keys(value);
137
- const arrays = [];
138
- const walk = (v, path, depth) => {
139
- if (arrays.length >= maxPaths || depth > 3) return;
140
- if (Array.isArray(v)) { arrays.push({ path, items: v.length }); return; }
141
- if (!isRecord(v)) return;
142
- for (const k of Object.keys(v)) walk(v[k], path ? `${path}.${k}` : k, depth + 1);
143
- };
144
- walk(value, '', 0);
145
- return { kind: 'object', keys: keys.slice(0, maxKeys), moreKeys: Math.max(0, keys.length - maxKeys), arrays };
146
- }
147
- return { kind: typeof value };
148
- }
149
-
150
- function shapeSentence(shape) {
151
- if (!shape) return '';
152
- if (shape.kind === 'text') return `${shape.chars.toLocaleString('en-US')} characters of text`;
153
- if (shape.kind === 'array') {
154
- const keys = shape.keys?.length ? ` with keys ${shape.keys.join(', ')}${shape.moreKeys ? ` (+${shape.moreKeys} more)` : ''}` : '';
155
- return `an array of ${shape.items} ${shape.itemKind === 'object' ? 'objects' : 'items'}${keys}`;
156
- }
157
- if (shape.kind === 'object') {
158
- const keys = shape.keys?.length ? `keys ${shape.keys.join(', ')}${shape.moreKeys ? ` (+${shape.moreKeys} more)` : ''}` : 'no keys';
159
- const arrays = shape.arrays?.length ? `; arrays at ${shape.arrays.map((a) => `${a.path} (${a.items})`).join(', ')}` : '';
160
- return `an object with ${keys}${arrays}`;
161
- }
162
- return `a ${shape.kind}`;
163
- }
164
-
165
- // ── compaction ───────────────────────────────────────────────────────────────────────
166
-
167
- /** A structurally-faithful preview: same nesting, fewer items, shorter strings. */
168
- export function compactValue(value, opts = {}) {
169
- const o = { ...DEFAULT_SHIELD, ...opts };
170
- let truncated = false;
171
- const walk = (v, depth) => {
172
- if (typeof v === 'string') {
173
- if (v.length <= o.maxStringChars) return v;
174
- truncated = true;
175
- return `${v.slice(0, o.maxStringChars)}…[+${v.length - o.maxStringChars} chars]`;
176
- }
177
- if (Array.isArray(v)) {
178
- if (depth >= o.maxDepth) { truncated = true; return `[array of ${v.length}]`; }
179
- const head = v.slice(0, o.maxArrayItems).map((x) => walk(x, depth + 1));
180
- if (v.length > o.maxArrayItems) { truncated = true; head.push(`…[+${v.length - o.maxArrayItems} more items]`); }
181
- return head;
182
- }
183
- if (isRecord(v)) {
184
- if (depth >= o.maxDepth) { truncated = true; return '{…}'; }
185
- const keys = Object.keys(v);
186
- const out = {};
187
- for (const k of keys.slice(0, o.maxKeys)) out[k] = walk(v[k], depth + 1);
188
- if (keys.length > o.maxKeys) { truncated = true; out['…'] = `+${keys.length - o.maxKeys} more keys`; }
189
- return out;
190
- }
191
- return v;
192
- };
193
- const out = walk(value, 0);
194
- return { value: out, truncated };
195
- }
196
-
197
- // ── the shield ───────────────────────────────────────────────────────────────────────
198
-
199
- function retrievalNote({ ref, shape, totalChars, shownChars, preview }) {
200
- const n = (x) => Number(x || 0).toLocaleString('en-US');
201
- const parts = [`[ChatPanel result shield: this result is ${n(totalChars)} characters; showing a ${n(shownChars)}-character preview.`];
202
- parts.push(`Full result: ${shapeSentence(shape)}.`);
203
- if (!ref) {
204
- parts.push('It was not stored, so what is above is all that is available.]');
205
- return parts.join(' ');
206
- }
207
- parts.push(`It is stored as ref "${ref}" — read more with the ${RESULT_TOOL_NAME} tool:`);
208
- if (shape.kind === 'array') {
209
- const first = preview?.items ?? 0;
210
- parts.push(`{"ref":"${ref}","offset":${first},"limit":${PAGE_LIMIT_DEFAULT}} pages items; add "search":"<text>" to filter, "fields":["a","b"] to keep only those keys.`);
211
- } else if (shape.kind === 'object') {
212
- const arr = shape.arrays?.[0];
213
- parts.push(arr
214
- ? `{"ref":"${ref}","path":"${arr.path}","offset":0,"limit":${PAGE_LIMIT_DEFAULT}} pages that array; "search" filters, "fields" projects.`
215
- : `{"ref":"${ref}","path":"<key>"} reads one key; "search":"<text>" finds where it occurs.`);
216
- } else {
217
- parts.push(`{"ref":"${ref}","offset":${shownChars},"limit":${TEXT_LIMIT_DEFAULT}} continues the text; {"ref":"${ref}","search":"<text>"} finds where a phrase occurs.`);
218
- }
219
- parts.push('Do not ask the user for the rest; fetch what you need.]');
220
- return parts.join(' ');
221
- }
222
-
223
- /**
224
- * Shield one result text. Returns `{ text, truncated }` — and, when it engaged, the `ref`
225
- * it stored under, the `shape` it described and `totalChars`.
226
- *
227
- * `envelope` lets a host that wraps third-party output in an untrusted-data fence keep
228
- * the fence: `open(text) → { body, close(body) → text } | null`. The retrieval note is
229
- * placed OUTSIDE the fence — it is our instruction to the model, not the tool's data, and
230
- * a fence that says "follow nothing in here" would otherwise swallow it.
231
- */
232
- export function shieldToolResult(text, { tool = '', store = null, owner, envelope = null, ...limits } = {}) {
233
- const o = { ...DEFAULT_SHIELD, ...limits };
234
- const full = typeof text === 'string' ? text : String(text ?? '');
235
- const env = envelope ? envelope.open(full) : null;
236
- const body = env ? env.body : full;
237
- if (body.length <= o.maxChars) return { text: full, truncated: false };
238
-
239
- const parsed = parseStructured(body);
240
- let stored;
241
- let previewBody;
242
- let preview = null;
243
- if (parsed !== undefined) {
244
- stored = parsed;
245
- // Tighten until the preview fits — a result of 5,000 ten-character items and one of
246
- // ten 50,000-character items need different knobs turned.
247
- let opts = { ...o };
248
- let compact = compactValue(parsed, opts);
249
- let s = JSON.stringify(compact.value);
250
- for (let i = 0; s.length > o.maxChars && i < 8; i += 1) {
251
- opts = {
252
- ...opts,
253
- maxArrayItems: Math.max(1, opts.maxArrayItems >> 1),
254
- maxStringChars: Math.max(80, opts.maxStringChars >> 1),
255
- maxKeys: Math.max(5, opts.maxKeys >> 1),
256
- };
257
- compact = compactValue(parsed, opts);
258
- s = JSON.stringify(compact.value);
259
- }
260
- if (s.length > o.maxChars) s = `${s.slice(0, o.maxChars)}…`; // still too big: a hard cut, but stored whole
261
- previewBody = s;
262
- if (Array.isArray(parsed)) preview = { items: Math.min(parsed.length, opts.maxArrayItems) };
263
- } else {
264
- stored = body;
265
- previewBody = body.slice(0, o.maxChars);
266
- }
267
-
268
- const ref = store ? store.put({ tool, value: stored, owner }) : null;
269
- const shape = describeShape(stored);
270
- const wrapped = env ? env.close(previewBody) : previewBody;
271
- const note = retrievalNote({ ref, shape, totalChars: body.length, shownChars: previewBody.length, preview });
272
- return { text: `${wrapped}\n${note}`, truncated: true, ref, shape, totalChars: body.length };
273
- }
274
-
275
- // ── get_result ───────────────────────────────────────────────────────────────────────
276
-
277
- export function resultToolSpec() {
278
- return {
279
- name: RESULT_TOOL_NAME,
280
- description:
281
- 'Read more of a tool result that was shielded (truncated) — the result named this tool and '
282
- + 'gave a ref. Pages arrays by item and text by character; `search` filters, `fields` keeps '
283
- + 'only those keys, `path` descends into a nested value (dot path, e.g. "data.items").',
284
- parameters: {
285
- type: 'object',
286
- properties: {
287
- ref: { type: 'string', description: 'The ref from the shielded result.' },
288
- path: { type: 'string', description: 'Dot path inside the stored result.' },
289
- offset: { type: 'integer', minimum: 0, description: 'First item (arrays) or character (text). Default 0.' },
290
- limit: { type: 'integer', minimum: 1, description: `Items (default ${PAGE_LIMIT_DEFAULT}, max ${PAGE_LIMIT_MAX}) or characters (default ${TEXT_LIMIT_DEFAULT}, max ${TEXT_LIMIT_MAX}).` },
291
- fields: { type: 'array', items: { type: 'string' }, description: 'Keep only these keys of each object.' },
292
- search: { type: 'string', description: 'Case-insensitive text to filter items by, or to locate in text.' },
293
- },
294
- required: ['ref'],
295
- },
296
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
297
- };
298
- }
299
-
300
- function descend(value, path) {
301
- const segs = String(path || '').split('.').map((s) => s.trim()).filter(Boolean);
302
- let cur = value;
303
- const walked = [];
304
- for (const seg of segs) {
305
- if (Array.isArray(cur) && /^\d+$/.test(seg)) cur = cur[Number(seg)];
306
- else if (isRecord(cur) && Object.prototype.hasOwnProperty.call(cur, seg)) cur = cur[seg];
307
- else return { error: `No "${seg}" at "${walked.join('.') || '(root)'}"`, available: isRecord(cur) ? Object.keys(cur).slice(0, 40) : Array.isArray(cur) ? `array of ${cur.length}` : typeof cur };
308
- walked.push(seg);
309
- }
310
- return { value: cur };
311
- }
312
-
313
- const clampInt = (v, def, min, max) => {
314
- const n = Number.parseInt(v, 10);
315
- if (!Number.isFinite(n)) return def;
316
- return Math.min(max, Math.max(min, n));
317
- };
318
-
319
- /**
320
- * Answer one `get_result` call. Returns the JSON text the model receives — including
321
- * errors, which name what exists so the model can correct itself without another call.
322
- */
323
- export function runResultQuery(store, args = {}, { owner, maxChars = DEFAULT_SHIELD.maxChars } = {}) {
324
- const ref = String(args?.ref || '');
325
- const entry = store ? store.get(ref, owner) : null;
326
- if (!entry) {
327
- return JSON.stringify({ error: `Unknown or expired ref "${ref}".`, hint: 'Refs come from a shielded result in this conversation and expire when newer results replace them; re-run the original tool if needed.' });
328
- }
329
- const located = args.path ? descend(entry.value, args.path) : { value: entry.value };
330
- if (located.error) return JSON.stringify({ error: located.error, available: located.available, ref });
331
- const target = located.value;
332
- const search = args.search != null && String(args.search).trim() ? String(args.search).toLowerCase() : '';
333
- const base = { ref, ...(args.path ? { path: args.path } : {}) };
334
-
335
- if (Array.isArray(target)) {
336
- let items = target;
337
- if (search) items = items.filter((it) => JSON.stringify(it).toLowerCase().includes(search));
338
- const fields = Array.isArray(args.fields) ? args.fields.map(String).filter(Boolean) : [];
339
- const project = (it) => {
340
- if (!fields.length || !isRecord(it)) return it;
341
- const out = {};
342
- for (const f of fields) if (Object.prototype.hasOwnProperty.call(it, f)) out[f] = it[f];
343
- return out;
344
- };
345
- const offset = clampInt(args.offset, 0, 0, Number.MAX_SAFE_INTEGER);
346
- let limit = clampInt(args.limit, PAGE_LIMIT_DEFAULT, 1, PAGE_LIMIT_MAX);
347
- let page;
348
- let text;
349
- // A page that itself exceeds the budget shrinks until it fits — the shield's guarantee
350
- // holds for its own pages too.
351
- for (let i = 0; i < 8; i += 1) {
352
- page = items.slice(offset, offset + limit).map((it) => compactValue(project(it), { maxArrayItems: 20, maxStringChars: 2000 }).value);
353
- const next = offset + limit < items.length ? { offset: offset + limit, limit } : null;
354
- text = JSON.stringify({ ...base, total: target.length, ...(search ? { matched: items.length, search: args.search } : {}), offset, limit, count: page.length, items: page, next });
355
- if (text.length <= maxChars || limit === 1) break;
356
- limit = Math.max(1, limit >> 1);
357
- }
358
- return text;
359
- }
360
-
361
- if (typeof target === 'string') {
362
- if (search) {
363
- const hay = target.toLowerCase();
364
- const matches = [];
365
- let from = 0;
366
- while (matches.length < SEARCH_MATCHES_MAX) {
367
- const at = hay.indexOf(search, from);
368
- if (at < 0) break;
369
- const start = Math.max(0, at - SEARCH_WINDOW);
370
- const end = Math.min(target.length, at + search.length + SEARCH_WINDOW);
371
- matches.push({ offset: at, excerpt: target.slice(start, end) });
372
- from = at + search.length;
373
- }
374
- return JSON.stringify({ ...base, chars: target.length, search: args.search, matches, more: matches.length >= SEARCH_MATCHES_MAX });
375
- }
376
- const offset = clampInt(args.offset, 0, 0, target.length);
377
- const limit = Math.min(clampInt(args.limit, TEXT_LIMIT_DEFAULT, 1, TEXT_LIMIT_MAX), Math.max(1, maxChars - 200));
378
- const slice = target.slice(offset, offset + limit);
379
- const next = offset + limit < target.length ? { offset: offset + limit, limit } : null;
380
- return JSON.stringify({ ...base, chars: target.length, offset, limit, text: slice, next });
381
- }
382
-
383
- if (isRecord(target)) {
384
- const shape = describeShape(target);
385
- const compact = compactValue(target, { maxArrayItems: 10, maxStringChars: 1000, maxKeys: 40 });
386
- let text = JSON.stringify({ ...base, shape, value: compact.value, hint: shape.arrays?.length ? `Use "path" to page an array, e.g. "${shape.arrays[0].path}".` : 'Use "path" to read one key.' });
387
- if (text.length > maxChars) text = JSON.stringify({ ...base, shape, hint: 'Too large to show whole; use "path" to descend.' });
388
- return text;
389
- }
390
-
391
- return JSON.stringify({ ...base, value: target });
392
- }
393
-
394
- // ── the wrapper ──────────────────────────────────────────────────────────────────────
395
-
396
- /**
397
- * Wrap a toolset `{ specs, execute, … }` so every result passes the shield and `get_result`
398
- * exists to page what it stored. Everything else on the toolset is kept as-is.
399
- *
400
- * @param exempt `(name) => boolean` — tools whose results are never shielded (a host
401
- * that already sizes a result itself).
402
- * @param textOf how to read a result's text; `setText` how to write it back. Defaults fit
403
- * the `string | { text, … }` executor contract.
404
- */
405
- export function withResultShield(toolset, { store, owner, envelope = null, exempt = () => false, limits = {}, textOf = defaultTextOf, setText = defaultSetText } = {}) {
406
- if (!toolset || typeof toolset.execute !== 'function') return toolset;
407
- const spec = resultToolSpec();
408
- const specs = [...(toolset.specs || []).filter((s) => s?.name !== RESULT_TOOL_NAME), spec];
409
- const base = toolset.execute.bind(toolset);
410
- const shielded = new Map(); // ref -> tool, for the host's activity log
411
- return {
412
- ...toolset,
413
- specs,
414
- shieldStore: store,
415
- async execute(name, input, meta) {
416
- if (name === RESULT_TOOL_NAME) return runResultQuery(store, input || {}, { owner, maxChars: limits.maxChars });
417
- const raw = await base(name, input, meta);
418
- if (exempt(name, input)) return raw;
419
- const text = textOf(raw);
420
- if (typeof text !== 'string') return raw;
421
- const out = shieldToolResult(text, { tool: name, store, owner, envelope, ...limits });
422
- if (!out.truncated) return raw;
423
- if (out.ref) shielded.set(out.ref, name);
424
- return setText(raw, out.text, out);
425
- },
426
- };
427
- }
428
-
429
- function defaultTextOf(result) {
430
- if (typeof result === 'string') return result;
431
- if (result && typeof result === 'object' && typeof result.text === 'string') return result.text;
432
- return undefined;
433
- }
434
-
435
- function defaultSetText(result, text, shield) {
436
- if (typeof result === 'string') return text;
437
- return { ...result, text, shielded: { ref: shield.ref, totalChars: shield.totalChars } };
438
- }
1
+ const k="get_result",j=Object.freeze({maxChars:48e3,maxArrayItems:50,maxStringChars:4e3,maxKeys:60,maxDepth:8}),I=Object.freeze({maxEntries:100,maxBytes:64*1024*1024}),A=50,N=200,C=8e3,T=32e3,_=20,E=160,D=()=>`r_${Math.random().toString(36).slice(2,8)}${Date.now().toString(36).slice(-3)}`,S=t=>t!==null&&typeof t=="object"&&!Array.isArray(t);function J(t){const e=typeof t=="string"?t:JSON.stringify(t);return e==null?0:e.length}function K(t){const e=String(t||"").trim();if(!(!e||e[0]!=="{"&&e[0]!=="["))try{const o=JSON.parse(e);return o&&typeof o=="object"?o:void 0}catch{return}}function G({maxEntries:t=I.maxEntries,maxBytes:e=I.maxBytes,now:o=()=>Date.now(),newId:n=D}={}){const i=new Map;let l=0;const r=f=>{const c=i.get(f);return c?(i.delete(f),l-=c.bytes,!0):!1},s=()=>{for(;i.size&&(i.size>t||l>e);)r(i.keys().next().value)};return{put({tool:f="",value:c,owner:a}={}){let h=n();for(;i.has(h);)h=n();const u={ref:h,tool:String(f||""),owner:a,createdAt:o(),bytes:J(c),value:c};return i.set(h,u),l+=u.bytes,s(),h},get(f,c){const a=i.get(String(f||""));return!a||a.owner!==void 0&&a.owner!==c?null:a},drop:r,clear(){i.clear(),l=0},get size(){return i.size},get bytes(){return l}}}function L(t,{maxKeys:e=20,maxPaths:o=8}={}){if(typeof t=="string")return{kind:"text",chars:t.length};if(Array.isArray(t)){const n=new Set;let i=0;for(const r of t.slice(0,50))if(S(r)){i+=1;for(const s of Object.keys(r))n.add(s)}const l=i===Math.min(t.length,50)&&t.length?"object":t.length?typeof t[0]:"empty";return{kind:"array",items:t.length,itemKind:l,keys:[...n].slice(0,e),moreKeys:Math.max(0,n.size-e)}}if(S(t)){const n=Object.keys(t),i=[],l=(r,s,f)=>{if(!(i.length>=o||f>3)){if(Array.isArray(r)){i.push({path:s,items:r.length});return}if(S(r))for(const c of Object.keys(r))l(r[c],s?`${s}.${c}`:c,f+1)}};return l(t,"",0),{kind:"object",keys:n.slice(0,e),moreKeys:Math.max(0,n.length-e),arrays:i}}return{kind:typeof t}}function R(t){if(!t)return"";if(t.kind==="text")return`${t.chars.toLocaleString("en-US")} characters of text`;if(t.kind==="array"){const e=t.keys?.length?` with keys ${t.keys.join(", ")}${t.moreKeys?` (+${t.moreKeys} more)`:""}`:"";return`an array of ${t.items} ${t.itemKind==="object"?"objects":"items"}${e}`}if(t.kind==="object"){const e=t.keys?.length?`keys ${t.keys.join(", ")}${t.moreKeys?` (+${t.moreKeys} more)`:""}`:"no keys",o=t.arrays?.length?`; arrays at ${t.arrays.map(n=>`${n.path} (${n.items})`).join(", ")}`:"";return`an object with ${e}${o}`}return`a ${t.kind}`}function w(t,e={}){const o={...j,...e};let n=!1;const i=(r,s)=>{if(typeof r=="string")return r.length<=o.maxStringChars?r:(n=!0,`${r.slice(0,o.maxStringChars)}…[+${r.length-o.maxStringChars} chars]`);if(Array.isArray(r)){if(s>=o.maxDepth)return n=!0,`[array of ${r.length}]`;const f=r.slice(0,o.maxArrayItems).map(c=>i(c,s+1));return r.length>o.maxArrayItems&&(n=!0,f.push(`…[+${r.length-o.maxArrayItems} more items]`)),f}if(S(r)){if(s>=o.maxDepth)return n=!0,"{…}";const f=Object.keys(r),c={};for(const a of f.slice(0,o.maxKeys))c[a]=i(r[a],s+1);return f.length>o.maxKeys&&(n=!0,c["…"]=`+${f.length-o.maxKeys} more keys`),c}return r};return{value:i(t,0),truncated:n}}function U({ref:t,shape:e,totalChars:o,shownChars:n,preview:i}){const l=s=>Number(s||0).toLocaleString("en-US"),r=[`[ChatPanel result shield: this result is ${l(o)} characters; showing a ${l(n)}-character preview.`];if(r.push(`Full result: ${R(e)}.`),!t)return r.push("It was not stored, so what is above is all that is available.]"),r.join(" ");if(r.push(`It is stored as ref "${t}" — read more with the ${k} tool:`),e.kind==="array"){const s=i?.items??0;r.push(`{"ref":"${t}","offset":${s},"limit":${A}} pages items; add "search":"<text>" to filter, "fields":["a","b"] to keep only those keys.`)}else if(e.kind==="object"){const s=e.arrays?.[0];r.push(s?`{"ref":"${t}","path":"${s.path}","offset":0,"limit":${A}} pages that array; "search" filters, "fields" projects.`:`{"ref":"${t}","path":"<key>"} reads one key; "search":"<text>" finds where it occurs.`)}else r.push(`{"ref":"${t}","offset":${n},"limit":${C}} continues the text; {"ref":"${t}","search":"<text>"} finds where a phrase occurs.`);return r.push("Do not ask the user for the rest; fetch what you need.]"),r.join(" ")}function F(t,{tool:e="",store:o=null,owner:n,envelope:i=null,...l}={}){const r={...j,...l},s=typeof t=="string"?t:String(t??""),f=i?i.open(s):null,c=f?f.body:s;if(c.length<=r.maxChars)return{text:s,truncated:!1};const a=K(c);let h,u,g=null;if(a!==void 0){h=a;let p={...r},$=w(a,p),b=JSON.stringify($.value);for(let M=0;b.length>r.maxChars&&M<8;M+=1)p={...p,maxArrayItems:Math.max(1,p.maxArrayItems>>1),maxStringChars:Math.max(80,p.maxStringChars>>1),maxKeys:Math.max(5,p.maxKeys>>1)},$=w(a,p),b=JSON.stringify($.value);b.length>r.maxChars&&(b=`${b.slice(0,r.maxChars)}…`),u=b,Array.isArray(a)&&(g={items:Math.min(a.length,p.maxArrayItems)})}else h=c,u=c.slice(0,r.maxChars);const d=o?o.put({tool:e,value:h,owner:n}):null,m=L(h),x=f?f.close(u):u,y=U({ref:d,shape:m,totalChars:c.length,shownChars:u.length,preview:g});return{text:`${x}
2
+ ${y}`,truncated:!0,ref:d,shape:m,totalChars:c.length}}function z(){return{name:k,description:'Read more of a tool result that was shielded (truncated) — the result named this tool and gave a ref. Pages arrays by item and text by character; `search` filters, `fields` keeps only those keys, `path` descends into a nested value (dot path, e.g. "data.items").',parameters:{type:"object",properties:{ref:{type:"string",description:"The ref from the shielded result."},path:{type:"string",description:"Dot path inside the stored result."},offset:{type:"integer",minimum:0,description:"First item (arrays) or character (text). Default 0."},limit:{type:"integer",minimum:1,description:`Items (default ${A}, max ${N}) or characters (default ${C}, max ${T}).`},fields:{type:"array",items:{type:"string"},description:"Keep only these keys of each object."},search:{type:"string",description:"Case-insensitive text to filter items by, or to locate in text."}},required:["ref"]},annotations:{readOnlyHint:!0,idempotentHint:!0,openWorldHint:!1}}}function H(t,e){const o=String(e||"").split(".").map(l=>l.trim()).filter(Boolean);let n=t;const i=[];for(const l of o){if(Array.isArray(n)&&/^\d+$/.test(l))n=n[Number(l)];else if(S(n)&&Object.prototype.hasOwnProperty.call(n,l))n=n[l];else return{error:`No "${l}" at "${i.join(".")||"(root)"}"`,available:S(n)?Object.keys(n).slice(0,40):Array.isArray(n)?`array of ${n.length}`:typeof n};i.push(l)}return{value:n}}const O=(t,e,o,n)=>{const i=Number.parseInt(t,10);return Number.isFinite(i)?Math.min(n,Math.max(o,i)):e};function P(t,e={},{owner:o,maxChars:n=j.maxChars}={}){const i=String(e?.ref||""),l=t?t.get(i,o):null;if(!l)return JSON.stringify({error:`Unknown or expired ref "${i}".`,hint:"Refs come from a shielded result in this conversation and expire when newer results replace them; re-run the original tool if needed."});const r=e.path?H(l.value,e.path):{value:l.value};if(r.error)return JSON.stringify({error:r.error,available:r.available,ref:i});const s=r.value,f=e.search!=null&&String(e.search).trim()?String(e.search).toLowerCase():"",c={ref:i,...e.path?{path:e.path}:{}};if(Array.isArray(s)){let a=s;f&&(a=a.filter(y=>JSON.stringify(y).toLowerCase().includes(f)));const h=Array.isArray(e.fields)?e.fields.map(String).filter(Boolean):[],u=y=>{if(!h.length||!S(y))return y;const p={};for(const $ of h)Object.prototype.hasOwnProperty.call(y,$)&&(p[$]=y[$]);return p},g=O(e.offset,0,0,Number.MAX_SAFE_INTEGER);let d=O(e.limit,A,1,N),m,x;for(let y=0;y<8;y+=1){m=a.slice(g,g+d).map($=>w(u($),{maxArrayItems:20,maxStringChars:2e3}).value);const p=g+d<a.length?{offset:g+d,limit:d}:null;if(x=JSON.stringify({...c,total:s.length,...f?{matched:a.length,search:e.search}:{},offset:g,limit:d,count:m.length,items:m,next:p}),x.length<=n||d===1)break;d=Math.max(1,d>>1)}return x}if(typeof s=="string"){if(f){const d=s.toLowerCase(),m=[];let x=0;for(;m.length<_;){const y=d.indexOf(f,x);if(y<0)break;const p=Math.max(0,y-E),$=Math.min(s.length,y+f.length+E);m.push({offset:y,excerpt:s.slice(p,$)}),x=y+f.length}return JSON.stringify({...c,chars:s.length,search:e.search,matches:m,more:m.length>=_})}const a=O(e.offset,0,0,s.length),h=Math.min(O(e.limit,C,1,T),Math.max(1,n-200)),u=s.slice(a,a+h),g=a+h<s.length?{offset:a+h,limit:h}:null;return JSON.stringify({...c,chars:s.length,offset:a,limit:h,text:u,next:g})}if(S(s)){const a=L(s),h=w(s,{maxArrayItems:10,maxStringChars:1e3,maxKeys:40});let u=JSON.stringify({...c,shape:a,value:h.value,hint:a.arrays?.length?`Use "path" to page an array, e.g. "${a.arrays[0].path}".`:'Use "path" to read one key.'});return u.length>n&&(u=JSON.stringify({...c,shape:a,hint:'Too large to show whole; use "path" to descend.'})),u}return JSON.stringify({...c,value:s})}function W(t,{store:e,owner:o,envelope:n=null,exempt:i=()=>!1,limits:l={},textOf:r=X,setText:s=B}={}){if(!t||typeof t.execute!="function")return t;const f=z(),c=[...(t.specs||[]).filter(u=>u?.name!==k),f],a=t.execute.bind(t),h=new Map;return{...t,specs:c,shieldStore:e,async execute(u,g,d){if(u===k)return P(e,g||{},{owner:o,maxChars:l.maxChars});const m=await a(u,g,d);if(i(u,g))return m;const x=r(m);if(typeof x!="string")return m;const y=F(x,{tool:u,store:e,owner:o,envelope:n,...l});return y.truncated?(y.ref&&h.set(y.ref,u),s(m,y.text,y)):m}}}function X(t){if(typeof t=="string")return t;if(t&&typeof t=="object"&&typeof t.text=="string")return t.text}function B(t,e,o){return typeof t=="string"?e:{...t,text:e,shielded:{ref:o.ref,totalChars:o.totalChars}}}export{j as DEFAULT_SHIELD,I as DEFAULT_STORE,k as RESULT_TOOL_NAME,w as compactValue,G as createResultStore,L as describeShape,z as resultToolSpec,P as runResultQuery,F as shieldToolResult,W as withResultShield};
package/tool-round.js CHANGED
Binary file
package/tool-schema.js CHANGED
@@ -1,155 +1 @@
1
- // Schema compression a tool's contract, with the prose it does not need for the model to
2
- // call it correctly taken out.
3
- //
4
- // A connected server's tools arrive as full JSON schemas: a paragraph of description per
5
- // tool, a sentence per parameter, `title`s, `examples`, `$schema` and `$comment`. All of
6
- // it is re-sent on every turn the tool is armed, whether it is called or not. What the
7
- // model actually needs to make a correct call is small and structural — the names, the
8
- // types, which fields are required, the enum values, the bounds and defaults — plus enough
9
- // description to choose the tool and to disambiguate a parameter whose name does not say
10
- // what it holds.
11
- //
12
- // So `balanced` (the default) keeps everything structural, caps the tool description at a
13
- // sentence or two, and keeps a parameter's description only when it adds something the
14
- // name and type do not already say — `cursor: "opaque token from the previous page"` stays;
15
- // `issue_number: "The number of the issue"` goes. `aggressive` keeps parameter descriptions
16
- // only for the handful of names that are ambiguous everywhere (`ref`, `mode`, `sort`).
17
- // `off` is the schema as the server sent it.
18
- //
19
- // Pure and shared: the extension applies it as tools arrive from a server, the gateway
20
- // as tools pass through its relay, and the stats function makes the saving a number a
21
- // person can see rather than a claim.
22
-
23
- export const COMPRESSION_MODES = Object.freeze(['off', 'balanced', 'aggressive']);
24
-
25
- export const DEFAULT_COMPRESSION = Object.freeze({
26
- mode: 'balanced',
27
- maxDescriptionChars: 200,
28
- maxParamDescriptionChars: 100,
29
- });
30
-
31
- // Names that mean something different on every server, so their description always earns
32
- // its place — a `ref` is a git ref here and a result ref there.
33
- const AMBIGUOUS = new Set([
34
- 'ref', 'mode', 'sort', 'order', 'direction', 'format', 'type', 'kind', 'state', 'status',
35
- 'cursor', 'after', 'before', 'anchor', 'sha', 'q', 'query', 'filter', 'scope', 'target',
36
- 'id', 'key', 'path', 'name', 'value', 'data', 'body', 'input', 'output', 'options',
37
- ]);
38
-
39
- // Dropped outright: nothing a model needs to form a call.
40
- const DROP_KEYS = new Set(['$schema', '$comment', 'title', 'examples', 'example', 'deprecated', 'readOnly', 'writeOnly', '$id', 'contentMediaType', 'contentEncoding']);
41
-
42
- const collapse = (s) => String(s || '').replace(/\s+/g, ' ').trim();
43
-
44
- /** Cut at a sentence end when one falls comfortably inside the budget; else at a word. */
45
- export function trimDescription(text, max) {
46
- const s = collapse(text);
47
- if (!max || s.length <= max) return s;
48
- const head = s.slice(0, max);
49
- const sentence = Math.max(head.lastIndexOf('. '), head.lastIndexOf('! '), head.lastIndexOf('? '));
50
- if (sentence >= max * 0.4) return head.slice(0, sentence + 1);
51
- const word = head.lastIndexOf(' ');
52
- return `${(word > max * 0.6 ? head.slice(0, word) : head).trimEnd()}…`;
53
- }
54
-
55
- const words = (s) => new Set(collapse(s).toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2));
56
-
57
- /** Does a parameter description say anything its name and type do not? */
58
- const FILLER = new Set([
59
- 'the', 'this', 'that', 'for', 'and', 'with', 'optional', 'required', 'string', 'number', 'integer',
60
- 'boolean', 'array', 'object', 'value', 'values', 'field', 'parameter', 'param', 'name', 'identifier',
61
- 'text', 'list', 'given', 'specified', 'target', 'item', 'items', 'new', 'existing', 'which', 'whose',
62
- 'will', 'should', 'must', 'can', 'may', 'used', 'use', 'set', 'get', 'input', 'output',
63
- ]);
64
-
65
- function informative(name, desc, node) {
66
- const d = words(desc);
67
- const own = [...words(name.replace(/([a-z])([A-Z])/g, '$1 $2'))];
68
- for (const w of [...d]) {
69
- if (FILLER.has(w)) { d.delete(w); continue; }
70
- // `repo` covers "repository", `issue_number` covers "issues": a shared stem is the name.
71
- if (own.some((n) => n.length >= 3 && (w.startsWith(n) || n.startsWith(w)))) d.delete(w);
72
- }
73
- if (node && typeof node.type === 'string') d.delete(node.type);
74
- return d.size >= 2;
75
- }
76
-
77
- function compressNode(node, o, { name = '', depth = 0 } = {}) {
78
- if (!node || typeof node !== 'object') return node;
79
- if (Array.isArray(node)) return node.map((n) => compressNode(n, o, { depth: depth + 1 }));
80
- const out = {};
81
- for (const [k, v] of Object.entries(node)) {
82
- if (DROP_KEYS.has(k)) continue;
83
- if (k === 'description') {
84
- if (depth === 0) { out[k] = trimDescription(v, o.maxDescriptionChars); continue; }
85
- const keep = o.mode === 'aggressive'
86
- ? AMBIGUOUS.has(name.toLowerCase())
87
- : (AMBIGUOUS.has(name.toLowerCase()) || informative(name, v, node));
88
- if (keep) {
89
- const t = trimDescription(v, o.maxParamDescriptionChars);
90
- if (t) out[k] = t;
91
- }
92
- continue;
93
- }
94
- if (k === 'properties' && v && typeof v === 'object') {
95
- const props = {};
96
- for (const [pn, pv] of Object.entries(v)) props[pn] = compressNode(pv, o, { name: pn, depth: depth + 1 });
97
- out[k] = props;
98
- continue;
99
- }
100
- if ((k === 'items' || k === 'additionalProperties' || k === 'not') && v && typeof v === 'object') {
101
- out[k] = compressNode(v, o, { name, depth: depth + 1 });
102
- continue;
103
- }
104
- if ((k === 'anyOf' || k === 'oneOf' || k === 'allOf') && Array.isArray(v)) {
105
- out[k] = v.map((n) => compressNode(n, o, { name, depth: depth + 1 }));
106
- continue;
107
- }
108
- if (k === '$defs' || k === 'definitions') {
109
- const defs = {};
110
- for (const [dn, dv] of Object.entries(v || {})) defs[dn] = compressNode(dv, o, { name: dn, depth: depth + 1 });
111
- out[k] = defs;
112
- continue;
113
- }
114
- out[k] = v;
115
- }
116
- return out;
117
- }
118
-
119
- /**
120
- * Compress one spec. Works on the extension shape (`parameters`) and the MCP shape
121
- * (`inputSchema`); everything else on the spec (name, annotations, …) is kept as-is.
122
- */
123
- export function compressToolSpec(spec, opts = {}) {
124
- const o = { ...DEFAULT_COMPRESSION, ...opts };
125
- if (!spec || o.mode === 'off' || !COMPRESSION_MODES.includes(o.mode)) return spec;
126
- const out = { ...spec };
127
- if (typeof spec.description === 'string') {
128
- const d = trimDescription(spec.description, o.maxDescriptionChars);
129
- if (d) out.description = d; else delete out.description;
130
- }
131
- for (const k of ['parameters', 'inputSchema']) {
132
- if (spec[k] && typeof spec[k] === 'object') out[k] = compressNode(spec[k], o, { depth: 1 });
133
- }
134
- return out;
135
- }
136
-
137
- export function compressToolSpecs(specs, opts) {
138
- return (specs || []).map((s) => compressToolSpec(s, opts));
139
- }
140
-
141
- /** The saving, as a number: bytes of JSON before and after, per tool and in total. */
142
- export function compressionStats(specs, opts = {}) {
143
- const list = specs || [];
144
- const size = (v) => JSON.stringify(v ?? null).length;
145
- let before = 0;
146
- let after = 0;
147
- const tools = [];
148
- for (const s of list) {
149
- const b = size(s);
150
- const a = size(compressToolSpec(s, opts));
151
- before += b; after += a;
152
- tools.push({ name: s?.name, before: b, after: a });
153
- }
154
- return { mode: opts.mode || DEFAULT_COMPRESSION.mode, tools: list.length, before, after, saved: before - after, savedPercent: before ? Math.round(((before - after) / before) * 100) : 0, perTool: tools };
155
- }
1
+ const y=Object.freeze(["off","balanced","aggressive"]),d=Object.freeze({mode:"balanced",maxDescriptionChars:200,maxParamDescriptionChars:100}),u=new Set(["ref","mode","sort","order","direction","format","type","kind","state","status","cursor","after","before","anchor","sha","q","query","filter","scope","target","id","key","path","name","value","data","body","input","output","options"]),O=new Set(["$schema","$comment","title","examples","example","deprecated","readOnly","writeOnly","$id","contentMediaType","contentEncoding"]),m=t=>String(t||"").replace(/\s+/g," ").trim();function l(t,r){const s=m(t);if(!r||s.length<=r)return s;const o=s.slice(0,r),n=Math.max(o.lastIndexOf(". "),o.lastIndexOf("! "),o.lastIndexOf("? "));if(n>=r*.4)return o.slice(0,n+1);const e=o.lastIndexOf(" ");return`${(e>r*.6?o.slice(0,e):o).trimEnd()}…`}const h=t=>new Set(m(t).toLowerCase().split(/[^a-z0-9]+/).filter(r=>r.length>2)),b=new Set(["the","this","that","for","and","with","optional","required","string","number","integer","boolean","array","object","value","values","field","parameter","param","name","identifier","text","list","given","specified","target","item","items","new","existing","which","whose","will","should","must","can","may","used","use","set","get","input","output"]);function w(t,r,s){const o=h(r),n=[...h(t.replace(/([a-z])([A-Z])/g,"$1 $2"))];for(const e of[...o]){if(b.has(e)){o.delete(e);continue}n.some(i=>i.length>=3&&(e.startsWith(i)||i.startsWith(e)))&&o.delete(e)}return s&&typeof s.type=="string"&&o.delete(s.type),o.size>=2}function p(t,r,{name:s="",depth:o=0}={}){if(!t||typeof t!="object")return t;if(Array.isArray(t))return t.map(e=>p(e,r,{depth:o+1}));const n={};for(const[e,i]of Object.entries(t))if(!O.has(e)){if(e==="description"){if(o===0){n[e]=l(i,r.maxDescriptionChars);continue}if(r.mode==="aggressive"?u.has(s.toLowerCase()):u.has(s.toLowerCase())||w(s,i,t)){const c=l(i,r.maxParamDescriptionChars);c&&(n[e]=c)}continue}if(e==="properties"&&i&&typeof i=="object"){const a={};for(const[c,f]of Object.entries(i))a[c]=p(f,r,{name:c,depth:o+1});n[e]=a;continue}if((e==="items"||e==="additionalProperties"||e==="not")&&i&&typeof i=="object"){n[e]=p(i,r,{name:s,depth:o+1});continue}if((e==="anyOf"||e==="oneOf"||e==="allOf")&&Array.isArray(i)){n[e]=i.map(a=>p(a,r,{name:s,depth:o+1}));continue}if(e==="$defs"||e==="definitions"){const a={};for(const[c,f]of Object.entries(i||{}))a[c]=p(f,r,{name:c,depth:o+1});n[e]=a;continue}n[e]=i}return n}function g(t,r={}){const s={...d,...r};if(!t||s.mode==="off"||!y.includes(s.mode))return t;const o={...t};if(typeof t.description=="string"){const n=l(t.description,s.maxDescriptionChars);n?o.description=n:delete o.description}for(const n of["parameters","inputSchema"])t[n]&&typeof t[n]=="object"&&(o[n]=p(t[n],s,{depth:1}));return o}function S(t,r){return(t||[]).map(s=>g(s,r))}function x(t,r={}){const s=t||[],o=a=>JSON.stringify(a??null).length;let n=0,e=0;const i=[];for(const a of s){const c=o(a),f=o(g(a,r));n+=c,e+=f,i.push({name:a?.name,before:c,after:f})}return{mode:r.mode||d.mode,tools:s.length,before:n,after:e,saved:n-e,savedPercent:n?Math.round((n-e)/n*100):0,perTool:i}}export{y as COMPRESSION_MODES,d as DEFAULT_COMPRESSION,g as compressToolSpec,S as compressToolSpecs,x as compressionStats,l as trimDescription};