@omercnet/paseo-omp 0.3.0-next.96.1 → 0.3.0-next.97.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "0.3.0-next.96.1",
3
+ "version": "0.3.0-next.97.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -3,6 +3,7 @@ import type {
3
3
  ProviderMcpServerConfig,
4
4
  ProviderSessionConfig,
5
5
  } from "@getpaseo/plugin/server/provider";
6
+ import { isValidImagePayload } from "./image";
6
7
  import {
7
8
  type ConnectedMcpClient,
8
9
  type ConnectedMcpTool,
@@ -37,7 +38,13 @@ const MAX_HOST_TOOL_DESCRIPTION_BYTES = 64 * 1024;
37
38
  const MAX_HOST_TOOL_SCHEMA_BYTES = 256 * 1024;
38
39
  const MAX_HOST_TOOL_CATALOG_BYTES = 768 * 1024;
39
40
  const MAX_HOST_TOOL_RESULT_BYTES = 12 * 1024 * 1024;
41
+ const MAX_HOST_TOOL_CONTENT_BLOCKS = 512;
42
+ const MAX_HOST_TOOL_TEXT_BYTES = 1024 * 1024;
43
+ const MAX_HOST_TOOL_IMAGE_DATA_BYTES = 8 * 1024 * 1024;
40
44
  const MAX_STRUCTURED_CONTENT_FALLBACK_BYTES = 1024 * 1024;
45
+ const CONTENT_TRUNCATED_NOTICE = "[MCP result content truncated: exceeded size limits]";
46
+ const DETAILS_OMITTED_NOTICE = "[MCP structured content omitted: exceeded size limits]";
47
+ const RESULT_TRUNCATED_NOTICE = "[MCP content truncated; details omitted]";
41
48
  const MAX_PENDING_HOST_TOOL_CALLS = 64;
42
49
  const MAX_PENDING_HOST_TOOL_BYTES = 8 * 1024 * 1024;
43
50
  const DEFAULT_INITIALIZATION_TIMEOUT_MS = 20_000;
@@ -272,42 +279,171 @@ async function discoverMcpTools(
272
279
  throw new OmpPublicError("MCP server tool-list pagination exceeds the supported limit");
273
280
  }
274
281
 
275
- function normalizeResult(result: unknown): OmpHostToolResult["result"] {
276
- if (
277
- !result ||
278
- typeof result !== "object" ||
282
+ type HostToolContentBlock = OmpHostToolResult["result"]["content"][number];
283
+
284
+ function hostToolContentIsBounded(content: readonly HostToolContentBlock[]): boolean {
285
+ return (
286
+ boundedJsonBytes(
287
+ content,
288
+ MAX_HOST_TOOL_RESULT_BYTES,
289
+ MAX_HOST_TOOL_CONTENT_BLOCKS,
290
+ MAX_HOST_TOOL_IMAGE_DATA_BYTES,
291
+ 4_096,
292
+ ) !== Number.POSITIVE_INFINITY
293
+ );
294
+ }
295
+
296
+ function noticeBlock(text: string): HostToolContentBlock {
297
+ return { type: "text", text };
298
+ }
299
+
300
+ function appendBoundedNotice(
301
+ content: readonly HostToolContentBlock[],
302
+ notice: string,
303
+ ): HostToolContentBlock[] {
304
+ const trailingNotice = content.at(-1)?.text;
305
+ const contentWasTruncated =
306
+ trailingNotice === CONTENT_TRUNCATED_NOTICE || trailingNotice === RESULT_TRUNCATED_NOTICE;
307
+ const prefix = (contentWasTruncated ? content.slice(0, -1) : content).slice(
308
+ 0,
309
+ MAX_HOST_TOOL_CONTENT_BLOCKS - 1,
310
+ );
311
+ const markerText =
312
+ notice === DETAILS_OMITTED_NOTICE && contentWasTruncated ? RESULT_TRUNCATED_NOTICE : notice;
313
+ const marker = noticeBlock(markerText);
314
+ while (prefix.length > 0 && !hostToolContentIsBounded([...prefix, marker])) prefix.pop();
315
+ return [...prefix, marker];
316
+ }
317
+
318
+ function boundedTextPrefix(
319
+ prefix: readonly HostToolContentBlock[],
320
+ block: HostToolContentBlock,
321
+ ): HostToolContentBlock | undefined {
322
+ if (block.type !== "text" || typeof block.text !== "string") return;
323
+ const marker = noticeBlock(CONTENT_TRUNCATED_NOTICE);
324
+ let low = 0;
325
+ let high = block.text.length;
326
+ let best = "";
327
+ while (low <= high) {
328
+ const middle = Math.floor((low + high) / 2);
329
+ let text = block.text.slice(0, middle);
330
+ if (/\p{Surrogate}$/u.test(text)) text = text.slice(0, -1);
331
+ const candidate = { type: "text", text } satisfies HostToolContentBlock;
332
+ if (hostToolContentIsBounded([...prefix, candidate, marker])) {
333
+ best = text;
334
+ low = middle + 1;
335
+ } else {
336
+ high = middle - 1;
337
+ }
338
+ }
339
+ return best ? { type: "text", text: best } : undefined;
340
+ }
341
+
342
+ function normalizeContent(content: readonly unknown[]): HostToolContentBlock[] {
343
+ const normalized: HostToolContentBlock[] = [];
344
+ for (const value of content) {
345
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
346
+ throw new Error("MCP tool returned invalid content");
347
+ }
348
+ const block = value as Record<string, unknown>;
349
+ if (typeof block.type !== "string" || !block.type || utf8Bytes(block.type) > 256) {
350
+ throw new Error("MCP tool returned invalid content");
351
+ }
352
+
353
+ let candidate: HostToolContentBlock;
354
+ let blockWasTruncated = false;
355
+ if (block.type === "text") {
356
+ if (typeof block.text !== "string") throw new Error("MCP tool returned invalid text content");
357
+ const text = truncateUtf8(block.text, MAX_HOST_TOOL_TEXT_BYTES);
358
+ candidate = { ...block, type: "text", text } as HostToolContentBlock;
359
+ blockWasTruncated = text !== block.text;
360
+ } else if (block.type === "image") {
361
+ if (typeof block.data !== "string" || typeof block.mimeType !== "string") {
362
+ throw new Error("MCP tool returned invalid image content");
363
+ }
364
+ if (block.data.length > MAX_HOST_TOOL_IMAGE_DATA_BYTES) {
365
+ return appendBoundedNotice(normalized, CONTENT_TRUNCATED_NOTICE);
366
+ }
367
+ if (!isValidImagePayload(block.data, block.mimeType, MAX_HOST_TOOL_IMAGE_DATA_BYTES)) {
368
+ throw new Error("MCP tool returned invalid image content");
369
+ }
370
+ candidate = {
371
+ ...block,
372
+ type: "image",
373
+ data: block.data,
374
+ mimeType: block.mimeType,
375
+ } as HostToolContentBlock;
376
+ } else {
377
+ candidate = block as HostToolContentBlock;
378
+ }
379
+
380
+ if (normalized.length >= MAX_HOST_TOOL_CONTENT_BLOCKS) {
381
+ return appendBoundedNotice(normalized, CONTENT_TRUNCATED_NOTICE);
382
+ }
383
+ if (!hostToolContentIsBounded([...normalized, candidate])) {
384
+ const prefix = boundedTextPrefix(normalized, candidate);
385
+ return appendBoundedNotice(
386
+ prefix ? [...normalized, prefix] : normalized,
387
+ CONTENT_TRUNCATED_NOTICE,
388
+ );
389
+ }
390
+ normalized.push(candidate);
391
+ if (blockWasTruncated) return appendBoundedNotice(normalized, CONTENT_TRUNCATED_NOTICE);
392
+ }
393
+ return normalized;
394
+ }
395
+
396
+ function structuredContentIsBounded(value: unknown): boolean {
397
+ return (
279
398
  boundedJsonBytes(
280
- result,
399
+ value,
281
400
  MAX_HOST_TOOL_RESULT_BYTES,
282
401
  1_024,
283
402
  MAX_HOST_TOOL_RESULT_BYTES,
284
403
  4_096,
285
- ) === Number.POSITIVE_INFINITY
286
- ) {
287
- throw new Error("MCP tool returned an invalid or oversized result");
404
+ ) !== Number.POSITIVE_INFINITY
405
+ );
406
+ }
407
+
408
+ function normalizeResult(result: unknown): OmpHostToolResult["result"] {
409
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
410
+ throw new Error("MCP tool returned an invalid result");
288
411
  }
289
412
  const record = result as Record<string, unknown>;
290
413
  if (Array.isArray(record.content)) {
291
414
  const details = record.structuredContent;
292
- const content =
293
- record.content.length === 0 && details !== undefined
294
- ? [
295
- {
296
- type: "text",
297
- text: truncateUtf8(JSON.stringify(details), MAX_STRUCTURED_CONTENT_FALLBACK_BYTES),
298
- },
299
- ]
300
- : record.content;
301
- return parseOmpHostToolAgentResult({
415
+ const detailsAreBounded = details === undefined || structuredContentIsBounded(details);
416
+ let content = normalizeContent(record.content);
417
+ if (content.length === 0 && details !== undefined && detailsAreBounded) {
418
+ content = [
419
+ {
420
+ type: "text",
421
+ text: truncateUtf8(JSON.stringify(details), MAX_STRUCTURED_CONTENT_FALLBACK_BYTES),
422
+ },
423
+ ];
424
+ }
425
+ if (!detailsAreBounded) content = appendBoundedNotice(content, DETAILS_OMITTED_NOTICE);
426
+
427
+ let normalized = parseOmpHostToolAgentResult({
302
428
  content,
303
- ...(details !== undefined ? { details } : {}),
429
+ ...(details !== undefined && detailsAreBounded ? { details } : {}),
304
430
  ...(typeof record.isError === "boolean" ? { isError: record.isError } : {}),
305
431
  });
432
+ if (!structuredContentIsBounded(normalized)) {
433
+ normalized = parseOmpHostToolAgentResult({
434
+ content: appendBoundedNotice(content, DETAILS_OMITTED_NOTICE),
435
+ ...(typeof record.isError === "boolean" ? { isError: record.isError } : {}),
436
+ });
437
+ }
438
+ return normalized;
306
439
  }
307
440
  if (Object.hasOwn(record, "toolResult")) {
441
+ const details = record.toolResult;
442
+ const detailsAreBounded = structuredContentIsBounded(details);
308
443
  return parseOmpHostToolAgentResult({
309
- content: [{ type: "text", text: "MCP tool completed" }],
310
- details: record.toolResult,
444
+ content: [noticeBlock(detailsAreBounded ? "MCP tool completed" : DETAILS_OMITTED_NOTICE)],
445
+ ...(detailsAreBounded ? { details } : {}),
446
+ ...(typeof record.isError === "boolean" ? { isError: record.isError } : {}),
311
447
  });
312
448
  }
313
449
  throw new Error("MCP tool returned an unsupported result");
@@ -704,12 +840,73 @@ export class OmpHostToolsBridge {
704
840
  result: OmpHostToolResult,
705
841
  ): OmpHostToolResult {
706
842
  const limit = runtime.maxHostToolFrameBytes ?? 1024 * 1024;
707
- try {
708
- if (Buffer.byteLength(`${JSON.stringify(result)}\n`) <= limit) return result;
709
- } catch {
710
- // Fall through to the bounded error result.
843
+ const fits = (candidate: OmpHostToolResult) => {
844
+ try {
845
+ return Buffer.byteLength(`${JSON.stringify(candidate)}\n`) <= limit;
846
+ } catch {
847
+ return false;
848
+ }
849
+ };
850
+ if (fits(result)) return result;
851
+
852
+ const withoutDetails: OmpHostToolResult = {
853
+ ...result,
854
+ result: {
855
+ content: appendBoundedNotice(result.result.content, DETAILS_OMITTED_NOTICE),
856
+ ...(result.result.isError !== undefined ? { isError: result.result.isError } : {}),
857
+ },
858
+ };
859
+ if (result.result.details !== undefined && fits(withoutDetails)) return withoutDetails;
860
+
861
+ const trailingNotice = result.result.content.at(-1)?.text;
862
+ const detailsWereOmitted =
863
+ trailingNotice === DETAILS_OMITTED_NOTICE || trailingNotice === RESULT_TRUNCATED_NOTICE;
864
+ const contentWasTruncated =
865
+ trailingNotice === CONTENT_TRUNCATED_NOTICE || trailingNotice === RESULT_TRUNCATED_NOTICE;
866
+ const marker = noticeBlock(
867
+ result.result.details !== undefined || detailsWereOmitted
868
+ ? RESULT_TRUNCATED_NOTICE
869
+ : CONTENT_TRUNCATED_NOTICE,
870
+ );
871
+ const sourceContent =
872
+ detailsWereOmitted || contentWasTruncated
873
+ ? result.result.content.slice(0, -1)
874
+ : result.result.content;
875
+ const boundedContent: HostToolContentBlock[] = [];
876
+ const terminalWith = (content: HostToolContentBlock[]): OmpHostToolResult => ({
877
+ ...result,
878
+ result: {
879
+ content,
880
+ ...(result.result.isError !== undefined ? { isError: result.result.isError } : {}),
881
+ },
882
+ });
883
+ for (const block of sourceContent) {
884
+ const candidate = terminalWith([...boundedContent, block, marker]);
885
+ if (fits(candidate)) {
886
+ boundedContent.push(block);
887
+ continue;
888
+ }
889
+ if (block.type === "text" && typeof block.text === "string") {
890
+ let low = 0;
891
+ let high = block.text.length;
892
+ let best = "";
893
+ while (low <= high) {
894
+ const middle = Math.floor((low + high) / 2);
895
+ let text = block.text.slice(0, middle);
896
+ if (/\p{Surrogate}$/u.test(text)) text = text.slice(0, -1);
897
+ if (fits(terminalWith([...boundedContent, { type: "text", text }, marker]))) {
898
+ best = text;
899
+ low = middle + 1;
900
+ } else {
901
+ high = middle - 1;
902
+ }
903
+ }
904
+ if (best) boundedContent.push({ type: "text", text: best });
905
+ }
906
+ break;
711
907
  }
712
- return errorResult(result.id, OMP_HOST_TOOL_FRAME_LIMIT_ERROR);
908
+ const bounded = terminalWith([...boundedContent, marker]);
909
+ return fits(bounded) ? bounded : errorResult(result.id, OMP_HOST_TOOL_FRAME_LIMIT_ERROR);
713
910
  }
714
911
 
715
912
  private sendTerminal(