@pasko70/pibo 1.7.12 → 1.8.0
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/dist/core/context-guard.js +172 -5
- package/dist/core/provider-telemetry.js +97 -4
- package/dist/core/routed-session.js +103 -16
- package/dist/core/runtime-telemetry.js +251 -91
- package/dist/core/runtime.js +50 -11
- package/dist/core/session-router.js +105 -21
- package/dist/data/telemetry.js +115 -26
- package/package.json +1 -1
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.7.12.vsix +0 -0
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { BestEffortTelemetryService, } from "../data/telemetry.js";
|
|
2
2
|
import { isTerminalProviderStatus } from "./provider-telemetry.js";
|
|
3
3
|
const TERMINAL_TURN_STATUSES = new Set(["ok", "error", "aborted", "timeout"]);
|
|
4
|
+
const DEFAULT_PROGRESS_FLUSH_INTERVAL_MS = 1_000;
|
|
4
5
|
export class PiboRuntimeTelemetryRecorder {
|
|
5
6
|
store;
|
|
6
7
|
onError;
|
|
7
8
|
telemetry;
|
|
8
9
|
providerEventMode;
|
|
10
|
+
progressFlushIntervalMs;
|
|
11
|
+
pendingProviderProgress = new Map();
|
|
12
|
+
providerRequestCache = new Map();
|
|
13
|
+
lastProviderFlushAtMs = new Map();
|
|
14
|
+
lastProgressWriteAtMs = new Map();
|
|
9
15
|
constructor(store, onError, options = {}) {
|
|
10
16
|
this.store = store;
|
|
11
17
|
this.onError = onError;
|
|
12
18
|
this.telemetry = new BestEffortTelemetryService(store, onError);
|
|
13
19
|
this.providerEventMode = options.providerEventMode ?? "aggregate";
|
|
20
|
+
const progressFlushIntervalMs = options.progressFlushIntervalMs;
|
|
21
|
+
this.progressFlushIntervalMs = typeof progressFlushIntervalMs === "number" && Number.isFinite(progressFlushIntervalMs) && progressFlushIntervalMs >= 0
|
|
22
|
+
? progressFlushIntervalMs
|
|
23
|
+
: DEFAULT_PROGRESS_FLUSH_INTERVAL_MS;
|
|
14
24
|
}
|
|
15
25
|
recordOutput(event, context = {}) {
|
|
16
26
|
if (!this.store)
|
|
@@ -32,6 +42,15 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
32
42
|
this.onError?.(error);
|
|
33
43
|
}
|
|
34
44
|
}
|
|
45
|
+
recordMessagesInterrupted(messages, context = {}, reason = "message interrupted") {
|
|
46
|
+
if (!this.store)
|
|
47
|
+
return;
|
|
48
|
+
for (const message of messages) {
|
|
49
|
+
if (!message.id)
|
|
50
|
+
continue;
|
|
51
|
+
this.recordTurnTerminal({ piboSessionId: message.piboSessionId, eventId: message.id }, context, "aborted", "abort", reason, "runtime_abort");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
35
54
|
recordOutputUnsafe(event, context) {
|
|
36
55
|
switch (event.type) {
|
|
37
56
|
case "message_queued":
|
|
@@ -44,15 +63,19 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
44
63
|
this.recordMessageFinished(event, context);
|
|
45
64
|
return;
|
|
46
65
|
case "assistant_delta":
|
|
47
|
-
case "assistant_message":
|
|
48
66
|
this.recordProviderStreamProgress(event, context, "assistant_text", "assistant text progress");
|
|
49
67
|
return;
|
|
68
|
+
case "assistant_message":
|
|
69
|
+
this.recordProviderStreamProgress(event, context, "assistant_text", "assistant text progress", true);
|
|
70
|
+
return;
|
|
50
71
|
case "thinking_started":
|
|
72
|
+
this.recordProviderStreamProgress(event, context, "reasoning", "reasoning progress", true);
|
|
73
|
+
return;
|
|
51
74
|
case "thinking_delta":
|
|
52
75
|
this.recordProviderStreamProgress(event, context, "reasoning", "reasoning progress");
|
|
53
76
|
return;
|
|
54
77
|
case "thinking_finished": {
|
|
55
|
-
this.recordProviderStreamProgress(event, context, "reasoning", "reasoning finished");
|
|
78
|
+
this.recordProviderStreamProgress(event, context, "reasoning", "reasoning finished", true);
|
|
56
79
|
const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context) ?? this.activeTurnContext(event.piboSessionId, context);
|
|
57
80
|
this.finishOpenPhasesByName(turn?.turnId, "reasoning", "ok");
|
|
58
81
|
return;
|
|
@@ -61,15 +84,19 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
61
84
|
this.recordToolArgsPhase(event, context);
|
|
62
85
|
return;
|
|
63
86
|
case "tool_execution_started":
|
|
87
|
+
this.recordToolExecutionPhase(event, context, true);
|
|
88
|
+
return;
|
|
64
89
|
case "tool_execution_updated":
|
|
65
|
-
this.recordToolExecutionPhase(event, context);
|
|
90
|
+
this.recordToolExecutionPhase(event, context, false);
|
|
66
91
|
return;
|
|
67
92
|
case "tool_execution_finished":
|
|
68
93
|
this.recordToolExecutionFinished(event, context);
|
|
69
94
|
return;
|
|
70
|
-
case "session_error":
|
|
71
|
-
|
|
95
|
+
case "session_error": {
|
|
96
|
+
const status = terminalTurnStatusForSessionError(event);
|
|
97
|
+
this.recordTurnTerminal(event, context, status, status === "aborted" ? "abort" : status === "timeout" ? "timeout" : "error", event.error, event.errorDetails?.category ?? event.errorDetails?.errorClass);
|
|
72
98
|
return;
|
|
99
|
+
}
|
|
73
100
|
case "execution_result":
|
|
74
101
|
this.recordExecutionResult(event, context);
|
|
75
102
|
return;
|
|
@@ -82,43 +109,52 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
82
109
|
if (!summary)
|
|
83
110
|
return;
|
|
84
111
|
const turn = context.activeEventId
|
|
85
|
-
? this.
|
|
112
|
+
? this.progressTurnContextForEvent(piboSessionId, context.activeEventId, context)
|
|
86
113
|
: this.activeTurnContext(piboSessionId, context);
|
|
87
114
|
if (!turn)
|
|
88
115
|
return;
|
|
89
|
-
const providerRequest = this.activeProviderRequestForTurn(turn.turnId);
|
|
90
|
-
if (!providerRequest)
|
|
91
|
-
return;
|
|
92
116
|
const now = new Date().toISOString();
|
|
93
|
-
if (summary.
|
|
94
|
-
this.
|
|
117
|
+
if (summary.assistantEventType === "start") {
|
|
118
|
+
this.flushProviderProgress(turn.turnId, now, { force: true, includeLatest: true });
|
|
119
|
+
this.clearProviderProgress(turn.turnId);
|
|
95
120
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
byteSize: summary.byteSize,
|
|
104
|
-
parseStatus: summary.parseStatus,
|
|
105
|
-
normalizedType: summary.normalizedType,
|
|
106
|
-
normalizedEventDelta: 0,
|
|
107
|
-
eventId: turn.eventId,
|
|
108
|
-
itemId: summary.itemId,
|
|
109
|
-
toolCallId: summary.toolCallId,
|
|
110
|
-
safeFields: summary.safeFields,
|
|
111
|
-
};
|
|
112
|
-
if (this.providerEventMode === "detailed") {
|
|
113
|
-
this.telemetry.appendProviderEventSummary(providerEventInput);
|
|
121
|
+
const providerRequest = this.providerEventMode === "detailed"
|
|
122
|
+
? this.providerRequestForTurn(turn.turnId, { includeLatest: summary.messageEnded, refresh: summary.messageEnded })
|
|
123
|
+
: this.accumulateProviderEvent(turn, summary, now);
|
|
124
|
+
if (!providerRequest) {
|
|
125
|
+
if (summary.messageEnded)
|
|
126
|
+
this.clearProviderProgress(turn.turnId);
|
|
127
|
+
return;
|
|
114
128
|
}
|
|
115
|
-
|
|
116
|
-
this.telemetry.
|
|
129
|
+
if (this.providerEventMode === "detailed") {
|
|
130
|
+
this.telemetry.appendProviderEventSummary({
|
|
131
|
+
providerRequestId: providerRequest.providerRequestId,
|
|
132
|
+
piboSessionId: turn.piboSessionId,
|
|
133
|
+
turnId: turn.turnId,
|
|
134
|
+
phaseId: providerRequest.phaseId,
|
|
135
|
+
receivedAt: now,
|
|
136
|
+
eventType: summary.eventType,
|
|
137
|
+
byteSize: summary.byteSize,
|
|
138
|
+
parseStatus: summary.parseStatus,
|
|
139
|
+
normalizedType: summary.normalizedType,
|
|
140
|
+
normalizedEventDelta: 0,
|
|
141
|
+
eventId: turn.eventId,
|
|
142
|
+
itemId: summary.itemId,
|
|
143
|
+
toolCallId: summary.toolCallId,
|
|
144
|
+
safeFields: summary.safeFields,
|
|
145
|
+
});
|
|
117
146
|
}
|
|
118
147
|
if (summary.toolCallId && summary.assistantEventType?.startsWith("toolcall_")) {
|
|
119
|
-
|
|
148
|
+
const forceToolProgress = summary.assistantEventType !== "toolcall_delta";
|
|
149
|
+
if (this.shouldPersistProgress(`${turn.turnId}:tool_args:${summary.toolCallId}`, forceToolProgress)) {
|
|
150
|
+
this.recordPiToolCallProgress(turn, providerRequest.providerRequestId, summary, now);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!summary.messageEnded && !isTerminalProviderStatus(providerRequest.status) && !summary.normalizedType && this.shouldPersistProgress(`${turn.turnId}:provider_stream:${providerRequest.providerRequestId}`)) {
|
|
154
|
+
this.startOrProgressPhase(turn, "provider_stream", now, "provider event metadata", { providerRequestId: providerRequest.providerRequestId });
|
|
120
155
|
}
|
|
121
|
-
|
|
156
|
+
if (summary.messageEnded)
|
|
157
|
+
this.clearProviderProgress(turn.turnId);
|
|
122
158
|
}
|
|
123
159
|
recordMessageQueued(event, context) {
|
|
124
160
|
const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, event.source, context);
|
|
@@ -197,23 +233,32 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
197
233
|
this.recordTurnTerminal(event, context, "ok", "finish", "message finished");
|
|
198
234
|
}
|
|
199
235
|
recordExecutionResult(event, context) {
|
|
200
|
-
if (event.action === "abort" || event.action === "dispose" || event.action === "kill_all") {
|
|
236
|
+
if (event.action === "abort" || event.action === "dispose" || event.action === "kill" || event.action === "kill_all") {
|
|
201
237
|
this.recordTurnTerminal(event, context, "aborted", "abort", `${event.action} requested`);
|
|
202
238
|
return;
|
|
203
239
|
}
|
|
204
|
-
if (event.action === "clear_queue") {
|
|
205
|
-
this.recordTurnProgress(event, context, "queued", "clear_queue requested");
|
|
206
|
-
}
|
|
207
240
|
}
|
|
208
|
-
recordProviderStreamProgress(event, context, phaseName, summary) {
|
|
209
|
-
const turn =
|
|
241
|
+
recordProviderStreamProgress(event, context, phaseName, summary, force = false) {
|
|
242
|
+
const turn = event.eventId
|
|
243
|
+
? this.progressTurnContextForEvent(event.piboSessionId, event.eventId, context)
|
|
244
|
+
: this.activeTurnContext(event.piboSessionId, context);
|
|
210
245
|
if (!turn)
|
|
211
246
|
return;
|
|
212
247
|
const now = new Date().toISOString();
|
|
213
|
-
const providerRequest = this.
|
|
248
|
+
const providerRequest = this.accumulateNormalizedProviderProgress(turn, now, force);
|
|
249
|
+
const progressKey = `${turn.turnId}:${phaseName}:${providerRequest?.providerRequestId ?? "none"}`;
|
|
250
|
+
if (!this.shouldPersistProgress(progressKey, force))
|
|
251
|
+
return;
|
|
252
|
+
const storedTurn = this.store?.getTurn(turn.turnId);
|
|
253
|
+
if (storedTurn && TERMINAL_TURN_STATUSES.has(storedTurn.status)) {
|
|
254
|
+
this.clearTurnProgress(turn.turnId);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
214
257
|
this.closeOpenPhasesByName(turn.turnId, "message_started", "ok", now);
|
|
215
|
-
|
|
216
|
-
|
|
258
|
+
const providerStreamKey = `${turn.turnId}:provider_stream:${providerRequest?.providerRequestId ?? "none"}`;
|
|
259
|
+
if ((!providerRequest || !isTerminalProviderStatus(providerRequest.status)) && this.shouldPersistProgress(providerStreamKey, force)) {
|
|
260
|
+
this.startOrProgressPhase(turn, "provider_stream", now, "normalized provider stream progress", { providerRequestId: providerRequest?.providerRequestId });
|
|
261
|
+
}
|
|
217
262
|
this.startOrProgressPhase(turn, phaseName, now, summary, { updateTurn: true, providerRequestId: providerRequest?.providerRequestId });
|
|
218
263
|
}
|
|
219
264
|
recordToolArgsPhase(event, context) {
|
|
@@ -224,8 +269,8 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
224
269
|
this.closeOpenPhasesByName(turn.turnId, "message_started", "ok", now);
|
|
225
270
|
this.closeOpenPhasesByName(turn.turnId, "assistant_text", "ok", now);
|
|
226
271
|
this.closeOpenPhasesByName(turn.turnId, "reasoning", "ok", now);
|
|
227
|
-
const providerRequest = this.
|
|
228
|
-
|
|
272
|
+
const providerRequest = this.accumulateNormalizedProviderProgress(turn, now, true)
|
|
273
|
+
?? this.providerRequestForTurn(turn.turnId, { includeLatest: true });
|
|
229
274
|
this.upsertToolCallArgs(turn, {
|
|
230
275
|
toolCallId: event.toolCallId,
|
|
231
276
|
toolName: event.toolName,
|
|
@@ -244,10 +289,19 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
244
289
|
this.telemetry.finishPhase(phase.phaseId, { status: "ok", endedAt: now, lastProgressAt: now, summary: "tool arguments complete" });
|
|
245
290
|
}
|
|
246
291
|
}
|
|
247
|
-
recordToolExecutionPhase(event, context) {
|
|
248
|
-
const turn =
|
|
292
|
+
recordToolExecutionPhase(event, context, force) {
|
|
293
|
+
const turn = event.eventId
|
|
294
|
+
? this.progressTurnContextForEvent(event.piboSessionId, event.eventId, context)
|
|
295
|
+
: this.activeTurnContext(event.piboSessionId, context);
|
|
249
296
|
if (!turn)
|
|
250
297
|
return;
|
|
298
|
+
if (!this.shouldPersistProgress(`${turn.turnId}:tool_execution:${event.toolCallId}`, force))
|
|
299
|
+
return;
|
|
300
|
+
const storedTurn = this.store?.getTurn(turn.turnId);
|
|
301
|
+
if (storedTurn && TERMINAL_TURN_STATUSES.has(storedTurn.status)) {
|
|
302
|
+
this.clearTurnProgress(turn.turnId);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
251
305
|
const now = new Date().toISOString();
|
|
252
306
|
const existing = this.store?.getToolCall(event.toolCallId);
|
|
253
307
|
const args = toolArgsMetadata(event.args, true);
|
|
@@ -362,17 +416,17 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
362
416
|
eventId: turn.eventId,
|
|
363
417
|
});
|
|
364
418
|
}
|
|
365
|
-
recordTurnProgress(event, context, phaseName, summary) {
|
|
366
|
-
const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context) ?? this.activeTurnContext(event.piboSessionId, context);
|
|
367
|
-
if (!turn)
|
|
368
|
-
return;
|
|
369
|
-
this.startOrProgressPhase(turn, phaseName, new Date().toISOString(), summary, { updateTurn: true });
|
|
370
|
-
}
|
|
371
419
|
recordTurnTerminal(event, context, status, phaseName, summary, errorCategory) {
|
|
372
|
-
const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context) ?? this.activeTurnContext(event.piboSessionId, context);
|
|
420
|
+
const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context, { includeTerminal: true }) ?? this.activeTurnContext(event.piboSessionId, context);
|
|
373
421
|
if (!turn)
|
|
374
422
|
return;
|
|
423
|
+
const existingTurn = this.store?.getTurn(turn.turnId);
|
|
424
|
+
if (existingTurn && TERMINAL_TURN_STATUSES.has(existingTurn.status)) {
|
|
425
|
+
this.clearTurnProgress(turn.turnId);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
375
428
|
const now = new Date().toISOString();
|
|
429
|
+
this.flushProviderProgress(turn.turnId, now, { force: true, includeLatest: true });
|
|
376
430
|
this.finishOpenPhases(turn.turnId, terminalPhaseStatus(status), now);
|
|
377
431
|
this.finishActiveProviderRequests(turn.turnId, providerStatusForTurnStatus(status), now, summary, errorCategory);
|
|
378
432
|
this.finishActiveToolCalls(turn.turnId, status, now, summary);
|
|
@@ -403,8 +457,12 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
403
457
|
queueDepth: turn.queueDepth,
|
|
404
458
|
summary: safeSummary(summary),
|
|
405
459
|
});
|
|
460
|
+
this.clearTurnProgress(turn.turnId);
|
|
406
461
|
}
|
|
407
462
|
startOrProgressPhase(turn, phaseName, now, summary, options = {}) {
|
|
463
|
+
const storedTurn = this.store?.getTurn(turn.turnId);
|
|
464
|
+
if (storedTurn && TERMINAL_TURN_STATUSES.has(storedTurn.status))
|
|
465
|
+
return undefined;
|
|
408
466
|
const existing = this.openPhaseByName(turn.turnId, phaseName);
|
|
409
467
|
const phase = this.telemetry.upsertPhase({
|
|
410
468
|
phaseId: existing?.phaseId ?? this.nextPhaseId(turn.turnId, phaseName),
|
|
@@ -438,22 +496,20 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
438
496
|
return phase;
|
|
439
497
|
}
|
|
440
498
|
openPhaseByName(turnId, phaseName) {
|
|
441
|
-
|
|
442
|
-
return timeline?.phases.find((phase) => phase.name === phaseName && phase.status === "open");
|
|
499
|
+
return this.store?.getOpenPhaseForTurn(turnId, phaseName);
|
|
443
500
|
}
|
|
444
501
|
nextPhaseId(turnId, phaseName) {
|
|
445
502
|
const base = phaseId(turnId, phaseName);
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
503
|
+
const count = this.store?.countPhasesForTurn(turnId, phaseName) ?? 0;
|
|
504
|
+
if (count === 0)
|
|
448
505
|
return base;
|
|
449
|
-
return `${base}:${
|
|
506
|
+
return `${base}:${count + 1}`;
|
|
450
507
|
}
|
|
451
508
|
finishOpenPhasesByName(turnId, phaseName, status, now = new Date().toISOString(), summary) {
|
|
452
509
|
if (!turnId)
|
|
453
510
|
return;
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
if (phase.name !== phaseName || phase.status !== "open")
|
|
511
|
+
for (const phase of this.store?.listOpenPhasesForTurn(turnId) ?? []) {
|
|
512
|
+
if (phase.name !== phaseName)
|
|
457
513
|
continue;
|
|
458
514
|
this.telemetry.finishPhase(phase.phaseId, { status, endedAt: now, lastProgressAt: now, summary });
|
|
459
515
|
}
|
|
@@ -462,35 +518,113 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
462
518
|
this.finishOpenPhasesByName(turnId, phaseName, status, now, summary);
|
|
463
519
|
}
|
|
464
520
|
finishOpenPhases(turnId, status, now) {
|
|
465
|
-
const
|
|
466
|
-
for (const phase of timeline?.phases ?? []) {
|
|
467
|
-
if (phase.status !== "open")
|
|
468
|
-
continue;
|
|
521
|
+
for (const phase of this.store?.listOpenPhasesForTurn(turnId) ?? []) {
|
|
469
522
|
this.telemetry.finishPhase(phase.phaseId, { status, endedAt: now, lastProgressAt: now });
|
|
470
523
|
}
|
|
471
524
|
}
|
|
472
525
|
activeProviderRequestForTurn(turnId) {
|
|
473
|
-
|
|
474
|
-
return [...(timeline?.providerRequests ?? [])].reverse().find((request) => !isTerminalProviderStatus(request.status));
|
|
526
|
+
return this.store?.getActiveProviderRequestForTurn(turnId);
|
|
475
527
|
}
|
|
476
528
|
latestProviderRequestForTurn(turnId) {
|
|
477
|
-
|
|
478
|
-
|
|
529
|
+
return this.store?.getLatestProviderRequestForTurn(turnId);
|
|
530
|
+
}
|
|
531
|
+
providerRequestForTurn(turnId, options = {}) {
|
|
532
|
+
const cached = this.providerRequestCache.get(turnId);
|
|
533
|
+
if (!options.refresh && cached && (options.includeLatest || !isTerminalProviderStatus(cached.status)))
|
|
534
|
+
return cached;
|
|
535
|
+
const request = this.activeProviderRequestForTurn(turnId)
|
|
536
|
+
?? (options.includeLatest ? this.latestProviderRequestForTurn(turnId) : undefined);
|
|
537
|
+
if (request)
|
|
538
|
+
this.providerRequestCache.set(turnId, request);
|
|
539
|
+
else
|
|
540
|
+
this.providerRequestCache.delete(turnId);
|
|
541
|
+
return request;
|
|
542
|
+
}
|
|
543
|
+
accumulateProviderEvent(turn, summary, now) {
|
|
544
|
+
const pending = this.pendingProviderProgress.get(turn.turnId) ?? emptyPendingProviderProgress();
|
|
545
|
+
pending.lastRawEventAt = now;
|
|
546
|
+
pending.upstreamResponseId = summary.upstreamResponseId ?? pending.upstreamResponseId;
|
|
547
|
+
pending.rawEventCount += 1;
|
|
548
|
+
pending.parseErrorCount += summary.parseStatus === "invalid_json" ? 1 : 0;
|
|
549
|
+
pending.unknownEventCount += summary.parseStatus === "unknown_type" ? 1 : 0;
|
|
550
|
+
pending.bytesReceived += summary.byteSize;
|
|
551
|
+
pending.eventTypeCounts[summary.eventType] = (pending.eventTypeCounts[summary.eventType] ?? 0) + 1;
|
|
552
|
+
this.pendingProviderProgress.set(turn.turnId, pending);
|
|
553
|
+
return this.flushProviderProgress(turn.turnId, now, {
|
|
554
|
+
force: summary.messageEnded,
|
|
555
|
+
includeLatest: summary.messageEnded,
|
|
556
|
+
refresh: summary.messageEnded,
|
|
557
|
+
});
|
|
479
558
|
}
|
|
480
|
-
|
|
559
|
+
accumulateNormalizedProviderProgress(turn, now, force = false) {
|
|
560
|
+
const request = this.providerRequestForTurn(turn.turnId, { includeLatest: force });
|
|
481
561
|
if (!request)
|
|
482
|
-
return;
|
|
483
|
-
this.
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
562
|
+
return undefined;
|
|
563
|
+
const pending = this.pendingProviderProgress.get(turn.turnId) ?? emptyPendingProviderProgress();
|
|
564
|
+
pending.lastNormalizedEventAt = now;
|
|
565
|
+
pending.normalizedEventCount += 1;
|
|
566
|
+
this.pendingProviderProgress.set(turn.turnId, pending);
|
|
567
|
+
const flushNow = force || request.status === "started" || request.status === "headers";
|
|
568
|
+
return this.flushProviderProgress(turn.turnId, now, { force: flushNow, includeLatest: force }) ?? request;
|
|
569
|
+
}
|
|
570
|
+
flushProviderProgress(turnId, now, options = {}) {
|
|
571
|
+
const pending = this.pendingProviderProgress.get(turnId);
|
|
572
|
+
const request = this.providerRequestForTurn(turnId, options);
|
|
573
|
+
if (!pending)
|
|
574
|
+
return request;
|
|
575
|
+
const nowMs = Date.now();
|
|
576
|
+
const lastFlushAtMs = this.lastProviderFlushAtMs.get(turnId);
|
|
577
|
+
if (!options.force && lastFlushAtMs !== undefined && nowMs - lastFlushAtMs < this.progressFlushIntervalMs)
|
|
578
|
+
return request;
|
|
579
|
+
if (!request) {
|
|
580
|
+
const turn = this.store?.getTurn(turnId);
|
|
581
|
+
if (turn && TERMINAL_TURN_STATUSES.has(turn.status))
|
|
582
|
+
this.clearTurnProgress(turnId);
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
const updated = this.telemetry.recordProviderProgress({
|
|
586
|
+
providerRequestId: request.providerRequestId,
|
|
587
|
+
status: pending.normalizedEventCount > 0 ? "streaming" : undefined,
|
|
588
|
+
lastRawEventAt: pending.lastRawEventAt,
|
|
589
|
+
lastNormalizedEventAt: pending.lastNormalizedEventAt,
|
|
590
|
+
upstreamResponseId: pending.upstreamResponseId,
|
|
591
|
+
rawEventCount: pending.rawEventCount,
|
|
592
|
+
normalizedEventCount: pending.normalizedEventCount,
|
|
593
|
+
parseErrorCount: pending.parseErrorCount,
|
|
594
|
+
unknownEventCount: pending.unknownEventCount,
|
|
595
|
+
bytesReceived: pending.bytesReceived,
|
|
596
|
+
eventTypeCounts: pending.eventTypeCounts,
|
|
597
|
+
updatedAt: now,
|
|
487
598
|
});
|
|
599
|
+
this.pendingProviderProgress.delete(turnId);
|
|
600
|
+
this.lastProviderFlushAtMs.set(turnId, nowMs);
|
|
601
|
+
if (updated)
|
|
602
|
+
this.providerRequestCache.set(turnId, updated);
|
|
603
|
+
return updated ?? request;
|
|
604
|
+
}
|
|
605
|
+
shouldPersistProgress(key, force = false) {
|
|
606
|
+
const nowMs = Date.now();
|
|
607
|
+
const lastWriteAtMs = this.lastProgressWriteAtMs.get(key);
|
|
608
|
+
if (!force && lastWriteAtMs !== undefined && nowMs - lastWriteAtMs < this.progressFlushIntervalMs)
|
|
609
|
+
return false;
|
|
610
|
+
this.lastProgressWriteAtMs.set(key, nowMs);
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
clearProviderProgress(turnId) {
|
|
614
|
+
this.pendingProviderProgress.delete(turnId);
|
|
615
|
+
this.providerRequestCache.delete(turnId);
|
|
616
|
+
this.lastProviderFlushAtMs.delete(turnId);
|
|
617
|
+
}
|
|
618
|
+
clearTurnProgress(turnId) {
|
|
619
|
+
this.clearProviderProgress(turnId);
|
|
620
|
+
const prefix = `${turnId}:`;
|
|
621
|
+
for (const key of this.lastProgressWriteAtMs.keys()) {
|
|
622
|
+
if (key.startsWith(prefix))
|
|
623
|
+
this.lastProgressWriteAtMs.delete(key);
|
|
624
|
+
}
|
|
488
625
|
}
|
|
489
626
|
finishActiveProviderRequests(turnId, status, now, summary, errorCategory) {
|
|
490
|
-
const
|
|
491
|
-
for (const request of timeline?.providerRequests ?? []) {
|
|
492
|
-
if (isTerminalProviderStatus(request.status))
|
|
493
|
-
continue;
|
|
627
|
+
for (const request of this.store?.listActiveProviderRequestsForTurn(turnId) ?? []) {
|
|
494
628
|
this.upsertProviderRequestFromExisting(request, {
|
|
495
629
|
status,
|
|
496
630
|
completedAt: now,
|
|
@@ -502,10 +636,7 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
502
636
|
finishActiveToolCalls(turnId, status, now, summary) {
|
|
503
637
|
if (status === "ok")
|
|
504
638
|
return;
|
|
505
|
-
const
|
|
506
|
-
for (const toolCall of timeline?.toolCalls ?? []) {
|
|
507
|
-
if (isTerminalToolCallStatus(toolCall.status))
|
|
508
|
-
continue;
|
|
639
|
+
for (const toolCall of this.store?.listActiveToolCallsForTurn(turnId) ?? []) {
|
|
509
640
|
const terminalStatus = terminalToolCallStatus(status);
|
|
510
641
|
this.telemetry.upsertToolCall({
|
|
511
642
|
toolCallId: toolCall.toolCallId,
|
|
@@ -566,20 +697,34 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
566
697
|
retentionClass: request.retentionClass,
|
|
567
698
|
});
|
|
568
699
|
}
|
|
569
|
-
|
|
570
|
-
if (!eventId)
|
|
571
|
-
return undefined;
|
|
572
|
-
const rootSessionId = rootSessionIdFor(context.session, piboSessionId);
|
|
700
|
+
progressTurnContextForEvent(piboSessionId, eventId, context) {
|
|
573
701
|
return {
|
|
574
702
|
turnId: turnIdForEvent(eventId),
|
|
575
703
|
eventId,
|
|
576
704
|
piboSessionId,
|
|
577
|
-
rootSessionId,
|
|
705
|
+
rootSessionId: rootSessionIdFor(context.session, piboSessionId),
|
|
578
706
|
roomId: roomIdFor(context.session),
|
|
579
|
-
source:
|
|
707
|
+
source: "system",
|
|
580
708
|
queueDepth: context.status?.queuedMessages,
|
|
581
709
|
};
|
|
582
710
|
}
|
|
711
|
+
turnContextForEvent(piboSessionId, eventId, source, context, options = {}) {
|
|
712
|
+
if (!eventId)
|
|
713
|
+
return undefined;
|
|
714
|
+
const turnId = turnIdForEvent(eventId);
|
|
715
|
+
const existing = this.store?.getTurn(turnId);
|
|
716
|
+
if (!options.includeTerminal && existing && TERMINAL_TURN_STATUSES.has(existing.status))
|
|
717
|
+
return undefined;
|
|
718
|
+
return {
|
|
719
|
+
turnId,
|
|
720
|
+
eventId,
|
|
721
|
+
piboSessionId,
|
|
722
|
+
rootSessionId: existing?.rootSessionId ?? rootSessionIdFor(context.session, piboSessionId),
|
|
723
|
+
roomId: existing?.roomId ?? roomIdFor(context.session),
|
|
724
|
+
source: existing?.source ?? telemetrySource(source),
|
|
725
|
+
queueDepth: context.status?.queuedMessages ?? existing?.queueDepth,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
583
728
|
activeTurnContext(piboSessionId, context) {
|
|
584
729
|
const detail = this.store?.getSessionTelemetry(piboSessionId, { limit: 10 });
|
|
585
730
|
const active = detail?.activeTurn;
|
|
@@ -596,6 +741,16 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
596
741
|
};
|
|
597
742
|
}
|
|
598
743
|
}
|
|
744
|
+
function emptyPendingProviderProgress() {
|
|
745
|
+
return {
|
|
746
|
+
rawEventCount: 0,
|
|
747
|
+
normalizedEventCount: 0,
|
|
748
|
+
parseErrorCount: 0,
|
|
749
|
+
unknownEventCount: 0,
|
|
750
|
+
bytesReceived: 0,
|
|
751
|
+
eventTypeCounts: {},
|
|
752
|
+
};
|
|
753
|
+
}
|
|
599
754
|
function providerEventSummaryForPiEvent(event) {
|
|
600
755
|
if (!event || typeof event !== "object")
|
|
601
756
|
return undefined;
|
|
@@ -636,6 +791,7 @@ function providerEventSummaryForPiEvent(event) {
|
|
|
636
791
|
const byteSize = safeJsonByteSize(safeFields);
|
|
637
792
|
return {
|
|
638
793
|
eventType,
|
|
794
|
+
messageEnded: candidate.type === "message_end",
|
|
639
795
|
assistantEventType: assistantType,
|
|
640
796
|
parseStatus,
|
|
641
797
|
normalizedType: normalizedTypeForPiAssistantEvent(assistantType),
|
|
@@ -717,8 +873,12 @@ function toolStatusForPiAssistantEvent(type, argsBytes, existingStatus) {
|
|
|
717
873
|
return argsBytes > 0 ? "args_partial" : existingStatus ?? "args_started";
|
|
718
874
|
return existingStatus ?? "args_started";
|
|
719
875
|
}
|
|
720
|
-
function
|
|
721
|
-
|
|
876
|
+
function terminalTurnStatusForSessionError(event) {
|
|
877
|
+
if (event.errorDetails?.category === "runtime_abort" || event.errorDetails?.errorClass === "runtime_abort")
|
|
878
|
+
return "aborted";
|
|
879
|
+
if (event.errorDetails?.code === "timeout")
|
|
880
|
+
return "timeout";
|
|
881
|
+
return "error";
|
|
722
882
|
}
|
|
723
883
|
function terminalToolCallStatus(status) {
|
|
724
884
|
if (status === "aborted")
|
package/dist/core/runtime.js
CHANGED
|
@@ -14,7 +14,7 @@ import { getMcpAgentContextFile } from "../mcp/agent-context.js";
|
|
|
14
14
|
import { createPiboSystemPromptTemplateExtension } from "./system-prompt-template.js";
|
|
15
15
|
import { getActivePiboBasePromptPath } from "./base-prompt.js";
|
|
16
16
|
import { createPiboCompactionPromptExtension } from "./compaction-prompt.js";
|
|
17
|
-
import { createPiboAssistantContextGuardExtension } from "./context-guard.js";
|
|
17
|
+
import { cancelPiboAssistantContextGuardRecovery, createPiboAssistantContextGuardExtension, createPiboAssistantContextGuardRecovery, isPiboAssistantContextGuardRecoveryPending, registerPiboAssistantContextGuardRecovery, } from "./context-guard.js";
|
|
18
18
|
import { getPiPackageRuntimeOptions } from "../pi-packages/runtime.js";
|
|
19
19
|
import { getDefaultPiboWorkspace } from "./workspace.js";
|
|
20
20
|
import { DEFAULT_USER_TIMEZONE } from "./user-settings.js";
|
|
@@ -166,10 +166,10 @@ function getBuiltinToolAllowlist(profile, customTools) {
|
|
|
166
166
|
return undefined;
|
|
167
167
|
return [...selectedBuiltinTools, ...customTools.map((tool) => tool.name)];
|
|
168
168
|
}
|
|
169
|
-
function getProfileExtensionFactories(profile, extensionFactories) {
|
|
169
|
+
function getProfileExtensionFactories(profile, extensionFactories, contextGuardRecovery) {
|
|
170
170
|
const piboPromptTemplateExtension = createPiboSystemPromptTemplateExtension();
|
|
171
171
|
const piboCompactionPromptExtension = createPiboCompactionPromptExtension();
|
|
172
|
-
const piboContextGuardExtension = createPiboAssistantContextGuardExtension();
|
|
172
|
+
const piboContextGuardExtension = createPiboAssistantContextGuardExtension({}, contextGuardRecovery);
|
|
173
173
|
const providerToolExtensions = profile.tools
|
|
174
174
|
.filter((tool) => tool.enabled !== false)
|
|
175
175
|
.filter(isWebSearchProviderTool)
|
|
@@ -234,6 +234,7 @@ export async function createPiboRuntime(options = {}) {
|
|
|
234
234
|
const sessionManager = await createSessionManager(cwd, profile, options.persistSession !== false);
|
|
235
235
|
const authStorage = AuthStorage.create();
|
|
236
236
|
const createRuntime = async ({ cwd: runtimeCwd, agentDir: runtimeAgentDir, sessionManager: runtimeSessionManager, sessionStartEvent, }) => {
|
|
237
|
+
const contextGuardRecovery = createPiboAssistantContextGuardRecovery();
|
|
237
238
|
const contextFiles = await loadContextFiles(runtimeCwd, profile.contextFiles);
|
|
238
239
|
const sessionContextFile = createSessionContextFile({ piboSessionId: profile.sessionId, ...options.sessionContext });
|
|
239
240
|
const installedToolContextFile = getInstalledCliToolContextFile();
|
|
@@ -247,7 +248,7 @@ export async function createPiboRuntime(options = {}) {
|
|
|
247
248
|
resourceLoaderOptions: {
|
|
248
249
|
...piPackageOptions.resourceLoaderOptions,
|
|
249
250
|
additionalSkillPaths: skillPaths,
|
|
250
|
-
extensionFactories: getProfileExtensionFactories(profile, options.extensionFactories),
|
|
251
|
+
extensionFactories: getProfileExtensionFactories(profile, options.extensionFactories, contextGuardRecovery),
|
|
251
252
|
noExtensions: true,
|
|
252
253
|
noSkills: true,
|
|
253
254
|
noPromptTemplates: true,
|
|
@@ -293,6 +294,10 @@ export async function createPiboRuntime(options = {}) {
|
|
|
293
294
|
tools: getBuiltinToolAllowlist(profile, customTools),
|
|
294
295
|
});
|
|
295
296
|
installValidationOutputCompaction(created.session.agent);
|
|
297
|
+
registerPiboAssistantContextGuardRecovery(created.session, contextGuardRecovery);
|
|
298
|
+
if (options.contextGuardTuiQueueOrdering === true) {
|
|
299
|
+
installPiboContextGuardTuiQueueOrdering(created.session);
|
|
300
|
+
}
|
|
296
301
|
const resourceLoader = services.resourceLoader;
|
|
297
302
|
const diagnostics = [
|
|
298
303
|
...piPackageOptions.diagnostics,
|
|
@@ -303,13 +308,14 @@ export async function createPiboRuntime(options = {}) {
|
|
|
303
308
|
message: `Failed to load extension "${path}": ${error}`,
|
|
304
309
|
})),
|
|
305
310
|
];
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
created.session
|
|
311
|
+
const originalDispose = created.session.dispose.bind(created.session);
|
|
312
|
+
created.session.dispose = () => {
|
|
313
|
+
cancelPiboAssistantContextGuardRecovery(created.session, new Error("Context guard recovery cancelled because the Pi session was disposed"));
|
|
314
|
+
if (localRuntimeRegistry) {
|
|
309
315
|
void localRuntimeRegistry.closeControllerSessions(profile.sessionId ?? "local", { force: true });
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
316
|
+
}
|
|
317
|
+
originalDispose();
|
|
318
|
+
};
|
|
313
319
|
return {
|
|
314
320
|
...created,
|
|
315
321
|
services,
|
|
@@ -456,6 +462,39 @@ export async function inspectPiboProfile(options = {}) {
|
|
|
456
462
|
await runtime.dispose();
|
|
457
463
|
}
|
|
458
464
|
}
|
|
465
|
+
function installPiboContextGuardTuiQueueOrdering(session) {
|
|
466
|
+
const originalSubscribe = session.subscribe.bind(session);
|
|
467
|
+
const originalPrompt = session.prompt.bind(session);
|
|
468
|
+
const originalSteer = session.steer.bind(session);
|
|
469
|
+
session.subscribe = ((listener) => originalSubscribe((event) => {
|
|
470
|
+
if (event.type === "compaction_end"
|
|
471
|
+
&& event.result
|
|
472
|
+
&& isPiboAssistantContextGuardRecoveryPending(session)) {
|
|
473
|
+
listener({ ...event, willRetry: true });
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
listener(event);
|
|
477
|
+
}));
|
|
478
|
+
session.prompt = async (text, options) => {
|
|
479
|
+
if (isPiboAssistantContextGuardRecoveryPending(session)) {
|
|
480
|
+
if (!session.isStreaming) {
|
|
481
|
+
await session.followUp(text, options?.images);
|
|
482
|
+
options?.preflightResult?.(true);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
await originalPrompt(text, { ...options, streamingBehavior: "followUp" });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
await originalPrompt(text, options);
|
|
489
|
+
};
|
|
490
|
+
session.steer = async (text, images) => {
|
|
491
|
+
if (isPiboAssistantContextGuardRecoveryPending(session)) {
|
|
492
|
+
await session.followUp(text, images);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
await originalSteer(text, images);
|
|
496
|
+
};
|
|
497
|
+
}
|
|
459
498
|
export async function runPiboTui(options = {}) {
|
|
460
499
|
const profile = options.profile ?? createDefaultPiboProfile();
|
|
461
500
|
const hasEnabledSubagents = profile.subagents.some((subagent) => subagent.enabled !== false);
|
|
@@ -465,7 +504,7 @@ export async function runPiboTui(options = {}) {
|
|
|
465
504
|
process.exitCode = 1;
|
|
466
505
|
return;
|
|
467
506
|
}
|
|
468
|
-
const runtime = await createPiboRuntime({ ...options, profile });
|
|
507
|
+
const runtime = await createPiboRuntime({ ...options, profile, contextGuardTuiQueueOrdering: true });
|
|
469
508
|
try {
|
|
470
509
|
const fatal = runtime.diagnostics.find((diagnostic) => diagnostic.type === "error");
|
|
471
510
|
for (const diagnostic of runtime.diagnostics) {
|