pi-openai-toolkit 0.15.1 → 0.17.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/README.md +60 -68
- package/README.zh.md +58 -66
- package/docs/configuration.md +9 -3
- package/docs/internals.md +31 -6
- package/package.json +11 -9
- package/src/auto-mode/classifier.ts +161 -165
- package/src/auto-mode/extension.ts +691 -661
- package/src/auto-mode/model-selection-guard.ts +52 -0
- package/src/auto-mode/reviewer.ts +325 -335
- package/src/auto-mode/transcript.ts +135 -133
- package/src/compaction-projection.ts +35 -31
- package/src/context-management/manual-compact.ts +433 -0
- package/src/context-management/tools.ts +11 -2
- package/src/context-management/window-manager.ts +35 -9
- package/src/extension-runtime.ts +64 -30
- package/src/payload-rewrite.ts +34 -64
- package/src/pi-context-hook.ts +117 -116
- package/src/remote-v2-client.ts +108 -33
package/src/remote-v2-client.ts
CHANGED
|
@@ -156,41 +156,53 @@ export function buildRemoteV2CompactionRequest(
|
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
159
|
+
/** Shared line framing; yielding lazily keeps bytes after a terminal out of parsing. */
|
|
160
|
+
class SseEventParser {
|
|
161
|
+
private line = "";
|
|
162
|
+
private skipLf = false;
|
|
163
|
+
private event: string | undefined;
|
|
164
|
+
private dataLines: string[] = [];
|
|
165
|
+
|
|
166
|
+
*push(text: string): Generator<ParsedSseEvent> {
|
|
167
|
+
for (const character of text) {
|
|
168
|
+
if (this.skipLf) {
|
|
169
|
+
this.skipLf = false;
|
|
170
|
+
if (character === "\n") continue;
|
|
171
|
+
}
|
|
172
|
+
if (character !== "\r" && character !== "\n") {
|
|
173
|
+
this.line += character;
|
|
172
174
|
continue;
|
|
173
175
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
this.skipLf = character === "\r";
|
|
177
|
+
const line = this.line;
|
|
178
|
+
this.line = "";
|
|
179
|
+
if (line === "") {
|
|
180
|
+
const event = this.event;
|
|
181
|
+
const dataLines = this.dataLines;
|
|
182
|
+
this.event = undefined;
|
|
183
|
+
this.dataLines = [];
|
|
184
|
+
if (dataLines.length === 0) continue;
|
|
185
|
+
const dataText = dataLines.join("\n");
|
|
186
|
+
yield dataText === "[DONE]"
|
|
187
|
+
? { event, dataText }
|
|
188
|
+
: { event, dataText, data: JSON.parse(dataText) };
|
|
189
|
+
} else if (line.startsWith("event:")) {
|
|
190
|
+
this.event = line.slice("event:".length).trim();
|
|
191
|
+
} else if (line === "data" || line.startsWith("data:")) {
|
|
192
|
+
this.dataLines.push(line.slice("data:".length).replace(/^ /, ""));
|
|
176
193
|
}
|
|
177
194
|
}
|
|
178
|
-
|
|
179
|
-
if (dataLines.length === 0) continue;
|
|
180
|
-
const dataText = dataLines.join("\n");
|
|
181
|
-
if (dataText === "[DONE]") {
|
|
182
|
-
events.push({ event, dataText });
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
events.push({ event, dataText, data: JSON.parse(dataText) });
|
|
188
|
-
} catch {
|
|
189
|
-
return undefined;
|
|
190
|
-
}
|
|
191
195
|
}
|
|
196
|
+
}
|
|
192
197
|
|
|
193
|
-
|
|
198
|
+
export function parseSseEvents(raw: string): ParsedSseEvent[] | undefined {
|
|
199
|
+
try {
|
|
200
|
+
// Retain the buffered helper's historical support for a final unterminated block.
|
|
201
|
+
const events = [...new SseEventParser().push(`${raw}\n\n`)];
|
|
202
|
+
return events.length > 0 ? events : undefined;
|
|
203
|
+
} catch {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
194
206
|
}
|
|
195
207
|
|
|
196
208
|
function isRemoteV2CompactionItem(value: unknown): value is RemoteV2CompactionItem {
|
|
@@ -560,6 +572,68 @@ function reconcileSseCheckpoint(events: readonly ParsedSseEvent[]): CheckpointRe
|
|
|
560
572
|
};
|
|
561
573
|
}
|
|
562
574
|
|
|
575
|
+
type CollectedSse = {
|
|
576
|
+
responseText: string;
|
|
577
|
+
events?: ParsedSseEvent[];
|
|
578
|
+
reconciliation?: CheckpointReconciliation;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
async function collectSseCheckpoint(response: Response, signal?: AbortSignal): Promise<CollectedSse> {
|
|
582
|
+
const reader = response.body?.getReader();
|
|
583
|
+
if (!reader) {
|
|
584
|
+
if (signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
|
|
585
|
+
return { responseText: "" };
|
|
586
|
+
}
|
|
587
|
+
const decoder = new TextDecoder();
|
|
588
|
+
const parser = new SseEventParser();
|
|
589
|
+
const events: ParsedSseEvent[] = [];
|
|
590
|
+
const text: string[] = [];
|
|
591
|
+
let onAbort: (() => void) | undefined;
|
|
592
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
593
|
+
onAbort = () => reject(new DOMException("Compaction aborted", "AbortError"));
|
|
594
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
595
|
+
});
|
|
596
|
+
try {
|
|
597
|
+
while (true) {
|
|
598
|
+
if (signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
|
|
599
|
+
const chunk = await Promise.race([reader.read(), aborted]);
|
|
600
|
+
if (signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
|
|
601
|
+
const decoded = chunk.done ? decoder.decode() : decoder.decode(chunk.value, { stream: true });
|
|
602
|
+
text.push(decoded);
|
|
603
|
+
try {
|
|
604
|
+
for (const event of parser.push(decoded)) {
|
|
605
|
+
events.push(event);
|
|
606
|
+
const type = getEventType(event);
|
|
607
|
+
if (type === "response.completed" || type === "error" || type === "response.failed" || type === "response.incomplete") {
|
|
608
|
+
// Validate synchronously before cleanup; later cancellation cannot undo this boundary.
|
|
609
|
+
return { responseText: "", events, reconciliation: reconcileSseCheckpoint(events) };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} catch (error) {
|
|
613
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
614
|
+
return { responseText: text.join("") };
|
|
615
|
+
}
|
|
616
|
+
// EOF never dispatches an unfinished frame, even if its JSON happens to be complete.
|
|
617
|
+
if (chunk.done) return { responseText: text.join(""), events: events.length ? events : undefined };
|
|
618
|
+
}
|
|
619
|
+
} finally {
|
|
620
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
621
|
+
// Cancellation is best-effort cleanup, not part of the completion boundary.
|
|
622
|
+
// Observe rejection immediately and release even if the transport never settles cancel().
|
|
623
|
+
try {
|
|
624
|
+
void reader.cancel().catch(() => undefined);
|
|
625
|
+
} catch {
|
|
626
|
+
// A custom reader may throw synchronously during cleanup.
|
|
627
|
+
} finally {
|
|
628
|
+
try {
|
|
629
|
+
reader.releaseLock();
|
|
630
|
+
} catch {
|
|
631
|
+
// Cleanup failures must not replace a validated checkpoint or cancellation.
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
563
637
|
export async function executeRemoteV2Compaction(
|
|
564
638
|
options: ExecuteRemoteV2CompactionOptions,
|
|
565
639
|
): Promise<RemoteV2CompactionClientResult> {
|
|
@@ -588,13 +662,13 @@ export async function executeRemoteV2Compaction(
|
|
|
588
662
|
body: JSON.stringify(request),
|
|
589
663
|
signal,
|
|
590
664
|
});
|
|
591
|
-
const responseText = await response.text();
|
|
592
665
|
const responseHeaders: Record<string, string> = {};
|
|
593
666
|
response.headers.forEach((value, key) => {
|
|
594
667
|
responseHeaders[key] = value;
|
|
595
668
|
});
|
|
596
669
|
|
|
597
670
|
if (!response.ok) {
|
|
671
|
+
const responseText = await response.text();
|
|
598
672
|
let responseJson: unknown;
|
|
599
673
|
if (responseText.trim()) {
|
|
600
674
|
try {
|
|
@@ -623,7 +697,9 @@ export async function executeRemoteV2Compaction(
|
|
|
623
697
|
return failure;
|
|
624
698
|
}
|
|
625
699
|
|
|
626
|
-
|
|
700
|
+
const collected = await collectSseCheckpoint(response, signal);
|
|
701
|
+
const { responseText, events } = collected;
|
|
702
|
+
if (!events && !responseText.trim()) {
|
|
627
703
|
const failure: RemoteV2CompactionClientFailure = {
|
|
628
704
|
ok: false,
|
|
629
705
|
reason: "empty-body",
|
|
@@ -642,7 +718,6 @@ export async function executeRemoteV2Compaction(
|
|
|
642
718
|
return failure;
|
|
643
719
|
}
|
|
644
720
|
|
|
645
|
-
const events = parseSseEvents(responseText);
|
|
646
721
|
if (!events) {
|
|
647
722
|
const failure: RemoteV2CompactionClientFailure = {
|
|
648
723
|
ok: false,
|
|
@@ -663,7 +738,7 @@ export async function executeRemoteV2Compaction(
|
|
|
663
738
|
return failure;
|
|
664
739
|
}
|
|
665
740
|
|
|
666
|
-
const reconciliation = reconcileSseCheckpoint(events);
|
|
741
|
+
const reconciliation = collected.reconciliation ?? reconcileSseCheckpoint(events);
|
|
667
742
|
if (!reconciliation.ok) {
|
|
668
743
|
const failure: RemoteV2CompactionClientFailure = {
|
|
669
744
|
ok: false,
|