@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,7 +1,92 @@
|
|
|
1
1
|
import { DEFAULT_COMPACTION_SETTINGS, buildSessionContext, estimateTokens, } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
export const PIBO_CONTEXT_GUARD_NOTICE = "Context safety interrupted this response before adding it to long-term context. Pibo is compacting the session before continuing.";
|
|
3
|
+
export const PIBO_CONTEXT_GUARD_RESUME_MESSAGE_TYPE = "pibo-context-guard-resume";
|
|
4
|
+
export const PIBO_CONTEXT_GUARD_RESUME_PROMPT = "Continue the interrupted task autonomously from the compacted context. Do not wait for additional user input and do not repeat the context-safety notice.";
|
|
3
5
|
const DEFAULT_MIN_COMPACTION_RESERVE_TOKENS = 1024;
|
|
4
6
|
const FALLBACK_CONTEXT_WINDOW = 0;
|
|
7
|
+
const MAX_CONTEXT_GUARD_RECOVERY_ATTEMPTS = 3;
|
|
8
|
+
const contextGuardRecoveries = new WeakMap();
|
|
9
|
+
export function createPiboAssistantContextGuardRecovery() {
|
|
10
|
+
let pending;
|
|
11
|
+
let claimed = false;
|
|
12
|
+
let cancelError;
|
|
13
|
+
let cancelHandler;
|
|
14
|
+
return {
|
|
15
|
+
begin() {
|
|
16
|
+
if (pending && !pending.settled)
|
|
17
|
+
return;
|
|
18
|
+
cancelError = undefined;
|
|
19
|
+
let resolve;
|
|
20
|
+
const promise = new Promise((done) => {
|
|
21
|
+
resolve = done;
|
|
22
|
+
});
|
|
23
|
+
pending = { promise, resolve, settled: false };
|
|
24
|
+
},
|
|
25
|
+
complete() {
|
|
26
|
+
if (!pending || pending.settled)
|
|
27
|
+
return;
|
|
28
|
+
pending.settled = true;
|
|
29
|
+
pending.resolve({ ok: true });
|
|
30
|
+
},
|
|
31
|
+
fail(error) {
|
|
32
|
+
if (!pending || pending.settled)
|
|
33
|
+
return;
|
|
34
|
+
pending.settled = true;
|
|
35
|
+
pending.resolve({ ok: false, error });
|
|
36
|
+
},
|
|
37
|
+
cancel(error) {
|
|
38
|
+
cancelError = error;
|
|
39
|
+
if (pending && !pending.settled) {
|
|
40
|
+
pending.settled = true;
|
|
41
|
+
pending.resolve({ ok: false, error });
|
|
42
|
+
}
|
|
43
|
+
cancelHandler?.();
|
|
44
|
+
},
|
|
45
|
+
claim() {
|
|
46
|
+
claimed = true;
|
|
47
|
+
},
|
|
48
|
+
isClaimed() {
|
|
49
|
+
return claimed;
|
|
50
|
+
},
|
|
51
|
+
isPending() {
|
|
52
|
+
return pending !== undefined && !pending.settled;
|
|
53
|
+
},
|
|
54
|
+
onCancel(handler) {
|
|
55
|
+
cancelHandler = handler;
|
|
56
|
+
},
|
|
57
|
+
async wait() {
|
|
58
|
+
const current = pending;
|
|
59
|
+
if (!current)
|
|
60
|
+
return false;
|
|
61
|
+
const outcome = await current.promise;
|
|
62
|
+
const cancelled = cancelError;
|
|
63
|
+
if (pending === current) {
|
|
64
|
+
pending = undefined;
|
|
65
|
+
cancelError = undefined;
|
|
66
|
+
}
|
|
67
|
+
if (cancelled)
|
|
68
|
+
throw cancelled;
|
|
69
|
+
if (!outcome.ok)
|
|
70
|
+
throw outcome.error;
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function registerPiboAssistantContextGuardRecovery(session, recovery) {
|
|
76
|
+
contextGuardRecoveries.set(session, recovery);
|
|
77
|
+
}
|
|
78
|
+
export function claimPiboAssistantContextGuardRecovery(session) {
|
|
79
|
+
contextGuardRecoveries.get(session)?.claim();
|
|
80
|
+
}
|
|
81
|
+
export function cancelPiboAssistantContextGuardRecovery(session, error) {
|
|
82
|
+
contextGuardRecoveries.get(session)?.cancel(error);
|
|
83
|
+
}
|
|
84
|
+
export function isPiboAssistantContextGuardRecoveryPending(session) {
|
|
85
|
+
return contextGuardRecoveries.get(session)?.isPending() ?? false;
|
|
86
|
+
}
|
|
87
|
+
export async function waitForPiboAssistantContextGuardRecovery(session) {
|
|
88
|
+
return await contextGuardRecoveries.get(session)?.wait() ?? false;
|
|
89
|
+
}
|
|
5
90
|
function textTokensByLength(length) {
|
|
6
91
|
return Math.ceil(length / 4);
|
|
7
92
|
}
|
|
@@ -79,15 +164,43 @@ function compactionInstructions(projection) {
|
|
|
79
164
|
: "";
|
|
80
165
|
return `Pibo interrupted the previous assistant response before persisting the full output because it would exceed the safe context budget.${usage} Summarize the durable conversation up to the guard notice, preserve the user's current task and important recent facts, and leave enough context budget for the next response.`;
|
|
81
166
|
}
|
|
82
|
-
export function createPiboAssistantContextGuardExtension(options = {}) {
|
|
167
|
+
export function createPiboAssistantContextGuardExtension(options = {}, recovery = createPiboAssistantContextGuardRecovery()) {
|
|
83
168
|
return (pi) => {
|
|
84
|
-
const state = {
|
|
169
|
+
const state = {
|
|
170
|
+
deltaChars: 0,
|
|
171
|
+
tripped: false,
|
|
172
|
+
compactQueued: false,
|
|
173
|
+
resumeRequested: false,
|
|
174
|
+
resumeInProgress: false,
|
|
175
|
+
resumeSettling: false,
|
|
176
|
+
recoveryAttempts: 0,
|
|
177
|
+
};
|
|
85
178
|
function resetAssistantState() {
|
|
86
179
|
state.deltaChars = 0;
|
|
87
180
|
state.tripped = false;
|
|
88
181
|
state.compactQueued = false;
|
|
89
182
|
state.lastProjection = undefined;
|
|
90
183
|
}
|
|
184
|
+
function clearResumeStartTimer() {
|
|
185
|
+
if (state.resumeStartTimer === undefined)
|
|
186
|
+
return;
|
|
187
|
+
clearTimeout(state.resumeStartTimer);
|
|
188
|
+
state.resumeStartTimer = undefined;
|
|
189
|
+
}
|
|
190
|
+
function resetRecoveryState() {
|
|
191
|
+
clearResumeStartTimer();
|
|
192
|
+
state.tripped = false;
|
|
193
|
+
state.compactQueued = false;
|
|
194
|
+
state.resumeRequested = false;
|
|
195
|
+
state.resumeInProgress = false;
|
|
196
|
+
state.resumeSettling = false;
|
|
197
|
+
state.recoveryAttempts = 0;
|
|
198
|
+
}
|
|
199
|
+
function failRecovery(error) {
|
|
200
|
+
resetRecoveryState();
|
|
201
|
+
recovery.fail(error);
|
|
202
|
+
}
|
|
203
|
+
recovery.onCancel(resetRecoveryState);
|
|
91
204
|
function tripIfNeeded(ctx, assistantTokens) {
|
|
92
205
|
const projection = projectAssistantContextGuard(ctx, assistantTokens, options);
|
|
93
206
|
state.lastProjection = projection;
|
|
@@ -96,6 +209,20 @@ export function createPiboAssistantContextGuardExtension(options = {}) {
|
|
|
96
209
|
state.tripped = true;
|
|
97
210
|
return true;
|
|
98
211
|
}
|
|
212
|
+
pi.on("agent_start", () => {
|
|
213
|
+
if (!state.resumeRequested)
|
|
214
|
+
return;
|
|
215
|
+
clearResumeStartTimer();
|
|
216
|
+
state.resumeRequested = false;
|
|
217
|
+
state.resumeInProgress = true;
|
|
218
|
+
});
|
|
219
|
+
pi.on("agent_settled", () => {
|
|
220
|
+
if (!state.resumeSettling)
|
|
221
|
+
return;
|
|
222
|
+
state.resumeSettling = false;
|
|
223
|
+
state.recoveryAttempts = 0;
|
|
224
|
+
recovery.complete();
|
|
225
|
+
});
|
|
99
226
|
pi.on("message_start", (event) => {
|
|
100
227
|
if (event.message.role === "assistant")
|
|
101
228
|
resetAssistantState();
|
|
@@ -117,10 +244,50 @@ export function createPiboAssistantContextGuardExtension(options = {}) {
|
|
|
117
244
|
return { message: replacementAssistantMessage(event.message) };
|
|
118
245
|
});
|
|
119
246
|
pi.on("agent_end", (_event, ctx) => {
|
|
120
|
-
if (
|
|
247
|
+
if (state.tripped && !state.compactQueued) {
|
|
248
|
+
if (state.recoveryAttempts >= MAX_CONTEXT_GUARD_RECOVERY_ATTEMPTS) {
|
|
249
|
+
failRecovery(new Error(`Context guard recovery exceeded ${MAX_CONTEXT_GUARD_RECOVERY_ATTEMPTS} compaction attempts`));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
state.compactQueued = true;
|
|
253
|
+
state.recoveryAttempts++;
|
|
254
|
+
recovery.begin();
|
|
255
|
+
let callbackHandled = false;
|
|
256
|
+
ctx.compact({
|
|
257
|
+
customInstructions: compactionInstructions(state.lastProjection),
|
|
258
|
+
onComplete: () => {
|
|
259
|
+
if (callbackHandled || !recovery.isPending())
|
|
260
|
+
return;
|
|
261
|
+
callbackHandled = true;
|
|
262
|
+
state.tripped = false;
|
|
263
|
+
state.compactQueued = false;
|
|
264
|
+
state.resumeRequested = true;
|
|
265
|
+
if (recovery.isClaimed()) {
|
|
266
|
+
recovery.complete();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
state.resumeStartTimer = setTimeout(() => {
|
|
270
|
+
failRecovery(new Error("Context guard continuation did not start"));
|
|
271
|
+
}, 5000);
|
|
272
|
+
pi.sendMessage({
|
|
273
|
+
customType: PIBO_CONTEXT_GUARD_RESUME_MESSAGE_TYPE,
|
|
274
|
+
content: [{ type: "text", text: PIBO_CONTEXT_GUARD_RESUME_PROMPT }],
|
|
275
|
+
display: false,
|
|
276
|
+
}, { triggerTurn: true });
|
|
277
|
+
},
|
|
278
|
+
onError: (error) => {
|
|
279
|
+
if (callbackHandled || !recovery.isPending())
|
|
280
|
+
return;
|
|
281
|
+
callbackHandled = true;
|
|
282
|
+
failRecovery(error);
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!state.resumeInProgress)
|
|
121
288
|
return;
|
|
122
|
-
state.
|
|
123
|
-
|
|
289
|
+
state.resumeInProgress = false;
|
|
290
|
+
state.resumeSettling = true;
|
|
124
291
|
});
|
|
125
292
|
};
|
|
126
293
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { BestEffortTelemetryService, } from "../data/telemetry.js";
|
|
3
|
+
import { normalizeSessionErrorDetails } from "./session-errors.js";
|
|
3
4
|
export class PiboProviderTelemetryRecorder {
|
|
4
5
|
options;
|
|
5
6
|
telemetry;
|
|
@@ -16,6 +17,9 @@ export class PiboProviderTelemetryRecorder {
|
|
|
16
17
|
if (!turn)
|
|
17
18
|
return undefined;
|
|
18
19
|
const now = options.at ?? new Date().toISOString();
|
|
20
|
+
if (this.activeProviderRequest()) {
|
|
21
|
+
this.finishActiveProviderRequest("aborted", now, "provider request superseded", undefined, "provider_superseded");
|
|
22
|
+
}
|
|
19
23
|
const model = modelInfo(options.model, this.options.model, payload);
|
|
20
24
|
const providerRequestId = `pr_${randomUUID()}`;
|
|
21
25
|
const phaseId = this.nextPhaseId(turn.turnId, "provider_request");
|
|
@@ -83,7 +87,7 @@ export class PiboProviderTelemetryRecorder {
|
|
|
83
87
|
lastProgressAt: now,
|
|
84
88
|
summary: "provider response received",
|
|
85
89
|
});
|
|
86
|
-
const streamPhaseId =
|
|
90
|
+
const streamPhaseId = providerStreamPhaseId(request);
|
|
87
91
|
this.telemetry.upsertPhase({
|
|
88
92
|
phaseId: streamPhaseId,
|
|
89
93
|
turnId: request.turnId,
|
|
@@ -122,6 +126,56 @@ export class PiboProviderTelemetryRecorder {
|
|
|
122
126
|
return undefined;
|
|
123
127
|
}
|
|
124
128
|
}
|
|
129
|
+
// The assistant message boundary ends the provider stream even when the wider
|
|
130
|
+
// Pibo turn continues with a long-running tool or another provider request.
|
|
131
|
+
recordMessageEnd(message, options = {}) {
|
|
132
|
+
if (!this.options.store || !isAssistantMessage(message))
|
|
133
|
+
return undefined;
|
|
134
|
+
const status = providerStatusForMessage(message);
|
|
135
|
+
const summary = providerSummaryForStatus(status);
|
|
136
|
+
const errorMessage = safeMessageError(message);
|
|
137
|
+
const errorDetails = status === "error"
|
|
138
|
+
? normalizeSessionErrorDetails(errorMessage ?? "Provider request failed.", {
|
|
139
|
+
api: optionalString(message.api),
|
|
140
|
+
provider: optionalString(message.provider),
|
|
141
|
+
model: optionalString(message.model),
|
|
142
|
+
})
|
|
143
|
+
: undefined;
|
|
144
|
+
return this.finishActiveProviderRequest(status, options.at ?? new Date().toISOString(), summary, errorMessage, errorDetails?.category ?? errorDetails?.errorClass);
|
|
145
|
+
}
|
|
146
|
+
recordShutdown(reason, at = new Date().toISOString()) {
|
|
147
|
+
return this.finishActiveProviderRequest("aborted", at, reason, undefined, "runtime_abort");
|
|
148
|
+
}
|
|
149
|
+
finishActiveProviderRequest(status, now, summary, errorMessage, errorCategory) {
|
|
150
|
+
if (!this.options.store)
|
|
151
|
+
return undefined;
|
|
152
|
+
try {
|
|
153
|
+
const request = this.activeProviderRequest();
|
|
154
|
+
if (!request)
|
|
155
|
+
return undefined;
|
|
156
|
+
const phaseStatus = status === "completed" ? "ok" : status;
|
|
157
|
+
const requestPhaseId = request.phaseId ?? `${request.turnId}:provider_request`;
|
|
158
|
+
if (this.options.store.getPhase(requestPhaseId)?.status === "open") {
|
|
159
|
+
this.telemetry.finishPhase(requestPhaseId, { status: phaseStatus, endedAt: now, lastProgressAt: now, summary });
|
|
160
|
+
}
|
|
161
|
+
const streamPhaseId = providerStreamPhaseId(request);
|
|
162
|
+
if (this.options.store.getPhase(streamPhaseId)?.status === "open") {
|
|
163
|
+
this.telemetry.finishPhase(streamPhaseId, { status: phaseStatus, endedAt: now, lastProgressAt: now, summary });
|
|
164
|
+
}
|
|
165
|
+
const updated = this.updateProviderRequest(request, {
|
|
166
|
+
status,
|
|
167
|
+
completedAt: now,
|
|
168
|
+
errorCategory: status === "error" ? errorCategory ?? "provider_error" : status === "aborted" ? errorCategory ?? "runtime_abort" : undefined,
|
|
169
|
+
errorMessage,
|
|
170
|
+
});
|
|
171
|
+
this.activeProviderRequestId = undefined;
|
|
172
|
+
return updated;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
this.options.onError?.(error);
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
125
179
|
activeTurn() {
|
|
126
180
|
const detail = this.options.store?.getSessionTelemetry(this.options.session.id, { limit: 10 });
|
|
127
181
|
const active = detail?.activeTurn;
|
|
@@ -138,11 +192,10 @@ export class PiboProviderTelemetryRecorder {
|
|
|
138
192
|
const turn = this.activeTurn();
|
|
139
193
|
if (!turn)
|
|
140
194
|
return undefined;
|
|
141
|
-
|
|
142
|
-
return [...(timeline?.providerRequests ?? [])].reverse().find((request) => !isTerminalProviderStatus(request.status));
|
|
195
|
+
return this.options.store?.getActiveProviderRequestForTurn(turn.turnId);
|
|
143
196
|
}
|
|
144
197
|
nextPhaseId(turnId, phaseName) {
|
|
145
|
-
const count = this.options.store?.
|
|
198
|
+
const count = this.options.store?.countPhasesForTurn(turnId, phaseName) ?? 0;
|
|
146
199
|
return count === 0 ? `${turnId}:${phaseName}` : `${turnId}:${phaseName}:${count + 1}`;
|
|
147
200
|
}
|
|
148
201
|
updateProviderRequest(existing, input) {
|
|
@@ -162,12 +215,18 @@ export class PiboProviderTelemetryRecorder {
|
|
|
162
215
|
responseHeadersAt: input.responseHeadersAt,
|
|
163
216
|
firstByteAt: input.firstByteAt,
|
|
164
217
|
httpStatus: input.httpStatus,
|
|
218
|
+
completedAt: input.completedAt,
|
|
165
219
|
rawEventCount: existing.rawEventCount,
|
|
166
220
|
normalizedEventCount: existing.normalizedEventCount,
|
|
167
221
|
parseErrorCount: existing.parseErrorCount,
|
|
168
222
|
unknownEventCount: existing.unknownEventCount,
|
|
169
223
|
bytesReceived: existing.bytesReceived,
|
|
170
224
|
eventTypeCounts: existing.eventTypeCounts,
|
|
225
|
+
eventStreamId: existing.eventStreamId,
|
|
226
|
+
eventId: existing.eventId,
|
|
227
|
+
payloadRef: existing.payloadRef,
|
|
228
|
+
errorCategory: input.errorCategory,
|
|
229
|
+
errorMessage: input.errorMessage,
|
|
171
230
|
captureMode: existing.captureMode,
|
|
172
231
|
retentionClass: existing.retentionClass,
|
|
173
232
|
});
|
|
@@ -182,11 +241,45 @@ export function createPiboProviderTelemetryExtension(options) {
|
|
|
182
241
|
pi.on("after_provider_response", (event) => {
|
|
183
242
|
recorder.recordResponse({ status: event.status, headers: event.headers });
|
|
184
243
|
});
|
|
244
|
+
pi.on("message_end", (event) => {
|
|
245
|
+
recorder.recordMessageEnd(event.message);
|
|
246
|
+
});
|
|
247
|
+
pi.on("session_shutdown", (event) => {
|
|
248
|
+
recorder.recordShutdown(`provider session shutdown: ${event.reason}`);
|
|
249
|
+
});
|
|
185
250
|
};
|
|
186
251
|
}
|
|
187
252
|
export function isTerminalProviderStatus(status) {
|
|
188
253
|
return status === "completed" || status === "error" || status === "aborted" || status === "timeout";
|
|
189
254
|
}
|
|
255
|
+
function isAssistantMessage(message) {
|
|
256
|
+
return Boolean(message && typeof message === "object" && message.role === "assistant");
|
|
257
|
+
}
|
|
258
|
+
function providerStatusForMessage(message) {
|
|
259
|
+
if (message.stopReason === "error")
|
|
260
|
+
return "error";
|
|
261
|
+
if (message.stopReason === "aborted")
|
|
262
|
+
return "aborted";
|
|
263
|
+
return "completed";
|
|
264
|
+
}
|
|
265
|
+
function providerSummaryForStatus(status) {
|
|
266
|
+
if (status === "error")
|
|
267
|
+
return "provider stream failed";
|
|
268
|
+
if (status === "aborted")
|
|
269
|
+
return "provider stream aborted";
|
|
270
|
+
return "provider stream completed";
|
|
271
|
+
}
|
|
272
|
+
function providerStreamPhaseId(request) {
|
|
273
|
+
return `${request.turnId}:provider_stream:${request.providerRequestId}`;
|
|
274
|
+
}
|
|
275
|
+
function safeMessageError(message) {
|
|
276
|
+
return typeof message.errorMessage === "string" && message.errorMessage.trim().length > 0
|
|
277
|
+
? message.errorMessage.replace(/\s+/g, " ").trim().slice(0, 512)
|
|
278
|
+
: undefined;
|
|
279
|
+
}
|
|
280
|
+
function optionalString(value) {
|
|
281
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
282
|
+
}
|
|
190
283
|
function modelInfo(primary, fallback, payload) {
|
|
191
284
|
const payloadModel = modelIdFromPayload(payload);
|
|
192
285
|
return {
|
|
@@ -2,6 +2,7 @@ import { SessionManager, shouldCompact } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { getOpenAiCodexProviderUsageForActiveModel } from "../auth/openai-codex-usage.js";
|
|
3
3
|
import { normalizeSessionErrorDetails, runtimeSessionErrorDetails } from "./session-errors.js";
|
|
4
4
|
import { expandInlineSkills } from "./skill-expansion.js";
|
|
5
|
+
import { PIBO_CONTEXT_GUARD_RESUME_MESSAGE_TYPE, PIBO_CONTEXT_GUARD_RESUME_PROMPT, cancelPiboAssistantContextGuardRecovery, claimPiboAssistantContextGuardRecovery, waitForPiboAssistantContextGuardRecovery, } from "./context-guard.js";
|
|
5
6
|
const FAST_SERVICE_TIER = "priority";
|
|
6
7
|
function modelSupportsFastServiceTier(model) {
|
|
7
8
|
if (!model)
|
|
@@ -21,6 +22,9 @@ function withFastServiceTierOption(options) {
|
|
|
21
22
|
function errorMessage(error) {
|
|
22
23
|
return error instanceof Error ? error.message : String(error);
|
|
23
24
|
}
|
|
25
|
+
function isAssistantMessage(message) {
|
|
26
|
+
return Boolean(message && typeof message === "object" && message.role === "assistant");
|
|
27
|
+
}
|
|
24
28
|
function numberValue(value) {
|
|
25
29
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
26
30
|
}
|
|
@@ -388,6 +392,7 @@ export class RoutedSession {
|
|
|
388
392
|
onSessionOperation;
|
|
389
393
|
onKillChildren;
|
|
390
394
|
onStateChange;
|
|
395
|
+
onMessagesInterrupted;
|
|
391
396
|
queue = [];
|
|
392
397
|
processing = false;
|
|
393
398
|
disposed = false;
|
|
@@ -398,9 +403,12 @@ export class RoutedSession {
|
|
|
398
403
|
nextAssistantIndex = 0;
|
|
399
404
|
activeThinkingIndex;
|
|
400
405
|
nextThinkingIndex = 0;
|
|
406
|
+
pendingAssistantError;
|
|
407
|
+
activeMessageFailed = false;
|
|
401
408
|
unsubscribe;
|
|
409
|
+
recoverySession;
|
|
402
410
|
isContinuePatched = false;
|
|
403
|
-
constructor(piboSessionId, runtime, emit, pluginRegistry, forwardPiEvents, onPiEventTelemetry, initialFastMode, onSessionOperation, onKillChildren, onStateChange) {
|
|
411
|
+
constructor(piboSessionId, runtime, emit, pluginRegistry, forwardPiEvents, onPiEventTelemetry, initialFastMode, onSessionOperation, onKillChildren, onStateChange, onMessagesInterrupted) {
|
|
404
412
|
this.piboSessionId = piboSessionId;
|
|
405
413
|
this.runtime = runtime;
|
|
406
414
|
this.emit = emit;
|
|
@@ -410,6 +418,7 @@ export class RoutedSession {
|
|
|
410
418
|
this.onSessionOperation = onSessionOperation;
|
|
411
419
|
this.onKillChildren = onKillChildren;
|
|
412
420
|
this.onStateChange = onStateChange;
|
|
421
|
+
this.onMessagesInterrupted = onMessagesInterrupted;
|
|
413
422
|
this.fastMode = initialFastMode && this.fastModeSupported();
|
|
414
423
|
this.bindRuntimeSession();
|
|
415
424
|
this.patchFastModeProviderRequest();
|
|
@@ -485,19 +494,44 @@ export class RoutedSession {
|
|
|
485
494
|
}
|
|
486
495
|
bindRuntimeSession() {
|
|
487
496
|
this.unsubscribe?.();
|
|
488
|
-
|
|
497
|
+
const session = this.runtime.session;
|
|
498
|
+
if (this.recoverySession && this.recoverySession !== session) {
|
|
499
|
+
cancelPiboAssistantContextGuardRecovery(this.recoverySession, new Error("Context guard recovery cancelled because the Pi session changed"));
|
|
500
|
+
}
|
|
501
|
+
this.recoverySession = session;
|
|
502
|
+
claimPiboAssistantContextGuardRecovery(session);
|
|
503
|
+
this.unsubscribe = session.subscribe((event) => {
|
|
489
504
|
this.onPiEventTelemetry?.(this.piboSessionId, event, { status: this.getStatus(), activeEventId: this.activeMessage?.id });
|
|
490
505
|
const model = this.runtime.session.model;
|
|
491
506
|
const normalized = normalizePiEvent(this.piboSessionId, event, { contextWindow: numberValue(model?.contextWindow) });
|
|
492
|
-
|
|
493
|
-
|
|
507
|
+
const candidate = event && typeof event === "object" ? event : undefined;
|
|
508
|
+
const assistantMessageEnded = candidate?.type === "message_end" && isAssistantMessage(candidate.message);
|
|
509
|
+
// Pi may recover an assistant error through retry or compaction. Publish it only
|
|
510
|
+
// after agent_settled confirms that no automatic continuation remains.
|
|
511
|
+
if (assistantMessageEnded && normalized?.type === "session_error") {
|
|
512
|
+
this.pendingAssistantError = this.withActiveMessage(normalized);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
if (assistantMessageEnded)
|
|
516
|
+
this.pendingAssistantError = undefined;
|
|
517
|
+
if (normalized)
|
|
518
|
+
this.emit(this.withActiveMessage(normalized));
|
|
494
519
|
}
|
|
520
|
+
if (candidate?.type === "agent_settled")
|
|
521
|
+
this.flushPendingAssistantError();
|
|
495
522
|
if (this.forwardPiEvents) {
|
|
496
523
|
this.emit({ type: "pi_event", piboSessionId: this.piboSessionId, event });
|
|
497
524
|
}
|
|
498
525
|
this.handleCompactionEvent(event);
|
|
499
526
|
});
|
|
500
527
|
}
|
|
528
|
+
flushPendingAssistantError() {
|
|
529
|
+
if (!this.pendingAssistantError)
|
|
530
|
+
return;
|
|
531
|
+
this.activeMessageFailed = true;
|
|
532
|
+
this.emit(this.pendingAssistantError);
|
|
533
|
+
this.pendingAssistantError = undefined;
|
|
534
|
+
}
|
|
501
535
|
handleCompactionEvent(event) {
|
|
502
536
|
if (!event || typeof event !== "object")
|
|
503
537
|
return;
|
|
@@ -602,15 +636,17 @@ export class RoutedSession {
|
|
|
602
636
|
}
|
|
603
637
|
removeQueuedMessages(predicate) {
|
|
604
638
|
this.assertActive();
|
|
605
|
-
|
|
639
|
+
const removedMessages = [];
|
|
606
640
|
for (let index = this.queue.length - 1; index >= 0; index -= 1) {
|
|
607
641
|
const item = this.queue[index];
|
|
608
642
|
if (item.kind !== "message" || !predicate(item.event))
|
|
609
643
|
continue;
|
|
610
644
|
this.queue.splice(index, 1);
|
|
611
|
-
|
|
645
|
+
removedMessages.push(item.event);
|
|
612
646
|
}
|
|
613
|
-
|
|
647
|
+
removedMessages.reverse();
|
|
648
|
+
this.notifyMessagesInterrupted(removedMessages, "queued message removed");
|
|
649
|
+
return removedMessages.length;
|
|
614
650
|
}
|
|
615
651
|
getCurrentSession() {
|
|
616
652
|
return this.createSessionSnapshot();
|
|
@@ -733,16 +769,23 @@ export class RoutedSession {
|
|
|
733
769
|
async dispose() {
|
|
734
770
|
if (this.disposed)
|
|
735
771
|
return;
|
|
772
|
+
this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), "session disposed");
|
|
736
773
|
this.queue.length = 0;
|
|
737
774
|
this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: true });
|
|
738
775
|
this.unsubscribe?.();
|
|
739
776
|
this.unsubscribe = undefined;
|
|
777
|
+
if (this.recoverySession) {
|
|
778
|
+
this.cancelContextGuardRecovery("Context guard recovery cancelled because the routed session was disposed");
|
|
779
|
+
this.recoverySession = undefined;
|
|
780
|
+
}
|
|
740
781
|
this.disposed = true;
|
|
741
782
|
await this.runtime.dispose();
|
|
742
783
|
}
|
|
743
784
|
async kill() {
|
|
785
|
+
this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), "session killed");
|
|
744
786
|
this.queue.length = 0;
|
|
745
787
|
this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
|
|
788
|
+
this.cancelContextGuardRecovery("Context guard recovery cancelled because the routed session was killed");
|
|
746
789
|
await this.runtime.session.abort();
|
|
747
790
|
return this.piboSessionId;
|
|
748
791
|
}
|
|
@@ -750,11 +793,15 @@ export class RoutedSession {
|
|
|
750
793
|
this.assertActive();
|
|
751
794
|
const queuedIndex = this.queue.findIndex((item) => item.event.id === eventId);
|
|
752
795
|
if (queuedIndex >= 0) {
|
|
753
|
-
this.queue.splice(queuedIndex, 1);
|
|
796
|
+
const [removed] = this.queue.splice(queuedIndex, 1);
|
|
797
|
+
if (removed?.kind === "message")
|
|
798
|
+
this.notifyMessagesInterrupted([removed.event], "message cancelled");
|
|
754
799
|
this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
|
|
755
800
|
return true;
|
|
756
801
|
}
|
|
757
802
|
if (this.activeMessage?.id === eventId) {
|
|
803
|
+
this.notifyMessagesInterrupted([this.activeMessage], "message cancelled");
|
|
804
|
+
this.cancelContextGuardRecovery("Context guard recovery cancelled with the active message");
|
|
758
805
|
await this.runtime.session.abort();
|
|
759
806
|
return true;
|
|
760
807
|
}
|
|
@@ -792,18 +839,37 @@ export class RoutedSession {
|
|
|
792
839
|
});
|
|
793
840
|
try {
|
|
794
841
|
this.activeMessage = event;
|
|
842
|
+
this.pendingAssistantError = undefined;
|
|
843
|
+
this.activeMessageFailed = false;
|
|
795
844
|
this.activeAssistantIndex = undefined;
|
|
796
845
|
this.nextAssistantIndex = 0;
|
|
797
846
|
this.activeThinkingIndex = undefined;
|
|
798
847
|
this.nextThinkingIndex = 0;
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
848
|
+
const session = this.runtime.session;
|
|
849
|
+
const expandedText = expandInlineSkills(event.text, session.resourceLoader.getSkills().skills);
|
|
850
|
+
await session.prompt(expandedText, { source: promptSource(event.source) });
|
|
851
|
+
while (await waitForPiboAssistantContextGuardRecovery(session)) {
|
|
852
|
+
try {
|
|
853
|
+
await session.sendCustomMessage({
|
|
854
|
+
customType: PIBO_CONTEXT_GUARD_RESUME_MESSAGE_TYPE,
|
|
855
|
+
content: [{ type: "text", text: PIBO_CONTEXT_GUARD_RESUME_PROMPT }],
|
|
856
|
+
display: false,
|
|
857
|
+
}, { triggerTurn: true });
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
const resumeError = error instanceof Error ? error : new Error(String(error));
|
|
861
|
+
cancelPiboAssistantContextGuardRecovery(session, resumeError);
|
|
862
|
+
throw resumeError;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (!this.activeMessageFailed) {
|
|
866
|
+
this.emit({
|
|
867
|
+
type: "message_finished",
|
|
868
|
+
piboSessionId: this.piboSessionId,
|
|
869
|
+
eventId: event.id,
|
|
870
|
+
source: event.source,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
807
873
|
}
|
|
808
874
|
catch (error) {
|
|
809
875
|
const message = errorMessage(error);
|
|
@@ -817,6 +883,8 @@ export class RoutedSession {
|
|
|
817
883
|
}
|
|
818
884
|
finally {
|
|
819
885
|
this.activeMessage = undefined;
|
|
886
|
+
this.pendingAssistantError = undefined;
|
|
887
|
+
this.activeMessageFailed = false;
|
|
820
888
|
this.activeAssistantIndex = undefined;
|
|
821
889
|
this.nextAssistantIndex = 0;
|
|
822
890
|
this.activeThinkingIndex = undefined;
|
|
@@ -873,6 +941,9 @@ export class RoutedSession {
|
|
|
873
941
|
getProviderUsage: () => this.getProviderUsage(),
|
|
874
942
|
clearQueue: () => this.clearQueue(),
|
|
875
943
|
abort: async () => {
|
|
944
|
+
if (this.activeMessage)
|
|
945
|
+
this.notifyMessagesInterrupted([this.activeMessage], "abort requested");
|
|
946
|
+
this.cancelContextGuardRecovery("Context guard recovery cancelled by abort");
|
|
876
947
|
await this.runtime.session.abort();
|
|
877
948
|
},
|
|
878
949
|
dispose: () => this.dispose(),
|
|
@@ -913,6 +984,11 @@ export class RoutedSession {
|
|
|
913
984
|
},
|
|
914
985
|
}, event);
|
|
915
986
|
}
|
|
987
|
+
cancelContextGuardRecovery(message) {
|
|
988
|
+
const session = this.recoverySession ?? this.runtime.session;
|
|
989
|
+
cancelPiboAssistantContextGuardRecovery(session, new Error(message));
|
|
990
|
+
session.abortCompaction?.();
|
|
991
|
+
}
|
|
916
992
|
assertActive() {
|
|
917
993
|
if (this.disposed) {
|
|
918
994
|
throw new Error(`Session "${this.piboSessionId}" has been disposed`);
|
|
@@ -934,10 +1010,21 @@ export class RoutedSession {
|
|
|
934
1010
|
}
|
|
935
1011
|
clearQueue() {
|
|
936
1012
|
const cleared = this.queue.length;
|
|
1013
|
+
const removedMessages = this.queue.flatMap((item) => item.kind === "message" ? [item.event] : []);
|
|
937
1014
|
this.queue.length = 0;
|
|
1015
|
+
this.notifyMessagesInterrupted(removedMessages, "queue cleared");
|
|
938
1016
|
this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
|
|
939
1017
|
return cleared;
|
|
940
1018
|
}
|
|
1019
|
+
activeAndQueuedMessages() {
|
|
1020
|
+
const messages = this.queue.flatMap((item) => item.kind === "message" ? [item.event] : []);
|
|
1021
|
+
return this.activeMessage ? [this.activeMessage, ...messages] : messages;
|
|
1022
|
+
}
|
|
1023
|
+
notifyMessagesInterrupted(messages, reason) {
|
|
1024
|
+
if (messages.length === 0)
|
|
1025
|
+
return;
|
|
1026
|
+
this.onMessagesInterrupted?.(messages, reason);
|
|
1027
|
+
}
|
|
941
1028
|
createSessionSnapshot() {
|
|
942
1029
|
const session = this.runtime.session;
|
|
943
1030
|
const manager = session.sessionManager;
|