@agent-wire/terminal 1.7.7 → 1.7.10
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/README.md +519 -435
- package/dist/api-token.d.ts +9 -0
- package/dist/api-token.js +46 -0
- package/dist/api-token.js.map +1 -0
- package/dist/api.d.ts +50 -10
- package/dist/api.js +78 -31
- package/dist/api.js.map +1 -1
- package/dist/b107-copy.d.ts +68 -0
- package/dist/b107-copy.js +69 -0
- package/dist/b107-copy.js.map +1 -0
- package/dist/catalog-follow.d.ts +32 -0
- package/dist/catalog-follow.js +184 -0
- package/dist/catalog-follow.js.map +1 -0
- package/dist/cli-projection.js +20 -5
- package/dist/cli-projection.js.map +1 -1
- package/dist/cli.js +32 -19
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +14 -1
- package/dist/config.js +54 -9
- package/dist/config.js.map +1 -1
- package/dist/mcp-server.d.ts +1 -6
- package/dist/mcp-server.js +161 -144
- package/dist/mcp-server.js.map +1 -1
- package/dist/read-tool-input.d.ts +15 -0
- package/dist/read-tool-input.js +15 -0
- package/dist/read-tool-input.js.map +1 -0
- package/dist/response-shaper.js +76 -15
- package/dist/response-shaper.js.map +1 -1
- package/dist/resume.d.ts +7 -0
- package/dist/resume.js +15 -0
- package/dist/resume.js.map +1 -0
- package/dist/slots.d.ts +1 -0
- package/dist/slots.js +1 -1
- package/dist/slots.js.map +1 -1
- package/dist/sync/diff.js +7 -1
- package/dist/sync/diff.js.map +1 -1
- package/dist/sync/pull.d.ts +1 -0
- package/dist/sync/pull.js +17 -9
- package/dist/sync/pull.js.map +1 -1
- package/dist/sync/types.d.ts +2 -1
- package/dist/tool-projection.d.ts +19 -2
- package/dist/tool-projection.js +544 -8
- package/dist/tool-projection.js.map +1 -1
- package/package.json +58 -58
package/dist/tool-projection.js
CHANGED
|
@@ -15,6 +15,37 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { createHash } from 'crypto';
|
|
17
17
|
import { z } from 'zod';
|
|
18
|
+
export const CHAIN_RETRY_HELP = 'Reuse the same key to retry safely; a replay returns the first run and charges nothing.';
|
|
19
|
+
/** REST run controls shared by hosted, terminal, and generated chain tools. */
|
|
20
|
+
export function chainRunInputShape() {
|
|
21
|
+
return {
|
|
22
|
+
mode: z.enum(['quote', 'trusted', 'auto']),
|
|
23
|
+
input: z.record(z.unknown()).optional(),
|
|
24
|
+
idempotency_key: z.string().trim().min(1).max(256).optional().describe(CHAIN_RETRY_HELP),
|
|
25
|
+
chain_id: z.string().uuid().optional(),
|
|
26
|
+
quote_id: z.string().optional(),
|
|
27
|
+
max_credits: z.number().int().positive().safe().optional(),
|
|
28
|
+
entry_mode: z.enum(['start', 'direct']).optional(),
|
|
29
|
+
entry: z.object({ mode: z.enum(['start', 'direct']).optional() }).optional(),
|
|
30
|
+
include_steps: z.boolean().optional(),
|
|
31
|
+
async_ack: z.boolean().optional(),
|
|
32
|
+
timeout_ms: z.number().optional(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function toolDefinitionInputShape(def) {
|
|
36
|
+
const shape = jsonSchemaToZodShape(def.parameterSchema);
|
|
37
|
+
if (['wire_poll_chain_run', 'wire_run_chain_poll'].includes(def.toolName)) {
|
|
38
|
+
return { ...shape, chain: z.string().optional(), run_id: z.string().optional(),
|
|
39
|
+
run_handle_path: z.string().optional().describe('The run_handle_path returned by wire_run_chain.'), include_steps: z.boolean().optional() };
|
|
40
|
+
}
|
|
41
|
+
return isChainRunBinding(def.binding)
|
|
42
|
+
? { chain: z.string().min(1), ...chainRunInputShape(), ...shape }
|
|
43
|
+
: shape;
|
|
44
|
+
}
|
|
45
|
+
function isChainRunBinding(binding) {
|
|
46
|
+
return (binding.kind === 'api_method' && binding.method === 'runChain')
|
|
47
|
+
|| (binding.kind === 'http_api' && /^\/(?:api\/v1\/)?wire\/chains\/run$/.test(binding.path ?? ''));
|
|
48
|
+
}
|
|
18
49
|
/**
|
|
19
50
|
* JSON Schema → zod raw shape, degrade-open.
|
|
20
51
|
*
|
|
@@ -85,7 +116,7 @@ export function parseToolDefinition(item) {
|
|
|
85
116
|
const toolName = typeof data.tool_name === 'string' ? data.tool_name.trim() : '';
|
|
86
117
|
if (!/^[a-z][a-z0-9_]*$/.test(toolName))
|
|
87
118
|
return null;
|
|
88
|
-
|
|
119
|
+
let parameterSchema = data.parameter_schema && typeof data.parameter_schema === 'object' && !Array.isArray(data.parameter_schema)
|
|
89
120
|
? data.parameter_schema
|
|
90
121
|
: null;
|
|
91
122
|
if (!parameterSchema)
|
|
@@ -110,9 +141,23 @@ export function parseToolDefinition(item) {
|
|
|
110
141
|
else {
|
|
111
142
|
return null;
|
|
112
143
|
}
|
|
144
|
+
let description = typeof data.description === 'string' ? data.description : (item.title ?? toolName);
|
|
145
|
+
if (isChainRunBinding(binding))
|
|
146
|
+
description = `${description} ${CHAIN_RETRY_HELP}`;
|
|
147
|
+
// Advertise the create endpoint's scope alias for older published schemas.
|
|
148
|
+
// Both hosted and packaged clients use this parser before strict validation.
|
|
149
|
+
if ((binding.kind === 'api_method' && binding.method === 'createAction')
|
|
150
|
+
|| (binding.kind === 'http_api' && /^\/(?:api\/v1\/)?wire\/actions?\/create$/.test(binding.path ?? ''))) {
|
|
151
|
+
const scopeHelp = 'scope: fleet limits the chain to agents of your operator; unscoped is public. The endpoint maps top-level scope to action_definition.scope (stored in definition.scope and structured_data.definition.scope). Both fields must agree when supplied.';
|
|
152
|
+
parameterSchema = { ...parameterSchema, properties: {
|
|
153
|
+
scope: { type: 'string', enum: ['fleet', 'unscoped'], description: scopeHelp },
|
|
154
|
+
...(parameterSchema.properties ?? {}),
|
|
155
|
+
} };
|
|
156
|
+
description = `${description} ${scopeHelp}`;
|
|
157
|
+
}
|
|
113
158
|
return {
|
|
114
159
|
toolName,
|
|
115
|
-
description
|
|
160
|
+
description,
|
|
116
161
|
parameterSchema,
|
|
117
162
|
binding,
|
|
118
163
|
gate: data.gate === 'identity' ? 'identity' : 'confirmed',
|
|
@@ -157,7 +202,8 @@ export async function invokeToolDefinition(def, api, params) {
|
|
|
157
202
|
if (typeof method !== 'function') {
|
|
158
203
|
throw new Error(`tool_definition ${def.configHandle ?? def.toolName} binds to unknown adapter method '${def.binding.method}'`);
|
|
159
204
|
}
|
|
160
|
-
|
|
205
|
+
const result = await method.call(api, params);
|
|
206
|
+
return isChainRunBinding(def.binding) || /chain|action_invoke/.test(def.toolName) ? compactChainReply(result) : result;
|
|
161
207
|
}
|
|
162
208
|
const httpMethod = def.binding.http_method ?? 'POST';
|
|
163
209
|
const path = ('/' + (def.binding.path ?? '/').trim().replace(/^\/+/, '')).replace(/^\/api\/v1(?=\/|$)/, '') || '/';
|
|
@@ -167,20 +213,25 @@ export async function invokeToolDefinition(def, api, params) {
|
|
|
167
213
|
if (value !== undefined)
|
|
168
214
|
query.set(key, String(value));
|
|
169
215
|
}
|
|
170
|
-
|
|
216
|
+
const result = await api.request(httpMethod, `${path}${query.size ? `${path.includes('?') ? '&' : '?'}${query}` : ''}`);
|
|
217
|
+
return /chain|action_invoke/.test(def.toolName) ? compactChainReply(result) : result;
|
|
171
218
|
}
|
|
172
|
-
|
|
219
|
+
const result = await api.request(httpMethod, path, params);
|
|
220
|
+
return isChainRunBinding(def.binding) ? compactChainReply(result) : result;
|
|
173
221
|
}
|
|
174
|
-
/** Discover
|
|
175
|
-
export async function fetchCatalogDefinitionItems(api, kind) {
|
|
222
|
+
/** Discover through the free catalog. Only explicit user refreshes force cursor zero. */
|
|
223
|
+
export async function fetchCatalogDefinitionItems(api, kind, forceRefresh = false) {
|
|
176
224
|
const items = [];
|
|
177
225
|
let cursor = 0;
|
|
178
226
|
while (cursor !== null) {
|
|
179
|
-
const page = await api.request('GET', `/wire/catalog?action=list&kinds=${kind}&limit=200&cursor=${cursor}&refresh=${cursor === 0}`);
|
|
227
|
+
const page = await api.request('GET', `/wire/catalog?action=list&kinds=${kind}&limit=200&cursor=${cursor}&refresh=${forceRefresh && cursor === 0}`);
|
|
180
228
|
const handles = page.entries.map(e => e.handle ?? e.name);
|
|
181
229
|
if (handles.length) {
|
|
182
230
|
const inspected = await api.request('GET', `/wire/catalog?action=inspect&names=${encodeURIComponent(handles.join(','))}`);
|
|
183
231
|
const results = inspected.results ?? (inspected.entry && inspected.definition ? [{ entry: inspected.entry, definition: inspected.definition }] : []);
|
|
232
|
+
if (results.length !== handles.length) {
|
|
233
|
+
throw new Error('Catalog inspection was incomplete; retaining the previous tool set');
|
|
234
|
+
}
|
|
184
235
|
for (const result of results) {
|
|
185
236
|
items.push({ type: kind === 'surface' ? 'surface_config' : kind, handle_path: result.entry.handle, structured_data: result.definition });
|
|
186
237
|
}
|
|
@@ -192,4 +243,489 @@ export async function fetchCatalogDefinitionItems(api, kind) {
|
|
|
192
243
|
}
|
|
193
244
|
return items;
|
|
194
245
|
}
|
|
246
|
+
/** Reporting only: execution inputs and stored invocation results are untouched. */
|
|
247
|
+
export const MAX_CHAIN_REPORT_BYTES = 64 * 1024;
|
|
248
|
+
export const MAX_CHAIN_REPORT_ARRAY_ITEMS = 50;
|
|
249
|
+
const STRING_BYTES = 2048;
|
|
250
|
+
const OPAQUE_FIELDS = new Set(['token', 'quote_id', 'idempotency_key', 'signature', 'cursor', 'next_cursor']);
|
|
251
|
+
const PRIMARY_LOCATOR_KEYS = ['handle_path', 'contribution_handle', 'head_handle'];
|
|
252
|
+
const isReportLocator = (key, value) => /^(?:handle(?:_path|Path)?|.+(?:_handle(?:_path)?|Handle(?:Path)?))$/.test(key)
|
|
253
|
+
&& typeof value === 'string' && value.length <= 256 && /^[a-z0-9_-]+\/\d+\/\d+$/i.test(value);
|
|
254
|
+
const encoder = new TextEncoder();
|
|
255
|
+
const bytes = (value) => encoder.encode(JSON.stringify(value) ?? 'null').length;
|
|
256
|
+
const renderedReportBytes = (value, trace = false) => encoder.encode(JSON.stringify(value, null, trace ? undefined : 2) ?? 'null').length;
|
|
257
|
+
const isOmissionCounts = (value) => Boolean(value && typeof value === 'object'
|
|
258
|
+
&& !Array.isArray(value) && Object.keys(value).length
|
|
259
|
+
&& Object.entries(value).every(([path, count]) => typeof count === 'number'
|
|
260
|
+
? Number.isSafeInteger(count) && count >= 0
|
|
261
|
+
: ['', '/step_results', '/stepResults'].includes(path) && typeof count === 'string' && count.length <= 64 && /^(?:0|[1-9]\d*)$/.test(count)));
|
|
262
|
+
// Large aggregate and direct step totals use decimal text without rounding.
|
|
263
|
+
const encodeCount = (count) => count <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(count) : count.toString();
|
|
264
|
+
// The empty JSON pointer summarizes descendant rows, not direct step rows.
|
|
265
|
+
// Keep direct step counts separate so replay never counts descendants as steps.
|
|
266
|
+
function summarizeOmissions(counts) {
|
|
267
|
+
const summary = {};
|
|
268
|
+
let descendants = BigInt(0);
|
|
269
|
+
for (const [path, count] of Object.entries(counts)) {
|
|
270
|
+
if (path === '/step_results' || path === '/stepResults')
|
|
271
|
+
summary[path] = count;
|
|
272
|
+
else
|
|
273
|
+
descendants += BigInt(count);
|
|
274
|
+
}
|
|
275
|
+
summary[''] = encodeCount(descendants);
|
|
276
|
+
return summary;
|
|
277
|
+
}
|
|
278
|
+
function normalizeOmissions(value, trace = false) {
|
|
279
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
280
|
+
return value;
|
|
281
|
+
const row = value;
|
|
282
|
+
if (!isOmissionCounts(row._omitted))
|
|
283
|
+
return value;
|
|
284
|
+
const required = { _omitted: row._omitted };
|
|
285
|
+
for (const key of PRIMARY_LOCATOR_KEYS)
|
|
286
|
+
if (isReportLocator(key, row[key]))
|
|
287
|
+
required[key] = row[key];
|
|
288
|
+
return renderedReportBytes(required, trace) > MAX_CHAIN_REPORT_BYTES - 512
|
|
289
|
+
? { ...row, _omitted: summarizeOmissions(row._omitted), report_truncated: true } : value;
|
|
290
|
+
}
|
|
291
|
+
// Reporting must not turn a completed invocation into a tool failure. This
|
|
292
|
+
// bounded fallback preserves recovery locators and counts all removed steps.
|
|
293
|
+
function minimalChainReply(value, trace = false) {
|
|
294
|
+
const row = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
295
|
+
const reply = {};
|
|
296
|
+
for (const key of PRIMARY_LOCATOR_KEYS)
|
|
297
|
+
if (isReportLocator(key, row[key]))
|
|
298
|
+
reply[key] = row[key];
|
|
299
|
+
for (const key of ['status', 'run_id'])
|
|
300
|
+
if (typeof row[key] === 'string' && bytes(row[key]) <= 256)
|
|
301
|
+
reply[key] = row[key];
|
|
302
|
+
const omissions = isOmissionCounts(row._omitted) ? { ...row._omitted } : {};
|
|
303
|
+
for (const key of ['step_results', 'stepResults'])
|
|
304
|
+
if (Array.isArray(row[key]) && row[key].length) {
|
|
305
|
+
omissions[`/${key}`] = encodeCount(BigInt(omissions[`/${key}`] ?? 0) + BigInt(row[key].length));
|
|
306
|
+
}
|
|
307
|
+
if (Object.keys(omissions).length)
|
|
308
|
+
reply._omitted = omissions;
|
|
309
|
+
reply.report_truncated = true;
|
|
310
|
+
if (renderedReportBytes(reply, trace) > MAX_CHAIN_REPORT_BYTES)
|
|
311
|
+
reply._omitted = summarizeOmissions(omissions);
|
|
312
|
+
return reply;
|
|
313
|
+
}
|
|
314
|
+
const hint = (handle) => handle
|
|
315
|
+
? `read the full item with wire_read(${handle})`
|
|
316
|
+
: "inspect the run's authenticated invocation receipt";
|
|
317
|
+
const marker = (handle) => ` [truncated; ${hint(handle)}]`;
|
|
318
|
+
// With no locator in scope, report the cut without inventing a retrieval hint.
|
|
319
|
+
const displayHint = (message) => message.replace(/; inspect the run's authenticated invocation receipt/g, '');
|
|
320
|
+
function prefix(value, limit) {
|
|
321
|
+
const encoded = encoder.encode(value);
|
|
322
|
+
if (encoded.length <= limit)
|
|
323
|
+
return value;
|
|
324
|
+
let end = Math.max(0, limit);
|
|
325
|
+
while (end > 0 && (encoded[end] & 0xc0) === 0x80)
|
|
326
|
+
end--;
|
|
327
|
+
return new TextDecoder().decode(encoded.subarray(0, end));
|
|
328
|
+
}
|
|
329
|
+
function bounded(value, limit, handle, trace = false, reservedResult = 0, resultFits = false, keepResultWhole = resultFits) {
|
|
330
|
+
const budget = { left: limit, truncated: false, savings: 0 };
|
|
331
|
+
const take = (n) => { if (budget.left < n)
|
|
332
|
+
return false; budget.left -= n; return true; };
|
|
333
|
+
// Locator preference changes presentation, never the established row shares.
|
|
334
|
+
// Charge the original hint and account for any shorter rendered text when
|
|
335
|
+
// checking the pretty budget, so rounding cannot discard another locator.
|
|
336
|
+
const present = (original, preferred) => {
|
|
337
|
+
const cost = bytes(original);
|
|
338
|
+
const output = displayHint(bytes(preferred) <= cost ? preferred : original);
|
|
339
|
+
budget.savings += cost - bytes(output);
|
|
340
|
+
return output;
|
|
341
|
+
};
|
|
342
|
+
const note = (message, preferred = message) => {
|
|
343
|
+
budget.truncated = true;
|
|
344
|
+
const fallback = '[truncated; reply size limit]';
|
|
345
|
+
return take(bytes(message)) ? present(message, preferred) : take(bytes(fallback)) ? fallback : take(4) ? null : null;
|
|
346
|
+
};
|
|
347
|
+
const share = (allowance, work) => {
|
|
348
|
+
const remaining = budget.left;
|
|
349
|
+
budget.left = Math.max(0, allowance);
|
|
350
|
+
const result = work();
|
|
351
|
+
budget.left = remaining - (Math.max(0, allowance) - budget.left);
|
|
352
|
+
return result;
|
|
353
|
+
};
|
|
354
|
+
const addOmission = (context, path, count) => {
|
|
355
|
+
const exists = Object.hasOwn(context.values, path);
|
|
356
|
+
const previous = exists ? context.values[path] : 0;
|
|
357
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
358
|
+
context.collision = true;
|
|
359
|
+
budget.truncated = true;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const exact = BigInt(previous) + BigInt(count);
|
|
363
|
+
if (exact > BigInt(Number.MAX_SAFE_INTEGER) && !['', '/step_results', '/stepResults'].includes(path)) {
|
|
364
|
+
// Preserve the existing safe per-array count and summarize the new rows
|
|
365
|
+
// in its containing object instead of rounding or losing them.
|
|
366
|
+
context.collision = true;
|
|
367
|
+
budget.truncated = true;
|
|
368
|
+
addOmission(context, '', count);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const next = encodeCount(exact);
|
|
372
|
+
const overhead = context.reserved ? (Object.keys(context.values).length ? 1 : 0) : bytes('_omitted') + 4;
|
|
373
|
+
const cost = exists ? bytes(next) - bytes(context.values[path]) : overhead + bytes(path) + bytes(next) + 1;
|
|
374
|
+
if (cost <= 0 || take(cost)) {
|
|
375
|
+
context.values[path] = next;
|
|
376
|
+
context.reserved = true;
|
|
377
|
+
}
|
|
378
|
+
else
|
|
379
|
+
budget.truncated = true;
|
|
380
|
+
};
|
|
381
|
+
const omit = (context, path, count) => {
|
|
382
|
+
budget.truncated = true;
|
|
383
|
+
if (!context)
|
|
384
|
+
return;
|
|
385
|
+
if (!context.enabled) {
|
|
386
|
+
context.collision = true;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
addOmission(context, path, count);
|
|
390
|
+
};
|
|
391
|
+
const walk = (item, inherited, depth = 0, opaque = false, isTrace = trace, stepList = false, omissions, path = '', preferred = inherited) => {
|
|
392
|
+
if (isTrace && depth > 20)
|
|
393
|
+
return note(`[truncated; nesting limit; ${hint(inherited)}]`, `[truncated; nesting limit; ${hint(preferred)}]`);
|
|
394
|
+
if (typeof item === 'string') {
|
|
395
|
+
// Signed quotes and continuation tokens must remain executable, not become
|
|
396
|
+
// seemingly valid tokens with a reporting suffix. The total budget still applies.
|
|
397
|
+
if (opaque) {
|
|
398
|
+
if (take(bytes(item)))
|
|
399
|
+
return item;
|
|
400
|
+
budget.truncated = true;
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
const suffix = marker(inherited);
|
|
404
|
+
const preferredSuffix = displayHint(marker(preferred));
|
|
405
|
+
const stringLimit = isTrace ? STRING_BYTES : Infinity;
|
|
406
|
+
const existingSuffix = [suffix, preferredSuffix].find(s => item.endsWith(s)
|
|
407
|
+
&& encoder.encode(item.slice(0, -s.length)).length <= stringLimit);
|
|
408
|
+
const already = existingSuffix !== undefined;
|
|
409
|
+
const render = (original, source) => {
|
|
410
|
+
if (suffix === preferredSuffix)
|
|
411
|
+
return original;
|
|
412
|
+
const cost = bytes(original);
|
|
413
|
+
const clean = item.endsWith(preferredSuffix) ? item.slice(0, -preferredSuffix.length) : source;
|
|
414
|
+
let low = 0, high = Math.min(encoder.encode(source).length, encoder.encode(clean).length);
|
|
415
|
+
while (low < high) {
|
|
416
|
+
const mid = Math.ceil((low + high) / 2);
|
|
417
|
+
if (bytes(prefix(clean, mid) + preferredSuffix) <= cost)
|
|
418
|
+
low = mid;
|
|
419
|
+
else
|
|
420
|
+
high = mid - 1;
|
|
421
|
+
}
|
|
422
|
+
return present(original, prefix(clean, low) + preferredSuffix);
|
|
423
|
+
};
|
|
424
|
+
const plainFits = encoder.encode(item).length <= stringLimit;
|
|
425
|
+
if (!plainFits && existingSuffix === preferredSuffix && suffix !== preferredSuffix && bytes(item) <= budget.left) {
|
|
426
|
+
// A REST pass may already have rendered the preferred suffix. Keep it
|
|
427
|
+
// byte-exact without marking a new cut, but charge the inherited-hint
|
|
428
|
+
// allowance that established the surrounding rows' shares. Otherwise
|
|
429
|
+
// recognizing the suffix can silently displace a later array on retry.
|
|
430
|
+
let low = 0, high = Math.min(stringLimit, encoder.encode(item).length);
|
|
431
|
+
while (low < high) {
|
|
432
|
+
const mid = Math.ceil((low + high) / 2);
|
|
433
|
+
if (bytes(prefix(item, mid) + suffix) <= budget.left)
|
|
434
|
+
low = mid;
|
|
435
|
+
else
|
|
436
|
+
high = mid - 1;
|
|
437
|
+
}
|
|
438
|
+
const cost = Math.max(bytes(item), bytes(prefix(item, low) + suffix));
|
|
439
|
+
take(cost);
|
|
440
|
+
budget.savings += cost - bytes(item);
|
|
441
|
+
return item;
|
|
442
|
+
}
|
|
443
|
+
if ((plainFits || already) && take(bytes(item))) {
|
|
444
|
+
return !plainFits && already && existingSuffix !== preferredSuffix
|
|
445
|
+
? render(item, item.slice(0, -existingSuffix.length)) : item;
|
|
446
|
+
}
|
|
447
|
+
budget.truncated = true;
|
|
448
|
+
const source = already ? item.slice(0, -existingSuffix.length) : item;
|
|
449
|
+
// Search the serialized size, including real JSON escaping and UTF-8.
|
|
450
|
+
let low = 0, high = Math.min(stringLimit, encoder.encode(source).length);
|
|
451
|
+
while (low < high) {
|
|
452
|
+
const mid = Math.ceil((low + high) / 2);
|
|
453
|
+
if (bytes(prefix(source, mid) + suffix) <= budget.left)
|
|
454
|
+
low = mid;
|
|
455
|
+
else
|
|
456
|
+
high = mid - 1;
|
|
457
|
+
}
|
|
458
|
+
const clipped = prefix(source, low) + suffix;
|
|
459
|
+
return take(bytes(clipped)) ? render(clipped, prefix(source, low)) : note('[truncated; reply size limit]');
|
|
460
|
+
}
|
|
461
|
+
if (!item || typeof item !== 'object') {
|
|
462
|
+
if (take(bytes(item)))
|
|
463
|
+
return item;
|
|
464
|
+
budget.truncated = true;
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
if (item instanceof Date)
|
|
468
|
+
return walk(item.toJSON(), inherited, depth + 1, false, isTrace, false, undefined, '', preferred);
|
|
469
|
+
if (Array.isArray(item)) {
|
|
470
|
+
if (!take(2))
|
|
471
|
+
return null;
|
|
472
|
+
const output = [];
|
|
473
|
+
const count = isTrace && !stepList ? Math.min(item.length, MAX_CHAIN_REPORT_ARRAY_ITEMS) : item.length;
|
|
474
|
+
for (let i = 0; i < count; i++) {
|
|
475
|
+
if (budget.left < 256)
|
|
476
|
+
break;
|
|
477
|
+
const available = budget.left - 128 - (output.length ? 1 : 0);
|
|
478
|
+
const slots = isTrace ? Math.min(count - i, Math.floor(available / 256)) : 1;
|
|
479
|
+
const allowance = slots > 0 ? Math.floor(available / slots) : 0;
|
|
480
|
+
// A tiny share cannot hold a useful item. Count the remaining items
|
|
481
|
+
// instead of emitting empty objects or null locators for each of them.
|
|
482
|
+
if (allowance < 256)
|
|
483
|
+
break;
|
|
484
|
+
const remaining = budget.left;
|
|
485
|
+
const savings = budget.savings;
|
|
486
|
+
take(output.length ? 1 : 0);
|
|
487
|
+
const child = share(allowance, () => walk(item[i], inherited, depth + 1, false, isTrace, false, omissions, `${path}/${i}`, preferred));
|
|
488
|
+
const source = item[i];
|
|
489
|
+
if (source && typeof source === 'object' && !Array.isArray(source) && child && typeof child === 'object') {
|
|
490
|
+
const row = source;
|
|
491
|
+
const kept = child;
|
|
492
|
+
const lostLocator = PRIMARY_LOCATOR_KEYS.some(key => typeof row[key] === 'string' && row[key].length <= 256 && /^[a-z0-9_-]+\/\d+\/\d+$/i.test(row[key]) && kept[key] !== row[key]);
|
|
493
|
+
if (lostLocator || (Object.values(row).some(v => v !== undefined) && Object.values(kept).every(v => v === undefined))) {
|
|
494
|
+
budget.left = remaining;
|
|
495
|
+
budget.savings = savings;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
output.push(child);
|
|
500
|
+
}
|
|
501
|
+
if (output.length < item.length)
|
|
502
|
+
omit(omissions, path, item.length - output.length);
|
|
503
|
+
return output;
|
|
504
|
+
}
|
|
505
|
+
if (!take(2))
|
|
506
|
+
return null;
|
|
507
|
+
const row = item;
|
|
508
|
+
const locatorKey = (key) => isReportLocator(key, row[key]);
|
|
509
|
+
// Prefer the row's primary locator, then current aliases, then relational
|
|
510
|
+
// aliases (prior/root/parent/source/target). Insertion order only breaks ties.
|
|
511
|
+
const aliasRank = (key) => /^(?:prior|previous|root|parent|source|target|chain)(?:_|[A-Z])/.test(key) ? 1 : 0;
|
|
512
|
+
const originalAliases = depth > 0 ? Object.keys(row) : [];
|
|
513
|
+
const aliases = [...originalAliases].sort((a, b) => aliasRank(a) - aliasRank(b));
|
|
514
|
+
const ownHandleKey = [...PRIMARY_LOCATOR_KEYS, ...aliases].find(locatorKey);
|
|
515
|
+
const allowanceHandleKey = [...PRIMARY_LOCATOR_KEYS, ...originalAliases].find(locatorKey);
|
|
516
|
+
const currentHandle = allowanceHandleKey ? row[allowanceHandleKey] : inherited;
|
|
517
|
+
const preferredHandle = ownHandleKey ? row[ownHandleKey] : preferred;
|
|
518
|
+
const output = {};
|
|
519
|
+
// Recovery locators are paid before the answer, inherited omission maps,
|
|
520
|
+
// or any share reservation can spend their bytes.
|
|
521
|
+
const prepaidLocators = new Set();
|
|
522
|
+
if (depth === 0)
|
|
523
|
+
for (const [key, child] of Object.entries(row)) {
|
|
524
|
+
if (!PRIMARY_LOCATOR_KEYS.includes(key) || !locatorKey(key))
|
|
525
|
+
continue;
|
|
526
|
+
if (!take(bytes(key) + bytes(child) + 1 + (prepaidLocators.size ? 1 : 0))) {
|
|
527
|
+
budget.truncated = true;
|
|
528
|
+
return minimalChainReply(row, isTrace);
|
|
529
|
+
}
|
|
530
|
+
output[key] = child;
|
|
531
|
+
prepaidLocators.add(key);
|
|
532
|
+
}
|
|
533
|
+
// Pay the fitting answer before metadata. Keep its former reserve in the
|
|
534
|
+
// allocation calculation below so existing trace shares remain stable.
|
|
535
|
+
const wantsWholeResult = depth === 0 && !isTrace && keepResultWhole && Object.hasOwn(row, 'result');
|
|
536
|
+
const wholeResult = wantsWholeResult && take(bytes(row.result));
|
|
537
|
+
const prepaidResult = wholeResult ? bytes(row.result) : 0;
|
|
538
|
+
if (wantsWholeResult && !wholeResult)
|
|
539
|
+
budget.truncated = true;
|
|
540
|
+
const existingOmissions = row._omitted;
|
|
541
|
+
const numericOmissions = isOmissionCounts(existingOmissions);
|
|
542
|
+
const localOmissions = { values: {}, enabled: !Object.hasOwn(row, '_omitted') || Boolean(numericOmissions), collision: false, reserved: false };
|
|
543
|
+
if (numericOmissions) {
|
|
544
|
+
const counts = existingOmissions;
|
|
545
|
+
// Mechanical counts from a REST trace survive the MCP pass as numbers.
|
|
546
|
+
// Pay for them before any payload, then accumulate further cuts below.
|
|
547
|
+
if (take(bytes('_omitted') + bytes(counts) + 2)) {
|
|
548
|
+
localOmissions.values = { ...counts };
|
|
549
|
+
localOmissions.reserved = true;
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
budget.truncated = true;
|
|
553
|
+
for (const [path, count] of Object.entries(counts)) {
|
|
554
|
+
if (typeof count === 'number')
|
|
555
|
+
addOmission(localOmissions, path, count);
|
|
556
|
+
else
|
|
557
|
+
localOmissions.collision = true;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
let resultReserve = depth === 0 && Object.hasOwn(row, 'result')
|
|
562
|
+
? Math.max(0, Math.min(reservedResult, Math.max(wholeResult && resultFits ? bytes(row.result) + 128 : 0, limit - 4096)) - prepaidResult) : 0;
|
|
563
|
+
// Primary locators survive even when aliases and payload fill the remaining share.
|
|
564
|
+
const priority = (key) => locatorKey(key) ? (PRIMARY_LOCATOR_KEYS.includes(key) ? -2 : -1) : depth === 0 && ['receipt', 'cascade', 'child_invocation_receipts'].includes(key) ? 4
|
|
565
|
+
: /^(stepName|status|.*handle.*|id|.*_id|cost|total_cost)$/.test(key) ? 0
|
|
566
|
+
: depth === 0 && key === 'result' && resultFits ? 1.5
|
|
567
|
+
: ['step_results', 'stepResults'].includes(key) ? 2 : ['output', 'result'].includes(key) ? 3 : 1;
|
|
568
|
+
const entries = Object.entries(row).filter(([key]) => !prepaidLocators.has(key)
|
|
569
|
+
&& !(numericOmissions && key === '_omitted')).sort(([a], [b]) => priority(a) - priority(b));
|
|
570
|
+
const followers = depth === 0 && entries.some(([key]) => priority(key) === 4);
|
|
571
|
+
let processed = 0;
|
|
572
|
+
for (const [key, child] of entries) {
|
|
573
|
+
const overhead = bytes(key) + 1 + (Object.keys(output).length ? 1 : 0);
|
|
574
|
+
// Reserve the omission marker before consuming payload bytes.
|
|
575
|
+
const minimum = priority(key) <= 0 && (child === null || typeof child !== 'object') ? Math.min(bytes(child), 512) : 128;
|
|
576
|
+
if (budget.left < overhead + 128 + minimum && !(wholeResult && key === 'result'))
|
|
577
|
+
break;
|
|
578
|
+
const remaining = budget.left;
|
|
579
|
+
const savings = budget.savings;
|
|
580
|
+
take(overhead);
|
|
581
|
+
const steps = depth === 0 && (key === 'step_results' || key === 'stepResults');
|
|
582
|
+
const receipt = depth === 0 && ['receipt', 'cascade', 'child_invocation_receipts'].includes(key);
|
|
583
|
+
const available = Math.max(0, budget.left - (depth === 0 && key !== 'result' ? Math.max(128, resultReserve) : 128));
|
|
584
|
+
const stepShare = !Object.hasOwn(row, 'result') ? available : resultFits || wholeResult
|
|
585
|
+
? available - (followers ? Math.min(1024, Math.floor(available / 2)) : 0)
|
|
586
|
+
: Math.floor(available / 2);
|
|
587
|
+
const childPath = `/${key.replace(/~/g, '~0').replace(/\//g, '~1')}`;
|
|
588
|
+
const reported = wholeResult && key === 'result' ? child : steps
|
|
589
|
+
? share(stepShare, () => walk(child, currentHandle, depth + 1, false, true, true, localOmissions, childPath, preferredHandle))
|
|
590
|
+
: share(receipt && resultReserve ? Math.min(available, Math.floor(budget.left / 2)) : available, () => walk(child, currentHandle, depth + 1, OPAQUE_FIELDS.has(key), isTrace || receipt, false, localOmissions, childPath, preferredHandle));
|
|
591
|
+
const emptyObject = reported && typeof reported === 'object' && !Array.isArray(reported)
|
|
592
|
+
&& Object.values(reported).every(v => v === undefined) && child && typeof child === 'object' && Object.values(child).some(v => v !== undefined);
|
|
593
|
+
if ((reported === null && child !== null) || emptyObject) {
|
|
594
|
+
budget.left = remaining;
|
|
595
|
+
budget.savings = savings;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
output[key] = reported;
|
|
599
|
+
if (depth === 0 && key === 'result')
|
|
600
|
+
resultReserve = 0;
|
|
601
|
+
processed++;
|
|
602
|
+
}
|
|
603
|
+
// An earlier metadata field may stop the walk. Its reservation still
|
|
604
|
+
// belongs to the fitting answer, which must not disappear with the trace.
|
|
605
|
+
if (wholeResult && !Object.hasOwn(output, 'result')) {
|
|
606
|
+
take(bytes('result') + 1 + (Object.keys(output).length ? 1 : 0));
|
|
607
|
+
output.result = row.result;
|
|
608
|
+
}
|
|
609
|
+
// Keep the array's existing charge when its count fits in the child share.
|
|
610
|
+
// If that share ran out, the parent's reserved 128 bytes pay the missing
|
|
611
|
+
// count, even when the entire step field was omitted.
|
|
612
|
+
if (depth === 0) {
|
|
613
|
+
for (const key of ['step_results', 'stepResults']) {
|
|
614
|
+
if (!Array.isArray(row[key]))
|
|
615
|
+
continue;
|
|
616
|
+
const kept = Array.isArray(output[key]) ? output[key].length : 0;
|
|
617
|
+
const dropped = row[key].length - kept;
|
|
618
|
+
if (dropped) {
|
|
619
|
+
// The count also describes an absent field: zero rows were kept.
|
|
620
|
+
// Do not synthesize an empty array, including when an authored
|
|
621
|
+
// non-numeric _omitted field prevents mechanical counting.
|
|
622
|
+
const path = `/${key}`;
|
|
623
|
+
const previous = numericOmissions ? BigInt(existingOmissions[path] ?? 0) : BigInt(0);
|
|
624
|
+
const counted = BigInt(localOmissions.values[path] ?? 0) - previous;
|
|
625
|
+
const missing = BigInt(dropped) - counted;
|
|
626
|
+
if (missing > BigInt(0))
|
|
627
|
+
omit(localOmissions, path, Number(missing));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (localOmissions.reserved)
|
|
632
|
+
output._omitted = localOmissions.values;
|
|
633
|
+
if (localOmissions.collision)
|
|
634
|
+
output.report_truncated = true;
|
|
635
|
+
if (processed < entries.length) {
|
|
636
|
+
const message = `[truncated; additional fields omitted; ${hint(currentHandle)}]`;
|
|
637
|
+
const extra = bytes('_truncated') + 2 + bytes(message);
|
|
638
|
+
budget.truncated = true;
|
|
639
|
+
if (take(extra))
|
|
640
|
+
output._truncated = present(message, `[truncated; additional fields omitted; ${hint(preferredHandle)}]`);
|
|
641
|
+
}
|
|
642
|
+
return output;
|
|
643
|
+
};
|
|
644
|
+
return { value: walk(value, handle, 0, false, trace, trace && Array.isArray(value)), truncated: budget.truncated, savings: budget.savings };
|
|
645
|
+
}
|
|
646
|
+
function report(value, handle, trace = false) {
|
|
647
|
+
value = normalizeOmissions(value, trace);
|
|
648
|
+
let limit = 60 * 1024; // Leave room for the REST run/poll envelope additions.
|
|
649
|
+
const hasResult = value && typeof value === 'object' && !Array.isArray(value) && Object.hasOwn(value, 'result');
|
|
650
|
+
const answer = hasResult ? value.result : undefined;
|
|
651
|
+
const resultBytes = hasResult ? bytes(answer) + 1024 : 0;
|
|
652
|
+
// Keep a fitting answer whole; oversized answers still share room with steps.
|
|
653
|
+
// MCP must fit pretty JSON too: promoting a compact-only fit costs trace
|
|
654
|
+
// handles even though the result still cannot be shown whole.
|
|
655
|
+
const prettyFits = trace || !hasResult || (resultBytes <= MAX_CHAIN_REPORT_BYTES
|
|
656
|
+
&& encoder.encode(JSON.stringify({ result: answer }, null, 2)).length <= MAX_CHAIN_REPORT_BYTES - 1024);
|
|
657
|
+
const resultFits = resultBytes <= 60 * 1024 - 4096 && prettyFits;
|
|
658
|
+
const keepResultWhole = !trace && Boolean(hasResult) && prettyFits;
|
|
659
|
+
const reservedResult = resultFits ? resultBytes : Math.floor(limit / 2);
|
|
660
|
+
const primaryBytes = value && typeof value === 'object' && !Array.isArray(value) ? Object.entries(value)
|
|
661
|
+
.filter(([key, value]) => PRIMARY_LOCATOR_KEYS.includes(key) && isReportLocator(key, value))
|
|
662
|
+
.reduce((sum, [key, value]) => sum + bytes(key) + bytes(value) + 2, 0) : 0;
|
|
663
|
+
const priorOmissions = value && typeof value === 'object' && !Array.isArray(value)
|
|
664
|
+
? value._omitted : undefined;
|
|
665
|
+
const omissionBytes = isOmissionCounts(priorOmissions)
|
|
666
|
+
? bytes('_omitted') + bytes(priorOmissions) + 2 : 0;
|
|
667
|
+
const protectedBytes = primaryBytes + omissionBytes;
|
|
668
|
+
const fixedBytes = keepResultWhole ? bytes('result') + bytes(answer) + 1 + protectedBytes : 0;
|
|
669
|
+
const minimum = fixedBytes ? fixedBytes + 128 : 512 + protectedBytes;
|
|
670
|
+
// Large whole MCP answers can use the transport's remaining headroom;
|
|
671
|
+
// the compact REST envelope reserve does not belong to their trace.
|
|
672
|
+
if (keepResultWhole && !resultFits)
|
|
673
|
+
limit = MAX_CHAIN_REPORT_BYTES - 512;
|
|
674
|
+
limit = Math.max(limit, minimum);
|
|
675
|
+
let result = bounded(value, limit, handle, trace, reservedResult, resultFits, keepResultWhole);
|
|
676
|
+
// Only MCP renders pretty JSON; REST traces use the compact JSON budget.
|
|
677
|
+
let prettyLen = trace ? 0 : encoder.encode(JSON.stringify(result.value, null, 2) ?? 'null').length + result.savings;
|
|
678
|
+
while (!trace && prettyLen > MAX_CHAIN_REPORT_BYTES - 512 && limit > minimum) {
|
|
679
|
+
// Preserve the existing retry sequence for locator retention, while a
|
|
680
|
+
// whole result and the parent's omission reserve remain fixed costs.
|
|
681
|
+
const next = Math.max(minimum, Math.min(limit - 1024, Math.floor(limit * (MAX_CHAIN_REPORT_BYTES - 512) / prettyLen)));
|
|
682
|
+
if (next >= limit)
|
|
683
|
+
break;
|
|
684
|
+
const floor = resultFits && limit <= reservedResult + 4096 ? reservedResult : reservedResult + 4096;
|
|
685
|
+
limit = limit > floor && next < floor ? floor : next;
|
|
686
|
+
result = bounded(value, limit, handle, trace, reservedResult, resultFits, keepResultWhole);
|
|
687
|
+
prettyLen = encoder.encode(JSON.stringify(result.value, null, 2) ?? 'null').length + result.savings;
|
|
688
|
+
}
|
|
689
|
+
if (result.truncated && result.value && typeof result.value === 'object' && !Array.isArray(result.value)) {
|
|
690
|
+
result.value.report_truncated = true;
|
|
691
|
+
}
|
|
692
|
+
// The actual rendered reply, including its flag, is the final authority.
|
|
693
|
+
// Metadata can make an otherwise fitting answer impossible alongside the
|
|
694
|
+
// primary locator and omission counts. Cut the answer in that case.
|
|
695
|
+
const renderedBytes = (reply) => renderedReportBytes(reply, trace);
|
|
696
|
+
while (renderedBytes(result.value) > MAX_CHAIN_REPORT_BYTES) {
|
|
697
|
+
const next = Math.max(512 + protectedBytes, Math.min(limit - 1024, Math.floor(limit * (MAX_CHAIN_REPORT_BYTES - 512) / renderedBytes(result.value))));
|
|
698
|
+
if (next >= limit)
|
|
699
|
+
return minimalChainReply(value, trace);
|
|
700
|
+
limit = next;
|
|
701
|
+
result = bounded(value, limit, handle, trace, reservedResult, resultFits, false);
|
|
702
|
+
if (result.value && typeof result.value === 'object' && !Array.isArray(result.value)) {
|
|
703
|
+
result.value.report_truncated = true;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return result.value;
|
|
707
|
+
}
|
|
708
|
+
/** Bounded list for local use; transport replies use compactStepTraceReport. */
|
|
709
|
+
export function compactStepTrace(value, inheritedHandle) {
|
|
710
|
+
try {
|
|
711
|
+
return report(value, inheritedHandle, true);
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
return Array.isArray(value) ? [] : minimalChainReply(value, true);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/** Portable REST envelope: JSON arrays cannot carry their own omission count. */
|
|
718
|
+
export function compactStepTraceReport(value, inheritedHandle) {
|
|
719
|
+
const step_results = compactStepTrace(value, inheritedHandle);
|
|
720
|
+
const dropped = Array.isArray(value) ? value.length - (Array.isArray(step_results) ? step_results.length : 0) : 0;
|
|
721
|
+
return { step_results, ...(dropped ? { _omitted: { '/step_results': dropped }, report_truncated: true } : {}) };
|
|
722
|
+
}
|
|
723
|
+
export function compactChainReply(value) {
|
|
724
|
+
try {
|
|
725
|
+
return report(value);
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
return minimalChainReply(value);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
195
731
|
//# sourceMappingURL=tool-projection.js.map
|