@pasko70/pibo 1.8.1 → 1.8.2
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.
|
@@ -10,6 +10,10 @@ export class PiboProviderTelemetryRecorder {
|
|
|
10
10
|
this.telemetry = new BestEffortTelemetryService(options.store, options.onError);
|
|
11
11
|
}
|
|
12
12
|
recordRequestStart(payload, options = {}) {
|
|
13
|
+
const capturedOptions = { ...options, at: options.at ?? new Date().toISOString() };
|
|
14
|
+
return this.schedule(() => this.recordRequestStartNow(providerPayloadSnapshot(payload), capturedOptions));
|
|
15
|
+
}
|
|
16
|
+
recordRequestStartNow(payload, options) {
|
|
13
17
|
if (!this.options.store)
|
|
14
18
|
return undefined;
|
|
15
19
|
try {
|
|
@@ -74,6 +78,10 @@ export class PiboProviderTelemetryRecorder {
|
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
recordResponse(input) {
|
|
81
|
+
const captured = { status: input.status, at: input.at ?? new Date().toISOString() };
|
|
82
|
+
return this.schedule(() => this.recordResponseNow(captured));
|
|
83
|
+
}
|
|
84
|
+
recordResponseNow(input) {
|
|
77
85
|
if (!this.options.store)
|
|
78
86
|
return undefined;
|
|
79
87
|
try {
|
|
@@ -129,7 +137,14 @@ export class PiboProviderTelemetryRecorder {
|
|
|
129
137
|
// The assistant message boundary ends the provider stream even when the wider
|
|
130
138
|
// Pibo turn continues with a long-running tool or another provider request.
|
|
131
139
|
recordMessageEnd(message, options = {}) {
|
|
132
|
-
if (!
|
|
140
|
+
if (!isAssistantMessage(message))
|
|
141
|
+
return undefined;
|
|
142
|
+
const captured = providerAssistantMessageSnapshot(message);
|
|
143
|
+
const capturedOptions = { ...options, at: options.at ?? new Date().toISOString() };
|
|
144
|
+
return this.schedule(() => this.recordMessageEndNow(captured, capturedOptions));
|
|
145
|
+
}
|
|
146
|
+
recordMessageEndNow(message, options) {
|
|
147
|
+
if (!this.options.store)
|
|
133
148
|
return undefined;
|
|
134
149
|
const status = providerStatusForMessage(message);
|
|
135
150
|
const summary = providerSummaryForStatus(status);
|
|
@@ -144,7 +159,14 @@ export class PiboProviderTelemetryRecorder {
|
|
|
144
159
|
return this.finishActiveProviderRequest(status, options.at ?? new Date().toISOString(), summary, errorMessage, errorDetails?.category ?? errorDetails?.errorClass);
|
|
145
160
|
}
|
|
146
161
|
recordShutdown(reason, at = new Date().toISOString()) {
|
|
147
|
-
return this.finishActiveProviderRequest("aborted", at, reason, undefined, "runtime_abort");
|
|
162
|
+
return this.schedule(() => this.finishActiveProviderRequest("aborted", at, reason, undefined, "runtime_abort"));
|
|
163
|
+
}
|
|
164
|
+
schedule(write) {
|
|
165
|
+
if (this.options.writer) {
|
|
166
|
+
this.options.writer.enqueue(write, this.options.onError);
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
return write();
|
|
148
170
|
}
|
|
149
171
|
finishActiveProviderRequest(status, now, summary, errorMessage, errorCategory) {
|
|
150
172
|
if (!this.options.store)
|
|
@@ -301,6 +323,22 @@ function modelFromContext(ctx) {
|
|
|
301
323
|
const api = typeof candidate.api === "string" && candidate.api.length > 0 ? candidate.api : undefined;
|
|
302
324
|
return provider && id ? { provider, id, api } : undefined;
|
|
303
325
|
}
|
|
326
|
+
function providerAssistantMessageSnapshot(message) {
|
|
327
|
+
return {
|
|
328
|
+
role: message.role,
|
|
329
|
+
stopReason: message.stopReason,
|
|
330
|
+
errorMessage: message.errorMessage,
|
|
331
|
+
api: message.api,
|
|
332
|
+
provider: message.provider,
|
|
333
|
+
model: message.model,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function providerPayloadSnapshot(payload) {
|
|
337
|
+
return {
|
|
338
|
+
model: modelIdFromPayload(payload),
|
|
339
|
+
service_tier: serviceTierFromPayload(payload),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
304
342
|
function modelIdFromPayload(payload) {
|
|
305
343
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
306
344
|
return undefined;
|
|
@@ -8,6 +8,7 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
8
8
|
telemetry;
|
|
9
9
|
providerEventMode;
|
|
10
10
|
progressFlushIntervalMs;
|
|
11
|
+
writer;
|
|
11
12
|
pendingProviderProgress = new Map();
|
|
12
13
|
providerRequestCache = new Map();
|
|
13
14
|
lastProviderFlushAtMs = new Map();
|
|
@@ -17,6 +18,7 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
17
18
|
this.onError = onError;
|
|
18
19
|
this.telemetry = new BestEffortTelemetryService(store, onError);
|
|
19
20
|
this.providerEventMode = options.providerEventMode ?? "aggregate";
|
|
21
|
+
this.writer = options.writer;
|
|
20
22
|
const progressFlushIntervalMs = options.progressFlushIntervalMs;
|
|
21
23
|
this.progressFlushIntervalMs = typeof progressFlushIntervalMs === "number" && Number.isFinite(progressFlushIntervalMs) && progressFlushIntervalMs >= 0
|
|
22
24
|
? progressFlushIntervalMs
|
|
@@ -25,31 +27,43 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
25
27
|
recordOutput(event, context = {}) {
|
|
26
28
|
if (!this.store)
|
|
27
29
|
return;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
catch (error) {
|
|
32
|
-
this.onError?.(error);
|
|
33
|
-
}
|
|
30
|
+
const captured = captureTelemetryContext(context);
|
|
31
|
+
const capturedEvent = telemetryOutputEventSnapshot(event);
|
|
32
|
+
this.schedule(() => this.recordOutputUnsafe(capturedEvent, captured));
|
|
34
33
|
}
|
|
35
34
|
recordPiEvent(piboSessionId, event, context = {}) {
|
|
36
35
|
if (!this.store)
|
|
37
36
|
return;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
37
|
+
const summary = providerEventSummaryForPiEvent(event);
|
|
38
|
+
if (!summary)
|
|
39
|
+
return;
|
|
40
|
+
const captured = captureTelemetryContext(context);
|
|
41
|
+
this.schedule(() => this.recordPiEventSummaryUnsafe(piboSessionId, summary, captured));
|
|
44
42
|
}
|
|
45
43
|
recordMessagesInterrupted(messages, context = {}, reason = "message interrupted") {
|
|
46
44
|
if (!this.store)
|
|
47
45
|
return;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
const captured = captureTelemetryContext(context);
|
|
47
|
+
const interrupted = messages.flatMap((message) => message.id ? [{ piboSessionId: message.piboSessionId, eventId: message.id }] : []);
|
|
48
|
+
this.schedule(() => {
|
|
49
|
+
for (const message of interrupted) {
|
|
50
|
+
this.recordTurnTerminal(message, captured, "aborted", "abort", reason, "runtime_abort");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
schedule(write) {
|
|
55
|
+
const guarded = () => {
|
|
56
|
+
try {
|
|
57
|
+
write();
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
this.onError?.(error);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
if (this.writer)
|
|
64
|
+
this.writer.enqueue(guarded, this.onError);
|
|
65
|
+
else
|
|
66
|
+
guarded();
|
|
53
67
|
}
|
|
54
68
|
recordOutputUnsafe(event, context) {
|
|
55
69
|
switch (event.type) {
|
|
@@ -77,7 +91,7 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
77
91
|
case "thinking_finished": {
|
|
78
92
|
this.recordProviderStreamProgress(event, context, "reasoning", "reasoning finished", true);
|
|
79
93
|
const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context) ?? this.activeTurnContext(event.piboSessionId, context);
|
|
80
|
-
this.finishOpenPhasesByName(turn?.turnId, "reasoning", "ok");
|
|
94
|
+
this.finishOpenPhasesByName(turn?.turnId, "reasoning", "ok", telemetryTimestamp(context));
|
|
81
95
|
return;
|
|
82
96
|
}
|
|
83
97
|
case "tool_call":
|
|
@@ -104,23 +118,21 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
104
118
|
return;
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
|
-
|
|
108
|
-
const summary = providerEventSummaryForPiEvent(event);
|
|
109
|
-
if (!summary)
|
|
110
|
-
return;
|
|
121
|
+
recordPiEventSummaryUnsafe(piboSessionId, summary, context) {
|
|
111
122
|
const turn = context.activeEventId
|
|
112
123
|
? this.progressTurnContextForEvent(piboSessionId, context.activeEventId, context)
|
|
113
124
|
: this.activeTurnContext(piboSessionId, context);
|
|
114
125
|
if (!turn)
|
|
115
126
|
return;
|
|
116
|
-
const now =
|
|
127
|
+
const now = telemetryTimestamp(context);
|
|
128
|
+
const nowMs = telemetryTimestampMs(context);
|
|
117
129
|
if (summary.assistantEventType === "start") {
|
|
118
|
-
this.flushProviderProgress(turn.turnId, now, { force: true, includeLatest: true });
|
|
130
|
+
this.flushProviderProgress(turn.turnId, now, { force: true, includeLatest: true, nowMs });
|
|
119
131
|
this.clearProviderProgress(turn.turnId);
|
|
120
132
|
}
|
|
121
133
|
const providerRequest = this.providerEventMode === "detailed"
|
|
122
134
|
? this.providerRequestForTurn(turn.turnId, { includeLatest: summary.messageEnded, refresh: summary.messageEnded })
|
|
123
|
-
: this.accumulateProviderEvent(turn, summary, now);
|
|
135
|
+
: this.accumulateProviderEvent(turn, summary, now, nowMs);
|
|
124
136
|
if (!providerRequest) {
|
|
125
137
|
if (summary.messageEnded)
|
|
126
138
|
this.clearProviderProgress(turn.turnId);
|
|
@@ -146,11 +158,11 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
146
158
|
}
|
|
147
159
|
if (summary.toolCallId && summary.assistantEventType?.startsWith("toolcall_")) {
|
|
148
160
|
const forceToolProgress = summary.assistantEventType !== "toolcall_delta";
|
|
149
|
-
if (this.shouldPersistProgress(`${turn.turnId}:tool_args:${summary.toolCallId}`, forceToolProgress)) {
|
|
161
|
+
if (this.shouldPersistProgress(`${turn.turnId}:tool_args:${summary.toolCallId}`, forceToolProgress, nowMs)) {
|
|
150
162
|
this.recordPiToolCallProgress(turn, providerRequest.providerRequestId, summary, now);
|
|
151
163
|
}
|
|
152
164
|
}
|
|
153
|
-
if (!summary.messageEnded && !isTerminalProviderStatus(providerRequest.status) && !summary.normalizedType && this.shouldPersistProgress(`${turn.turnId}:provider_stream:${providerRequest.providerRequestId}
|
|
165
|
+
if (!summary.messageEnded && !isTerminalProviderStatus(providerRequest.status) && !summary.normalizedType && this.shouldPersistProgress(`${turn.turnId}:provider_stream:${providerRequest.providerRequestId}`, false, nowMs)) {
|
|
154
166
|
this.startOrProgressPhase(turn, "provider_stream", now, "provider event metadata", { providerRequestId: providerRequest.providerRequestId });
|
|
155
167
|
}
|
|
156
168
|
if (summary.messageEnded)
|
|
@@ -160,7 +172,7 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
160
172
|
const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, event.source, context);
|
|
161
173
|
if (!turn)
|
|
162
174
|
return;
|
|
163
|
-
const now =
|
|
175
|
+
const now = telemetryTimestamp(context);
|
|
164
176
|
const queueDepth = event.queuedMessages;
|
|
165
177
|
this.telemetry.upsertTurn({
|
|
166
178
|
turnId: turn.turnId,
|
|
@@ -197,7 +209,7 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
197
209
|
const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, event.source, context);
|
|
198
210
|
if (!turn)
|
|
199
211
|
return;
|
|
200
|
-
const now =
|
|
212
|
+
const now = telemetryTimestamp(context);
|
|
201
213
|
this.telemetry.finishPhase(phaseId(turn.turnId, "queued"), { status: "ok", endedAt: now, lastProgressAt: now });
|
|
202
214
|
this.telemetry.upsertPhase({
|
|
203
215
|
phaseId: phaseId(turn.turnId, "message_started"),
|
|
@@ -244,10 +256,10 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
244
256
|
: this.activeTurnContext(event.piboSessionId, context);
|
|
245
257
|
if (!turn)
|
|
246
258
|
return;
|
|
247
|
-
const now =
|
|
248
|
-
const providerRequest = this.accumulateNormalizedProviderProgress(turn, now, force);
|
|
259
|
+
const now = telemetryTimestamp(context);
|
|
260
|
+
const providerRequest = this.accumulateNormalizedProviderProgress(turn, now, force, telemetryTimestampMs(context));
|
|
249
261
|
const progressKey = `${turn.turnId}:${phaseName}:${providerRequest?.providerRequestId ?? "none"}`;
|
|
250
|
-
if (!this.shouldPersistProgress(progressKey, force))
|
|
262
|
+
if (!this.shouldPersistProgress(progressKey, force, telemetryTimestampMs(context)))
|
|
251
263
|
return;
|
|
252
264
|
const storedTurn = this.store?.getTurn(turn.turnId);
|
|
253
265
|
if (storedTurn && TERMINAL_TURN_STATUSES.has(storedTurn.status)) {
|
|
@@ -256,7 +268,7 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
256
268
|
}
|
|
257
269
|
this.closeOpenPhasesByName(turn.turnId, "message_started", "ok", now);
|
|
258
270
|
const providerStreamKey = `${turn.turnId}:provider_stream:${providerRequest?.providerRequestId ?? "none"}`;
|
|
259
|
-
if ((!providerRequest || !isTerminalProviderStatus(providerRequest.status)) && this.shouldPersistProgress(providerStreamKey, force)) {
|
|
271
|
+
if ((!providerRequest || !isTerminalProviderStatus(providerRequest.status)) && this.shouldPersistProgress(providerStreamKey, force, telemetryTimestampMs(context))) {
|
|
260
272
|
this.startOrProgressPhase(turn, "provider_stream", now, "normalized provider stream progress", { providerRequestId: providerRequest?.providerRequestId });
|
|
261
273
|
}
|
|
262
274
|
this.startOrProgressPhase(turn, phaseName, now, summary, { updateTurn: true, providerRequestId: providerRequest?.providerRequestId });
|
|
@@ -265,11 +277,11 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
265
277
|
const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context) ?? this.activeTurnContext(event.piboSessionId, context);
|
|
266
278
|
if (!turn)
|
|
267
279
|
return;
|
|
268
|
-
const now =
|
|
280
|
+
const now = telemetryTimestamp(context);
|
|
269
281
|
this.closeOpenPhasesByName(turn.turnId, "message_started", "ok", now);
|
|
270
282
|
this.closeOpenPhasesByName(turn.turnId, "assistant_text", "ok", now);
|
|
271
283
|
this.closeOpenPhasesByName(turn.turnId, "reasoning", "ok", now);
|
|
272
|
-
const providerRequest = this.accumulateNormalizedProviderProgress(turn, now, true)
|
|
284
|
+
const providerRequest = this.accumulateNormalizedProviderProgress(turn, now, true, telemetryTimestampMs(context))
|
|
273
285
|
?? this.providerRequestForTurn(turn.turnId, { includeLatest: true });
|
|
274
286
|
this.upsertToolCallArgs(turn, {
|
|
275
287
|
toolCallId: event.toolCallId,
|
|
@@ -295,14 +307,14 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
295
307
|
: this.activeTurnContext(event.piboSessionId, context);
|
|
296
308
|
if (!turn)
|
|
297
309
|
return;
|
|
298
|
-
if (!this.shouldPersistProgress(`${turn.turnId}:tool_execution:${event.toolCallId}`, force))
|
|
310
|
+
if (!this.shouldPersistProgress(`${turn.turnId}:tool_execution:${event.toolCallId}`, force, telemetryTimestampMs(context)))
|
|
299
311
|
return;
|
|
300
312
|
const storedTurn = this.store?.getTurn(turn.turnId);
|
|
301
313
|
if (storedTurn && TERMINAL_TURN_STATUSES.has(storedTurn.status)) {
|
|
302
314
|
this.clearTurnProgress(turn.turnId);
|
|
303
315
|
return;
|
|
304
316
|
}
|
|
305
|
-
const now =
|
|
317
|
+
const now = telemetryTimestamp(context);
|
|
306
318
|
const existing = this.store?.getToolCall(event.toolCallId);
|
|
307
319
|
const args = toolArgsMetadata(event.args, true);
|
|
308
320
|
const providerRequestId = existing?.providerRequestId ?? this.latestProviderRequestForTurn(turn.turnId)?.providerRequestId;
|
|
@@ -336,7 +348,7 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
336
348
|
const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context) ?? this.activeTurnContext(event.piboSessionId, context);
|
|
337
349
|
if (!turn)
|
|
338
350
|
return;
|
|
339
|
-
const now =
|
|
351
|
+
const now = telemetryTimestamp(context);
|
|
340
352
|
const existing = this.store?.getToolCall(event.toolCallId);
|
|
341
353
|
const executionStartedAt = existing?.executionStartedAt;
|
|
342
354
|
this.telemetry.upsertToolCall({
|
|
@@ -425,8 +437,8 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
425
437
|
this.clearTurnProgress(turn.turnId);
|
|
426
438
|
return;
|
|
427
439
|
}
|
|
428
|
-
const now =
|
|
429
|
-
this.flushProviderProgress(turn.turnId, now, { force: true, includeLatest: true });
|
|
440
|
+
const now = telemetryTimestamp(context);
|
|
441
|
+
this.flushProviderProgress(turn.turnId, now, { force: true, includeLatest: true, nowMs: telemetryTimestampMs(context) });
|
|
430
442
|
this.finishOpenPhases(turn.turnId, terminalPhaseStatus(status), now);
|
|
431
443
|
this.finishActiveProviderRequests(turn.turnId, providerStatusForTurnStatus(status), now, summary, errorCategory);
|
|
432
444
|
this.finishActiveToolCalls(turn.turnId, status, now, summary);
|
|
@@ -540,7 +552,7 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
540
552
|
this.providerRequestCache.delete(turnId);
|
|
541
553
|
return request;
|
|
542
554
|
}
|
|
543
|
-
accumulateProviderEvent(turn, summary, now) {
|
|
555
|
+
accumulateProviderEvent(turn, summary, now, nowMs) {
|
|
544
556
|
const pending = this.pendingProviderProgress.get(turn.turnId) ?? emptyPendingProviderProgress();
|
|
545
557
|
pending.lastRawEventAt = now;
|
|
546
558
|
pending.upstreamResponseId = summary.upstreamResponseId ?? pending.upstreamResponseId;
|
|
@@ -554,9 +566,10 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
554
566
|
force: summary.messageEnded,
|
|
555
567
|
includeLatest: summary.messageEnded,
|
|
556
568
|
refresh: summary.messageEnded,
|
|
569
|
+
nowMs,
|
|
557
570
|
});
|
|
558
571
|
}
|
|
559
|
-
accumulateNormalizedProviderProgress(turn, now, force = false) {
|
|
572
|
+
accumulateNormalizedProviderProgress(turn, now, force = false, nowMs = Date.now()) {
|
|
560
573
|
const request = this.providerRequestForTurn(turn.turnId, { includeLatest: force });
|
|
561
574
|
if (!request)
|
|
562
575
|
return undefined;
|
|
@@ -565,14 +578,14 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
565
578
|
pending.normalizedEventCount += 1;
|
|
566
579
|
this.pendingProviderProgress.set(turn.turnId, pending);
|
|
567
580
|
const flushNow = force || request.status === "started" || request.status === "headers";
|
|
568
|
-
return this.flushProviderProgress(turn.turnId, now, { force: flushNow, includeLatest: force }) ?? request;
|
|
581
|
+
return this.flushProviderProgress(turn.turnId, now, { force: flushNow, includeLatest: force, nowMs }) ?? request;
|
|
569
582
|
}
|
|
570
583
|
flushProviderProgress(turnId, now, options = {}) {
|
|
571
584
|
const pending = this.pendingProviderProgress.get(turnId);
|
|
572
585
|
const request = this.providerRequestForTurn(turnId, options);
|
|
573
586
|
if (!pending)
|
|
574
587
|
return request;
|
|
575
|
-
const nowMs = Date.now();
|
|
588
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
576
589
|
const lastFlushAtMs = this.lastProviderFlushAtMs.get(turnId);
|
|
577
590
|
if (!options.force && lastFlushAtMs !== undefined && nowMs - lastFlushAtMs < this.progressFlushIntervalMs)
|
|
578
591
|
return request;
|
|
@@ -602,8 +615,7 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
602
615
|
this.providerRequestCache.set(turnId, updated);
|
|
603
616
|
return updated ?? request;
|
|
604
617
|
}
|
|
605
|
-
shouldPersistProgress(key, force = false) {
|
|
606
|
-
const nowMs = Date.now();
|
|
618
|
+
shouldPersistProgress(key, force = false, nowMs = Date.now()) {
|
|
607
619
|
const lastWriteAtMs = this.lastProgressWriteAtMs.get(key);
|
|
608
620
|
if (!force && lastWriteAtMs !== undefined && nowMs - lastWriteAtMs < this.progressFlushIntervalMs)
|
|
609
621
|
return false;
|
|
@@ -960,6 +972,32 @@ function safeJsonByteSize(value) {
|
|
|
960
972
|
function utf8Bytes(value) {
|
|
961
973
|
return Buffer.byteLength(value, "utf8");
|
|
962
974
|
}
|
|
975
|
+
function telemetryOutputEventSnapshot(event) {
|
|
976
|
+
if (event.type === "tool_execution_updated")
|
|
977
|
+
return { ...event, partialResult: undefined };
|
|
978
|
+
if (event.type === "tool_execution_finished")
|
|
979
|
+
return { ...event, result: event.isError ? safeErrorMessage(event.result) : undefined };
|
|
980
|
+
if (event.type === "execution_result")
|
|
981
|
+
return { ...event, result: undefined };
|
|
982
|
+
return { ...event };
|
|
983
|
+
}
|
|
984
|
+
function captureTelemetryContext(context) {
|
|
985
|
+
const parsedAtMs = context.at ? Date.parse(context.at) : Number.NaN;
|
|
986
|
+
const atMs = context.atMs ?? (Number.isFinite(parsedAtMs) ? parsedAtMs : Date.now());
|
|
987
|
+
return {
|
|
988
|
+
...context,
|
|
989
|
+
session: context.session ? { ...context.session, metadata: context.session.metadata ? { ...context.session.metadata } : undefined } : undefined,
|
|
990
|
+
status: context.status ? { ...context.status, activeTools: [...context.status.activeTools], enabledTools: [...context.status.enabledTools] } : undefined,
|
|
991
|
+
at: context.at ?? new Date(atMs).toISOString(),
|
|
992
|
+
atMs,
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
function telemetryTimestamp(context) {
|
|
996
|
+
return context.at ?? new Date().toISOString();
|
|
997
|
+
}
|
|
998
|
+
function telemetryTimestampMs(context) {
|
|
999
|
+
return context.atMs ?? Date.now();
|
|
1000
|
+
}
|
|
963
1001
|
export function turnIdForEvent(eventId) {
|
|
964
1002
|
return `turn_${eventId}`;
|
|
965
1003
|
}
|
|
@@ -19,6 +19,7 @@ import { assertGatewayResourceAvailableForWork } from "./gateway-resource-guard.
|
|
|
19
19
|
import { withWorkflowSessionKind } from "../sessions/workflow-session-kind.js";
|
|
20
20
|
import { PiboRuntimeTelemetryRecorder } from "./runtime-telemetry.js";
|
|
21
21
|
import { createPiboProviderTelemetryExtension } from "./provider-telemetry.js";
|
|
22
|
+
import { AsyncTelemetryWriter } from "../data/telemetry-writer.js";
|
|
22
23
|
const DEFAULT_SUBAGENT_REPLY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
23
24
|
const DEFAULT_ROUTED_SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
24
25
|
export const RALPH_RUNTIME_RETRY_DEFAULTS = {
|
|
@@ -140,14 +141,21 @@ export class PiboSessionRouter {
|
|
|
140
141
|
sessionStore;
|
|
141
142
|
reliabilityStore;
|
|
142
143
|
telemetryStore;
|
|
144
|
+
telemetryWriter;
|
|
143
145
|
telemetryRecorder;
|
|
146
|
+
disposePromise;
|
|
147
|
+
closing = false;
|
|
144
148
|
constructor(options = {}) {
|
|
145
149
|
this.options = options;
|
|
146
150
|
this.pluginRegistry = options.pluginRegistry ?? createDefaultPiboPluginRegistry();
|
|
147
151
|
this.sessionStore = options.sessionStore ?? new InMemoryPiboSessionStore();
|
|
148
152
|
this.telemetryStore = options.telemetryStore ?? telemetryStoreFromSessionStore(this.sessionStore);
|
|
153
|
+
this.telemetryWriter = this.telemetryStore ? new AsyncTelemetryWriter(this.telemetryStore) : undefined;
|
|
149
154
|
this.telemetryRecorder = this.telemetryStore
|
|
150
|
-
? new PiboRuntimeTelemetryRecorder(this.telemetryStore, undefined, {
|
|
155
|
+
? new PiboRuntimeTelemetryRecorder(this.telemetryStore, undefined, {
|
|
156
|
+
providerEventMode: providerEventTelemetryModeFromEnv(),
|
|
157
|
+
writer: this.telemetryWriter,
|
|
158
|
+
})
|
|
151
159
|
: undefined;
|
|
152
160
|
const idleTimeoutMs = options.routedSessionIdleTimeoutMs;
|
|
153
161
|
this.routedSessionIdleTimeoutMs = idleTimeoutMs === false
|
|
@@ -173,6 +181,8 @@ export class PiboSessionRouter {
|
|
|
173
181
|
};
|
|
174
182
|
}
|
|
175
183
|
async emit(event) {
|
|
184
|
+
if (this.closing)
|
|
185
|
+
throw new Error("Pibo session router is disposed.");
|
|
176
186
|
const session = await this.getOrCreateSession(event.piboSessionId);
|
|
177
187
|
this.clearIdleSessionTimer(event.piboSessionId);
|
|
178
188
|
try {
|
|
@@ -229,8 +239,13 @@ export class PiboSessionRouter {
|
|
|
229
239
|
sessions.push(cached);
|
|
230
240
|
this.sessions.delete(id);
|
|
231
241
|
}
|
|
232
|
-
|
|
233
|
-
|
|
242
|
+
try {
|
|
243
|
+
await Promise.all(ids.map((id) => this.runtimeRegistry.closeControllerSessions(id, { force: true })));
|
|
244
|
+
await Promise.all(sessions.map((session) => session.dispose()));
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
await this.telemetryWriter?.flush();
|
|
248
|
+
}
|
|
234
249
|
for (const id of ids) {
|
|
235
250
|
this.signalRegistry.project({ type: "session_disposed", piboSessionId: id, reason });
|
|
236
251
|
}
|
|
@@ -369,17 +384,30 @@ export class PiboSessionRouter {
|
|
|
369
384
|
});
|
|
370
385
|
}
|
|
371
386
|
async disposeAll() {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
this.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
387
|
+
if (this.disposePromise)
|
|
388
|
+
return this.disposePromise;
|
|
389
|
+
this.closing = true;
|
|
390
|
+
this.disposePromise = this.disposeAllUnsafe();
|
|
391
|
+
return this.disposePromise;
|
|
392
|
+
}
|
|
393
|
+
async disposeAllUnsafe() {
|
|
394
|
+
try {
|
|
395
|
+
await Promise.allSettled([...this.pendingSessions.values()]);
|
|
396
|
+
const sessions = [...this.sessions.values()];
|
|
397
|
+
this.sessions.clear();
|
|
398
|
+
for (const timer of this.idleSessionTimers.values())
|
|
399
|
+
clearTimeout(timer);
|
|
400
|
+
this.idleSessionTimers.clear();
|
|
401
|
+
this.runRegistry.cancelAll("Pibo session router was disposed.");
|
|
402
|
+
for (const session of sessions)
|
|
403
|
+
this.signalRegistry.project({ type: "session_disposed", piboSessionId: session.getStatus().piboSessionId, reason: "router disposed" });
|
|
404
|
+
this.scheduledRunReminders.clear();
|
|
405
|
+
await this.runtimeRegistry.closeAll({ force: true });
|
|
406
|
+
await Promise.all(sessions.map((session) => session.dispose()));
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
await this.telemetryWriter?.dispose();
|
|
410
|
+
}
|
|
383
411
|
}
|
|
384
412
|
clearIdleSessionTimer(piboSessionId) {
|
|
385
413
|
const timer = this.idleSessionTimers.get(piboSessionId);
|
|
@@ -424,6 +452,8 @@ export class PiboSessionRouter {
|
|
|
424
452
|
await this.resetCachedSession(piboSessionId, "routed runtime idle timeout");
|
|
425
453
|
}
|
|
426
454
|
async getOrCreateSession(piboSessionId) {
|
|
455
|
+
if (this.closing)
|
|
456
|
+
throw new Error("Pibo session router is disposed.");
|
|
427
457
|
const existing = this.sessions.get(piboSessionId);
|
|
428
458
|
if (existing) {
|
|
429
459
|
this.clearIdleSessionTimer(piboSessionId);
|
|
@@ -453,7 +483,7 @@ export class PiboSessionRouter {
|
|
|
453
483
|
const initialThinkingLevel = resolvePiboSessionInitialThinkingLevel(piboSession);
|
|
454
484
|
const userSettings = loadPiboUserSettings();
|
|
455
485
|
const telemetryExtension = this.telemetryStore
|
|
456
|
-
? createPiboProviderTelemetryExtension({ store: this.telemetryStore, session: piboSession, model: activeModel })
|
|
486
|
+
? createPiboProviderTelemetryExtension({ store: this.telemetryStore, writer: this.telemetryWriter, session: piboSession, model: activeModel })
|
|
457
487
|
: undefined;
|
|
458
488
|
const runtime = await createPiboRuntime({
|
|
459
489
|
cwd: piboSession.workspace ?? this.options.cwd,
|
|
@@ -549,8 +579,13 @@ export class PiboSessionRouter {
|
|
|
549
579
|
const cached = this.sessions.get(piboSessionId);
|
|
550
580
|
this.clearIdleSessionTimer(piboSessionId);
|
|
551
581
|
this.sessions.delete(piboSessionId);
|
|
552
|
-
|
|
553
|
-
|
|
582
|
+
try {
|
|
583
|
+
await this.runtimeRegistry.closeControllerSessions(piboSessionId, { force: true });
|
|
584
|
+
await cached?.dispose();
|
|
585
|
+
}
|
|
586
|
+
finally {
|
|
587
|
+
await this.telemetryWriter?.flush();
|
|
588
|
+
}
|
|
554
589
|
if (reason)
|
|
555
590
|
this.signalRegistry.project({ type: "session_disposed", piboSessionId, reason });
|
|
556
591
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 25;
|
|
2
|
+
const DEFAULT_MAX_PENDING_OPERATIONS = 1_024;
|
|
3
|
+
/**
|
|
4
|
+
* Gateway-scoped, ordered telemetry writer.
|
|
5
|
+
*
|
|
6
|
+
* Normal writes are deferred briefly so telemetry from multiple routed sessions
|
|
7
|
+
* shares one SQLite transaction. The queue never drops lifecycle events: when
|
|
8
|
+
* the hard bound is reached, it drains immediately in the caller instead.
|
|
9
|
+
*/
|
|
10
|
+
export class AsyncTelemetryWriter {
|
|
11
|
+
store;
|
|
12
|
+
options;
|
|
13
|
+
flushIntervalMs;
|
|
14
|
+
maxPendingOperations;
|
|
15
|
+
pending = [];
|
|
16
|
+
flushTimer;
|
|
17
|
+
flushing = false;
|
|
18
|
+
closed = false;
|
|
19
|
+
constructor(store, options = {}) {
|
|
20
|
+
this.store = store;
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.flushIntervalMs = nonNegativeFinite(options.flushIntervalMs, DEFAULT_FLUSH_INTERVAL_MS);
|
|
23
|
+
this.maxPendingOperations = positiveInteger(options.maxPendingOperations, DEFAULT_MAX_PENDING_OPERATIONS);
|
|
24
|
+
}
|
|
25
|
+
enqueue(write, onError) {
|
|
26
|
+
if (this.closed) {
|
|
27
|
+
this.reportError(new Error("Telemetry writer is closed."), onError);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
this.pending.push({ write, onError });
|
|
31
|
+
if (this.pending.length >= this.maxPendingOperations) {
|
|
32
|
+
this.flushNow();
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.scheduleFlush();
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
async flush() {
|
|
40
|
+
this.flushNow();
|
|
41
|
+
}
|
|
42
|
+
async dispose() {
|
|
43
|
+
if (this.closed)
|
|
44
|
+
return;
|
|
45
|
+
this.flushNow();
|
|
46
|
+
this.closed = true;
|
|
47
|
+
}
|
|
48
|
+
scheduleFlush() {
|
|
49
|
+
if (this.flushTimer)
|
|
50
|
+
return;
|
|
51
|
+
this.flushTimer = setTimeout(() => {
|
|
52
|
+
this.flushTimer = undefined;
|
|
53
|
+
this.flushNow();
|
|
54
|
+
}, this.flushIntervalMs);
|
|
55
|
+
this.flushTimer.unref();
|
|
56
|
+
}
|
|
57
|
+
flushNow() {
|
|
58
|
+
if (this.flushing)
|
|
59
|
+
return;
|
|
60
|
+
if (this.flushTimer)
|
|
61
|
+
clearTimeout(this.flushTimer);
|
|
62
|
+
this.flushTimer = undefined;
|
|
63
|
+
this.flushing = true;
|
|
64
|
+
try {
|
|
65
|
+
while (this.pending.length > 0) {
|
|
66
|
+
const batch = this.pending;
|
|
67
|
+
this.pending = [];
|
|
68
|
+
try {
|
|
69
|
+
this.store.transaction(() => {
|
|
70
|
+
for (const operation of batch) {
|
|
71
|
+
try {
|
|
72
|
+
operation.write();
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
this.reportError(error, operation.onError);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
for (const operation of batch)
|
|
82
|
+
this.reportError(error, operation.onError);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
this.flushing = false;
|
|
88
|
+
if (!this.closed && this.pending.length > 0)
|
|
89
|
+
this.scheduleFlush();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
reportError(error, operationHandler) {
|
|
93
|
+
try {
|
|
94
|
+
operationHandler?.(error);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Telemetry error reporting must not affect runtime work.
|
|
98
|
+
}
|
|
99
|
+
if (operationHandler === this.options.onError)
|
|
100
|
+
return;
|
|
101
|
+
try {
|
|
102
|
+
this.options.onError?.(error);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Telemetry error reporting must not affect runtime work.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function nonNegativeFinite(value, fallback) {
|
|
110
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
111
|
+
}
|
|
112
|
+
function positiveInteger(value, fallback) {
|
|
113
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
114
|
+
}
|
package/dist/data/telemetry.js
CHANGED
|
@@ -8,6 +8,20 @@ export class TelemetryStore {
|
|
|
8
8
|
constructor(db) {
|
|
9
9
|
this.db = db;
|
|
10
10
|
}
|
|
11
|
+
transaction(action) {
|
|
12
|
+
if (this.db.isTransaction)
|
|
13
|
+
return action();
|
|
14
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
15
|
+
try {
|
|
16
|
+
const result = action();
|
|
17
|
+
this.db.exec("COMMIT");
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
this.db.exec("ROLLBACK");
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
11
25
|
listSessions(input = {}) {
|
|
12
26
|
return listTelemetrySessions(this.db, input);
|
|
13
27
|
}
|