pi-openai-toolkit 0.22.3 → 0.23.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/LICENSE +21 -21
- package/NOTICE +83 -83
- package/README.md +4 -2
- package/README.zh.md +4 -2
- package/config.schema.json +278 -278
- package/docs/configuration.md +298 -298
- package/docs/internals.md +181 -181
- package/package.json +134 -134
- package/src/compact-client.ts +412 -412
- package/src/compaction-projection.ts +35 -35
- package/src/config/migration.ts +103 -103
- package/src/config.ts +310 -310
- package/src/context-management/codex-provider.ts +270 -270
- package/src/context-management/history-notes.ts +357 -357
- package/src/context-management/manual-compact.ts +181 -4
- package/src/context-management/tools.ts +371 -371
- package/src/context-management/types.ts +168 -168
- package/src/context-management/window-manager.ts +905 -811
- package/src/debug.ts +238 -238
- package/src/deferred-tool-carryover.ts +282 -282
- package/src/details-store.ts +151 -151
- package/src/extension-runtime.ts +1986 -1661
- package/src/image-generation/extension.ts +189 -189
- package/src/image-generation/protocol.ts +579 -579
- package/src/image-generation/types.ts +300 -300
- package/src/payload-rewrite.ts +515 -515
- package/src/pi-context-hook.ts +117 -117
- package/src/remote-v2-client.ts +811 -811
- package/src/request-context-cache.ts +155 -155
- package/src/responses-headers.ts +180 -180
- package/src/runtime.ts +865 -865
- package/src/serializer.ts +882 -882
- package/src/supported-environment.ts +27 -27
- package/src/types.ts +565 -565
- package/src/web-search/alpha-client.ts +876 -876
- package/src/web-search/extension.ts +530 -530
- package/src/web-search/payload.ts +299 -299
- package/src/web-search/prompt.ts +60 -60
- package/src/web-search/types.ts +166 -166
- package/tsconfig.check.json +50 -50
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
3
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
3
|
-
import { buildSessionContext, estimateTokens, type ExtensionAPI, type ExtensionContext, type SessionBeforeCompactEvent, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { buildSessionContext, estimateTokens, type CompactionResult, type ExtensionAPI, type ExtensionContext, type SessionBeforeCompactEvent, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { resolveToolkitConfig, type ResolvedToolkitConfig } from "../config";
|
|
5
6
|
import { parseModelSpec } from "../runtime";
|
|
6
|
-
import { THINKING_LEVELS } from "../types";
|
|
7
|
+
import { THINKING_LEVELS, type NativeCompactionDetails } from "../types";
|
|
7
8
|
import { resolveCodexContextProvider } from "./codex-provider";
|
|
8
9
|
import { findLatestNotesCheckpointSinceBoundary, findLatestWindowBoundaryEntry, type CodexContextWindowManager } from "./window-manager";
|
|
9
10
|
import { isNonEmptyString, isRecord, type CodexContextProvider, type ContextWindowIdentity } from "./types";
|
|
@@ -79,7 +80,183 @@ function freshCheckpoint(entries: readonly SessionEntry[], record: ManualCompact
|
|
|
79
80
|
return findLatestNotesCheckpointSinceBoundary(entries, record.sessionId, afterId);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
function branchFingerprint(entries: readonly SessionEntry[]): string {
|
|
84
|
+
return createHash("sha256").update(JSON.stringify(entries)).digest("hex");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type RemoteManualCompactOperation = {
|
|
88
|
+
generation: number;
|
|
89
|
+
sessionId: string;
|
|
90
|
+
sessionFile: string | undefined;
|
|
91
|
+
anchorId: string;
|
|
92
|
+
branchLength: number;
|
|
93
|
+
branchFingerprint: string;
|
|
94
|
+
windowId: string;
|
|
95
|
+
model: string;
|
|
96
|
+
api: string;
|
|
97
|
+
baseUrl: string;
|
|
98
|
+
thinking: ThinkingLevel;
|
|
99
|
+
controller: AbortController;
|
|
100
|
+
signal: AbortSignal;
|
|
101
|
+
removeAbortListeners: () => void;
|
|
102
|
+
policy: ResolvedToolkitConfig;
|
|
103
|
+
candidate?: CompactionResult<NativeCompactionDetails>;
|
|
104
|
+
persistedId?: string;
|
|
105
|
+
deadline?: number;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* One transient Remote V2 operation. It never selects a model, writes recovery
|
|
110
|
+
* records, takes host queues, or resumes from reload. Legacy duty recovery below
|
|
111
|
+
* remains independent.
|
|
112
|
+
*/
|
|
113
|
+
export class RemoteManualCompact {
|
|
114
|
+
private generation = 0;
|
|
115
|
+
private operation: RemoteManualCompactOperation | undefined;
|
|
116
|
+
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
117
|
+
|
|
118
|
+
constructor(private readonly pi: ExtensionAPI, private readonly windows: CodexContextWindowManager,
|
|
119
|
+
private readonly idleTimeoutMs = 10_000) {}
|
|
120
|
+
|
|
121
|
+
get snapshot() { return this.operation?.policy.snapshot; }
|
|
122
|
+
|
|
123
|
+
invalidate(): void {
|
|
124
|
+
++this.generation;
|
|
125
|
+
clearTimeout(this.timer);
|
|
126
|
+
this.timer = undefined;
|
|
127
|
+
const operation = this.operation;
|
|
128
|
+
this.operation = undefined;
|
|
129
|
+
operation?.removeAbortListeners();
|
|
130
|
+
operation?.controller.abort();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
cancel(operation: RemoteManualCompactOperation | undefined): void {
|
|
134
|
+
if (operation && this.operation === operation) this.invalidate();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
begin(event: SessionBeforeCompactEvent, ctx: ExtensionContext, policy: ResolvedToolkitConfig): RemoteManualCompactOperation | undefined {
|
|
138
|
+
this.invalidate();
|
|
139
|
+
const model = ctx.model;
|
|
140
|
+
const anchorId = ctx.sessionManager.getLeafId();
|
|
141
|
+
const windowId = this.windows.currentIdentity()?.currentWindowId;
|
|
142
|
+
if (!model || !anchorId || !windowId || event.signal.aborted) return undefined;
|
|
143
|
+
const branch = ctx.sessionManager.getBranch();
|
|
144
|
+
if (!isDeepStrictEqual(event.branchEntries, branch)) return undefined;
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const onAbort = () => { if (this.operation === operation) this.invalidate(); };
|
|
147
|
+
// Manual compact has already aborted the preceding agent run. Its old,
|
|
148
|
+
// aborted ctx.signal is not cancellation of this newly initiated operation.
|
|
149
|
+
const signals = [event.signal, ...(ctx.signal && !ctx.signal.aborted && ctx.signal !== event.signal ? [ctx.signal] : [])];
|
|
150
|
+
const operation: RemoteManualCompactOperation = {
|
|
151
|
+
generation: this.generation, sessionId: ctx.sessionManager.getSessionId(),
|
|
152
|
+
sessionFile: ctx.sessionManager.getSessionFile(), anchorId,
|
|
153
|
+
branchLength: branch.length, branchFingerprint: branchFingerprint(branch), windowId,
|
|
154
|
+
model: modelKey(model)!, api: model.api, baseUrl: model.baseUrl, thinking: this.pi.getThinkingLevel(),
|
|
155
|
+
controller, signal: controller.signal, policy,
|
|
156
|
+
removeAbortListeners: () => { for (const signal of signals) signal.removeEventListener("abort", onAbort); },
|
|
157
|
+
};
|
|
158
|
+
this.operation = operation;
|
|
159
|
+
for (const signal of signals) signal.addEventListener("abort", onAbort, { once: true });
|
|
160
|
+
return operation;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private owns(operation: RemoteManualCompactOperation, ctx: ExtensionContext): boolean {
|
|
164
|
+
if (this.operation !== operation || operation.generation !== this.generation || operation.signal.aborted
|
|
165
|
+
|| ctx.sessionManager.getSessionId() !== operation.sessionId || ctx.sessionManager.getSessionFile() !== operation.sessionFile
|
|
166
|
+
|| modelKey(ctx.model) !== operation.model || ctx.model?.api !== operation.api || ctx.model?.baseUrl !== operation.baseUrl
|
|
167
|
+
|| this.pi.getThinkingLevel() !== operation.thinking || ctx.hasPendingMessages()) return false;
|
|
168
|
+
this.windows.synchronize(ctx);
|
|
169
|
+
return this.windows.currentIdentity()?.currentWindowId === operation.windowId && !this.windows.hasPendingRollover(ctx);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
current(operation: RemoteManualCompactOperation, ctx: ExtensionContext): boolean {
|
|
173
|
+
try {
|
|
174
|
+
const branch = ctx.sessionManager.getBranch();
|
|
175
|
+
return this.owns(operation, ctx) && ctx.sessionManager.getLeafId() === operation.anchorId
|
|
176
|
+
&& branch.length === operation.branchLength && branchFingerprint(branch) === operation.branchFingerprint;
|
|
177
|
+
} catch { return false; }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
arm(
|
|
181
|
+
operation: RemoteManualCompactOperation,
|
|
182
|
+
ctx: ExtensionContext,
|
|
183
|
+
candidate: CompactionResult<NativeCompactionDetails>,
|
|
184
|
+
): { readonly cancel: boolean; compaction: CompactionResult<NativeCompactionDetails> } | undefined {
|
|
185
|
+
if (!this.current(operation, ctx)) return undefined;
|
|
186
|
+
operation.candidate = structuredClone(candidate);
|
|
187
|
+
const owner = this;
|
|
188
|
+
return {
|
|
189
|
+
compaction: candidate,
|
|
190
|
+
// Pi reads this public result again after later before-compact observers.
|
|
191
|
+
// Revalidate at consumption, not just at return: newer work must revoke
|
|
192
|
+
// the candidate before persistence, without ctx.abort() draining TUI queues.
|
|
193
|
+
get cancel() { return !owner.current(operation, ctx); },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private durableCandidate(operation: RemoteManualCompactOperation, ctx: ExtensionContext): string | undefined {
|
|
198
|
+
if (!this.owns(operation, ctx) || !operation.candidate) return undefined;
|
|
199
|
+
const branch = ctx.sessionManager.getBranch();
|
|
200
|
+
// The event's ID can refer to an OLD equal-summary checkpoint in Pi 1.0.
|
|
201
|
+
// Only the new actual branch child and its complete candidate are proof.
|
|
202
|
+
if (branch.length !== operation.branchLength + 1
|
|
203
|
+
|| branchFingerprint(branch.slice(0, operation.branchLength)) !== operation.branchFingerprint) return undefined;
|
|
204
|
+
const entry = branch[operation.branchLength];
|
|
205
|
+
const candidate = operation.candidate;
|
|
206
|
+
if (!entry || entry.type !== "compaction" || entry.parentId !== operation.anchorId
|
|
207
|
+
|| entry.id === operation.anchorId || ctx.sessionManager.getLeafId() !== entry.id || entry.fromHook !== true
|
|
208
|
+
|| entry.summary !== candidate.summary || entry.firstKeptEntryId !== candidate.firstKeptEntryId
|
|
209
|
+
|| entry.tokensBefore !== candidate.tokensBefore
|
|
210
|
+
// Persistence omits undefined optional fields; compare the actual JSON
|
|
211
|
+
// data, not object identity, summary text, or provider response ID alone.
|
|
212
|
+
|| !isDeepStrictEqual(JSON.parse(JSON.stringify(entry.details)), JSON.parse(JSON.stringify(candidate.details)))) return undefined;
|
|
213
|
+
return entry.id;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private requestManualResume(ctx: ExtensionContext, message = "Remote /compact saved its checkpoint; resume manually."): void {
|
|
217
|
+
try { if (ctx.hasUI) ctx.ui.notify(message, "warning"); } catch { /* stale UI must not escape a deferred callback */ }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Observer only: never await idle or submit a prompt inside session_compact. */
|
|
221
|
+
persisted(ctx: ExtensionContext): void {
|
|
222
|
+
const operation = this.operation;
|
|
223
|
+
if (!operation?.candidate || operation.persistedId) return;
|
|
224
|
+
try { operation.persistedId = this.durableCandidate(operation, ctx); } catch { /* fail closed */ }
|
|
225
|
+
if (!operation.persistedId) {
|
|
226
|
+
this.invalidate();
|
|
227
|
+
this.requestManualResume(ctx, "Remote /compact could not verify its exact saved checkpoint; resume manually.");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
operation.deadline = Date.now() + this.idleTimeoutMs;
|
|
231
|
+
this.schedule(operation, ctx, 0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private schedule(operation: RemoteManualCompactOperation, ctx: ExtensionContext, delay: number): void {
|
|
235
|
+
this.timer = setTimeout(() => {
|
|
236
|
+
this.timer = undefined;
|
|
237
|
+
if (this.operation !== operation) return;
|
|
238
|
+
try {
|
|
239
|
+
if (this.durableCandidate(operation, ctx) !== operation.persistedId) { this.invalidate(); return; }
|
|
240
|
+
if (Date.now() >= operation.deadline!) {
|
|
241
|
+
this.invalidate();
|
|
242
|
+
this.requestManualResume(ctx, "Remote /compact saved its checkpoint; automatic continuation could not safely reach idle. Resume manually.");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (!ctx.isIdle()) { this.schedule(operation, ctx, 25); return; }
|
|
246
|
+
// No await between final public ownership/queue/idle proof and send.
|
|
247
|
+
// Disarm first, so our own input/run events cannot invalidate a newer operation.
|
|
248
|
+
this.invalidate();
|
|
249
|
+
this.pi.sendUserMessage("Context compressed. Resume the active user task.");
|
|
250
|
+
} catch {
|
|
251
|
+
this.invalidate();
|
|
252
|
+
this.requestManualResume(ctx);
|
|
253
|
+
}
|
|
254
|
+
}, delay);
|
|
255
|
+
this.timer.unref?.();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Owns only legacy managed handoffs, not normal window rollovers or Remote V2. */
|
|
83
260
|
export class ManagedManualCompact implements ContextRolloverHandoff {
|
|
84
261
|
private record: ManualCompactRecord | undefined;
|
|
85
262
|
private generation = 0;
|