@omercnet/paseo-omp 0.2.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/CHANGELOG.md +87 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/SUPPORT.md +40 -0
- package/TESTING.md +147 -0
- package/client/hub-icon.tsx +12 -0
- package/client/hub-popover.tsx +132 -0
- package/client/hub-status.ts +29 -0
- package/client/memory-panel.tsx +71 -0
- package/client/memory-popover.tsx +70 -0
- package/client/omp-config-surface.tsx +1274 -0
- package/client/omp-doc-links.ts +117 -0
- package/client/omp-plugin-manager.tsx +833 -0
- package/client/provider-diagnostics-state.ts +250 -0
- package/client/provider-icon.tsx +27 -0
- package/client/provider-image.tsx +66 -0
- package/client/quota-popover.tsx +150 -0
- package/client/quota-state.ts +131 -0
- package/client/sessions-popover.tsx +73 -0
- package/docs/alpha-release-checklist.md +70 -0
- package/docs/configuration.md +122 -0
- package/docs/core-provider-issue-audit.md +108 -0
- package/docs/installation.md +73 -0
- package/index.client.tsx +272 -0
- package/index.server.ts +51 -0
- package/package.json +84 -0
- package/paseo-plugin.json +5 -0
- package/server/hub.ts +145 -0
- package/server/memory.ts +86 -0
- package/server/mutation-queue.ts +12 -0
- package/server/omp-config.ts +126 -0
- package/server/omp-plugins.ts +627 -0
- package/server/omp-settings.ts +291 -0
- package/server/paths.ts +64 -0
- package/server/provider/catalog.ts +173 -0
- package/server/provider/config-normalization.ts +148 -0
- package/server/provider/connection.ts +992 -0
- package/server/provider/host-tools.ts +706 -0
- package/server/provider/image.ts +143 -0
- package/server/provider/mcp-transport.ts +394 -0
- package/server/provider/omp-rpc.ts +2739 -0
- package/server/provider/omp.svg +5 -0
- package/server/provider/provider-options.ts +27 -0
- package/server/provider/registration.ts +151 -0
- package/server/provider/security.ts +317 -0
- package/server/provider/session-descriptors.ts +431 -0
- package/server/provider/session.ts +4451 -0
- package/server/provider/settings.ts +78 -0
- package/server/provider/subsessions.ts +847 -0
- package/server/provider/timeline-projector.ts +1764 -0
- package/server/provider-diagnostics.ts +1057 -0
- package/server/quota.ts +54 -0
- package/server/sessions.ts +58 -0
- package/shared/hub.ts +43 -0
- package/shared/memory.ts +23 -0
- package/shared/omp-config.ts +81 -0
- package/shared/omp-plugins.ts +223 -0
- package/shared/omp-settings.ts +207 -0
- package/shared/provider-diagnostics.ts +117 -0
- package/shared/provider-image.ts +160 -0
- package/shared/quota.ts +22 -0
- package/shared/sessions.ts +23 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,1764 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
ProviderEvent,
|
|
4
|
+
ProviderTimelineItem,
|
|
5
|
+
ProviderToolCallDetail,
|
|
6
|
+
} from "@getpaseo/plugin/server/provider";
|
|
7
|
+
import { isOmpImageMimeType, isValidImagePayload, type OmpImageMimeType } from "./image";
|
|
8
|
+
import type { OmpMessage, OmpRpcEvent } from "./omp-rpc";
|
|
9
|
+
import {
|
|
10
|
+
boundedJsonBytes,
|
|
11
|
+
type JsonValue,
|
|
12
|
+
OmpPublicDataSerializer,
|
|
13
|
+
OmpPublicError,
|
|
14
|
+
utf8Bytes,
|
|
15
|
+
} from "./security";
|
|
16
|
+
|
|
17
|
+
const STREAM_FRAME_MS = 32;
|
|
18
|
+
const MAX_STREAM_CONTENT_BLOCKS = 64;
|
|
19
|
+
const MAX_STREAM_TEXT_LENGTH = 4 * 1024 * 1024;
|
|
20
|
+
const MAX_ACTIVE_TOOLS = 64;
|
|
21
|
+
const MAX_TODOS = 256;
|
|
22
|
+
const MAX_TURN_NATIVE_IDENTITIES = 1_024;
|
|
23
|
+
const MAX_REPLAY_NATIVE_IDENTITIES = 100_000;
|
|
24
|
+
const MAX_PUBLIC_TOOL_PAYLOAD_BYTES = 256 * 1024;
|
|
25
|
+
const MAX_ACTIVE_TOOL_BYTES = 4 * 1024 * 1024;
|
|
26
|
+
const MAX_REVERT_TARGETS = MAX_REPLAY_NATIVE_IDENTITIES;
|
|
27
|
+
const REVERT_TOKEN_PATTERN = /^omp-revert:[A-Za-z0-9_-]{43}$/u;
|
|
28
|
+
const MAX_IMAGE_ENCODED_LENGTH = 8 * 1024 * 1024;
|
|
29
|
+
const MAX_STREAM_TOTAL_BYTES = (MAX_IMAGE_ENCODED_LENGTH + 256) * 2;
|
|
30
|
+
const MAX_NATIVE_IMAGE_RESULT_BYTES = 12 * 1024 * 1024;
|
|
31
|
+
const MAX_RETIRED_TOOL_IDS = 1_024;
|
|
32
|
+
|
|
33
|
+
type Emit = (event: ProviderEvent) => void;
|
|
34
|
+
|
|
35
|
+
type NativeImage = {
|
|
36
|
+
id: string;
|
|
37
|
+
data: string;
|
|
38
|
+
mimeType: OmpImageMimeType;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type StreamBlockKind = "assistant_message" | "reasoning" | "image";
|
|
42
|
+
|
|
43
|
+
const MAX_REPLAY_CANDIDATE_EVENTS = 512;
|
|
44
|
+
const MAX_REPLAY_CANDIDATE_BYTES = 4 * 1024 * 1024;
|
|
45
|
+
type StreamBlockSnapshot = {
|
|
46
|
+
kind: StreamBlockKind;
|
|
47
|
+
text: string;
|
|
48
|
+
publishedText?: string;
|
|
49
|
+
image?: NativeImage;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type StreamSnapshot = {
|
|
53
|
+
messageId: string;
|
|
54
|
+
nativeIdentity?: string;
|
|
55
|
+
published: boolean;
|
|
56
|
+
retainedBytes: number;
|
|
57
|
+
publishedBytes: number;
|
|
58
|
+
blocks: Map<number, StreamBlockSnapshot>;
|
|
59
|
+
dirtyBlocks: Set<number>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type ToolSnapshot = {
|
|
63
|
+
turnId: string;
|
|
64
|
+
generation: number;
|
|
65
|
+
publicId: string;
|
|
66
|
+
nativeName: string;
|
|
67
|
+
name: string;
|
|
68
|
+
input: JsonValue;
|
|
69
|
+
output: JsonValue;
|
|
70
|
+
retainedBytes: number;
|
|
71
|
+
specializedRendered: boolean;
|
|
72
|
+
silent: boolean;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export interface OmpTimelineScheduler {
|
|
76
|
+
set(callback: () => void | Promise<void>, delayMs: number): unknown;
|
|
77
|
+
clear(handle: unknown): void;
|
|
78
|
+
}
|
|
79
|
+
type AssistantStreamEvent = Extract<
|
|
80
|
+
OmpRpcEvent,
|
|
81
|
+
{ type: "message_start" | "message_update" | "message_end" }
|
|
82
|
+
>;
|
|
83
|
+
|
|
84
|
+
type ReplayCandidate = {
|
|
85
|
+
identity: string;
|
|
86
|
+
events: AssistantStreamEvent[];
|
|
87
|
+
retainedBytes: number;
|
|
88
|
+
};
|
|
89
|
+
type ReplayOccurrenceQueue = {
|
|
90
|
+
ordinals: number[];
|
|
91
|
+
consumed: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const defaultOmpTimelineScheduler: OmpTimelineScheduler = {
|
|
95
|
+
set: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
96
|
+
clear: (handle) => clearTimeout(handle as NodeJS.Timeout),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type AssistantMessageEvent = Extract<
|
|
100
|
+
OmpRpcEvent,
|
|
101
|
+
{ type: "message_update" }
|
|
102
|
+
>["assistantMessageEvent"];
|
|
103
|
+
|
|
104
|
+
function assistantIdentity(message: OmpMessage): string | undefined {
|
|
105
|
+
if (message.role !== "assistant") return;
|
|
106
|
+
return message.entryId ?? message.responseId ?? message.id;
|
|
107
|
+
}
|
|
108
|
+
function assistantContentFingerprint(message: OmpAssistantMessage): string {
|
|
109
|
+
const encoded =
|
|
110
|
+
typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? null);
|
|
111
|
+
return createHash("sha256").update(encoded).digest("base64url");
|
|
112
|
+
}
|
|
113
|
+
function imageBlock(data: string, mimeType: string): StreamBlockSnapshot | undefined {
|
|
114
|
+
if (!isValidImagePayload(data, mimeType, MAX_IMAGE_ENCODED_LENGTH)) return undefined;
|
|
115
|
+
if (!isOmpImageMimeType(mimeType)) return undefined;
|
|
116
|
+
return {
|
|
117
|
+
kind: "image",
|
|
118
|
+
text: `${mimeType}\n${data}`,
|
|
119
|
+
image: {
|
|
120
|
+
id: createHash("sha256")
|
|
121
|
+
.update(mimeType)
|
|
122
|
+
.update("\n")
|
|
123
|
+
.update(data)
|
|
124
|
+
.digest("base64url")
|
|
125
|
+
.slice(0, 16),
|
|
126
|
+
data,
|
|
127
|
+
mimeType,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
type OmpAssistantMessage = Extract<OmpMessage, { role: "assistant" }>;
|
|
133
|
+
|
|
134
|
+
function blockText(
|
|
135
|
+
message: OmpAssistantMessage,
|
|
136
|
+
contentIndex: number,
|
|
137
|
+
): StreamBlockSnapshot | undefined {
|
|
138
|
+
if (typeof message.content === "string") {
|
|
139
|
+
return contentIndex === 0 ? { kind: "assistant_message", text: message.content } : undefined;
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(message.content)) return undefined;
|
|
142
|
+
const part = message.content[contentIndex];
|
|
143
|
+
if (part?.type === "text") return { kind: "assistant_message", text: part.text ?? "" };
|
|
144
|
+
if (part?.type === "thinking") return { kind: "reasoning", text: part.thinking ?? "" };
|
|
145
|
+
if (part?.type === "image" && part.data && part.mimeType) {
|
|
146
|
+
return imageBlock(part.data, part.mimeType);
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function jsonRecord(value: JsonValue | undefined): Record<string, JsonValue> | undefined {
|
|
152
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
153
|
+
? (value as Record<string, JsonValue>)
|
|
154
|
+
: undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function firstString(
|
|
158
|
+
record: Record<string, JsonValue> | undefined,
|
|
159
|
+
...keys: string[]
|
|
160
|
+
): string | undefined {
|
|
161
|
+
for (const key of keys) {
|
|
162
|
+
const value = record?.[key];
|
|
163
|
+
if (typeof value === "string") return value;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
function xdeviceToolName(nativeName: string, input: JsonValue): string | undefined {
|
|
168
|
+
if (nativeName.toLowerCase() !== "write") return;
|
|
169
|
+
const record = jsonRecord(input);
|
|
170
|
+
const nestedInput = jsonRecord(record?.input) ?? record;
|
|
171
|
+
const path = firstString(nestedInput, "path", "filePath");
|
|
172
|
+
const match = path?.match(/^xd:\/\/([A-Za-z0-9_][A-Za-z0-9_.-]{0,255})(?:[/?#]|$)/u);
|
|
173
|
+
return match?.[1];
|
|
174
|
+
}
|
|
175
|
+
function friendlyXdeviceToolName(name: string): string {
|
|
176
|
+
const routedName = name.startsWith("mcp__") ? name.slice("mcp__".length) : name;
|
|
177
|
+
const words = routedName
|
|
178
|
+
.replace(/[-_.]+/gu, " ")
|
|
179
|
+
.replace(/\s+/gu, " ")
|
|
180
|
+
.trim();
|
|
181
|
+
return words ? `${words[0]?.toUpperCase() ?? ""}${words.slice(1)}` : name;
|
|
182
|
+
}
|
|
183
|
+
function publishableHttpUrl(value: string | undefined): string | undefined {
|
|
184
|
+
if (!value) return undefined;
|
|
185
|
+
try {
|
|
186
|
+
const parsed = new URL(value);
|
|
187
|
+
if (
|
|
188
|
+
(parsed.protocol !== "https:" && parsed.protocol !== "http:") ||
|
|
189
|
+
parsed.username ||
|
|
190
|
+
parsed.password
|
|
191
|
+
) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
} catch {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function todoPublicId(nativeId: string | undefined, index: number): string {
|
|
201
|
+
if (!nativeId) return `omp:todo:${index}`;
|
|
202
|
+
const digest = createHash("sha256").update(nativeId).digest("base64url").slice(0, 12);
|
|
203
|
+
return `omp:todo:${digest}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function toolResultText(value: JsonValue): string | undefined {
|
|
207
|
+
if (typeof value === "string") return value;
|
|
208
|
+
const result = jsonRecord(value);
|
|
209
|
+
if (!result) return undefined;
|
|
210
|
+
|
|
211
|
+
const directText = firstString(result, "output", "stdout", "text");
|
|
212
|
+
if (directText) return directText;
|
|
213
|
+
if (!Array.isArray(result.content)) return undefined;
|
|
214
|
+
|
|
215
|
+
const textParts = result.content.flatMap((part) => {
|
|
216
|
+
const block = jsonRecord(part);
|
|
217
|
+
return block?.type === "text" && typeof block.text === "string" ? [block.text] : [];
|
|
218
|
+
});
|
|
219
|
+
return textParts.length > 0 ? textParts.join("\n") : undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function resultDetails(value: JsonValue): Record<string, JsonValue> | undefined {
|
|
223
|
+
const envelope = jsonRecord(value);
|
|
224
|
+
return jsonRecord(envelope?.details);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
type NativeImageEnvelope = {
|
|
228
|
+
images: NativeImage[];
|
|
229
|
+
text?: string;
|
|
230
|
+
details?: JsonValue;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
type NativeImageResult = { image: NativeImageEnvelope; output: JsonValue };
|
|
234
|
+
|
|
235
|
+
function nativeImageResult(
|
|
236
|
+
value: unknown,
|
|
237
|
+
filter: OmpPublicDataSerializer,
|
|
238
|
+
): NativeImageResult | undefined {
|
|
239
|
+
if (
|
|
240
|
+
boundedJsonBytes(
|
|
241
|
+
value,
|
|
242
|
+
MAX_NATIVE_IMAGE_RESULT_BYTES,
|
|
243
|
+
MAX_STREAM_CONTENT_BLOCKS,
|
|
244
|
+
8 * 1024 * 1024,
|
|
245
|
+
512,
|
|
246
|
+
) === Number.POSITIVE_INFINITY
|
|
247
|
+
) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || !("content" in value)) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
if (!Array.isArray(value.content)) return undefined;
|
|
254
|
+
const images: NativeImageEnvelope["images"] = [];
|
|
255
|
+
const text: string[] = [];
|
|
256
|
+
let textBytes = 0;
|
|
257
|
+
const nonImageContent: unknown[] = [];
|
|
258
|
+
for (const part of value.content) {
|
|
259
|
+
if (
|
|
260
|
+
part &&
|
|
261
|
+
typeof part === "object" &&
|
|
262
|
+
!Array.isArray(part) &&
|
|
263
|
+
"type" in part &&
|
|
264
|
+
part.type === "image"
|
|
265
|
+
) {
|
|
266
|
+
if (
|
|
267
|
+
!("data" in part) ||
|
|
268
|
+
typeof part.data !== "string" ||
|
|
269
|
+
!("mimeType" in part) ||
|
|
270
|
+
typeof part.mimeType !== "string" ||
|
|
271
|
+
!isValidImagePayload(part.data, part.mimeType, MAX_IMAGE_ENCODED_LENGTH) ||
|
|
272
|
+
!isOmpImageMimeType(part.mimeType)
|
|
273
|
+
) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
images.push({
|
|
277
|
+
id: createHash("sha256")
|
|
278
|
+
.update(part.mimeType)
|
|
279
|
+
.update("\n")
|
|
280
|
+
.update(part.data)
|
|
281
|
+
.digest("base64url")
|
|
282
|
+
.slice(0, 16),
|
|
283
|
+
data: part.data,
|
|
284
|
+
mimeType: part.mimeType,
|
|
285
|
+
});
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
nonImageContent.push(part);
|
|
289
|
+
const sanitized = filter.json(
|
|
290
|
+
part,
|
|
291
|
+
MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
|
|
292
|
+
MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
|
|
293
|
+
);
|
|
294
|
+
const rendered = toolResultText(sanitized);
|
|
295
|
+
if (!rendered) continue;
|
|
296
|
+
const separatorBytes = text.length > 0 ? 1 : 0;
|
|
297
|
+
const remainingBytes = MAX_PUBLIC_TOOL_PAYLOAD_BYTES - textBytes - separatorBytes;
|
|
298
|
+
if (remainingBytes <= 0) continue;
|
|
299
|
+
const bounded = filter.text(rendered, remainingBytes);
|
|
300
|
+
if (!bounded) continue;
|
|
301
|
+
text.push(bounded);
|
|
302
|
+
textBytes += separatorBytes + utf8Bytes(bounded);
|
|
303
|
+
}
|
|
304
|
+
if (images.length === 0) return undefined;
|
|
305
|
+
const details =
|
|
306
|
+
"details" in value
|
|
307
|
+
? filter.json(value.details, MAX_PUBLIC_TOOL_PAYLOAD_BYTES, MAX_PUBLIC_TOOL_PAYLOAD_BYTES)
|
|
308
|
+
: undefined;
|
|
309
|
+
const output = filter.json(
|
|
310
|
+
{ ...value, content: nonImageContent },
|
|
311
|
+
MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
|
|
312
|
+
MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
|
|
313
|
+
);
|
|
314
|
+
return {
|
|
315
|
+
image: {
|
|
316
|
+
images,
|
|
317
|
+
...(text.length > 0 ? { text: text.join("\n") } : {}),
|
|
318
|
+
...(details !== undefined ? { details } : {}),
|
|
319
|
+
},
|
|
320
|
+
output,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
type CompactionSlot = {
|
|
325
|
+
id: string;
|
|
326
|
+
trigger: "auto" | "manual";
|
|
327
|
+
retrying: boolean;
|
|
328
|
+
action?: string;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export class OmpTimelineProjector {
|
|
332
|
+
private readonly tools = new Map<string, ToolSnapshot>();
|
|
333
|
+
private stream: StreamSnapshot | null = null;
|
|
334
|
+
private flushTimer: unknown;
|
|
335
|
+
private currentTurnId: string | null = null;
|
|
336
|
+
private assistantSequence = 0;
|
|
337
|
+
private readonly turnNativeMessageIds = new Map<string, string>();
|
|
338
|
+
private nativeIdentitySaturated = false;
|
|
339
|
+
private assistantIdentitySequence = 0;
|
|
340
|
+
private noticeSequence = 0;
|
|
341
|
+
private toolSequence = 0;
|
|
342
|
+
private userSequence = 0;
|
|
343
|
+
private replayTurnId: string | null = null;
|
|
344
|
+
private replaySequence = 0;
|
|
345
|
+
private readonly replayBoundaryOccurrences = new Map<
|
|
346
|
+
string,
|
|
347
|
+
Map<string, ReplayOccurrenceQueue>
|
|
348
|
+
>();
|
|
349
|
+
private replayBoundaryOccurrenceCount = 0;
|
|
350
|
+
private readonly replayCandidates = new Map<string, ReplayCandidate>();
|
|
351
|
+
private readonly replayOverflowCandidates = new Map<string, string>();
|
|
352
|
+
private projectingReplay = false;
|
|
353
|
+
private customSequence = 0;
|
|
354
|
+
private compactionSequence = 0;
|
|
355
|
+
private activeCompaction: CompactionSlot | null = null;
|
|
356
|
+
private discardedCompactionEnds = 0;
|
|
357
|
+
private goalItemId: string | null = null;
|
|
358
|
+
private runtimeGeneration = 0;
|
|
359
|
+
private readonly retiredToolCallIds = new Set<string>();
|
|
360
|
+
private toolIdentitySaturated = false;
|
|
361
|
+
private activeToolBytes = 0;
|
|
362
|
+
private readonly revertEntryByToken = new Map<string, string>();
|
|
363
|
+
private readonly revertTokenByEntry = new Map<string, string>();
|
|
364
|
+
private commandText = "";
|
|
365
|
+
private commandPublishedText = "";
|
|
366
|
+
private closed = false;
|
|
367
|
+
|
|
368
|
+
private readonly dataFilter: OmpPublicDataSerializer;
|
|
369
|
+
|
|
370
|
+
constructor(
|
|
371
|
+
private readonly sessionId: string,
|
|
372
|
+
private readonly emit: Emit,
|
|
373
|
+
private readonly scheduler: OmpTimelineScheduler = defaultOmpTimelineScheduler,
|
|
374
|
+
outputRedactionValues: readonly string[] = [],
|
|
375
|
+
private readonly conversationRevertEnabled = false,
|
|
376
|
+
private readonly hostToolLabels: ReadonlyMap<string, string> = new Map(),
|
|
377
|
+
) {
|
|
378
|
+
this.dataFilter = new OmpPublicDataSerializer(outputRedactionValues);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
project(event: OmpRpcEvent, turnId: string, bypassReplayFilter = false): void {
|
|
382
|
+
if (this.closed) return;
|
|
383
|
+
if (
|
|
384
|
+
!bypassReplayFilter &&
|
|
385
|
+
!this.projectingReplay &&
|
|
386
|
+
(event.type === "message_start" ||
|
|
387
|
+
event.type === "message_update" ||
|
|
388
|
+
event.type === "message_end") &&
|
|
389
|
+
event.message.role === "assistant"
|
|
390
|
+
) {
|
|
391
|
+
const accepted = this.filterReplayDelivery(event, turnId);
|
|
392
|
+
if (accepted === undefined) {
|
|
393
|
+
// This occurrence does not match replay history; project it normally.
|
|
394
|
+
} else {
|
|
395
|
+
for (const buffered of accepted) this.project(buffered, turnId, true);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (
|
|
400
|
+
event.type === "todo_reminder" ||
|
|
401
|
+
event.type === "todo_auto_clear" ||
|
|
402
|
+
event.type === "notice" ||
|
|
403
|
+
event.type === "extension_ui_request" ||
|
|
404
|
+
event.type === "auto_compaction_start" ||
|
|
405
|
+
event.type === "auto_compaction_end" ||
|
|
406
|
+
event.type === "compaction_start" ||
|
|
407
|
+
event.type === "compaction_end" ||
|
|
408
|
+
event.type === "advisor_yielded"
|
|
409
|
+
) {
|
|
410
|
+
this.projectPassive(event);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
this.ensureTurn(turnId);
|
|
414
|
+
switch (event.type) {
|
|
415
|
+
case "message_start":
|
|
416
|
+
if (event.message.role !== "assistant") return;
|
|
417
|
+
if (this.stream) {
|
|
418
|
+
this.flush(true);
|
|
419
|
+
this.stream = null;
|
|
420
|
+
}
|
|
421
|
+
this.beginStream(event.message, turnId);
|
|
422
|
+
this.updateAllBlocks(event.message);
|
|
423
|
+
this.scheduleFlush();
|
|
424
|
+
return;
|
|
425
|
+
case "message_update":
|
|
426
|
+
if (event.message.role !== "assistant") return;
|
|
427
|
+
this.updateStream(event.message, turnId, event.assistantMessageEvent);
|
|
428
|
+
this.scheduleFlush();
|
|
429
|
+
return;
|
|
430
|
+
case "message_end":
|
|
431
|
+
if (event.message.role === "custom" || event.message.role === "bashExecution") {
|
|
432
|
+
this.publishCustomMessage(event.message);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (event.message.role !== "assistant") return;
|
|
436
|
+
this.updateStream(event.message, turnId);
|
|
437
|
+
this.flush(true);
|
|
438
|
+
this.stream = null;
|
|
439
|
+
return;
|
|
440
|
+
case "tool_execution_start": {
|
|
441
|
+
this.flush(true);
|
|
442
|
+
if (this.toolIdentitySaturated || this.retiredToolCallIds.has(event.toolCallId)) return;
|
|
443
|
+
const previous = this.tools.get(event.toolCallId);
|
|
444
|
+
if (!previous && this.tools.size >= MAX_ACTIVE_TOOLS) return;
|
|
445
|
+
const input = this.dataFilter.json(
|
|
446
|
+
event.args,
|
|
447
|
+
MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
|
|
448
|
+
MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
|
|
449
|
+
);
|
|
450
|
+
const retainedBytes = boundedJsonBytes(input, MAX_PUBLIC_TOOL_PAYLOAD_BYTES);
|
|
451
|
+
if (
|
|
452
|
+
this.activeToolBytes - (previous?.retainedBytes ?? 0) + retainedBytes >
|
|
453
|
+
MAX_ACTIVE_TOOL_BYTES
|
|
454
|
+
) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (!previous) this.toolSequence += 1;
|
|
458
|
+
const routedToolName = xdeviceToolName(event.toolName, input);
|
|
459
|
+
const displayName =
|
|
460
|
+
this.hostToolLabels.get(routedToolName ?? event.toolName) ??
|
|
461
|
+
(routedToolName || event.toolName.startsWith("mcp__")
|
|
462
|
+
? friendlyXdeviceToolName(routedToolName ?? event.toolName)
|
|
463
|
+
: event.toolName);
|
|
464
|
+
const snapshot: ToolSnapshot = {
|
|
465
|
+
publicId: previous?.publicId ?? `omp:tool:${this.toolSequence}`,
|
|
466
|
+
nativeName: event.toolName,
|
|
467
|
+
name: this.dataFilter.text(displayName, 256),
|
|
468
|
+
input,
|
|
469
|
+
output: null,
|
|
470
|
+
retainedBytes,
|
|
471
|
+
turnId,
|
|
472
|
+
generation: this.runtimeGeneration,
|
|
473
|
+
specializedRendered: previous?.specializedRendered ?? false,
|
|
474
|
+
silent: ["ask_user", "todo"].includes(event.toolName.toLowerCase()),
|
|
475
|
+
};
|
|
476
|
+
this.activeToolBytes += retainedBytes - (previous?.retainedBytes ?? 0);
|
|
477
|
+
this.tools.set(event.toolCallId, snapshot);
|
|
478
|
+
if (!snapshot.silent) this.publishTool(snapshot, "running");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
case "tool_execution_update": {
|
|
482
|
+
const previous = this.tools.get(event.toolCallId);
|
|
483
|
+
if (!previous) return;
|
|
484
|
+
if (previous.turnId !== turnId || previous.generation !== this.runtimeGeneration) return;
|
|
485
|
+
const output = this.dataFilter.json(
|
|
486
|
+
event.partialResult,
|
|
487
|
+
MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
|
|
488
|
+
MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
|
|
489
|
+
);
|
|
490
|
+
const outputBytes = boundedJsonBytes(output, MAX_PUBLIC_TOOL_PAYLOAD_BYTES);
|
|
491
|
+
const inputBytes = boundedJsonBytes(previous.input, MAX_PUBLIC_TOOL_PAYLOAD_BYTES);
|
|
492
|
+
const retainedBytes = inputBytes + outputBytes;
|
|
493
|
+
if (this.activeToolBytes - previous.retainedBytes + retainedBytes > MAX_ACTIVE_TOOL_BYTES)
|
|
494
|
+
return;
|
|
495
|
+
const snapshot: ToolSnapshot = { ...previous, output, retainedBytes };
|
|
496
|
+
this.activeToolBytes += retainedBytes - previous.retainedBytes;
|
|
497
|
+
this.tools.set(event.toolCallId, snapshot);
|
|
498
|
+
if (!snapshot.silent) this.publishTool(snapshot, "running");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
case "tool_execution_end": {
|
|
502
|
+
const previous = this.tools.get(event.toolCallId);
|
|
503
|
+
if (!previous) return;
|
|
504
|
+
if (previous.turnId !== turnId || previous.generation !== this.runtimeGeneration) return;
|
|
505
|
+
const preservedImage = nativeImageResult(event.result, this.dataFilter);
|
|
506
|
+
this.tools.delete(event.toolCallId);
|
|
507
|
+
this.activeToolBytes -= previous.retainedBytes;
|
|
508
|
+
if (preservedImage && !event.isError) {
|
|
509
|
+
const { image, output } = preservedImage;
|
|
510
|
+
this.publishTool({ ...previous, output }, "completed");
|
|
511
|
+
this.publishImages(previous.publicId, previous.name, image);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const output = this.dataFilter.json(
|
|
515
|
+
event.result,
|
|
516
|
+
MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
|
|
517
|
+
MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
|
|
518
|
+
);
|
|
519
|
+
const snapshot: ToolSnapshot = { ...previous, output };
|
|
520
|
+
const specializedRendered =
|
|
521
|
+
previous.nativeName.toLowerCase() === "todo" && !event.isError
|
|
522
|
+
? this.publishTodoResult(snapshot)
|
|
523
|
+
: snapshot.specializedRendered;
|
|
524
|
+
if (snapshot.silent && (event.isError || !specializedRendered)) {
|
|
525
|
+
this.publishTool(
|
|
526
|
+
snapshot,
|
|
527
|
+
"failed",
|
|
528
|
+
event.isError
|
|
529
|
+
? snapshot.output
|
|
530
|
+
: `${snapshot.name} completed without a supported native rendering`,
|
|
531
|
+
);
|
|
532
|
+
} else if (!snapshot.silent) {
|
|
533
|
+
if (event.isError) this.publishTool(snapshot, "failed", snapshot.output);
|
|
534
|
+
else this.publishTool(snapshot, "completed");
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
case "command_output": {
|
|
539
|
+
if (!event.text) return;
|
|
540
|
+
const next = `${this.commandText}${event.text}`;
|
|
541
|
+
if (utf8Bytes(next) > MAX_STREAM_TEXT_LENGTH) return;
|
|
542
|
+
this.commandText = next;
|
|
543
|
+
this.publishCommand(turnId);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
projectPassive(event: OmpRpcEvent): void {
|
|
550
|
+
if (this.closed) return;
|
|
551
|
+
if (event.type === "todo_reminder" || event.type === "todo_auto_clear") {
|
|
552
|
+
const todos = event.type === "todo_reminder" ? event.todos : [];
|
|
553
|
+
this.publish({
|
|
554
|
+
type: "todo",
|
|
555
|
+
id: "omp:todos",
|
|
556
|
+
items: todos.slice(0, MAX_TODOS).map((todo, index) => ({
|
|
557
|
+
id: todoPublicId(todo.id, index),
|
|
558
|
+
text: this.dataFilter.text(todo.content, 16_384),
|
|
559
|
+
completed: todo.status === "completed" || todo.status === "abandoned",
|
|
560
|
+
status:
|
|
561
|
+
todo.status === "completed" || todo.status === "abandoned"
|
|
562
|
+
? "completed"
|
|
563
|
+
: todo.status === "blocked"
|
|
564
|
+
? "pending"
|
|
565
|
+
: todo.status,
|
|
566
|
+
})),
|
|
567
|
+
});
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (event.type === "goal_updated") {
|
|
571
|
+
this.publishGoal(event);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (event.type === "auto_retry_start" || event.type === "auto_retry_end") {
|
|
575
|
+
this.publishAutoRetry(event);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (event.type === "retry_fallback_applied" || event.type === "retry_fallback_succeeded") {
|
|
579
|
+
this.publishRetryFallback(event);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (event.type === "notice") {
|
|
583
|
+
this.noticeSequence += 1;
|
|
584
|
+
this.publish({
|
|
585
|
+
type: "notification",
|
|
586
|
+
id: `omp:notice:${this.noticeSequence}`,
|
|
587
|
+
level: event.level,
|
|
588
|
+
message: this.dataFilter.text(event.message, 64 * 1024),
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (event.type === "extension_ui_request" && event.method === "notify") {
|
|
593
|
+
if (!event.message) return;
|
|
594
|
+
this.noticeSequence += 1;
|
|
595
|
+
this.publish({
|
|
596
|
+
type: "notification",
|
|
597
|
+
id: `omp:ui:${this.noticeSequence}`,
|
|
598
|
+
level: event.notifyType ?? "info",
|
|
599
|
+
message: this.dataFilter.text(event.message, 64 * 1024),
|
|
600
|
+
});
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (event.type === "extension_ui_request" && event.method === "open_url") {
|
|
604
|
+
const url = publishableHttpUrl(event.launchUrl ?? event.url);
|
|
605
|
+
if (!url) return;
|
|
606
|
+
this.noticeSequence += 1;
|
|
607
|
+
const message = [event.instructions, url].filter(Boolean).join("\n");
|
|
608
|
+
this.publish({
|
|
609
|
+
type: "notification",
|
|
610
|
+
id: `omp:ui:${this.noticeSequence}`,
|
|
611
|
+
level: "info",
|
|
612
|
+
message: this.dataFilter.text(message, 64 * 1024),
|
|
613
|
+
});
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (event.type === "advisor_yielded") {
|
|
617
|
+
this.noticeSequence += 1;
|
|
618
|
+
this.publish({
|
|
619
|
+
type: "notification",
|
|
620
|
+
id: `omp:advisor:${this.noticeSequence}`,
|
|
621
|
+
level: "info",
|
|
622
|
+
message: "Advisor review completed",
|
|
623
|
+
});
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (event.type === "auto_compaction_start" || event.type === "compaction_start") {
|
|
627
|
+
const trigger = event.type === "auto_compaction_start" ? "auto" : "manual";
|
|
628
|
+
const action = event.type === "auto_compaction_start" ? event.action : undefined;
|
|
629
|
+
if (this.discardedCompactionEnds > 0) {
|
|
630
|
+
this.discardedCompactionEnds += 1;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const active = this.activeCompaction;
|
|
634
|
+
if (active?.retrying && active.trigger === trigger && active.action === action) {
|
|
635
|
+
active.retrying = false;
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (active) {
|
|
639
|
+
this.retireCompactions("OMP emitted overlapping compactions");
|
|
640
|
+
this.discardedCompactionEnds = 2;
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
this.compactionSequence += 1;
|
|
644
|
+
const slot: CompactionSlot = {
|
|
645
|
+
id: `omp:compaction:${this.compactionSequence}`,
|
|
646
|
+
trigger,
|
|
647
|
+
retrying: false,
|
|
648
|
+
...(action ? { action } : {}),
|
|
649
|
+
};
|
|
650
|
+
this.activeCompaction = slot;
|
|
651
|
+
this.publish({ type: "compaction", id: slot.id, status: "loading", trigger });
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (event.type === "auto_compaction_end" || event.type === "compaction_end") {
|
|
655
|
+
if (this.discardedCompactionEnds > 0) {
|
|
656
|
+
this.discardedCompactionEnds -= 1;
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const trigger = event.type === "auto_compaction_end" ? "auto" : "manual";
|
|
660
|
+
const action = event.type === "auto_compaction_end" ? event.action : undefined;
|
|
661
|
+
const slot = this.activeCompaction;
|
|
662
|
+
if (!slot) {
|
|
663
|
+
this.compactionSequence += 1;
|
|
664
|
+
this.publish({
|
|
665
|
+
type: "error",
|
|
666
|
+
id: `omp:compaction:${this.compactionSequence}:error`,
|
|
667
|
+
message: "OMP compaction ended without a matching start",
|
|
668
|
+
});
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (slot.trigger !== trigger || (action !== undefined && slot.action !== action)) {
|
|
672
|
+
this.retireCompactions("OMP emitted overlapping compactions");
|
|
673
|
+
this.discardedCompactionEnds = 1;
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (event.willRetry) {
|
|
677
|
+
slot.retrying = true;
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
this.activeCompaction = null;
|
|
681
|
+
const result = jsonRecord(this.dataFilter.json(event.result ?? null));
|
|
682
|
+
const rawPreTokens = result?.preTokens ?? result?.tokensBefore;
|
|
683
|
+
const preTokens =
|
|
684
|
+
typeof rawPreTokens === "number" && Number.isFinite(rawPreTokens)
|
|
685
|
+
? Math.max(0, Math.trunc(rawPreTokens))
|
|
686
|
+
: undefined;
|
|
687
|
+
this.publish({
|
|
688
|
+
type: "compaction",
|
|
689
|
+
id: slot.id,
|
|
690
|
+
status: "completed",
|
|
691
|
+
trigger,
|
|
692
|
+
...(preTokens !== undefined ? { preTokens } : {}),
|
|
693
|
+
});
|
|
694
|
+
if (event.skipped) {
|
|
695
|
+
this.publish({
|
|
696
|
+
type: "notification",
|
|
697
|
+
id: `${slot.id}:skipped`,
|
|
698
|
+
level: "warning",
|
|
699
|
+
message: "OMP compaction was skipped",
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
if (event.aborted || event.errorMessage) {
|
|
703
|
+
this.publish({
|
|
704
|
+
type: "error",
|
|
705
|
+
id: `${slot.id}:error`,
|
|
706
|
+
message: this.dataFilter.text(
|
|
707
|
+
event.errorMessage ??
|
|
708
|
+
(event.aborted ? "OMP compaction canceled" : "OMP compaction failed"),
|
|
709
|
+
4_096,
|
|
710
|
+
),
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
projectSubagent(
|
|
717
|
+
event: Extract<
|
|
718
|
+
OmpRpcEvent,
|
|
719
|
+
{ type: "subagent_lifecycle" | "subagent_progress" | "subagent_event" }
|
|
720
|
+
>,
|
|
721
|
+
): void {
|
|
722
|
+
if (this.closed) return;
|
|
723
|
+
switch (event.type) {
|
|
724
|
+
case "subagent_lifecycle":
|
|
725
|
+
case "subagent_progress":
|
|
726
|
+
case "subagent_event":
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
markAskPermissionRendered(): void {
|
|
732
|
+
const snapshots = [...this.tools.values()];
|
|
733
|
+
for (let index = snapshots.length - 1; index >= 0; index -= 1) {
|
|
734
|
+
const snapshot = snapshots[index];
|
|
735
|
+
if (snapshot?.nativeName.toLowerCase() !== "ask_user") continue;
|
|
736
|
+
snapshot.specializedRendered = true;
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
publishUser(text: string, clientMessageId: string, nativeId?: string): void {
|
|
742
|
+
this.userSequence += 1;
|
|
743
|
+
const nativeHash = nativeId
|
|
744
|
+
? createHash("sha256").update(nativeId).digest("base64url").slice(0, 12)
|
|
745
|
+
: "local";
|
|
746
|
+
const messageId = `omp:user:${this.userSequence}:${nativeHash}`;
|
|
747
|
+
const revertToken =
|
|
748
|
+
nativeId && this.conversationRevertEnabled ? this.revertTokenFor(nativeId) : undefined;
|
|
749
|
+
this.publish({
|
|
750
|
+
type: "user_message",
|
|
751
|
+
id: messageId,
|
|
752
|
+
messageId,
|
|
753
|
+
clientMessageId,
|
|
754
|
+
text: this.dataFilter.text(text),
|
|
755
|
+
...(revertToken ? { revertToken } : {}),
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
resolveRevertToken(token: unknown): string {
|
|
760
|
+
if (typeof token !== "string" || !REVERT_TOKEN_PATTERN.test(token)) {
|
|
761
|
+
throw new OmpPublicError("Invalid OMP conversation rewind token");
|
|
762
|
+
}
|
|
763
|
+
const entryId = this.revertEntryByToken.get(token);
|
|
764
|
+
if (!entryId) throw new OmpPublicError("OMP conversation rewind token is stale");
|
|
765
|
+
return entryId;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
resetForRewindReplay(): void {
|
|
769
|
+
this.clearFlushTimer();
|
|
770
|
+
this.stream = null;
|
|
771
|
+
this.currentTurnId = null;
|
|
772
|
+
this.assistantSequence = 0;
|
|
773
|
+
this.turnNativeMessageIds.clear();
|
|
774
|
+
this.nativeIdentitySaturated = false;
|
|
775
|
+
this.assistantIdentitySequence = 0;
|
|
776
|
+
this.toolSequence = 0;
|
|
777
|
+
this.userSequence = 0;
|
|
778
|
+
this.replayTurnId = null;
|
|
779
|
+
this.replaySequence = 0;
|
|
780
|
+
this.replayBoundaryOccurrences.clear();
|
|
781
|
+
this.replayBoundaryOccurrenceCount = 0;
|
|
782
|
+
this.replayCandidates.clear();
|
|
783
|
+
this.replayOverflowCandidates.clear();
|
|
784
|
+
this.projectingReplay = false;
|
|
785
|
+
this.activeToolBytes = 0;
|
|
786
|
+
this.tools.clear();
|
|
787
|
+
this.commandText = "";
|
|
788
|
+
this.commandPublishedText = "";
|
|
789
|
+
this.revertEntryByToken.clear();
|
|
790
|
+
this.revertTokenByEntry.clear();
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
projectReplayMessage(message: OmpMessage): void {
|
|
794
|
+
if (this.closed) return;
|
|
795
|
+
const nativeIdentity = assistantIdentity(message);
|
|
796
|
+
this.replaySequence += 1;
|
|
797
|
+
if (message.role === "user") {
|
|
798
|
+
if (this.replayTurnId) this.finishTurn(this.replayTurnId);
|
|
799
|
+
this.replayTurnId = `omp:replay-turn:${this.replaySequence}`;
|
|
800
|
+
const text =
|
|
801
|
+
typeof message.content === "string"
|
|
802
|
+
? message.content
|
|
803
|
+
: message.content
|
|
804
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
805
|
+
.map((part) => part.text ?? "")
|
|
806
|
+
.join("\n\n");
|
|
807
|
+
if (text) {
|
|
808
|
+
this.publishUser(
|
|
809
|
+
text,
|
|
810
|
+
`omp:replay-user:${this.replaySequence}`,
|
|
811
|
+
message.entryId ?? message.id,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (message.role === "assistant") {
|
|
817
|
+
this.replayTurnId ??= `omp:replay-turn:${this.replaySequence}`;
|
|
818
|
+
this.projectingReplay = true;
|
|
819
|
+
try {
|
|
820
|
+
this.project({ type: "message_start", message }, this.replayTurnId);
|
|
821
|
+
this.project({ type: "message_end", message }, this.replayTurnId);
|
|
822
|
+
if (Array.isArray(message.content)) {
|
|
823
|
+
for (const part of message.content) {
|
|
824
|
+
if (part.type !== "toolCall" || !part.id || !part.name || part.arguments === undefined)
|
|
825
|
+
continue;
|
|
826
|
+
this.project(
|
|
827
|
+
{
|
|
828
|
+
type: "tool_execution_start",
|
|
829
|
+
toolCallId: part.id,
|
|
830
|
+
toolName: part.name,
|
|
831
|
+
args: part.arguments,
|
|
832
|
+
},
|
|
833
|
+
this.replayTurnId,
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
} finally {
|
|
838
|
+
this.projectingReplay = false;
|
|
839
|
+
}
|
|
840
|
+
if (nativeIdentity) this.rememberReplayOccurrence(nativeIdentity, message);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
this.replayTurnId ??= `omp:replay-turn:${this.replaySequence}`;
|
|
844
|
+
if (message.role === "toolResult") {
|
|
845
|
+
if (!this.tools.has(message.toolCallId)) {
|
|
846
|
+
this.project(
|
|
847
|
+
{
|
|
848
|
+
type: "tool_execution_start",
|
|
849
|
+
toolCallId: message.toolCallId,
|
|
850
|
+
toolName: message.toolName,
|
|
851
|
+
args: null,
|
|
852
|
+
},
|
|
853
|
+
this.replayTurnId,
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
this.project(
|
|
857
|
+
{
|
|
858
|
+
type: "tool_execution_end",
|
|
859
|
+
toolCallId: message.toolCallId,
|
|
860
|
+
toolName: message.toolName,
|
|
861
|
+
result: {
|
|
862
|
+
content: message.content,
|
|
863
|
+
...(message.details !== undefined ? { details: message.details } : {}),
|
|
864
|
+
},
|
|
865
|
+
isError: message.isError,
|
|
866
|
+
},
|
|
867
|
+
this.replayTurnId,
|
|
868
|
+
);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (message.role === "bashExecution") {
|
|
872
|
+
const text = message.output
|
|
873
|
+
? `$ ${message.command}\n${message.output}`
|
|
874
|
+
: `$ ${message.command}`;
|
|
875
|
+
this.project({ type: "command_output", text }, this.replayTurnId);
|
|
876
|
+
this.finishTurn(this.replayTurnId);
|
|
877
|
+
this.replayTurnId = null;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
finishReplay(): void {
|
|
882
|
+
if (this.replayTurnId) this.finishTurn(this.replayTurnId);
|
|
883
|
+
this.replayTurnId = null;
|
|
884
|
+
}
|
|
885
|
+
acceptLiveTurn(turnId: string): void {
|
|
886
|
+
const candidate = this.replayCandidates.get(turnId);
|
|
887
|
+
if (candidate) {
|
|
888
|
+
this.replayCandidates.delete(turnId);
|
|
889
|
+
for (const event of candidate.events) this.project(event, turnId, true);
|
|
890
|
+
}
|
|
891
|
+
this.replayOverflowCandidates.delete(turnId);
|
|
892
|
+
this.replayBoundaryOccurrences.clear();
|
|
893
|
+
this.replayBoundaryOccurrenceCount = 0;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
private rememberReplayOccurrence(identity: string, message: OmpAssistantMessage): void {
|
|
897
|
+
if (this.replayBoundaryOccurrenceCount >= MAX_REPLAY_NATIVE_IDENTITIES) return;
|
|
898
|
+
const ordinal = this.replayBoundaryOccurrenceCount;
|
|
899
|
+
this.replayBoundaryOccurrenceCount += 1;
|
|
900
|
+
const fingerprint = assistantContentFingerprint(message);
|
|
901
|
+
const signatures = this.replayBoundaryOccurrences.get(identity) ?? new Map();
|
|
902
|
+
const occurrences = signatures.get(fingerprint) ?? { ordinals: [], consumed: 0 };
|
|
903
|
+
occurrences.ordinals.push(ordinal);
|
|
904
|
+
signatures.set(fingerprint, occurrences);
|
|
905
|
+
this.replayBoundaryOccurrences.set(identity, signatures);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private consumeReplayOccurrence(identity: string, message: OmpAssistantMessage): boolean {
|
|
909
|
+
const signatures = this.replayBoundaryOccurrences.get(identity);
|
|
910
|
+
if (!signatures) return false;
|
|
911
|
+
const fingerprint = assistantContentFingerprint(message);
|
|
912
|
+
const occurrences = signatures.get(fingerprint);
|
|
913
|
+
if (!occurrences || occurrences.consumed >= occurrences.ordinals.length) return false;
|
|
914
|
+
occurrences.consumed += 1;
|
|
915
|
+
if (occurrences.consumed === occurrences.ordinals.length) signatures.delete(fingerprint);
|
|
916
|
+
if (signatures.size === 0) this.replayBoundaryOccurrences.delete(identity);
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private filterReplayDelivery(
|
|
921
|
+
event: AssistantStreamEvent,
|
|
922
|
+
turnId: string,
|
|
923
|
+
): AssistantStreamEvent[] | undefined {
|
|
924
|
+
if (event.message.role !== "assistant") return undefined;
|
|
925
|
+
const identity = assistantIdentity(event.message);
|
|
926
|
+
const overflowIdentity = this.replayOverflowCandidates.get(turnId);
|
|
927
|
+
if (overflowIdentity) {
|
|
928
|
+
if (identity !== overflowIdentity) {
|
|
929
|
+
this.replayOverflowCandidates.delete(turnId);
|
|
930
|
+
return this.filterReplayDelivery(event, turnId);
|
|
931
|
+
}
|
|
932
|
+
if (event.type !== "message_end") return [];
|
|
933
|
+
this.replayOverflowCandidates.delete(turnId);
|
|
934
|
+
return this.consumeReplayOccurrence(identity, event.message) ? [] : [event];
|
|
935
|
+
}
|
|
936
|
+
if (!identity || !this.replayBoundaryOccurrences.has(identity)) return undefined;
|
|
937
|
+
const existing = this.replayCandidates.get(turnId);
|
|
938
|
+
if (existing && existing.identity !== identity) {
|
|
939
|
+
this.replayCandidates.delete(turnId);
|
|
940
|
+
const next = this.filterReplayDelivery(event, turnId);
|
|
941
|
+
return next === undefined ? [...existing.events, event] : [...existing.events, ...next];
|
|
942
|
+
}
|
|
943
|
+
const candidate = existing ?? { identity, events: [], retainedBytes: 0 };
|
|
944
|
+
const eventBytes = boundedJsonBytes(event, MAX_REPLAY_CANDIDATE_BYTES);
|
|
945
|
+
if (
|
|
946
|
+
eventBytes === Number.POSITIVE_INFINITY ||
|
|
947
|
+
candidate.events.length >= MAX_REPLAY_CANDIDATE_EVENTS ||
|
|
948
|
+
candidate.retainedBytes + eventBytes > MAX_REPLAY_CANDIDATE_BYTES
|
|
949
|
+
) {
|
|
950
|
+
this.replayCandidates.delete(turnId);
|
|
951
|
+
if (event.type === "message_end") {
|
|
952
|
+
return this.consumeReplayOccurrence(identity, event.message) ? [] : [event];
|
|
953
|
+
}
|
|
954
|
+
this.replayOverflowCandidates.set(turnId, identity);
|
|
955
|
+
return [];
|
|
956
|
+
}
|
|
957
|
+
candidate.events.push(event);
|
|
958
|
+
candidate.retainedBytes += eventBytes;
|
|
959
|
+
this.replayCandidates.set(turnId, candidate);
|
|
960
|
+
if (event.type !== "message_end") return [];
|
|
961
|
+
this.replayCandidates.delete(turnId);
|
|
962
|
+
return this.consumeReplayOccurrence(identity, event.message) ? [] : candidate.events;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
flush(finalizeFallback = false): void {
|
|
966
|
+
this.clearFlushTimer();
|
|
967
|
+
if (!this.stream || this.closed || this.stream.dirtyBlocks.size === 0) return;
|
|
968
|
+
const stream = this.stream;
|
|
969
|
+
if (!stream.nativeIdentity && !finalizeFallback) return;
|
|
970
|
+
const indexes = [...stream.dirtyBlocks].sort((left, right) => left - right);
|
|
971
|
+
stream.dirtyBlocks.clear();
|
|
972
|
+
for (const contentIndex of indexes) {
|
|
973
|
+
const block = stream.blocks.get(contentIndex);
|
|
974
|
+
if (!block?.text) continue;
|
|
975
|
+
const publicText = block.kind === "image" ? block.text : this.dataFilter.text(block.text);
|
|
976
|
+
if (!publicText || block.publishedText === publicText) continue;
|
|
977
|
+
const nextPublishedBytes = utf8Bytes(publicText);
|
|
978
|
+
if (
|
|
979
|
+
stream.retainedBytes + stream.publishedBytes + nextPublishedBytes >
|
|
980
|
+
MAX_STREAM_TOTAL_BYTES
|
|
981
|
+
) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
const suffix =
|
|
985
|
+
block.kind === "reasoning" ? "reasoning" : block.kind === "image" ? "image" : "text";
|
|
986
|
+
const id = `${stream.messageId}:content:${contentIndex}:${suffix}`;
|
|
987
|
+
if (block.kind === "reasoning") {
|
|
988
|
+
this.publish({ type: "reasoning", id, text: publicText });
|
|
989
|
+
} else if (block.kind === "image") {
|
|
990
|
+
if (block.image) this.publishImages(id, "Assistant image", { images: [block.image] });
|
|
991
|
+
} else {
|
|
992
|
+
this.publish({
|
|
993
|
+
type: "assistant_message",
|
|
994
|
+
id,
|
|
995
|
+
messageId: stream.messageId,
|
|
996
|
+
text: publicText,
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
stream.publishedBytes += nextPublishedBytes;
|
|
1000
|
+
block.publishedText = publicText;
|
|
1001
|
+
stream.published = true;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
finishTurn(turnId: string, preserveCompactions = false): void {
|
|
1006
|
+
const replayCandidate = this.replayCandidates.get(turnId);
|
|
1007
|
+
if (replayCandidate) {
|
|
1008
|
+
this.replayCandidates.delete(turnId);
|
|
1009
|
+
for (const event of replayCandidate.events) this.project(event, turnId, true);
|
|
1010
|
+
}
|
|
1011
|
+
this.replayOverflowCandidates.delete(turnId);
|
|
1012
|
+
if (!preserveCompactions) this.retireCompactions("OMP compaction ended with the turn");
|
|
1013
|
+
if (this.currentTurnId !== turnId) return;
|
|
1014
|
+
this.flush(true);
|
|
1015
|
+
this.retireTools("OMP tool ended with the turn");
|
|
1016
|
+
this.publishCommand(turnId);
|
|
1017
|
+
this.stream = null;
|
|
1018
|
+
this.commandText = "";
|
|
1019
|
+
this.currentTurnId = null;
|
|
1020
|
+
this.assistantSequence = 0;
|
|
1021
|
+
this.turnNativeMessageIds.clear();
|
|
1022
|
+
this.nativeIdentitySaturated = false;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
close(): void {
|
|
1026
|
+
this.flush(true);
|
|
1027
|
+
if (this.currentTurnId) this.publishCommand(this.currentTurnId);
|
|
1028
|
+
this.retireCompactions("OMP compaction ended when the session closed");
|
|
1029
|
+
this.closed = true;
|
|
1030
|
+
this.clearFlushTimer();
|
|
1031
|
+
this.stream = null;
|
|
1032
|
+
this.tools.clear();
|
|
1033
|
+
this.activeToolBytes = 0;
|
|
1034
|
+
this.replayCandidates.clear();
|
|
1035
|
+
this.replayOverflowCandidates.clear();
|
|
1036
|
+
this.replayBoundaryOccurrences.clear();
|
|
1037
|
+
this.replayBoundaryOccurrenceCount = 0;
|
|
1038
|
+
this.revertEntryByToken.clear();
|
|
1039
|
+
this.revertTokenByEntry.clear();
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
retireCompactions(message: string): void {
|
|
1043
|
+
this.discardedCompactionEnds = 0;
|
|
1044
|
+
const slot = this.activeCompaction;
|
|
1045
|
+
if (!slot) return;
|
|
1046
|
+
this.activeCompaction = null;
|
|
1047
|
+
this.publish({ type: "compaction", id: slot.id, status: "completed", trigger: slot.trigger });
|
|
1048
|
+
this.publish({ type: "error", id: `${slot.id}:error`, message });
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
resetRuntimeGeneration(message: string): void {
|
|
1052
|
+
this.retireCompactions(message);
|
|
1053
|
+
this.retireTools(message);
|
|
1054
|
+
this.retiredToolCallIds.clear();
|
|
1055
|
+
this.toolIdentitySaturated = false;
|
|
1056
|
+
this.runtimeGeneration += 1;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
private retireTools(message: string): void {
|
|
1060
|
+
for (const [nativeId, snapshot] of this.tools) {
|
|
1061
|
+
if (!snapshot.silent || !snapshot.specializedRendered) {
|
|
1062
|
+
this.publishTool(snapshot, "failed", message);
|
|
1063
|
+
}
|
|
1064
|
+
if (this.retiredToolCallIds.size < MAX_RETIRED_TOOL_IDS) {
|
|
1065
|
+
this.retiredToolCallIds.add(nativeId);
|
|
1066
|
+
} else {
|
|
1067
|
+
this.toolIdentitySaturated = true;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
this.activeToolBytes = 0;
|
|
1071
|
+
this.tools.clear();
|
|
1072
|
+
}
|
|
1073
|
+
private ensureTurn(turnId: string): void {
|
|
1074
|
+
if (this.currentTurnId === turnId) return;
|
|
1075
|
+
if (this.currentTurnId) this.finishTurn(this.currentTurnId);
|
|
1076
|
+
this.currentTurnId = turnId;
|
|
1077
|
+
this.assistantSequence = 0;
|
|
1078
|
+
this.commandText = "";
|
|
1079
|
+
this.commandPublishedText = "";
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
private beginStream(message: OmpAssistantMessage, turnId: string): StreamSnapshot | null {
|
|
1083
|
+
this.assistantSequence += 1;
|
|
1084
|
+
const nativeIdentity = assistantIdentity(message);
|
|
1085
|
+
const messageId = nativeIdentity
|
|
1086
|
+
? this.messageIdForNativeIdentity(nativeIdentity)
|
|
1087
|
+
: this.nextAssistantMessageId(`turn:${turnId}:${this.assistantSequence}`);
|
|
1088
|
+
if (!messageId) return null;
|
|
1089
|
+
this.stream = {
|
|
1090
|
+
messageId,
|
|
1091
|
+
...(nativeIdentity ? { nativeIdentity } : {}),
|
|
1092
|
+
published: false,
|
|
1093
|
+
retainedBytes: 0,
|
|
1094
|
+
publishedBytes: 0,
|
|
1095
|
+
blocks: new Map(),
|
|
1096
|
+
dirtyBlocks: new Set(),
|
|
1097
|
+
};
|
|
1098
|
+
return this.stream;
|
|
1099
|
+
}
|
|
1100
|
+
private messageIdForNativeIdentity(nativeIdentity: string): string | undefined {
|
|
1101
|
+
const existing = this.turnNativeMessageIds.get(nativeIdentity);
|
|
1102
|
+
if (existing) return existing;
|
|
1103
|
+
if (
|
|
1104
|
+
this.nativeIdentitySaturated ||
|
|
1105
|
+
this.turnNativeMessageIds.size >= MAX_TURN_NATIVE_IDENTITIES
|
|
1106
|
+
) {
|
|
1107
|
+
this.nativeIdentitySaturated = true;
|
|
1108
|
+
return undefined;
|
|
1109
|
+
}
|
|
1110
|
+
const messageId = this.nextAssistantMessageId(nativeIdentity);
|
|
1111
|
+
this.turnNativeMessageIds.set(nativeIdentity, messageId);
|
|
1112
|
+
return messageId;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
private nextAssistantMessageId(source: string): string {
|
|
1116
|
+
this.assistantIdentitySequence += 1;
|
|
1117
|
+
const digest = createHash("sha256").update(source).digest("base64url").slice(0, 12);
|
|
1118
|
+
return `omp:assistant:${this.assistantIdentitySequence}:${digest}`;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
private updateStream(
|
|
1122
|
+
message: OmpAssistantMessage,
|
|
1123
|
+
turnId: string,
|
|
1124
|
+
update?: AssistantMessageEvent,
|
|
1125
|
+
): void {
|
|
1126
|
+
const nativeIdentity = assistantIdentity(message);
|
|
1127
|
+
if (this.stream && nativeIdentity && this.stream.nativeIdentity !== nativeIdentity) {
|
|
1128
|
+
if (!this.stream.nativeIdentity && !this.stream.published) {
|
|
1129
|
+
const messageId = this.messageIdForNativeIdentity(nativeIdentity);
|
|
1130
|
+
if (!messageId) {
|
|
1131
|
+
this.stream = null;
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
this.stream.messageId = messageId;
|
|
1135
|
+
this.stream.nativeIdentity = nativeIdentity;
|
|
1136
|
+
} else {
|
|
1137
|
+
this.flush();
|
|
1138
|
+
this.stream = null;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
const stream = this.stream ?? this.beginStream(message, turnId);
|
|
1142
|
+
if (!stream) return;
|
|
1143
|
+
if (update?.contentIndex !== undefined) {
|
|
1144
|
+
this.updateBlock(stream, message, update.contentIndex, update);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
this.updateAllBlocks(message);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
private updateAllBlocks(message: OmpAssistantMessage): void {
|
|
1151
|
+
const stream = this.stream;
|
|
1152
|
+
if (!stream) return;
|
|
1153
|
+
if (typeof message.content === "string") {
|
|
1154
|
+
this.setBlock(stream, 0, { kind: "assistant_message", text: message.content });
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (!Array.isArray(message.content)) return;
|
|
1158
|
+
const blockCount = Math.min(message.content.length, MAX_STREAM_CONTENT_BLOCKS);
|
|
1159
|
+
for (let index = 0; index < blockCount; index += 1) {
|
|
1160
|
+
const block = blockText(message, index);
|
|
1161
|
+
if (block) this.setBlock(stream, index, block);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
private updateBlock(
|
|
1166
|
+
stream: StreamSnapshot,
|
|
1167
|
+
message: OmpAssistantMessage,
|
|
1168
|
+
contentIndex: number,
|
|
1169
|
+
update: NonNullable<AssistantMessageEvent>,
|
|
1170
|
+
): void {
|
|
1171
|
+
if (!this.isValidContentIndex(contentIndex)) return;
|
|
1172
|
+
const snapshot = blockText(message, contentIndex);
|
|
1173
|
+
if (snapshot) {
|
|
1174
|
+
this.setBlock(stream, contentIndex, snapshot);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
const content = update.content;
|
|
1178
|
+
if (
|
|
1179
|
+
content &&
|
|
1180
|
+
typeof content === "object" &&
|
|
1181
|
+
!Array.isArray(content) &&
|
|
1182
|
+
"type" in content &&
|
|
1183
|
+
content.type === "image" &&
|
|
1184
|
+
"data" in content &&
|
|
1185
|
+
typeof content.data === "string" &&
|
|
1186
|
+
"mimeType" in content &&
|
|
1187
|
+
typeof content.mimeType === "string"
|
|
1188
|
+
) {
|
|
1189
|
+
const image = imageBlock(content.data, content.mimeType);
|
|
1190
|
+
if (image) this.setBlock(stream, contentIndex, image);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const kind = update.type.startsWith("thinking_")
|
|
1194
|
+
? "reasoning"
|
|
1195
|
+
: update.type.startsWith("text_")
|
|
1196
|
+
? "assistant_message"
|
|
1197
|
+
: undefined;
|
|
1198
|
+
if (!kind) return;
|
|
1199
|
+
const previous = stream.blocks.get(contentIndex);
|
|
1200
|
+
const eventContent = typeof content === "string" ? content : undefined;
|
|
1201
|
+
const text =
|
|
1202
|
+
eventContent ??
|
|
1203
|
+
(update.delta !== undefined && previous?.kind === kind
|
|
1204
|
+
? `${previous.text}${update.delta}`
|
|
1205
|
+
: (update.delta ?? previous?.text ?? ""));
|
|
1206
|
+
this.setBlock(stream, contentIndex, { kind, text });
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private setBlock(
|
|
1210
|
+
stream: StreamSnapshot,
|
|
1211
|
+
contentIndex: number,
|
|
1212
|
+
snapshot: StreamBlockSnapshot,
|
|
1213
|
+
): void {
|
|
1214
|
+
if (!this.isValidContentIndex(contentIndex)) return;
|
|
1215
|
+
if (snapshot.kind === "image" && utf8Bytes(snapshot.text) > MAX_IMAGE_ENCODED_LENGTH + 256)
|
|
1216
|
+
return;
|
|
1217
|
+
const previous = stream.blocks.get(contentIndex);
|
|
1218
|
+
let retainedBytes = utf8Bytes(snapshot.text);
|
|
1219
|
+
let textBytes = snapshot.kind === "image" ? 0 : retainedBytes;
|
|
1220
|
+
for (const [index, block] of stream.blocks) {
|
|
1221
|
+
if (index === contentIndex) continue;
|
|
1222
|
+
const blockBytes = utf8Bytes(block.text);
|
|
1223
|
+
retainedBytes += blockBytes;
|
|
1224
|
+
if (block.kind !== "image") textBytes += blockBytes;
|
|
1225
|
+
if (
|
|
1226
|
+
retainedBytes + stream.publishedBytes > MAX_STREAM_TOTAL_BYTES ||
|
|
1227
|
+
textBytes > MAX_STREAM_TEXT_LENGTH
|
|
1228
|
+
) {
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (
|
|
1233
|
+
retainedBytes + stream.publishedBytes > MAX_STREAM_TOTAL_BYTES ||
|
|
1234
|
+
textBytes > MAX_STREAM_TEXT_LENGTH
|
|
1235
|
+
) {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (previous?.kind === snapshot.kind && previous.text === snapshot.text) return;
|
|
1239
|
+
stream.retainedBytes = retainedBytes;
|
|
1240
|
+
stream.blocks.set(contentIndex, {
|
|
1241
|
+
...snapshot,
|
|
1242
|
+
...(previous?.kind === snapshot.kind ? { publishedText: previous.publishedText } : {}),
|
|
1243
|
+
});
|
|
1244
|
+
stream.dirtyBlocks.add(contentIndex);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
private isValidContentIndex(contentIndex: number): boolean {
|
|
1248
|
+
return (
|
|
1249
|
+
Number.isSafeInteger(contentIndex) &&
|
|
1250
|
+
contentIndex >= 0 &&
|
|
1251
|
+
contentIndex < MAX_STREAM_CONTENT_BLOCKS
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
private publishCommand(turnId: string): void {
|
|
1256
|
+
if (!this.commandText) return;
|
|
1257
|
+
const publicText = this.dataFilter.text(this.commandText);
|
|
1258
|
+
if (!publicText || publicText === this.commandPublishedText) return;
|
|
1259
|
+
this.commandPublishedText = publicText;
|
|
1260
|
+
this.publish({
|
|
1261
|
+
type: "assistant_message",
|
|
1262
|
+
id: `omp:command:${turnId}`,
|
|
1263
|
+
messageId: `omp:command:${turnId}`,
|
|
1264
|
+
text: publicText,
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
private scheduleFlush(): void {
|
|
1269
|
+
if (this.flushTimer !== undefined) return;
|
|
1270
|
+
this.flushTimer = this.scheduler.set(() => {
|
|
1271
|
+
this.flushTimer = undefined;
|
|
1272
|
+
this.flush();
|
|
1273
|
+
}, STREAM_FRAME_MS);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private clearFlushTimer(): void {
|
|
1277
|
+
if (this.flushTimer === undefined) return;
|
|
1278
|
+
this.scheduler.clear(this.flushTimer);
|
|
1279
|
+
this.flushTimer = undefined;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
private revertTokenFor(entryId: string): string | undefined {
|
|
1283
|
+
const existing = this.revertTokenByEntry.get(entryId);
|
|
1284
|
+
if (existing) return existing;
|
|
1285
|
+
if (this.revertTokenByEntry.size >= MAX_REVERT_TARGETS) return undefined;
|
|
1286
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1287
|
+
let token: string;
|
|
1288
|
+
try {
|
|
1289
|
+
token = `omp-revert:${randomBytes(32).toString("base64url")}`;
|
|
1290
|
+
} catch {
|
|
1291
|
+
return undefined;
|
|
1292
|
+
}
|
|
1293
|
+
if (this.revertEntryByToken.has(token)) continue;
|
|
1294
|
+
this.revertTokenByEntry.set(entryId, token);
|
|
1295
|
+
this.revertEntryByToken.set(token, entryId);
|
|
1296
|
+
return token;
|
|
1297
|
+
}
|
|
1298
|
+
return undefined;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
private publishCustomMessage(message: OmpMessage): void {
|
|
1302
|
+
if (message.display === false) return;
|
|
1303
|
+
const rawType = message.customType ?? message.role;
|
|
1304
|
+
const publicType = this.dataFilter.text(rawType, 256);
|
|
1305
|
+
const lowerType = rawType.toLowerCase();
|
|
1306
|
+
const details = jsonRecord(this.dataFilter.json(message.details ?? null));
|
|
1307
|
+
const nativeIdentity = message.id ?? message.entryId ?? message.responseId;
|
|
1308
|
+
if (!nativeIdentity) this.customSequence += 1;
|
|
1309
|
+
const id = nativeIdentity
|
|
1310
|
+
? `omp:custom:${createHash("sha256").update(nativeIdentity).digest("base64url").slice(0, 12)}`
|
|
1311
|
+
: `omp:custom:${this.customSequence}`;
|
|
1312
|
+
const contentParts = Array.isArray(message.content) ? message.content : [];
|
|
1313
|
+
const imageResult = nativeImageResult(
|
|
1314
|
+
{
|
|
1315
|
+
content:
|
|
1316
|
+
message.role === "bashExecution"
|
|
1317
|
+
? [...contentParts, ...(message.images ?? [])]
|
|
1318
|
+
: contentParts,
|
|
1319
|
+
details: message.details,
|
|
1320
|
+
},
|
|
1321
|
+
this.dataFilter,
|
|
1322
|
+
);
|
|
1323
|
+
const content =
|
|
1324
|
+
typeof message.content === "string"
|
|
1325
|
+
? message.content
|
|
1326
|
+
: contentParts
|
|
1327
|
+
.flatMap((part) => (part.type === "text" && part.text ? [part.text] : []))
|
|
1328
|
+
.join("\n\n");
|
|
1329
|
+
if (message.role === "bashExecution" || /bash|shell|python/u.test(lowerType)) {
|
|
1330
|
+
const command = this.dataFilter.text(
|
|
1331
|
+
message.command ?? firstString(details, "command", "input") ?? publicType,
|
|
1332
|
+
);
|
|
1333
|
+
const output = message.output ?? content;
|
|
1334
|
+
this.publish({
|
|
1335
|
+
type: "tool_call",
|
|
1336
|
+
id,
|
|
1337
|
+
callId: id,
|
|
1338
|
+
name: publicType,
|
|
1339
|
+
detail: {
|
|
1340
|
+
type: "shell",
|
|
1341
|
+
command,
|
|
1342
|
+
...(firstString(details, "cwd") ? { cwd: firstString(details, "cwd") } : {}),
|
|
1343
|
+
...(output ? { output: this.dataFilter.text(output) } : {}),
|
|
1344
|
+
...(typeof message.exitCode === "number" || message.exitCode === null
|
|
1345
|
+
? { exitCode: message.exitCode }
|
|
1346
|
+
: typeof details?.exitCode === "number"
|
|
1347
|
+
? { exitCode: details.exitCode }
|
|
1348
|
+
: {}),
|
|
1349
|
+
},
|
|
1350
|
+
status: message.cancelled ? "canceled" : "completed",
|
|
1351
|
+
error: null,
|
|
1352
|
+
});
|
|
1353
|
+
if (imageResult) this.publishImages(id, publicType, imageResult.image);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (imageResult) {
|
|
1357
|
+
this.publishImages(id, publicType, imageResult.image);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
const advisor = lowerType.includes("advisor") || lowerType === "aside";
|
|
1361
|
+
const sharedSeverity = firstString(details, "severity");
|
|
1362
|
+
const sharedAdvisor = firstString(details, "advisor", "attribution", "name", "source");
|
|
1363
|
+
const noteLines = Array.isArray(details?.notes)
|
|
1364
|
+
? details.notes.flatMap((note) => {
|
|
1365
|
+
const record = jsonRecord(note);
|
|
1366
|
+
const noteText =
|
|
1367
|
+
typeof note === "string"
|
|
1368
|
+
? note
|
|
1369
|
+
: (firstString(record, "note", "text", "content", "message") ?? "");
|
|
1370
|
+
const severity = firstString(record, "severity") ?? sharedSeverity;
|
|
1371
|
+
const noteAdvisor =
|
|
1372
|
+
firstString(record, "advisor", "attribution", "name", "source") ?? sharedAdvisor;
|
|
1373
|
+
const prefix = [
|
|
1374
|
+
severity ? `[${severity}]` : undefined,
|
|
1375
|
+
noteAdvisor ? `[${noteAdvisor}]` : undefined,
|
|
1376
|
+
]
|
|
1377
|
+
.filter(Boolean)
|
|
1378
|
+
.join(" ");
|
|
1379
|
+
const line = [prefix, noteText].filter(Boolean).join(" ");
|
|
1380
|
+
return line ? [line] : [];
|
|
1381
|
+
})
|
|
1382
|
+
: typeof details?.notes === "string"
|
|
1383
|
+
? [
|
|
1384
|
+
[
|
|
1385
|
+
sharedSeverity ? `[${sharedSeverity}]` : undefined,
|
|
1386
|
+
sharedAdvisor ? `[${sharedAdvisor}]` : undefined,
|
|
1387
|
+
details.notes,
|
|
1388
|
+
]
|
|
1389
|
+
.filter(Boolean)
|
|
1390
|
+
.join(" "),
|
|
1391
|
+
]
|
|
1392
|
+
: [];
|
|
1393
|
+
const advisorMetadata = [
|
|
1394
|
+
sharedSeverity ? `[${sharedSeverity}]` : undefined,
|
|
1395
|
+
sharedAdvisor ? `[${sharedAdvisor}]` : undefined,
|
|
1396
|
+
]
|
|
1397
|
+
.filter(Boolean)
|
|
1398
|
+
.join(" ");
|
|
1399
|
+
const text = [content, ...noteLines].filter(Boolean).join("\n\n") || advisorMetadata;
|
|
1400
|
+
if (!text && !advisor) return;
|
|
1401
|
+
this.publish({
|
|
1402
|
+
type: "tool_call",
|
|
1403
|
+
id,
|
|
1404
|
+
callId: id,
|
|
1405
|
+
name: publicType,
|
|
1406
|
+
detail: {
|
|
1407
|
+
type: "plain_text",
|
|
1408
|
+
label: advisor ? "Advisor" : publicType,
|
|
1409
|
+
text: this.dataFilter.text(text || "Advisor update"),
|
|
1410
|
+
icon: advisor ? "brain" : "sparkles",
|
|
1411
|
+
},
|
|
1412
|
+
status: "completed",
|
|
1413
|
+
error: null,
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
private publishTodoResult(snapshot: ToolSnapshot): boolean {
|
|
1418
|
+
const phases = resultDetails(snapshot.output)?.phases;
|
|
1419
|
+
if (!Array.isArray(phases)) return false;
|
|
1420
|
+
const items: Array<{
|
|
1421
|
+
id: string;
|
|
1422
|
+
text: string;
|
|
1423
|
+
completed: boolean;
|
|
1424
|
+
status: "pending" | "in_progress" | "completed";
|
|
1425
|
+
activeForm?: string;
|
|
1426
|
+
}> = [];
|
|
1427
|
+
for (const phase of phases) {
|
|
1428
|
+
const phaseRecord = jsonRecord(phase);
|
|
1429
|
+
const tasks = phaseRecord?.tasks;
|
|
1430
|
+
if (!Array.isArray(tasks)) continue;
|
|
1431
|
+
const phaseName = firstString(phaseRecord, "name", "phase");
|
|
1432
|
+
for (const task of tasks) {
|
|
1433
|
+
if (items.length >= MAX_TODOS) break;
|
|
1434
|
+
const taskRecord = jsonRecord(task);
|
|
1435
|
+
const text = firstString(taskRecord, "content", "text");
|
|
1436
|
+
if (!text) continue;
|
|
1437
|
+
const status = firstString(taskRecord, "status");
|
|
1438
|
+
const completed = status === "completed" || status === "abandoned";
|
|
1439
|
+
items.push({
|
|
1440
|
+
id: todoPublicId(firstString(taskRecord, "id"), items.length),
|
|
1441
|
+
text: this.dataFilter.text(text, 16_384),
|
|
1442
|
+
completed,
|
|
1443
|
+
status: completed ? "completed" : status === "in_progress" ? "in_progress" : "pending",
|
|
1444
|
+
...(phaseName ? { activeForm: this.dataFilter.text(phaseName, 256) } : {}),
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
this.publish({ type: "todo", id: "omp:todos", items });
|
|
1449
|
+
return true;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
private toolDetail(snapshot: ToolSnapshot): ProviderToolCallDetail {
|
|
1453
|
+
const input = jsonRecord(snapshot.input);
|
|
1454
|
+
const nestedInput = jsonRecord(input?.input) ?? input;
|
|
1455
|
+
const output = jsonRecord(snapshot.output);
|
|
1456
|
+
const details = resultDetails(snapshot.output);
|
|
1457
|
+
const resultText = toolResultText(snapshot.output);
|
|
1458
|
+
const name = snapshot.nativeName.toLowerCase();
|
|
1459
|
+
if (xdeviceToolName(snapshot.nativeName, snapshot.input)) {
|
|
1460
|
+
return { type: "unknown", input: snapshot.input, output: snapshot.output };
|
|
1461
|
+
}
|
|
1462
|
+
if (["bash", "shell", "exec", "run_command"].includes(name)) {
|
|
1463
|
+
const exitCode = details?.exitCode ?? output?.exitCode;
|
|
1464
|
+
return {
|
|
1465
|
+
type: "shell",
|
|
1466
|
+
command: firstString(nestedInput, "command", "cmd") ?? snapshot.name,
|
|
1467
|
+
...(firstString(nestedInput, "cwd") ? { cwd: firstString(nestedInput, "cwd") } : {}),
|
|
1468
|
+
...(resultText !== undefined ? { output: resultText } : {}),
|
|
1469
|
+
...(typeof exitCode === "number" || exitCode === null ? { exitCode } : {}),
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
if (name === "read") {
|
|
1473
|
+
const filePath = firstString(nestedInput, "path", "filePath", "url");
|
|
1474
|
+
if (!filePath) return { type: "unknown", input: snapshot.input, output: snapshot.output };
|
|
1475
|
+
if (!/^[A-Za-z]:[\\/]/u.test(filePath) && /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(filePath)) {
|
|
1476
|
+
const url = publishableHttpUrl(filePath);
|
|
1477
|
+
if (!url) {
|
|
1478
|
+
return { type: "plain_text", label: snapshot.name, text: resultText };
|
|
1479
|
+
}
|
|
1480
|
+
return {
|
|
1481
|
+
type: "fetch",
|
|
1482
|
+
url,
|
|
1483
|
+
...(resultText !== undefined ? { result: resultText } : {}),
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
return {
|
|
1487
|
+
type: "read",
|
|
1488
|
+
filePath,
|
|
1489
|
+
...(resultText !== undefined ? { content: resultText } : {}),
|
|
1490
|
+
...(typeof nestedInput?.offset === "number" ? { offset: nestedInput.offset } : {}),
|
|
1491
|
+
...(typeof nestedInput?.limit === "number" ? { limit: nestedInput.limit } : {}),
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
if (name === "edit" || name === "apply_patch") {
|
|
1495
|
+
const perFileResults = Array.isArray(details?.perFileResults)
|
|
1496
|
+
? details.perFileResults.flatMap((result) => {
|
|
1497
|
+
const record = jsonRecord(result);
|
|
1498
|
+
return record ? [record] : [];
|
|
1499
|
+
})
|
|
1500
|
+
: [];
|
|
1501
|
+
const filePath =
|
|
1502
|
+
firstString(nestedInput, "path", "filePath") ??
|
|
1503
|
+
firstString(details, "path", "filePath") ??
|
|
1504
|
+
firstString(perFileResults[0], "path", "filePath");
|
|
1505
|
+
if (!filePath) return { type: "unknown", input: snapshot.input, output: snapshot.output };
|
|
1506
|
+
const perFileDiff = perFileResults
|
|
1507
|
+
.flatMap((result) => {
|
|
1508
|
+
const diff = firstString(result, "unifiedDiff", "diff", "patch");
|
|
1509
|
+
return diff ? [diff] : [];
|
|
1510
|
+
})
|
|
1511
|
+
.join("\n");
|
|
1512
|
+
const unifiedDiff =
|
|
1513
|
+
firstString(details, "unifiedDiff", "diff", "patch") ??
|
|
1514
|
+
firstString(output, "unifiedDiff", "diff", "patch") ??
|
|
1515
|
+
(perFileDiff || undefined);
|
|
1516
|
+
return {
|
|
1517
|
+
type: "edit",
|
|
1518
|
+
filePath,
|
|
1519
|
+
...(firstString(nestedInput, "oldString", "old_text")
|
|
1520
|
+
? { oldString: firstString(nestedInput, "oldString", "old_text") }
|
|
1521
|
+
: {}),
|
|
1522
|
+
...(firstString(nestedInput, "newString", "new_text")
|
|
1523
|
+
? { newString: firstString(nestedInput, "newString", "new_text") }
|
|
1524
|
+
: {}),
|
|
1525
|
+
...(unifiedDiff ? { unifiedDiff } : {}),
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
if (name === "write") {
|
|
1529
|
+
const filePath = firstString(nestedInput, "path", "filePath");
|
|
1530
|
+
if (!filePath) return { type: "unknown", input: snapshot.input, output: snapshot.output };
|
|
1531
|
+
return {
|
|
1532
|
+
type: "write",
|
|
1533
|
+
filePath,
|
|
1534
|
+
...(firstString(nestedInput, "content")
|
|
1535
|
+
? { content: firstString(nestedInput, "content") }
|
|
1536
|
+
: {}),
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
if (["grep", "glob", "search", "web_search"].includes(name)) {
|
|
1540
|
+
const toolName =
|
|
1541
|
+
name === "web_search"
|
|
1542
|
+
? "web_search"
|
|
1543
|
+
: name === "glob"
|
|
1544
|
+
? "glob"
|
|
1545
|
+
: name === "grep"
|
|
1546
|
+
? "grep"
|
|
1547
|
+
: "search";
|
|
1548
|
+
return {
|
|
1549
|
+
type: "search",
|
|
1550
|
+
query: firstString(nestedInput, "query", "pattern", "path") ?? "",
|
|
1551
|
+
toolName,
|
|
1552
|
+
...(resultText !== undefined ? { content: resultText } : {}),
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
if (name === "fetch" || name === "web_fetch") {
|
|
1556
|
+
const url = publishableHttpUrl(firstString(nestedInput, "url"));
|
|
1557
|
+
if (!url) return { type: "plain_text", label: snapshot.name, text: resultText };
|
|
1558
|
+
return {
|
|
1559
|
+
type: "fetch",
|
|
1560
|
+
url,
|
|
1561
|
+
...(firstString(nestedInput, "prompt")
|
|
1562
|
+
? { prompt: firstString(nestedInput, "prompt") }
|
|
1563
|
+
: {}),
|
|
1564
|
+
...(resultText !== undefined ? { result: resultText } : {}),
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
if (["task", "agent", "subagent"].includes(name)) {
|
|
1568
|
+
return {
|
|
1569
|
+
type: "sub_agent",
|
|
1570
|
+
...(firstString(nestedInput, "agent", "name")
|
|
1571
|
+
? { subAgentType: firstString(nestedInput, "agent", "name") }
|
|
1572
|
+
: {}),
|
|
1573
|
+
...(firstString(nestedInput, "description", "task")
|
|
1574
|
+
? { description: firstString(nestedInput, "description", "task") }
|
|
1575
|
+
: {}),
|
|
1576
|
+
log: resultText ?? "",
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
if (name === "advisor") {
|
|
1580
|
+
return { type: "plain_text", label: "Advisor", text: resultText, icon: "brain" };
|
|
1581
|
+
}
|
|
1582
|
+
if (name === "todo") {
|
|
1583
|
+
return { type: "plan", text: resultText ?? JSON.stringify(snapshot.input) };
|
|
1584
|
+
}
|
|
1585
|
+
return { type: "unknown", input: snapshot.input, output: snapshot.output };
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
private publishGoal(event: Extract<OmpRpcEvent, { type: "goal_updated" }>): void {
|
|
1589
|
+
const goal = event.goal ?? event.state?.goal;
|
|
1590
|
+
if (goal?.id) {
|
|
1591
|
+
const digest = createHash("sha256").update(goal.id).digest("base64url").slice(0, 12);
|
|
1592
|
+
this.goalItemId = `omp:goal:${digest}`;
|
|
1593
|
+
}
|
|
1594
|
+
const id = this.goalItemId ?? "omp:goal";
|
|
1595
|
+
const lines = goal
|
|
1596
|
+
? [
|
|
1597
|
+
goal.objective || "OMP goal updated.",
|
|
1598
|
+
goal.status ? `Status: ${goal.status}` : undefined,
|
|
1599
|
+
goal.tokensUsed !== undefined ? `Tokens used: ${goal.tokensUsed}` : undefined,
|
|
1600
|
+
goal.tokenBudget !== undefined ? `Token budget: ${goal.tokenBudget}` : undefined,
|
|
1601
|
+
goal.timeUsedSeconds !== undefined ? `Time used: ${goal.timeUsedSeconds}s` : undefined,
|
|
1602
|
+
event.state?.mode ? `Mode: ${event.state.mode}` : undefined,
|
|
1603
|
+
event.state?.reason ? `Reason: ${event.state.reason}` : undefined,
|
|
1604
|
+
]
|
|
1605
|
+
: ["OMP goal cleared.", event.state?.reason];
|
|
1606
|
+
this.publishStatusItem({
|
|
1607
|
+
id,
|
|
1608
|
+
name: "omp_goal_updated",
|
|
1609
|
+
label: goal?.status ? `OMP goal ${goal.status}` : "OMP goal updated",
|
|
1610
|
+
text: lines.filter((line): line is string => Boolean(line)).join("\n"),
|
|
1611
|
+
icon: "brain",
|
|
1612
|
+
status: "completed",
|
|
1613
|
+
});
|
|
1614
|
+
if (!goal) this.goalItemId = null;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
private publishAutoRetry(
|
|
1618
|
+
event: Extract<OmpRpcEvent, { type: "auto_retry_start" | "auto_retry_end" }>,
|
|
1619
|
+
): void {
|
|
1620
|
+
const id = `omp:auto-retry:${event.attempt}`;
|
|
1621
|
+
if (event.type === "auto_retry_start") {
|
|
1622
|
+
const delay =
|
|
1623
|
+
event.delayMs < 1_000
|
|
1624
|
+
? `${event.delayMs}ms`
|
|
1625
|
+
: event.delayMs % 1_000 === 0
|
|
1626
|
+
? `${event.delayMs / 1_000}s`
|
|
1627
|
+
: `${(event.delayMs / 1_000).toFixed(1)}s`;
|
|
1628
|
+
this.publishStatusItem({
|
|
1629
|
+
id,
|
|
1630
|
+
name: "omp_auto_retry",
|
|
1631
|
+
label: `OMP retry ${event.attempt}/${event.maxAttempts}`,
|
|
1632
|
+
text: `Retrying in ${delay}: ${event.errorMessage}`,
|
|
1633
|
+
icon: "sparkles",
|
|
1634
|
+
status: "running",
|
|
1635
|
+
});
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const text = event.finalError ?? (event.success ? "Retry recovered." : "Retry failed.");
|
|
1639
|
+
this.publishStatusItem({
|
|
1640
|
+
id,
|
|
1641
|
+
name: "omp_auto_retry",
|
|
1642
|
+
label: event.success
|
|
1643
|
+
? `OMP retry ${event.attempt} recovered`
|
|
1644
|
+
: `OMP retry ${event.attempt} failed`,
|
|
1645
|
+
text,
|
|
1646
|
+
icon: "sparkles",
|
|
1647
|
+
status: event.success ? "completed" : "failed",
|
|
1648
|
+
...(event.success ? {} : { error: text }),
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
private publishRetryFallback(
|
|
1653
|
+
event: Extract<OmpRpcEvent, { type: "retry_fallback_applied" | "retry_fallback_succeeded" }>,
|
|
1654
|
+
): void {
|
|
1655
|
+
const role = this.dataFilter.text(event.role, MAX_PUBLIC_TOOL_PAYLOAD_BYTES);
|
|
1656
|
+
const digest = createHash("sha256").update(role).digest("base64url").slice(0, 12);
|
|
1657
|
+
const id = `omp:retry-fallback:${digest}`;
|
|
1658
|
+
if (event.type === "retry_fallback_applied") {
|
|
1659
|
+
this.publishStatusItem({
|
|
1660
|
+
id,
|
|
1661
|
+
name: "omp_retry_fallback",
|
|
1662
|
+
label: `OMP fallback applied for ${role}`,
|
|
1663
|
+
text: `${event.from} -> ${event.to}`,
|
|
1664
|
+
icon: "sparkles",
|
|
1665
|
+
status: "running",
|
|
1666
|
+
});
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
this.publishStatusItem({
|
|
1670
|
+
id,
|
|
1671
|
+
name: "omp_retry_fallback",
|
|
1672
|
+
label: `OMP fallback succeeded for ${role}`,
|
|
1673
|
+
text: `Using ${event.model}`,
|
|
1674
|
+
icon: "sparkles",
|
|
1675
|
+
status: "completed",
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
private publishStatusItem(input: {
|
|
1680
|
+
id: string;
|
|
1681
|
+
name: string;
|
|
1682
|
+
label: string;
|
|
1683
|
+
text: string;
|
|
1684
|
+
icon: "brain" | "sparkles";
|
|
1685
|
+
status: "running" | "completed" | "failed";
|
|
1686
|
+
error?: string;
|
|
1687
|
+
}): void {
|
|
1688
|
+
const detail = {
|
|
1689
|
+
type: "plain_text" as const,
|
|
1690
|
+
label: this.dataFilter.text(input.label, 4_096),
|
|
1691
|
+
text: this.dataFilter.text(input.text, 64 * 1024),
|
|
1692
|
+
icon: input.icon,
|
|
1693
|
+
};
|
|
1694
|
+
if (input.status === "failed") {
|
|
1695
|
+
this.publish({
|
|
1696
|
+
type: "tool_call",
|
|
1697
|
+
id: input.id,
|
|
1698
|
+
callId: input.id,
|
|
1699
|
+
name: input.name,
|
|
1700
|
+
detail,
|
|
1701
|
+
status: "failed",
|
|
1702
|
+
error: this.dataFilter.text(input.error ?? input.text, 64 * 1024),
|
|
1703
|
+
});
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
this.publish({
|
|
1707
|
+
type: "tool_call",
|
|
1708
|
+
id: input.id,
|
|
1709
|
+
callId: input.id,
|
|
1710
|
+
name: input.name,
|
|
1711
|
+
detail,
|
|
1712
|
+
status: input.status,
|
|
1713
|
+
error: null,
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
private publishImages(id: string, label: string, image: NativeImageEnvelope): void {
|
|
1718
|
+
this.publish({
|
|
1719
|
+
type: "tool_call",
|
|
1720
|
+
id: `${id}:images`,
|
|
1721
|
+
callId: `${id}:images`,
|
|
1722
|
+
name: `${label} images`,
|
|
1723
|
+
detail: { type: "plain_text", label },
|
|
1724
|
+
metadata: { ompImageOwner: "omp", ompImage: { label, ...image } },
|
|
1725
|
+
status: "completed",
|
|
1726
|
+
error: null,
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
private publishTool(snapshot: ToolSnapshot, status: "running" | "completed"): void;
|
|
1731
|
+
private publishTool(snapshot: ToolSnapshot, status: "failed", error: JsonValue): void;
|
|
1732
|
+
private publishTool(
|
|
1733
|
+
snapshot: ToolSnapshot,
|
|
1734
|
+
status: "running" | "completed" | "failed",
|
|
1735
|
+
error?: JsonValue,
|
|
1736
|
+
): void {
|
|
1737
|
+
const detail = this.toolDetail(snapshot);
|
|
1738
|
+
if (status === "failed") {
|
|
1739
|
+
this.publish({
|
|
1740
|
+
type: "tool_call",
|
|
1741
|
+
id: snapshot.publicId,
|
|
1742
|
+
callId: snapshot.publicId,
|
|
1743
|
+
name: snapshot.name,
|
|
1744
|
+
detail,
|
|
1745
|
+
status,
|
|
1746
|
+
error: error ?? null,
|
|
1747
|
+
});
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
this.publish({
|
|
1751
|
+
type: "tool_call",
|
|
1752
|
+
id: snapshot.publicId,
|
|
1753
|
+
callId: snapshot.publicId,
|
|
1754
|
+
name: snapshot.name,
|
|
1755
|
+
detail,
|
|
1756
|
+
status,
|
|
1757
|
+
error: null,
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
private publish(item: ProviderTimelineItem): void {
|
|
1762
|
+
this.emit({ type: "timeline.item", sessionId: this.sessionId, item });
|
|
1763
|
+
}
|
|
1764
|
+
}
|