@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.
- package/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +4 -5
- package/pdf-layout.js +7 -253
- package/policy.js +1 -0
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -442
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/tool-result.js
CHANGED
|
@@ -1,438 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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};
|