@omercnet/paseo-omp 0.3.0-next.96.1 → 0.3.0-next.99.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 +1 -1
- package/server/provider/connection.ts +35 -8
- package/server/provider/host-tools.ts +223 -26
- package/server/provider/omp-rpc.ts +307 -46
- package/server/provider/subsessions.ts +20 -1
package/package.json
CHANGED
|
@@ -58,6 +58,7 @@ const MAX_CONNECTION_SESSIONS = 32;
|
|
|
58
58
|
const MAX_ACTIVE_OPERATIONS = 128;
|
|
59
59
|
const MAX_PROVIDER_INPUT_BYTES = 2 * 1024 * 1024;
|
|
60
60
|
const MAX_NESTED_OPTION_BYTES = 256 * 1024;
|
|
61
|
+
const MAX_ENV_ENTRIES = 256;
|
|
61
62
|
const MAX_NATIVE_SESSION_RESERVATIONS = 256;
|
|
62
63
|
|
|
63
64
|
function hasOwnEntries(value: unknown): boolean {
|
|
@@ -68,6 +69,19 @@ function hasOwnEntries(value: unknown): boolean {
|
|
|
68
69
|
return false;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function providerOptionsExceedPreflightLimits(value: unknown): boolean {
|
|
73
|
+
if (
|
|
74
|
+
boundedJsonBytes(value, MAX_NESTED_OPTION_BYTES, MAX_ENV_ENTRIES) === Number.POSITIVE_INFINITY
|
|
75
|
+
) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
79
|
+
const unrelatedOptions = Object.fromEntries(
|
|
80
|
+
Object.entries(value).filter(([key]) => key !== "env" && key !== "inheritEnv"),
|
|
81
|
+
);
|
|
82
|
+
return boundedJsonBytes(unrelatedOptions, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY;
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
function preflightProviderInput(input: unknown): void {
|
|
72
86
|
if (!input || typeof input !== "object") throw new OmpPublicError("Invalid provider request");
|
|
73
87
|
const record = input as Record<string, unknown>;
|
|
@@ -82,7 +96,16 @@ function preflightProviderInput(input: unknown): void {
|
|
|
82
96
|
throw new OmpPublicError("Session persistence input is too large");
|
|
83
97
|
}
|
|
84
98
|
const config = record.config as Record<string, unknown> | undefined;
|
|
85
|
-
|
|
99
|
+
if (
|
|
100
|
+
(config?.env !== undefined &&
|
|
101
|
+
boundedJsonBytes(config.env, MAX_NESTED_OPTION_BYTES, MAX_ENV_ENTRIES) ===
|
|
102
|
+
Number.POSITIVE_INFINITY) ||
|
|
103
|
+
(config?.providerOptions !== undefined &&
|
|
104
|
+
providerOptionsExceedPreflightLimits(config.providerOptions))
|
|
105
|
+
) {
|
|
106
|
+
throw new OmpPublicError("Session configuration is too large");
|
|
107
|
+
}
|
|
108
|
+
for (const value of [config?.mcpServers, config?.settings]) {
|
|
86
109
|
if (
|
|
87
110
|
value !== undefined &&
|
|
88
111
|
boundedJsonBytes(value, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY
|
|
@@ -109,13 +132,17 @@ function preflightProviderInput(input: unknown): void {
|
|
|
109
132
|
}
|
|
110
133
|
}
|
|
111
134
|
if (record.type === "catalog" || record.type === "sessions") {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
if (
|
|
136
|
+
record.providerOptions !== undefined &&
|
|
137
|
+
providerOptionsExceedPreflightLimits(record.providerOptions)
|
|
138
|
+
) {
|
|
139
|
+
throw new OmpPublicError("Provider configuration is too large");
|
|
140
|
+
}
|
|
141
|
+
if (
|
|
142
|
+
record.settings !== undefined &&
|
|
143
|
+
boundedJsonBytes(record.settings, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY
|
|
144
|
+
) {
|
|
145
|
+
throw new OmpPublicError("Provider configuration is too large");
|
|
119
146
|
}
|
|
120
147
|
}
|
|
121
148
|
if (record.type === "session.prompt") {
|
|
@@ -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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
286
|
-
)
|
|
287
|
-
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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: [
|
|
310
|
-
details:
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
|
|
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(
|
|
@@ -4,7 +4,13 @@ import { isAbsolute, join } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { ompDataDir } from "../paths";
|
|
6
6
|
import { isValidImagePayload } from "./image";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
boundedJsonBytes,
|
|
9
|
+
boundedJsonMetrics,
|
|
10
|
+
OmpCleanupFailure,
|
|
11
|
+
OmpPublicError,
|
|
12
|
+
utf8Bytes,
|
|
13
|
+
} from "./security";
|
|
8
14
|
import {
|
|
9
15
|
listOmpSessionDescriptors,
|
|
10
16
|
type OmpSessionDescriptor,
|
|
@@ -56,6 +62,14 @@ const MAX_PENDING_ONE_WAY_WRITES = 256;
|
|
|
56
62
|
const MAX_PENDING_WRITE_BYTES = 8 * 1024 * 1024;
|
|
57
63
|
const MAX_LINE_PARTS = 4_096;
|
|
58
64
|
const MAX_ARRAY_ITEMS = 512;
|
|
65
|
+
// OMP read metadata can contain one source entry per displayed line. Bound this optional,
|
|
66
|
+
// opaque field separately so over-budget metadata can be omitted without losing completion events.
|
|
67
|
+
const MAX_OPTIONAL_METADATA_BYTES = MAX_TOOL_PAYLOAD_LENGTH;
|
|
68
|
+
const MAX_OPTIONAL_METADATA_ITEMS = 2_048;
|
|
69
|
+
const MAX_OPTIONAL_METADATA_NODES = 4_096;
|
|
70
|
+
const MAX_TASK_CORRELATION_BYTES = 256 * 1024;
|
|
71
|
+
const MAX_TASK_CORRELATION_ITEMS = 1_024;
|
|
72
|
+
const MAX_TASK_CORRELATION_NODES = 4_096;
|
|
59
73
|
// Tool-intensive OMP turns legitimately exceed 64 blocks; transport byte/node budgets remain the
|
|
60
74
|
// primary resource bounds.
|
|
61
75
|
export const OMP_MAX_CONTENT_PARTS = 4_096;
|
|
@@ -118,6 +132,148 @@ function isBoundedJson(
|
|
|
118
132
|
boundedJsonBytes(value, maxBytes, maxItems, maxBytes, maxNodes) !== Number.POSITIVE_INFINITY
|
|
119
133
|
);
|
|
120
134
|
}
|
|
135
|
+
function optionalMetadataMetrics(value: unknown) {
|
|
136
|
+
return boundedJsonMetrics(
|
|
137
|
+
value,
|
|
138
|
+
MAX_OPTIONAL_METADATA_BYTES,
|
|
139
|
+
MAX_OPTIONAL_METADATA_ITEMS,
|
|
140
|
+
MAX_OPTIONAL_METADATA_BYTES,
|
|
141
|
+
MAX_OPTIONAL_METADATA_NODES,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function optionalMetadataIsBounded(value: unknown): boolean {
|
|
146
|
+
return optionalMetadataMetrics(value) !== undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function omitUnsafeOptionalDetails(value: unknown): unknown {
|
|
150
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
151
|
+
const record = value as Record<string, unknown>;
|
|
152
|
+
if (!Object.hasOwn(record, "details") || optionalMetadataIsBounded(record.details)) return value;
|
|
153
|
+
const { details: _details, ...safe } = record;
|
|
154
|
+
return safe;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const TASK_RESULT_STATUSES: Readonly<Record<string, true>> = {
|
|
158
|
+
pending: true,
|
|
159
|
+
running: true,
|
|
160
|
+
completed: true,
|
|
161
|
+
failed: true,
|
|
162
|
+
error: true,
|
|
163
|
+
aborted: true,
|
|
164
|
+
canceled: true,
|
|
165
|
+
cancelled: true,
|
|
166
|
+
};
|
|
167
|
+
const TASK_PROGRESS_STATUSES: Readonly<Record<string, true>> = {
|
|
168
|
+
pending: true,
|
|
169
|
+
running: true,
|
|
170
|
+
completed: true,
|
|
171
|
+
failed: true,
|
|
172
|
+
aborted: true,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
function boundedTaskId(value: unknown): value is string {
|
|
176
|
+
return typeof value === "string" && value.length > 0 && utf8Bytes(value) <= MAX_ID_LENGTH;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function taskCorrelationDetails(value: unknown): unknown {
|
|
180
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
181
|
+
const details = value as Record<string, unknown>;
|
|
182
|
+
if (!Array.isArray(details.results) || details.results.length > MAX_TASK_CORRELATION_ITEMS)
|
|
183
|
+
return;
|
|
184
|
+
const results: Record<string, unknown>[] = [];
|
|
185
|
+
for (const value of details.results) {
|
|
186
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
187
|
+
const result = value as Record<string, unknown>;
|
|
188
|
+
if (!boundedTaskId(result.id)) return;
|
|
189
|
+
const safe: Record<string, unknown> = { id: result.id };
|
|
190
|
+
if (typeof result.status === "string" && Object.hasOwn(TASK_RESULT_STATUSES, result.status)) {
|
|
191
|
+
safe.status = result.status;
|
|
192
|
+
}
|
|
193
|
+
if (typeof result.aborted === "boolean") safe.aborted = result.aborted;
|
|
194
|
+
if (typeof result.exitCode === "number" && Number.isFinite(result.exitCode)) {
|
|
195
|
+
safe.exitCode = result.exitCode;
|
|
196
|
+
}
|
|
197
|
+
if (
|
|
198
|
+
result.error !== undefined &&
|
|
199
|
+
boundedJsonMetrics(result.error, 4_096, 32, 4_096, 64) !== undefined
|
|
200
|
+
) {
|
|
201
|
+
safe.error = result.error;
|
|
202
|
+
}
|
|
203
|
+
results.push(safe);
|
|
204
|
+
}
|
|
205
|
+
let progress: Record<string, unknown>[] | undefined;
|
|
206
|
+
if (details.progress !== undefined) {
|
|
207
|
+
if (!Array.isArray(details.progress) || details.progress.length > MAX_TASK_CORRELATION_ITEMS) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
progress = [];
|
|
211
|
+
for (const value of details.progress) {
|
|
212
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
213
|
+
const item = value as Record<string, unknown>;
|
|
214
|
+
if (
|
|
215
|
+
!boundedTaskId(item.id) ||
|
|
216
|
+
typeof item.index !== "number" ||
|
|
217
|
+
!Number.isInteger(item.index) ||
|
|
218
|
+
item.index < 0 ||
|
|
219
|
+
item.index >= MAX_TASK_CORRELATION_ITEMS ||
|
|
220
|
+
typeof item.status !== "string" ||
|
|
221
|
+
!Object.hasOwn(TASK_PROGRESS_STATUSES, item.status)
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
progress.push({ id: item.id, index: item.index, status: item.status });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const correlation = { results, ...(progress ? { progress } : {}) };
|
|
229
|
+
return boundedJsonMetrics(
|
|
230
|
+
correlation,
|
|
231
|
+
MAX_TASK_CORRELATION_BYTES,
|
|
232
|
+
MAX_TASK_CORRELATION_ITEMS,
|
|
233
|
+
MAX_TASK_CORRELATION_BYTES,
|
|
234
|
+
MAX_TASK_CORRELATION_NODES,
|
|
235
|
+
)
|
|
236
|
+
? correlation
|
|
237
|
+
: undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function omitOptionalDetails(value: unknown, preserveTaskCorrelation = false): unknown {
|
|
241
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
242
|
+
const record = value as Record<string, unknown>;
|
|
243
|
+
if (!Object.hasOwn(record, "details")) return value;
|
|
244
|
+
const { details: _details, ...structural } = record;
|
|
245
|
+
if (!preserveTaskCorrelation) return structural;
|
|
246
|
+
const correlation = taskCorrelationDetails(record.details);
|
|
247
|
+
return correlation === undefined ? structural : { ...structural, details: correlation };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function createOptionalMetadataSanitizer(): (value: unknown, taskResult?: boolean) => unknown {
|
|
251
|
+
let retainedBytes = 0;
|
|
252
|
+
let retainedNodes = 0;
|
|
253
|
+
return (value, taskResult = false) => {
|
|
254
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
255
|
+
const record = value as Record<string, unknown>;
|
|
256
|
+
if (!Object.hasOwn(record, "details")) return value;
|
|
257
|
+
const metrics = optionalMetadataMetrics(record.details);
|
|
258
|
+
if (
|
|
259
|
+
metrics &&
|
|
260
|
+
retainedBytes + metrics.bytes <= MAX_OPTIONAL_METADATA_BYTES &&
|
|
261
|
+
retainedNodes + metrics.nodes <= MAX_OPTIONAL_METADATA_NODES
|
|
262
|
+
) {
|
|
263
|
+
retainedBytes += metrics.bytes;
|
|
264
|
+
retainedNodes += metrics.nodes;
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
const correlation = taskResult ? taskCorrelationDetails(record.details) : undefined;
|
|
268
|
+
const { details: _details, ...safe } = record;
|
|
269
|
+
return correlation === undefined ? safe : { ...safe, details: correlation };
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const OmpOptionalMetadataSchema = z.preprocess(
|
|
274
|
+
(value) => (optionalMetadataIsBounded(value) ? value : undefined),
|
|
275
|
+
z.unknown().optional(),
|
|
276
|
+
);
|
|
121
277
|
|
|
122
278
|
const OmpContentPartSchema = z
|
|
123
279
|
.object({
|
|
@@ -165,10 +321,7 @@ const OmpMessageIdentityShape = {
|
|
|
165
321
|
responseId: IDENTIFIER.optional(),
|
|
166
322
|
images: OmpImageArraySchema.optional(),
|
|
167
323
|
timestamp: z.number().finite().optional(),
|
|
168
|
-
details:
|
|
169
|
-
.unknown()
|
|
170
|
-
.refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096))
|
|
171
|
-
.optional(),
|
|
324
|
+
details: OmpOptionalMetadataSchema,
|
|
172
325
|
};
|
|
173
326
|
type OmpContentPart = z.infer<typeof OmpContentPartSchema>;
|
|
174
327
|
type OmpMessageIdentity = {
|
|
@@ -409,6 +562,16 @@ const JsonObjectSchema = z.record(z.string(), z.unknown());
|
|
|
409
562
|
const BoundedToolPayloadSchema = z
|
|
410
563
|
.unknown()
|
|
411
564
|
.refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096));
|
|
565
|
+
const OmpToolResultPayloadSchema = z.preprocess(
|
|
566
|
+
omitUnsafeOptionalDetails,
|
|
567
|
+
z
|
|
568
|
+
.unknown()
|
|
569
|
+
.refine(
|
|
570
|
+
(value) =>
|
|
571
|
+
isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, MAX_OPTIONAL_METADATA_ITEMS, 8_192) &&
|
|
572
|
+
isBoundedJson(omitOptionalDetails(value), MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096),
|
|
573
|
+
),
|
|
574
|
+
);
|
|
412
575
|
const OmpHostToolDefinitionSchema = z.object({
|
|
413
576
|
name: NAME,
|
|
414
577
|
label: NAME.optional(),
|
|
@@ -616,13 +779,13 @@ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
|
|
|
616
779
|
toolCallId: IDENTIFIER,
|
|
617
780
|
toolName: NAME,
|
|
618
781
|
args: BoundedToolPayloadSchema.optional(),
|
|
619
|
-
partialResult:
|
|
782
|
+
partialResult: OmpToolResultPayloadSchema,
|
|
620
783
|
}),
|
|
621
784
|
z.object({
|
|
622
785
|
type: z.literal("tool_execution_end"),
|
|
623
786
|
toolCallId: IDENTIFIER,
|
|
624
787
|
toolName: NAME,
|
|
625
|
-
result:
|
|
788
|
+
result: OmpToolResultPayloadSchema,
|
|
626
789
|
isError: z.boolean().optional(),
|
|
627
790
|
}),
|
|
628
791
|
OmpCompactionStartSchema,
|
|
@@ -873,6 +1036,102 @@ const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
|
|
|
873
1036
|
OmpToolApprovalCancelSchema,
|
|
874
1037
|
z.object({ type: z.literal("advisor_yielded") }),
|
|
875
1038
|
]);
|
|
1039
|
+
type OptionalDetailsMapper = (value: unknown, taskResult?: boolean) => unknown;
|
|
1040
|
+
|
|
1041
|
+
function mapRecordField(
|
|
1042
|
+
record: Record<string, unknown>,
|
|
1043
|
+
key: string,
|
|
1044
|
+
map: OptionalDetailsMapper,
|
|
1045
|
+
taskResult = false,
|
|
1046
|
+
): Record<string, unknown> {
|
|
1047
|
+
if (!Object.hasOwn(record, key)) return record;
|
|
1048
|
+
const next = map(record[key], taskResult);
|
|
1049
|
+
return next === record[key] ? record : { ...record, [key]: next };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function mapMessageList(value: unknown, map: OptionalDetailsMapper): unknown {
|
|
1053
|
+
if (!Array.isArray(value)) return value;
|
|
1054
|
+
let changed = false;
|
|
1055
|
+
const messages = value.map((message) => {
|
|
1056
|
+
const taskResult =
|
|
1057
|
+
message !== null &&
|
|
1058
|
+
typeof message === "object" &&
|
|
1059
|
+
!Array.isArray(message) &&
|
|
1060
|
+
(message as Record<string, unknown>).role === "toolResult" &&
|
|
1061
|
+
(message as Record<string, unknown>).toolName === "task";
|
|
1062
|
+
const next = map(message, taskResult);
|
|
1063
|
+
changed ||= next !== message;
|
|
1064
|
+
return next;
|
|
1065
|
+
});
|
|
1066
|
+
return changed ? messages : value;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function mapAgentEventDetails(
|
|
1070
|
+
frame: Record<string, unknown>,
|
|
1071
|
+
map: OptionalDetailsMapper,
|
|
1072
|
+
): Record<string, unknown> {
|
|
1073
|
+
switch (frame.type) {
|
|
1074
|
+
case "message_start":
|
|
1075
|
+
case "message_update":
|
|
1076
|
+
case "message_end": {
|
|
1077
|
+
const message = frame.message;
|
|
1078
|
+
const taskResult =
|
|
1079
|
+
message !== null &&
|
|
1080
|
+
typeof message === "object" &&
|
|
1081
|
+
!Array.isArray(message) &&
|
|
1082
|
+
(message as Record<string, unknown>).role === "toolResult" &&
|
|
1083
|
+
(message as Record<string, unknown>).toolName === "task";
|
|
1084
|
+
return mapRecordField(frame, "message", map, taskResult);
|
|
1085
|
+
}
|
|
1086
|
+
case "tool_execution_update":
|
|
1087
|
+
return mapRecordField(frame, "partialResult", map, frame.toolName === "task");
|
|
1088
|
+
case "tool_execution_end":
|
|
1089
|
+
return mapRecordField(frame, "result", map, frame.toolName === "task");
|
|
1090
|
+
case "agent_end":
|
|
1091
|
+
return mapRecordField(frame, "messages", (messages) => mapMessageList(messages, map));
|
|
1092
|
+
default:
|
|
1093
|
+
return frame;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function mapRuntimeFrameDetails(
|
|
1098
|
+
frame: Record<string, unknown>,
|
|
1099
|
+
map: OptionalDetailsMapper,
|
|
1100
|
+
): Record<string, unknown> {
|
|
1101
|
+
if (frame.type !== "subagent_event") return mapAgentEventDetails(frame, map);
|
|
1102
|
+
if (frame.payload === null || typeof frame.payload !== "object" || Array.isArray(frame.payload)) {
|
|
1103
|
+
return frame;
|
|
1104
|
+
}
|
|
1105
|
+
const payload = frame.payload as Record<string, unknown>;
|
|
1106
|
+
if (payload.event === null || typeof payload.event !== "object" || Array.isArray(payload.event)) {
|
|
1107
|
+
return frame;
|
|
1108
|
+
}
|
|
1109
|
+
const event = mapAgentEventDetails(payload.event as Record<string, unknown>, map);
|
|
1110
|
+
return event === payload.event ? frame : { ...frame, payload: { ...payload, event } };
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function sanitizeMessageListMetadata(value: unknown): unknown {
|
|
1114
|
+
return mapMessageList(value, createOptionalMetadataSanitizer());
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function sanitizeHistoryResponseData(value: unknown): unknown {
|
|
1118
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1119
|
+
return mapRecordField(value as Record<string, unknown>, "messages", sanitizeMessageListMetadata);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function runtimeFrameCollectionLimit(frame: Record<string, unknown>): number {
|
|
1123
|
+
const type = frame.type;
|
|
1124
|
+
if (
|
|
1125
|
+
type === "message_start" ||
|
|
1126
|
+
type === "message_update" ||
|
|
1127
|
+
type === "message_end" ||
|
|
1128
|
+
type === "agent_end" ||
|
|
1129
|
+
type === "subagent_event"
|
|
1130
|
+
) {
|
|
1131
|
+
return OMP_MAX_CONTENT_PARTS;
|
|
1132
|
+
}
|
|
1133
|
+
return 1_024;
|
|
1134
|
+
}
|
|
876
1135
|
const OmpModelsResultSchema = z.object({
|
|
877
1136
|
models: z.array(OmpModelSchema).min(1).max(256),
|
|
878
1137
|
});
|
|
@@ -1989,14 +2248,6 @@ class OmpRpcProcess {
|
|
|
1989
2248
|
return;
|
|
1990
2249
|
}
|
|
1991
2250
|
if (this.receiveKnownResponse(decoded)) return;
|
|
1992
|
-
if (this.receiveDegradedAgentEnd(decoded, true)) return;
|
|
1993
|
-
if (
|
|
1994
|
-
boundedJsonBytes(decoded, MAX_SEMANTIC_FRAME_BYTES, 1_024, MAX_IMAGE_DATA_LENGTH, 4_096) ===
|
|
1995
|
-
Number.POSITIVE_INFINITY
|
|
1996
|
-
) {
|
|
1997
|
-
this.recordProtocolViolation();
|
|
1998
|
-
return;
|
|
1999
|
-
}
|
|
2000
2251
|
const frame = JsonObjectSchema.safeParse(decoded);
|
|
2001
2252
|
if (!frame.success) {
|
|
2002
2253
|
this.recordProtocolViolation();
|
|
@@ -2073,19 +2324,6 @@ class OmpRpcProcess {
|
|
|
2073
2324
|
return;
|
|
2074
2325
|
}
|
|
2075
2326
|
if (this.receiveKnownResponse(decodedFrame)) return;
|
|
2076
|
-
if (this.receiveDegradedAgentEnd(decodedFrame, true)) return;
|
|
2077
|
-
if (
|
|
2078
|
-
boundedJsonBytes(
|
|
2079
|
-
decodedFrame,
|
|
2080
|
-
MAX_SEMANTIC_FRAME_BYTES,
|
|
2081
|
-
1_024,
|
|
2082
|
-
MAX_IMAGE_DATA_LENGTH,
|
|
2083
|
-
4_096,
|
|
2084
|
-
) === Number.POSITIVE_INFINITY
|
|
2085
|
-
) {
|
|
2086
|
-
this.recordProtocolViolation();
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
2327
|
const frameObject = JsonObjectSchema.safeParse(decodedFrame);
|
|
2090
2328
|
if (!frameObject.success) {
|
|
2091
2329
|
this.recordProtocolViolation();
|
|
@@ -2130,6 +2368,11 @@ class OmpRpcProcess {
|
|
|
2130
2368
|
const isBranchHistory = pending.command === "get_branch_messages";
|
|
2131
2369
|
const isHistory =
|
|
2132
2370
|
pending.command === "get_messages" || pending.command === "get_subagent_messages";
|
|
2371
|
+
const responseData = isHistory
|
|
2372
|
+
? sanitizeHistoryResponseData(response.data.data)
|
|
2373
|
+
: response.data.data;
|
|
2374
|
+
const boundedFrame =
|
|
2375
|
+
responseData === response.data.data ? frame : { ...frame, data: responseData };
|
|
2133
2376
|
const responseItemLimit = isBranchHistory ? 1_024 : isHistory ? 100_000 : MAX_ARRAY_ITEMS;
|
|
2134
2377
|
const responseByteLimit =
|
|
2135
2378
|
isBranchHistory || isHistory
|
|
@@ -2146,7 +2389,7 @@ class OmpRpcProcess {
|
|
|
2146
2389
|
: 2_048;
|
|
2147
2390
|
if (
|
|
2148
2391
|
boundedJsonBytes(
|
|
2149
|
-
|
|
2392
|
+
boundedFrame,
|
|
2150
2393
|
responseByteLimit,
|
|
2151
2394
|
responseItemLimit,
|
|
2152
2395
|
MAX_IMAGE_DATA_LENGTH,
|
|
@@ -2162,7 +2405,7 @@ class OmpRpcProcess {
|
|
|
2162
2405
|
if (!settled) return;
|
|
2163
2406
|
if (response.data.success) {
|
|
2164
2407
|
try {
|
|
2165
|
-
settled.beforeResolve?.(
|
|
2408
|
+
settled.beforeResolve?.(responseData);
|
|
2166
2409
|
if (settled.command === "prompt") {
|
|
2167
2410
|
if (this.acceptedPromptIds.size >= MAX_PENDING_REQUESTS) {
|
|
2168
2411
|
const oldest = this.acceptedPromptIds.values().next().value;
|
|
@@ -2170,7 +2413,7 @@ class OmpRpcProcess {
|
|
|
2170
2413
|
}
|
|
2171
2414
|
this.acceptedPromptIds.add(response.data.id);
|
|
2172
2415
|
}
|
|
2173
|
-
settled.resolve(
|
|
2416
|
+
settled.resolve(responseData);
|
|
2174
2417
|
} catch {
|
|
2175
2418
|
settled.reject(new Error("OMP RPC response is invalid"));
|
|
2176
2419
|
}
|
|
@@ -2219,21 +2462,27 @@ class OmpRpcProcess {
|
|
|
2219
2462
|
this.fail(new Error("OMP emitted invalid terminal metadata"));
|
|
2220
2463
|
return true;
|
|
2221
2464
|
}
|
|
2465
|
+
const structuralFrame = mapRuntimeFrameDetails(frame, omitOptionalDetails);
|
|
2222
2466
|
const messagesAreSafe =
|
|
2223
2467
|
frame.messages === undefined ||
|
|
2224
2468
|
(Array.isArray(frame.messages) &&
|
|
2225
2469
|
frame.messages.length <= MAX_ARRAY_ITEMS &&
|
|
2226
2470
|
boundedJsonBytes(
|
|
2227
|
-
|
|
2471
|
+
structuralFrame.messages as unknown[],
|
|
2228
2472
|
MAX_SEMANTIC_FRAME_BYTES,
|
|
2229
|
-
|
|
2473
|
+
OMP_MAX_CONTENT_PARTS,
|
|
2230
2474
|
MAX_TEXT_LENGTH,
|
|
2231
2475
|
4_096,
|
|
2232
2476
|
) !== Number.POSITIVE_INFINITY);
|
|
2233
2477
|
const payloadIsSafe =
|
|
2234
2478
|
messagesAreSafe &&
|
|
2235
|
-
boundedJsonBytes(
|
|
2236
|
-
|
|
2479
|
+
boundedJsonBytes(
|
|
2480
|
+
structuralFrame,
|
|
2481
|
+
MAX_SEMANTIC_FRAME_BYTES,
|
|
2482
|
+
OMP_MAX_CONTENT_PARTS,
|
|
2483
|
+
MAX_IMAGE_DATA_LENGTH,
|
|
2484
|
+
4_096,
|
|
2485
|
+
) !== Number.POSITIVE_INFINITY;
|
|
2237
2486
|
if (onlyUnsafePayload && payloadIsSafe) return false;
|
|
2238
2487
|
if (envelope.data.isTerminal === false) {
|
|
2239
2488
|
this.fail(new Error("OMP emitted an invalid nonterminal agent_end payload"));
|
|
@@ -2260,16 +2509,22 @@ class OmpRpcProcess {
|
|
|
2260
2509
|
this.recordProtocolViolation();
|
|
2261
2510
|
return;
|
|
2262
2511
|
}
|
|
2263
|
-
|
|
2512
|
+
const safeFrame = mapRuntimeFrameDetails(frame, createOptionalMetadataSanitizer());
|
|
2513
|
+
if (this.receiveDegradedAgentEnd(safeFrame, true)) return;
|
|
2264
2514
|
if (
|
|
2265
|
-
boundedJsonBytes(
|
|
2266
|
-
|
|
2515
|
+
boundedJsonBytes(
|
|
2516
|
+
mapRuntimeFrameDetails(safeFrame, omitOptionalDetails),
|
|
2517
|
+
MAX_SEMANTIC_FRAME_BYTES,
|
|
2518
|
+
runtimeFrameCollectionLimit(safeFrame),
|
|
2519
|
+
MAX_IMAGE_DATA_LENGTH,
|
|
2520
|
+
4_096,
|
|
2521
|
+
) === Number.POSITIVE_INFINITY
|
|
2267
2522
|
) {
|
|
2268
2523
|
this.recordProtocolViolation();
|
|
2269
2524
|
return;
|
|
2270
2525
|
}
|
|
2271
2526
|
if (type === "rpc_chunk") {
|
|
2272
|
-
const chunk = OmpChunkFrameSchema.safeParse(
|
|
2527
|
+
const chunk = OmpChunkFrameSchema.safeParse(safeFrame);
|
|
2273
2528
|
if (!chunk.success) this.rejectChunk();
|
|
2274
2529
|
else this.receiveChunk(chunk.data);
|
|
2275
2530
|
return;
|
|
@@ -2287,7 +2542,7 @@ class OmpRpcProcess {
|
|
|
2287
2542
|
this.recordProtocolViolation();
|
|
2288
2543
|
return;
|
|
2289
2544
|
}
|
|
2290
|
-
const ready = OmpReadyFrameSchema.safeParse(
|
|
2545
|
+
const ready = OmpReadyFrameSchema.safeParse(safeFrame);
|
|
2291
2546
|
if (!ready.success) {
|
|
2292
2547
|
this.recordProtocolViolation();
|
|
2293
2548
|
} else {
|
|
@@ -2297,13 +2552,13 @@ class OmpRpcProcess {
|
|
|
2297
2552
|
return;
|
|
2298
2553
|
}
|
|
2299
2554
|
if (type === "response") {
|
|
2300
|
-
this.receiveResponse(
|
|
2555
|
+
this.receiveResponse(safeFrame);
|
|
2301
2556
|
return;
|
|
2302
2557
|
}
|
|
2303
|
-
const event = OmpRuntimeEventSchema.safeParse(
|
|
2558
|
+
const event = OmpRuntimeEventSchema.safeParse(safeFrame);
|
|
2304
2559
|
if (!event.success) {
|
|
2305
|
-
this.rejectMatchingToolApproval(
|
|
2306
|
-
if (type === "agent_end" && this.receiveDegradedAgentEnd(
|
|
2560
|
+
this.rejectMatchingToolApproval(safeFrame);
|
|
2561
|
+
if (type === "agent_end" && this.receiveDegradedAgentEnd(safeFrame, false)) return;
|
|
2307
2562
|
this.recordProtocolViolation();
|
|
2308
2563
|
return;
|
|
2309
2564
|
}
|
|
@@ -2728,7 +2983,10 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
2728
2983
|
);
|
|
2729
2984
|
return {
|
|
2730
2985
|
...transcript,
|
|
2731
|
-
messages: z
|
|
2986
|
+
messages: z
|
|
2987
|
+
.array(OmpMessageSchema)
|
|
2988
|
+
.max(100_000)
|
|
2989
|
+
.parse(sanitizeMessageListMetadata(transcript.messages)),
|
|
2732
2990
|
};
|
|
2733
2991
|
}
|
|
2734
2992
|
async readPersistedSubagentTranscript(options: {
|
|
@@ -2745,7 +3003,10 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
2745
3003
|
);
|
|
2746
3004
|
return {
|
|
2747
3005
|
...transcript,
|
|
2748
|
-
messages: z
|
|
3006
|
+
messages: z
|
|
3007
|
+
.array(OmpMessageSchema)
|
|
3008
|
+
.max(100_000)
|
|
3009
|
+
.parse(sanitizeMessageListMetadata(transcript.messages)),
|
|
2749
3010
|
};
|
|
2750
3011
|
}
|
|
2751
3012
|
|
|
@@ -94,6 +94,18 @@ const TaskResultDetailsSchema = z.object({
|
|
|
94
94
|
exitCode: z.number().optional(),
|
|
95
95
|
error: z.unknown().optional(),
|
|
96
96
|
aborted: z.boolean().optional(),
|
|
97
|
+
status: z
|
|
98
|
+
.enum([
|
|
99
|
+
"pending",
|
|
100
|
+
"running",
|
|
101
|
+
"completed",
|
|
102
|
+
"failed",
|
|
103
|
+
"error",
|
|
104
|
+
"aborted",
|
|
105
|
+
"canceled",
|
|
106
|
+
"cancelled",
|
|
107
|
+
])
|
|
108
|
+
.optional(),
|
|
97
109
|
}),
|
|
98
110
|
)
|
|
99
111
|
.max(MAX_CHILDREN),
|
|
@@ -192,8 +204,15 @@ function replayChildren(messages: readonly OmpMessage[]): ReplayChildRef[] {
|
|
|
192
204
|
const details = taskResultDetails(message);
|
|
193
205
|
const results = details?.results ?? [];
|
|
194
206
|
for (const result of results) {
|
|
207
|
+
const canceled =
|
|
208
|
+
result.aborted === true ||
|
|
209
|
+
result.status === "aborted" ||
|
|
210
|
+
result.status === "canceled" ||
|
|
211
|
+
result.status === "cancelled";
|
|
195
212
|
const failed =
|
|
196
213
|
message.isError === true ||
|
|
214
|
+
result.status === "failed" ||
|
|
215
|
+
result.status === "error" ||
|
|
197
216
|
Boolean(result.error) ||
|
|
198
217
|
(typeof result.exitCode === "number" && result.exitCode !== 0);
|
|
199
218
|
children.push({
|
|
@@ -201,7 +220,7 @@ function replayChildren(messages: readonly OmpMessage[]): ReplayChildRef[] {
|
|
|
201
220
|
agent: result.agent ?? call?.title,
|
|
202
221
|
description: call?.description,
|
|
203
222
|
parentToolCallId: message.toolCallId,
|
|
204
|
-
status:
|
|
223
|
+
status: canceled ? "canceled" : failed ? "failed" : "completed",
|
|
205
224
|
});
|
|
206
225
|
}
|
|
207
226
|
const resultIds = new Set(results.map((result) => result.id));
|