@omercnet/paseo-omp 0.3.0-next.103.1 → 0.3.0-next.105.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/catalog.ts +39 -10
- package/server/provider/omp-rpc.ts +147 -36
- package/server/provider/session.ts +104 -46
package/package.json
CHANGED
|
@@ -48,6 +48,27 @@ const THINKING_OPTIONS: readonly ProviderThinkingOption[] = [
|
|
|
48
48
|
{ id: "xhigh", label: "XHigh", description: "Extra-high reasoning" },
|
|
49
49
|
{ id: "max", label: "Max", description: "Maximum reasoning" },
|
|
50
50
|
];
|
|
51
|
+
export const OMP_MAX_CATALOG_MODELS = 256;
|
|
52
|
+
|
|
53
|
+
export function selectOmpModels(
|
|
54
|
+
models: readonly OmpModel[],
|
|
55
|
+
activeModel: OmpModel | null | undefined,
|
|
56
|
+
): OmpModel[] {
|
|
57
|
+
if (models.length <= OMP_MAX_CATALOG_MODELS) return [...models];
|
|
58
|
+
const selected = models.slice(0, OMP_MAX_CATALOG_MODELS);
|
|
59
|
+
if (!activeModel) return selected;
|
|
60
|
+
const active = models.find(
|
|
61
|
+
(model) => model.provider === activeModel.provider && model.id === activeModel.id,
|
|
62
|
+
);
|
|
63
|
+
if (
|
|
64
|
+
!active ||
|
|
65
|
+
selected.some((model) => model.provider === active.provider && model.id === active.id)
|
|
66
|
+
) {
|
|
67
|
+
return selected;
|
|
68
|
+
}
|
|
69
|
+
selected[OMP_MAX_CATALOG_MODELS - 1] = active;
|
|
70
|
+
return selected;
|
|
71
|
+
}
|
|
51
72
|
|
|
52
73
|
export function nativeOmpModelId(model: OmpModel): string {
|
|
53
74
|
if (model.provider.includes("/")) {
|
|
@@ -60,14 +81,9 @@ export function ompModelId(model: OmpModel): string {
|
|
|
60
81
|
const nativeIdentity = `${Buffer.byteLength(model.provider, "utf8")}:${model.provider}${Buffer.byteLength(model.id, "utf8")}:${model.id}`;
|
|
61
82
|
return `omp:model:${createHash("sha256").update(nativeIdentity).digest("hex")}`;
|
|
62
83
|
}
|
|
63
|
-
|
|
64
|
-
export function mapOmpModels(
|
|
65
|
-
models: readonly OmpModel[],
|
|
66
|
-
serializer = new OmpPublicDataSerializer(),
|
|
67
|
-
): ProviderModel[] {
|
|
84
|
+
export function validateOmpModelIdentities(models: readonly OmpModel[]): void {
|
|
68
85
|
const seenIds = new Map<string, string>();
|
|
69
|
-
|
|
70
|
-
const thinkingOptions = thinkingForModel(model);
|
|
86
|
+
for (const model of models) {
|
|
71
87
|
const id = ompModelId(model);
|
|
72
88
|
const nativeIdentity = nativeOmpModelId(model);
|
|
73
89
|
const existing = seenIds.get(id);
|
|
@@ -76,6 +92,17 @@ export function mapOmpModels(
|
|
|
76
92
|
}
|
|
77
93
|
if (existing !== undefined) throw new Error("OMP reported a duplicate model identity");
|
|
78
94
|
seenIds.set(id, nativeIdentity);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function mapOmpModels(
|
|
99
|
+
models: readonly OmpModel[],
|
|
100
|
+
serializer = new OmpPublicDataSerializer(),
|
|
101
|
+
): ProviderModel[] {
|
|
102
|
+
validateOmpModelIdentities(models);
|
|
103
|
+
return models.map((model) => {
|
|
104
|
+
const thinkingOptions = thinkingForModel(model);
|
|
105
|
+
const id = ompModelId(model);
|
|
79
106
|
const provider = serializer.text(model.provider, 256);
|
|
80
107
|
const modelId = serializer.text(model.id, 256);
|
|
81
108
|
const name = model.name ? serializer.text(model.name, 256) : modelId;
|
|
@@ -143,14 +170,16 @@ export async function discoverOmpCatalog(
|
|
|
143
170
|
? [...configuredValues, ...(session.inheritedRedactionValues ?? [])]
|
|
144
171
|
: configuredValues,
|
|
145
172
|
);
|
|
146
|
-
|
|
173
|
+
validateOmpModelIdentities(nativeModels);
|
|
174
|
+
const selectedNativeModels = selectOmpModels(nativeModels, state.model);
|
|
175
|
+
const models = mapOmpModels(selectedNativeModels, serializer);
|
|
147
176
|
if (models.length === 0) throw new Error("OMP reported no available models");
|
|
148
177
|
const defaultModel = state.model ? ompModelId(state.model) : models[0]?.id;
|
|
149
178
|
const currentModel = state.model
|
|
150
|
-
?
|
|
179
|
+
? selectedNativeModels.find(
|
|
151
180
|
(model) => model.provider === state.model?.provider && model.id === state.model.id,
|
|
152
181
|
)
|
|
153
|
-
:
|
|
182
|
+
: selectedNativeModels[0];
|
|
154
183
|
if (state.model && !currentModel) throw new Error("OMP reported an unadvertised active model");
|
|
155
184
|
const thinkingOptions = thinkingForModel(currentModel);
|
|
156
185
|
const defaultThinkingOption = thinkingOptions.some(
|
|
@@ -70,6 +70,7 @@ const MAX_OPTIONAL_METADATA_NODES = 4_096;
|
|
|
70
70
|
const MAX_TASK_CORRELATION_BYTES = 256 * 1024;
|
|
71
71
|
const MAX_TASK_CORRELATION_ITEMS = 1_024;
|
|
72
72
|
const MAX_TASK_CORRELATION_NODES = 4_096;
|
|
73
|
+
const MAX_MODEL_CATALOG_ITEMS = 4_096;
|
|
73
74
|
// Tool-intensive OMP turns legitimately exceed 64 blocks; transport byte/node budgets remain the
|
|
74
75
|
// primary resource bounds.
|
|
75
76
|
export const OMP_MAX_CONTENT_PARTS = 4_096;
|
|
@@ -84,6 +85,32 @@ const MAX_COST_USD = 1_000_000_000;
|
|
|
84
85
|
const MAX_RPC_ERROR_BYTES = 4_096;
|
|
85
86
|
const MAX_RPC_ERROR_CODE_BYTES = 256;
|
|
86
87
|
const PROMPT_SCHEDULING_FAILURE = "OMP prompt scheduling failed";
|
|
88
|
+
const PROTOCOL_VIOLATION_COALESCE_MS = 10_000;
|
|
89
|
+
|
|
90
|
+
export type OmpProtocolViolationCategory =
|
|
91
|
+
| "duplicate-ready"
|
|
92
|
+
| "frame-limit"
|
|
93
|
+
| "incomplete-frame"
|
|
94
|
+
| "interleaved-chunk"
|
|
95
|
+
| "invalid-chunk"
|
|
96
|
+
| "invalid-envelope"
|
|
97
|
+
| "invalid-event"
|
|
98
|
+
| "invalid-event-state"
|
|
99
|
+
| "invalid-json"
|
|
100
|
+
| "invalid-ready"
|
|
101
|
+
| "invalid-response"
|
|
102
|
+
| "remote-frame-error";
|
|
103
|
+
|
|
104
|
+
export interface OmpProtocolViolationDiagnostic {
|
|
105
|
+
category: OmpProtocolViolationCategory;
|
|
106
|
+
occurrenceCount: number;
|
|
107
|
+
frameType?: "ready" | "response" | "rpc_chunk" | "rpc_frame_error";
|
|
108
|
+
maxByteSize?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type PendingProtocolViolation = Omit<OmpProtocolViolationDiagnostic, "occurrenceCount"> & {
|
|
112
|
+
occurrenceCount: number;
|
|
113
|
+
};
|
|
87
114
|
const MAX_CONTEXT_PERCENT = 1_000_000;
|
|
88
115
|
function boundedJsonString(maxBytes: number, minBytes = 0) {
|
|
89
116
|
return z.string().refine((value) => {
|
|
@@ -115,6 +142,17 @@ function boundedString(maxBytes: number, minBytes = 0) {
|
|
|
115
142
|
return bytes >= minBytes && bytes <= maxBytes;
|
|
116
143
|
});
|
|
117
144
|
}
|
|
145
|
+
function boundedOptionalStringArray(maxItems: number, maxBytes: number) {
|
|
146
|
+
return z.unknown().transform((value): string[] | undefined => {
|
|
147
|
+
if (!Array.isArray(value)) return undefined;
|
|
148
|
+
return value
|
|
149
|
+
.filter(
|
|
150
|
+
(item): item is string =>
|
|
151
|
+
typeof item === "string" && item.length > 0 && utf8Bytes(item) <= maxBytes,
|
|
152
|
+
)
|
|
153
|
+
.slice(0, maxItems);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
118
156
|
|
|
119
157
|
const IDENTIFIER = boundedString(MAX_ID_LENGTH, 1);
|
|
120
158
|
const NAME = boundedString(MAX_NAME_LENGTH, 1);
|
|
@@ -191,6 +229,7 @@ function sanitizeLiveDisplayFrame(value: unknown): unknown {
|
|
|
191
229
|
return changed ? { ...frame, messages } : value;
|
|
192
230
|
}
|
|
193
231
|
const OmpThinkingLevelSchema = z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
232
|
+
const OmpCurrentThinkingLevelSchema = boundedString(32).optional().catch(undefined);
|
|
194
233
|
|
|
195
234
|
function isBoundedJson(
|
|
196
235
|
value: unknown,
|
|
@@ -546,19 +585,33 @@ const OmpAvailableCommandSchema = z.object({
|
|
|
546
585
|
.optional(),
|
|
547
586
|
source: boundedString(64).optional(),
|
|
548
587
|
});
|
|
588
|
+
const OmpThinkingMetadataSchema = z
|
|
589
|
+
.unknown()
|
|
590
|
+
.transform((value): { efforts?: string[]; defaultLevel?: string } | undefined => {
|
|
591
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
592
|
+
const record = value as Record<string, unknown>;
|
|
593
|
+
const efforts = boundedOptionalStringArray(16, 32).parse(record.efforts);
|
|
594
|
+
const defaultLevel = boundedString(32).optional().catch(undefined).parse(record.defaultLevel);
|
|
595
|
+
return {
|
|
596
|
+
...(efforts !== undefined ? { efforts } : {}),
|
|
597
|
+
...(defaultLevel !== undefined ? { defaultLevel } : {}),
|
|
598
|
+
};
|
|
599
|
+
});
|
|
549
600
|
const OmpModelSchema = z.object({
|
|
550
601
|
provider: OMP_PROVIDER_NAME,
|
|
551
602
|
id: NAME,
|
|
552
|
-
name: boundedString(MAX_NAME_LENGTH).optional(),
|
|
553
|
-
reasoning: z.boolean().optional(),
|
|
554
|
-
thinking:
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
.
|
|
560
|
-
|
|
561
|
-
|
|
603
|
+
name: boundedString(MAX_NAME_LENGTH).optional().catch(undefined),
|
|
604
|
+
reasoning: z.boolean().optional().catch(undefined),
|
|
605
|
+
thinking: OmpThinkingMetadataSchema.optional(),
|
|
606
|
+
input: boundedOptionalStringArray(16, MAX_NAME_LENGTH).optional(),
|
|
607
|
+
contextWindow: z
|
|
608
|
+
.number()
|
|
609
|
+
.int()
|
|
610
|
+
.nonnegative()
|
|
611
|
+
.max(100_000_000)
|
|
612
|
+
.nullable()
|
|
613
|
+
.optional()
|
|
614
|
+
.catch(undefined),
|
|
562
615
|
});
|
|
563
616
|
const TokenCountSchema = z.number().int().nonnegative().max(MAX_TOKEN_COUNT);
|
|
564
617
|
const OptionalTokenCountSchema = TokenCountSchema.nullable().optional();
|
|
@@ -610,13 +663,13 @@ const OmpCompactionResultSchema = z.object({
|
|
|
610
663
|
});
|
|
611
664
|
const OmpSessionStateSchema = z.object({
|
|
612
665
|
model: OmpModelSchema.nullable().optional(),
|
|
613
|
-
thinkingLevel:
|
|
666
|
+
thinkingLevel: OmpCurrentThinkingLevelSchema,
|
|
614
667
|
isStreaming: z.boolean(),
|
|
615
668
|
isCompacting: z.boolean(),
|
|
616
669
|
sessionId: IDENTIFIER,
|
|
617
670
|
autoCompactionEnabled: z.boolean().optional(),
|
|
618
|
-
contextUsage: OmpContextUsageSchema.nullable().optional(),
|
|
619
|
-
sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
|
|
671
|
+
contextUsage: OmpContextUsageSchema.nullable().optional().catch(undefined),
|
|
672
|
+
sessionFile: boundedString(MAX_PATH_LENGTH).optional().catch(undefined),
|
|
620
673
|
});
|
|
621
674
|
const OmpReadyFrameSchema = z.object({
|
|
622
675
|
type: z.literal("ready"),
|
|
@@ -1217,7 +1270,7 @@ function runtimeFrameCollectionLimit(frame: Record<string, unknown>): number {
|
|
|
1217
1270
|
return 1_024;
|
|
1218
1271
|
}
|
|
1219
1272
|
const OmpModelsResultSchema = z.object({
|
|
1220
|
-
models: z.array(OmpModelSchema).min(1).max(
|
|
1273
|
+
models: z.array(OmpModelSchema).min(1).max(MAX_MODEL_CATALOG_ITEMS),
|
|
1221
1274
|
});
|
|
1222
1275
|
const OmpPromptAckSchema = z.object({ agentInvoked: z.boolean().optional() }).optional();
|
|
1223
1276
|
const OmpAvailableCommandsResultSchema = z.object({
|
|
@@ -1412,6 +1465,7 @@ export interface OmpRpcRuntimeOptions {
|
|
|
1412
1465
|
terminateProcessTree?: (pid: number) => Promise<boolean | "uncertain">;
|
|
1413
1466
|
environment?: NodeJS.ProcessEnv;
|
|
1414
1467
|
requestTimeoutMs?: number;
|
|
1468
|
+
reportProtocolViolation?: (diagnostic: OmpProtocolViolationDiagnostic) => void | Promise<void>;
|
|
1415
1469
|
listSessions?: (
|
|
1416
1470
|
options: OmpSessionListOptions,
|
|
1417
1471
|
) => OmpSessionDescriptor[] | Promise<OmpSessionDescriptor[]>;
|
|
@@ -1943,12 +1997,20 @@ class OmpRpcProcess {
|
|
|
1943
1997
|
private spawnFailedWithoutProcess = false;
|
|
1944
1998
|
private readyReceived = false;
|
|
1945
1999
|
private outputSettled = false;
|
|
2000
|
+
private readonly pendingProtocolViolations = new Map<
|
|
2001
|
+
OmpProtocolViolationCategory,
|
|
2002
|
+
PendingProtocolViolation
|
|
2003
|
+
>();
|
|
2004
|
+
private protocolViolationTimer: TimerHandle | null = null;
|
|
1946
2005
|
|
|
1947
2006
|
constructor(
|
|
1948
2007
|
options: OmpStartOptions,
|
|
1949
2008
|
spawnProcess?: OmpRpcRuntimeOptions["spawnProcess"],
|
|
1950
2009
|
terminateProcessTree?: OmpRpcRuntimeOptions["terminateProcessTree"],
|
|
1951
2010
|
private readonly requestTimeoutMs = REQUEST_TIMEOUT_MS,
|
|
2011
|
+
private readonly reportProtocolViolation: (
|
|
2012
|
+
diagnostic: OmpProtocolViolationDiagnostic,
|
|
2013
|
+
) => void | Promise<void> = (diagnostic) => console.error("OMP protocol violation", diagnostic),
|
|
1952
2014
|
) {
|
|
1953
2015
|
const ready = Promise.withResolvers<ReadyFrame>();
|
|
1954
2016
|
this.rejectReady = ready.reject;
|
|
@@ -2037,7 +2099,11 @@ class OmpRpcProcess {
|
|
|
2037
2099
|
private settleOutput(): void {
|
|
2038
2100
|
if (this.outputSettled) return;
|
|
2039
2101
|
this.outputSettled = true;
|
|
2040
|
-
if (this.lineBytes > 0 || this.discardingLine)
|
|
2102
|
+
if (this.lineBytes > 0 || this.discardingLine) {
|
|
2103
|
+
this.recordProtocolViolation("incomplete-frame", {
|
|
2104
|
+
maxByteSize: this.discardingLine ? this.discardedLineBytes : this.lineBytes,
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2041
2107
|
this.lineParts = [];
|
|
2042
2108
|
this.lineBytes = 0;
|
|
2043
2109
|
this.discardingLine = false;
|
|
@@ -2220,6 +2286,7 @@ class OmpRpcProcess {
|
|
|
2220
2286
|
|
|
2221
2287
|
private async closeProcess(): Promise<void> {
|
|
2222
2288
|
this.closed = true;
|
|
2289
|
+
this.flushProtocolViolations();
|
|
2223
2290
|
this.clearChunk();
|
|
2224
2291
|
this.failPending(new Error("OMP RPC process was closed"));
|
|
2225
2292
|
if (!this.exited) {
|
|
@@ -2316,7 +2383,7 @@ class OmpRpcProcess {
|
|
|
2316
2383
|
this.lineParts = [];
|
|
2317
2384
|
this.lineBytes = 0;
|
|
2318
2385
|
this.discardingLine = true;
|
|
2319
|
-
this.recordProtocolViolation();
|
|
2386
|
+
this.recordProtocolViolation("frame-limit", { maxByteSize: nextBytes });
|
|
2320
2387
|
return;
|
|
2321
2388
|
}
|
|
2322
2389
|
this.lineParts.push(part);
|
|
@@ -2337,7 +2404,7 @@ class OmpRpcProcess {
|
|
|
2337
2404
|
try {
|
|
2338
2405
|
decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload));
|
|
2339
2406
|
} catch {
|
|
2340
|
-
this.recordProtocolViolation();
|
|
2407
|
+
this.recordProtocolViolation("invalid-json", { maxByteSize: payload.byteLength });
|
|
2341
2408
|
return;
|
|
2342
2409
|
}
|
|
2343
2410
|
this.receiveDecodedFrame(decoded, payload.byteLength);
|
|
@@ -2407,7 +2474,7 @@ class OmpRpcProcess {
|
|
|
2407
2474
|
try {
|
|
2408
2475
|
decodedFrame = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(reassembled));
|
|
2409
2476
|
} catch {
|
|
2410
|
-
this.recordProtocolViolation();
|
|
2477
|
+
this.recordProtocolViolation("invalid-json", { maxByteSize: reassembled.byteLength });
|
|
2411
2478
|
return;
|
|
2412
2479
|
}
|
|
2413
2480
|
this.receiveDecodedFrame(decodedFrame, reassembled.byteLength);
|
|
@@ -2433,10 +2500,10 @@ class OmpRpcProcess {
|
|
|
2433
2500
|
const sanitized = sanitizeLiveDisplayFrame(value);
|
|
2434
2501
|
const frame = JsonObjectSchema.safeParse(sanitized);
|
|
2435
2502
|
if (!frame.success) {
|
|
2436
|
-
this.recordProtocolViolation();
|
|
2503
|
+
this.recordProtocolViolation("invalid-envelope", { maxByteSize: rawByteLength });
|
|
2437
2504
|
return;
|
|
2438
2505
|
}
|
|
2439
|
-
this.receiveFrame(frame.data);
|
|
2506
|
+
this.receiveFrame(frame.data, rawByteLength);
|
|
2440
2507
|
}
|
|
2441
2508
|
|
|
2442
2509
|
private receiveKnownResponse(value: unknown): boolean {
|
|
@@ -2461,7 +2528,7 @@ class OmpRpcProcess {
|
|
|
2461
2528
|
if (rawId && knownPending) {
|
|
2462
2529
|
this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
|
|
2463
2530
|
} else if (!rawId || !this.emitAcceptedPromptFailure(rawId, frame)) {
|
|
2464
|
-
this.recordProtocolViolation();
|
|
2531
|
+
this.recordProtocolViolation("invalid-response", { frameType: "response" });
|
|
2465
2532
|
}
|
|
2466
2533
|
return;
|
|
2467
2534
|
}
|
|
@@ -2480,13 +2547,19 @@ class OmpRpcProcess {
|
|
|
2480
2547
|
: response.data.data;
|
|
2481
2548
|
const boundedFrame =
|
|
2482
2549
|
responseData === response.data.data ? frame : { ...frame, data: responseData };
|
|
2483
|
-
const responseItemLimit = isBranchHistory
|
|
2550
|
+
const responseItemLimit = isBranchHistory
|
|
2551
|
+
? 1_024
|
|
2552
|
+
: isHistory
|
|
2553
|
+
? 100_000
|
|
2554
|
+
: pending.command === "get_available_models"
|
|
2555
|
+
? MAX_MODEL_CATALOG_ITEMS
|
|
2556
|
+
: MAX_ARRAY_ITEMS;
|
|
2484
2557
|
const responseByteLimit =
|
|
2485
2558
|
isBranchHistory || isHistory
|
|
2486
2559
|
? Math.min(MAX_REASSEMBLED_FRAME_BYTES, this.reassembledFrameLimit)
|
|
2487
2560
|
: 2 * 1024 * 1024;
|
|
2488
|
-
//
|
|
2489
|
-
// node
|
|
2561
|
+
// Model catalogs are truncated before publication, while the transport still enforces
|
|
2562
|
+
// aggregate byte and node budgets over the complete response.
|
|
2490
2563
|
const responseNodeLimit = isBranchHistory
|
|
2491
2564
|
? 4_096
|
|
2492
2565
|
: isHistory
|
|
@@ -2610,10 +2683,10 @@ class OmpRpcProcess {
|
|
|
2610
2683
|
return true;
|
|
2611
2684
|
}
|
|
2612
2685
|
|
|
2613
|
-
private receiveFrame(frame: Record<string, unknown
|
|
2686
|
+
private receiveFrame(frame: Record<string, unknown>, rawByteLength: number): void {
|
|
2614
2687
|
const type = typeof frame.type === "string" && frame.type.length <= 64 ? frame.type : null;
|
|
2615
2688
|
if (!type) {
|
|
2616
|
-
this.recordProtocolViolation();
|
|
2689
|
+
this.recordProtocolViolation("invalid-envelope", { maxByteSize: rawByteLength });
|
|
2617
2690
|
return;
|
|
2618
2691
|
}
|
|
2619
2692
|
const safeFrame = mapRuntimeFrameDetails(frame, createOptionalMetadataSanitizer());
|
|
@@ -2627,7 +2700,7 @@ class OmpRpcProcess {
|
|
|
2627
2700
|
4_096,
|
|
2628
2701
|
) === Number.POSITIVE_INFINITY
|
|
2629
2702
|
) {
|
|
2630
|
-
this.recordProtocolViolation();
|
|
2703
|
+
this.recordProtocolViolation("frame-limit", { maxByteSize: rawByteLength });
|
|
2631
2704
|
return;
|
|
2632
2705
|
}
|
|
2633
2706
|
if (type === "rpc_chunk") {
|
|
@@ -2638,20 +2711,20 @@ class OmpRpcProcess {
|
|
|
2638
2711
|
}
|
|
2639
2712
|
if (this.chunk) {
|
|
2640
2713
|
this.clearChunk();
|
|
2641
|
-
this.recordProtocolViolation();
|
|
2714
|
+
this.recordProtocolViolation("interleaved-chunk");
|
|
2642
2715
|
}
|
|
2643
2716
|
if (type === "rpc_frame_error") {
|
|
2644
|
-
this.recordProtocolViolation();
|
|
2717
|
+
this.recordProtocolViolation("remote-frame-error", { frameType: "rpc_frame_error" });
|
|
2645
2718
|
return;
|
|
2646
2719
|
}
|
|
2647
2720
|
if (type === "ready") {
|
|
2648
2721
|
if (this.readyReceived) {
|
|
2649
|
-
this.recordProtocolViolation();
|
|
2722
|
+
this.recordProtocolViolation("duplicate-ready", { frameType: "ready" });
|
|
2650
2723
|
return;
|
|
2651
2724
|
}
|
|
2652
2725
|
const ready = OmpReadyFrameSchema.safeParse(safeFrame);
|
|
2653
2726
|
if (!ready.success) {
|
|
2654
|
-
this.recordProtocolViolation();
|
|
2727
|
+
this.recordProtocolViolation("invalid-ready", { frameType: "ready" });
|
|
2655
2728
|
} else {
|
|
2656
2729
|
this.readyReceived = true;
|
|
2657
2730
|
this.resolveReady(ready.data);
|
|
@@ -2666,11 +2739,11 @@ class OmpRpcProcess {
|
|
|
2666
2739
|
if (!event.success) {
|
|
2667
2740
|
this.rejectMatchingToolApproval(safeFrame);
|
|
2668
2741
|
if (type === "agent_end" && this.receiveDegradedAgentEnd(safeFrame, false)) return;
|
|
2669
|
-
this.recordProtocolViolation();
|
|
2742
|
+
this.recordProtocolViolation("invalid-event", { maxByteSize: rawByteLength });
|
|
2670
2743
|
return;
|
|
2671
2744
|
}
|
|
2672
2745
|
if (!this.acceptEventState(event.data)) {
|
|
2673
|
-
this.recordProtocolViolation();
|
|
2746
|
+
this.recordProtocolViolation("invalid-event-state", { maxByteSize: rawByteLength });
|
|
2674
2747
|
return;
|
|
2675
2748
|
}
|
|
2676
2749
|
this.emit(event.data);
|
|
@@ -2775,11 +2848,48 @@ class OmpRpcProcess {
|
|
|
2775
2848
|
|
|
2776
2849
|
private rejectChunk(): void {
|
|
2777
2850
|
this.clearChunk();
|
|
2778
|
-
this.recordProtocolViolation();
|
|
2851
|
+
this.recordProtocolViolation("invalid-chunk", { frameType: "rpc_chunk" });
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
private recordProtocolViolation(
|
|
2855
|
+
category: OmpProtocolViolationCategory,
|
|
2856
|
+
metadata: Omit<OmpProtocolViolationDiagnostic, "category" | "occurrenceCount"> = {},
|
|
2857
|
+
): void {
|
|
2858
|
+
if (!this.protocolViolationTimer) {
|
|
2859
|
+
this.emitProtocolViolation({ category, occurrenceCount: 1, ...metadata });
|
|
2860
|
+
this.protocolViolationTimer = setTimeout(
|
|
2861
|
+
() => this.flushProtocolViolations(),
|
|
2862
|
+
PROTOCOL_VIOLATION_COALESCE_MS,
|
|
2863
|
+
);
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
const pending = this.pendingProtocolViolations.get(category);
|
|
2867
|
+
if (!pending) {
|
|
2868
|
+
this.pendingProtocolViolations.set(category, { category, occurrenceCount: 1, ...metadata });
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
pending.occurrenceCount = Math.min(Number.MAX_SAFE_INTEGER, pending.occurrenceCount + 1);
|
|
2872
|
+
pending.maxByteSize =
|
|
2873
|
+
Math.max(pending.maxByteSize ?? 0, metadata.maxByteSize ?? 0) || undefined;
|
|
2874
|
+
if (pending.frameType !== metadata.frameType) pending.frameType = undefined;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
private flushProtocolViolations(): void {
|
|
2878
|
+
if (this.protocolViolationTimer) clearTimeout(this.protocolViolationTimer);
|
|
2879
|
+
this.protocolViolationTimer = null;
|
|
2880
|
+
for (const diagnostic of this.pendingProtocolViolations.values()) {
|
|
2881
|
+
this.emitProtocolViolation(diagnostic);
|
|
2882
|
+
}
|
|
2883
|
+
this.pendingProtocolViolations.clear();
|
|
2779
2884
|
}
|
|
2780
2885
|
|
|
2781
|
-
private
|
|
2782
|
-
|
|
2886
|
+
private emitProtocolViolation(diagnostic: OmpProtocolViolationDiagnostic): void {
|
|
2887
|
+
try {
|
|
2888
|
+
const reporting = this.reportProtocolViolation(diagnostic);
|
|
2889
|
+
if (reporting) void reporting.catch(() => undefined);
|
|
2890
|
+
} catch {
|
|
2891
|
+
// Diagnostics must never alter transport flow or cleanup.
|
|
2892
|
+
}
|
|
2783
2893
|
}
|
|
2784
2894
|
|
|
2785
2895
|
private fail(error: Error): void {
|
|
@@ -3128,6 +3238,7 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
3128
3238
|
this.options.spawnProcess,
|
|
3129
3239
|
this.options.terminateProcessTree,
|
|
3130
3240
|
options.requestTimeoutMs ?? this.options.requestTimeoutMs,
|
|
3241
|
+
this.options.reportProtocolViolation,
|
|
3131
3242
|
);
|
|
3132
3243
|
const abort = () => void process.close().catch(() => undefined);
|
|
3133
3244
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -11,7 +11,15 @@ import type {
|
|
|
11
11
|
ProviderUsage,
|
|
12
12
|
} from "@getpaseo/plugin/server/provider";
|
|
13
13
|
import { getForgeDefinitionOrNeutral } from "@getpaseo/protocol/forge-manifest";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
mapOmpModels,
|
|
16
|
+
nativeOmpModelId,
|
|
17
|
+
OMP_MODES,
|
|
18
|
+
ompModelId,
|
|
19
|
+
selectOmpModels,
|
|
20
|
+
thinkingForModel,
|
|
21
|
+
validateOmpModelIdentities,
|
|
22
|
+
} from "./catalog";
|
|
15
23
|
import {
|
|
16
24
|
normalizeOmpSessionConfig,
|
|
17
25
|
type OmpRecoveryOptions,
|
|
@@ -939,6 +947,8 @@ export class OmpProviderSession {
|
|
|
939
947
|
private readonly pendingToolPermissions = new Map<string, PendingToolPermission>();
|
|
940
948
|
private readonly inFlightToolPermissions = new Map<string, PendingToolPermission>();
|
|
941
949
|
private readonly resolvedToolApprovalIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
|
|
950
|
+
private unsupportedThinkingNoticePending = false;
|
|
951
|
+
private unsupportedThinkingNoticePublished = false;
|
|
942
952
|
|
|
943
953
|
private constructor(
|
|
944
954
|
id: string,
|
|
@@ -1057,10 +1067,9 @@ export class OmpProviderSession {
|
|
|
1057
1067
|
}
|
|
1058
1068
|
const startOptions: OmpStartOptions = {
|
|
1059
1069
|
...normalizedConfig,
|
|
1060
|
-
//
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
: {}),
|
|
1070
|
+
// Thinking is authorized only after this runtime reports its exact model catalog.
|
|
1071
|
+
thinkingOption: undefined,
|
|
1072
|
+
...(resumeSessionId ? { systemPrompt: undefined, resumeSessionId } : {}),
|
|
1064
1073
|
signal,
|
|
1065
1074
|
environment,
|
|
1066
1075
|
};
|
|
@@ -1096,6 +1105,7 @@ export class OmpProviderSession {
|
|
|
1096
1105
|
() => ({ available: false, commands: [] }),
|
|
1097
1106
|
),
|
|
1098
1107
|
]);
|
|
1108
|
+
validateOmpModelIdentities(nativeModels);
|
|
1099
1109
|
if (effectiveConfig.persist && !native.canReplayHistory) {
|
|
1100
1110
|
throw new OmpPublicError("OMP session persistence requires negotiated RPC protocol v2");
|
|
1101
1111
|
}
|
|
@@ -1109,12 +1119,10 @@ export class OmpProviderSession {
|
|
|
1109
1119
|
if (resumeSessionId && initialState.sessionId !== resumeSessionId) {
|
|
1110
1120
|
throw new OmpPublicError("OMP resumed a different native session");
|
|
1111
1121
|
}
|
|
1112
|
-
const models = mapOmpModels(nativeModels, new OmpPublicDataSerializer(outputRedactionValues));
|
|
1113
|
-
const nativeModelsByPublicId = new Map(
|
|
1114
|
-
nativeModels.map((model) => [ompModelId(model), model] as const),
|
|
1115
|
-
);
|
|
1116
1122
|
if (!resumeSessionId && input.config.model) {
|
|
1117
|
-
const selected =
|
|
1123
|
+
const selected = selectOmpModels(nativeModels, state.model).find(
|
|
1124
|
+
(model) => ompModelId(model) === input.config.model,
|
|
1125
|
+
);
|
|
1118
1126
|
if (!selected) {
|
|
1119
1127
|
throw new OmpPublicError("OMP model is not advertised by the configured session runtime");
|
|
1120
1128
|
}
|
|
@@ -1123,9 +1131,29 @@ export class OmpProviderSession {
|
|
|
1123
1131
|
state = await native.getState();
|
|
1124
1132
|
}
|
|
1125
1133
|
}
|
|
1134
|
+
let currentModel = state.model
|
|
1135
|
+
? nativeModels.find(
|
|
1136
|
+
(model) => model.provider === state.model?.provider && model.id === state.model.id,
|
|
1137
|
+
)
|
|
1138
|
+
: undefined;
|
|
1139
|
+
if (state.model && !currentModel) {
|
|
1140
|
+
throw new OmpPublicError("OMP runtime selected an unadvertised model");
|
|
1141
|
+
}
|
|
1142
|
+
if (!resumeSessionId && input.config.thinkingOption !== undefined) {
|
|
1143
|
+
if (
|
|
1144
|
+
!thinkingForModel(currentModel).some(
|
|
1145
|
+
(option) => option.id === input.config.thinkingOption,
|
|
1146
|
+
)
|
|
1147
|
+
) {
|
|
1148
|
+
throw new OmpPublicError("OMP thinking level is unavailable for the selected model");
|
|
1149
|
+
}
|
|
1150
|
+
if (state.thinkingLevel !== input.config.thinkingOption) {
|
|
1151
|
+
await native.setThinkingLevel(input.config.thinkingOption);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1126
1154
|
const reconciledConfigRevision = bootstrapConfigRevision;
|
|
1127
1155
|
state = await native.getState();
|
|
1128
|
-
|
|
1156
|
+
currentModel = state.model
|
|
1129
1157
|
? nativeModels.find(
|
|
1130
1158
|
(model) => model.provider === state.model?.provider && model.id === state.model.id,
|
|
1131
1159
|
)
|
|
@@ -1133,21 +1161,21 @@ export class OmpProviderSession {
|
|
|
1133
1161
|
if (state.model && !currentModel) {
|
|
1134
1162
|
throw new OmpPublicError("OMP runtime selected an unadvertised model");
|
|
1135
1163
|
}
|
|
1164
|
+
const selectedNativeModels = selectOmpModels(nativeModels, state.model);
|
|
1165
|
+
const models = mapOmpModels(
|
|
1166
|
+
selectedNativeModels,
|
|
1167
|
+
new OmpPublicDataSerializer(outputRedactionValues),
|
|
1168
|
+
);
|
|
1169
|
+
const nativeModelsByPublicId = new Map(
|
|
1170
|
+
nativeModels.map((model) => [ompModelId(model), model] as const),
|
|
1171
|
+
);
|
|
1136
1172
|
const thinkingOptions = thinkingForModel(currentModel);
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
throw new OmpPublicError("OMP thinking level is unavailable for the selected model");
|
|
1144
|
-
}
|
|
1145
|
-
if (
|
|
1146
|
-
committedThinkingLevel &&
|
|
1147
|
-
!thinkingOptions.some((option) => option.id === committedThinkingLevel)
|
|
1148
|
-
) {
|
|
1149
|
-
throw new OmpPublicError("OMP runtime selected an unsupported thinking level");
|
|
1150
|
-
}
|
|
1173
|
+
const applicableLevel = applicableThinkingLevel(currentModel, state.thinkingLevel);
|
|
1174
|
+
const committedThinkingLevel = thinkingOptions.some((option) => option.id === applicableLevel)
|
|
1175
|
+
? applicableLevel
|
|
1176
|
+
: undefined;
|
|
1177
|
+
const unsupportedThinkingLevel =
|
|
1178
|
+
applicableLevel !== undefined && committedThinkingLevel === undefined;
|
|
1151
1179
|
const configState: ProviderConfigState = {
|
|
1152
1180
|
...(state.model ? { model: ompModelId(state.model) } : {}),
|
|
1153
1181
|
mode: normalizedConfig.mode ?? "full",
|
|
@@ -1210,6 +1238,7 @@ export class OmpProviderSession {
|
|
|
1210
1238
|
retireRewindSession,
|
|
1211
1239
|
scheduler,
|
|
1212
1240
|
);
|
|
1241
|
+
session.unsupportedThinkingNoticePending = unsupportedThinkingLevel;
|
|
1213
1242
|
|
|
1214
1243
|
if (bootstrapConfigRevision !== reconciledConfigRevision) {
|
|
1215
1244
|
session.configRefreshDirty = true;
|
|
@@ -1268,6 +1297,7 @@ export class OmpProviderSession {
|
|
|
1268
1297
|
: {}),
|
|
1269
1298
|
...(this.config.title ? { title: this.dataFilter.text(this.config.title, 256) } : {}),
|
|
1270
1299
|
});
|
|
1300
|
+
this.publishUnsupportedThinkingNotice();
|
|
1271
1301
|
this.emit({ type: "session.config", sessionId: this.id, config: this.configState });
|
|
1272
1302
|
if (this.replayHistoryOnOpen) await this.replayHistory(true);
|
|
1273
1303
|
this.publishCommands(this.commandCatalog);
|
|
@@ -1275,6 +1305,22 @@ export class OmpProviderSession {
|
|
|
1275
1305
|
this.readyPublished = true;
|
|
1276
1306
|
if (this.configRefreshDirty) this.scheduleCommittedConfigRefresh();
|
|
1277
1307
|
}
|
|
1308
|
+
private publishUnsupportedThinkingNotice(): void {
|
|
1309
|
+
if (!this.unsupportedThinkingNoticePending || this.unsupportedThinkingNoticePublished) return;
|
|
1310
|
+
this.unsupportedThinkingNoticePending = false;
|
|
1311
|
+
this.unsupportedThinkingNoticePublished = true;
|
|
1312
|
+
this.emit({
|
|
1313
|
+
type: "session.notice",
|
|
1314
|
+
sessionId: this.id,
|
|
1315
|
+
notice: {
|
|
1316
|
+
id: "omp:unsupported-thinking-level",
|
|
1317
|
+
severity: "warning",
|
|
1318
|
+
title: "OMP thinking level unavailable",
|
|
1319
|
+
description:
|
|
1320
|
+
"OMP reported a thinking level that the active model does not advertise. Paseo omitted it from the session configuration.",
|
|
1321
|
+
},
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1278
1324
|
|
|
1279
1325
|
private usageFrom(
|
|
1280
1326
|
state: OmpSessionState | undefined,
|
|
@@ -1685,6 +1731,17 @@ export class OmpProviderSession {
|
|
|
1685
1731
|
if (this.activeTurn || beforeState.isStreaming || beforeState.isCompacting) {
|
|
1686
1732
|
throw new OmpPublicError("Cannot rewind the OMP conversation while a turn is active");
|
|
1687
1733
|
}
|
|
1734
|
+
const beforeAdvertisedModel = beforeState.model
|
|
1735
|
+
? this.nativeModelsByPublicId.get(ompModelId(beforeState.model))
|
|
1736
|
+
: undefined;
|
|
1737
|
+
if (beforeState.model && !beforeAdvertisedModel) {
|
|
1738
|
+
throw new OmpPublicError("OMP runtime selected an unadvertised model");
|
|
1739
|
+
}
|
|
1740
|
+
const restorableThinkingLevel = thinkingForModel(beforeAdvertisedModel).some(
|
|
1741
|
+
(option) => option.id === beforeState.thinkingLevel,
|
|
1742
|
+
)
|
|
1743
|
+
? beforeState.thinkingLevel
|
|
1744
|
+
: undefined;
|
|
1688
1745
|
branchMutationPossible = true;
|
|
1689
1746
|
let result: OmpBranchResult;
|
|
1690
1747
|
try {
|
|
@@ -1727,13 +1784,10 @@ export class OmpProviderSession {
|
|
|
1727
1784
|
}
|
|
1728
1785
|
await runtime.setModel(beforeState.model.provider, beforeState.model.id);
|
|
1729
1786
|
}
|
|
1730
|
-
if (thinkingChanged) {
|
|
1731
|
-
|
|
1732
|
-
throw new OmpPublicError("OMP changed thinking level while rewinding the conversation");
|
|
1733
|
-
}
|
|
1734
|
-
await runtime.setThinkingLevel(beforeState.thinkingLevel);
|
|
1787
|
+
if (thinkingChanged && restorableThinkingLevel) {
|
|
1788
|
+
await runtime.setThinkingLevel(restorableThinkingLevel);
|
|
1735
1789
|
}
|
|
1736
|
-
if (modelChanged || thinkingChanged) {
|
|
1790
|
+
if (modelChanged || (thinkingChanged && restorableThinkingLevel)) {
|
|
1737
1791
|
state = await runtime.getState();
|
|
1738
1792
|
this.requireCurrentRuntime(runtime, generation);
|
|
1739
1793
|
}
|
|
@@ -1741,7 +1795,7 @@ export class OmpProviderSession {
|
|
|
1741
1795
|
state.sessionId !== this.nativeSessionId ||
|
|
1742
1796
|
state.model?.provider !== beforeState.model?.provider ||
|
|
1743
1797
|
state.model?.id !== beforeState.model?.id ||
|
|
1744
|
-
state.thinkingLevel !==
|
|
1798
|
+
(restorableThinkingLevel !== undefined && state.thinkingLevel !== restorableThinkingLevel)
|
|
1745
1799
|
) {
|
|
1746
1800
|
throw new OmpPublicError("OMP did not preserve session configuration while rewinding");
|
|
1747
1801
|
}
|
|
@@ -2341,7 +2395,10 @@ export class OmpProviderSession {
|
|
|
2341
2395
|
const targetModel = targetModelId
|
|
2342
2396
|
? this.nativeModelsByPublicId.get(targetModelId)
|
|
2343
2397
|
: undefined;
|
|
2344
|
-
|
|
2398
|
+
const targetModelPublished = this.configState.models.some(
|
|
2399
|
+
(model) => model.id === targetModelId,
|
|
2400
|
+
);
|
|
2401
|
+
if (input.changes.model !== undefined && (!targetModel || !targetModelPublished)) {
|
|
2345
2402
|
throw new OmpPublicError("OMP model selection is unavailable");
|
|
2346
2403
|
}
|
|
2347
2404
|
if (
|
|
@@ -2588,13 +2645,18 @@ export class OmpProviderSession {
|
|
|
2588
2645
|
throw new OmpCatalogEscape("OMP runtime selected an unadvertised model");
|
|
2589
2646
|
}
|
|
2590
2647
|
const thinkingOptions = thinkingForModel(advertisedModel);
|
|
2591
|
-
const
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
) {
|
|
2596
|
-
|
|
2648
|
+
const applicableLevel = applicableThinkingLevel(advertisedModel, state.thinkingLevel);
|
|
2649
|
+
const committedThinkingLevel = thinkingOptions.some((option) => option.id === applicableLevel)
|
|
2650
|
+
? applicableLevel
|
|
2651
|
+
: undefined;
|
|
2652
|
+
if (applicableLevel !== undefined && committedThinkingLevel === undefined) {
|
|
2653
|
+
this.unsupportedThinkingNoticePending = true;
|
|
2654
|
+
this.publishUnsupportedThinkingNotice();
|
|
2597
2655
|
}
|
|
2656
|
+
const models = mapOmpModels(
|
|
2657
|
+
selectOmpModels([...this.nativeModelsByPublicId.values()], state.model),
|
|
2658
|
+
this.dataFilter,
|
|
2659
|
+
);
|
|
2598
2660
|
this.configRefreshAttempts = 0;
|
|
2599
2661
|
this.cancelConfigRefreshRetry();
|
|
2600
2662
|
const nextConfig: ProviderConfigState = {
|
|
@@ -2603,12 +2665,15 @@ export class OmpProviderSession {
|
|
|
2603
2665
|
...(committedThinkingLevel
|
|
2604
2666
|
? { thinkingOption: committedThinkingLevel }
|
|
2605
2667
|
: { thinkingOption: undefined }),
|
|
2668
|
+
models,
|
|
2606
2669
|
thinkingOptions,
|
|
2607
2670
|
};
|
|
2608
2671
|
const changed =
|
|
2609
2672
|
force ||
|
|
2610
2673
|
nextConfig.model !== this.configState.model ||
|
|
2611
2674
|
nextConfig.thinkingOption !== this.configState.thinkingOption ||
|
|
2675
|
+
nextConfig.models.some((model, index) => model.id !== this.configState.models[index]?.id) ||
|
|
2676
|
+
this.configState.models.length !== nextConfig.models.length ||
|
|
2612
2677
|
nextConfig.thinkingOptions.length !== this.configState.thinkingOptions.length ||
|
|
2613
2678
|
nextConfig.thinkingOptions.some(
|
|
2614
2679
|
(option, index) =>
|
|
@@ -2929,13 +2994,6 @@ export class OmpProviderSession {
|
|
|
2929
2994
|
if (state.model && !advertisedModel) {
|
|
2930
2995
|
throw new Error("OMP recovered with an unadvertised model");
|
|
2931
2996
|
}
|
|
2932
|
-
const recoveredThinkingLevel = applicableThinkingLevel(advertisedModel, state.thinkingLevel);
|
|
2933
|
-
if (
|
|
2934
|
-
recoveredThinkingLevel &&
|
|
2935
|
-
!thinkingForModel(advertisedModel).some((option) => option.id === recoveredThinkingLevel)
|
|
2936
|
-
) {
|
|
2937
|
-
throw new Error("OMP recovered with an unsupported thinking level");
|
|
2938
|
-
}
|
|
2939
2997
|
if (this.closed) throw new Error("OMP session closed while runtime recovery was pending");
|
|
2940
2998
|
if (!this.hostTools.isBoundTo(recovered)) {
|
|
2941
2999
|
throw new Error("OMP host tool bridge detached during recovery");
|