pi-openai-toolkit 0.15.0 → 0.16.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 +24 -2
- package/README.zh.md +24 -2
- package/docs/configuration.md +9 -3
- package/docs/internals.md +25 -2
- package/package.json +3 -1
- 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/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 +49 -11
- package/src/remote-v2-client.ts +108 -33
- package/src/web-search/extension.ts +20 -9
- package/src/web-search/payload.ts +7 -3
package/src/extension-runtime.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { routeContextNamespaceToolMessage } from "./context-management/namespace-tools";
|
|
20
20
|
import { loadHistoryNotesThreadHint } from "./context-management/history-notes";
|
|
21
21
|
import { CodexContextWindowManager } from "./context-management/window-manager";
|
|
22
|
+
import { ManagedManualCompact } from "./context-management/manual-compact";
|
|
22
23
|
import {
|
|
23
24
|
BULK_CLIFF_RATIO,
|
|
24
25
|
decideBulkCliffAction,
|
|
@@ -110,6 +111,7 @@ type CompactionDependencies = {
|
|
|
110
111
|
remoteCompact: typeof executeRemoteV2Compaction;
|
|
111
112
|
nativeFallback: typeof runNativeFallbackCompaction;
|
|
112
113
|
contextWindows: CodexContextWindowManager;
|
|
114
|
+
manualCompact?: ManagedManualCompact;
|
|
113
115
|
/** Test seam for the same ordered projection exposed by the Pi host patch. */
|
|
114
116
|
projectCompactionContext?: CompactionContextProjection;
|
|
115
117
|
piContextHookPatch: PiContextHookPatchResult;
|
|
@@ -599,6 +601,10 @@ async function handleSessionBeforeCompact(
|
|
|
599
601
|
if (await remoteContextActive(ctx, config)) {
|
|
600
602
|
try {
|
|
601
603
|
dependencies.contextWindows.synchronize(ctx);
|
|
604
|
+
if (event.reason === "manual" && dependencies.manualCompact && !dependencies.manualCompact.isMaintenance) {
|
|
605
|
+
await dependencies.manualCompact.request(event, ctx, resolved);
|
|
606
|
+
return { cancel: true };
|
|
607
|
+
}
|
|
602
608
|
return dependencies.contextWindows.prepareCompaction(event);
|
|
603
609
|
} catch {
|
|
604
610
|
notifyRemoteContextFailure(ctx, "malformed-window-state");
|
|
@@ -783,6 +789,7 @@ async function handleWindowBulk(
|
|
|
783
789
|
loadConfig: typeof loadToolkitConfig,
|
|
784
790
|
trigger: "model-switch" | "turn-end",
|
|
785
791
|
resolved = contextOperation(loadConfig, ctx),
|
|
792
|
+
beginMaintenance?: () => () => void,
|
|
786
793
|
): Promise<void> {
|
|
787
794
|
requireContextPolicy(resolved, ctx);
|
|
788
795
|
const compaction = resolved.config.compaction;
|
|
@@ -830,11 +837,16 @@ async function handleWindowBulk(
|
|
|
830
837
|
|
|
831
838
|
notifyWarning(ctx, `${gap} to ${modelKey}, past ${Math.round(BULK_CLIFF_RATIO * 100)}% of its window; compacting the retired windows first.`);
|
|
832
839
|
if (typeof ctx.compact !== "function") return;
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
840
|
+
const done = beginMaintenance?.();
|
|
841
|
+
try {
|
|
842
|
+
ctx.compact({
|
|
843
|
+
onComplete: () => done?.(),
|
|
844
|
+
onError: (error: Error) => {
|
|
845
|
+
done?.();
|
|
846
|
+
notifyWarning(ctx, `automatic boundary compaction failed: ${error.message}`);
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
} catch (error) { done?.(); throw error; }
|
|
838
850
|
}
|
|
839
851
|
|
|
840
852
|
async function handleContextInternal(
|
|
@@ -1169,12 +1181,14 @@ export default function registerCompactionExtension(
|
|
|
1169
1181
|
const contextWindows = overrides.contextWindows ?? new CodexContextWindowManager((ctx, signal, gatewayModels) =>
|
|
1170
1182
|
loadHistoryNotesThreadHint(ctx, signal, gatewayModels)
|
|
1171
1183
|
);
|
|
1184
|
+
const manualCompact = new ManagedManualCompact(pi, contextWindows);
|
|
1172
1185
|
const dependencies: CompactionDependencies = {
|
|
1173
|
-
loadConfig,
|
|
1174
1186
|
remoteCompact: executeRemoteV2Compaction,
|
|
1175
1187
|
nativeFallback: runNativeFallbackCompaction,
|
|
1176
1188
|
contextWindows,
|
|
1177
1189
|
...overrides,
|
|
1190
|
+
manualCompact,
|
|
1191
|
+
loadConfig: () => manualCompact.snapshot ?? loadConfig(),
|
|
1178
1192
|
piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
|
|
1179
1193
|
};
|
|
1180
1194
|
let tools!: ReturnType<typeof registerContextManagementTools>;
|
|
@@ -1193,6 +1207,8 @@ export default function registerCompactionExtension(
|
|
|
1193
1207
|
assertConfigValid(resolved, "context", "compatibility");
|
|
1194
1208
|
return { active: await isContextRuntimeActive(ctx, resolved.config.compaction), gatewayModels: resolved.gatewayModelKeys };
|
|
1195
1209
|
},
|
|
1210
|
+
undefined,
|
|
1211
|
+
manualCompact,
|
|
1196
1212
|
);
|
|
1197
1213
|
const remoteContextActive: RemoteContextActive = isContextRuntimeActive;
|
|
1198
1214
|
const syncTools = async (
|
|
@@ -1233,6 +1249,7 @@ export default function registerCompactionExtension(
|
|
|
1233
1249
|
pi.on("session_start", async (_event, ctx) => {
|
|
1234
1250
|
const resolved = contextOperation(dependencies.loadConfig, ctx);
|
|
1235
1251
|
const syncOutcome = await syncTools(ctx, ctx.model, { notifyWindowFailure: true }, resolved);
|
|
1252
|
+
await manualCompact.recover(ctx);
|
|
1236
1253
|
const active = syncOutcome.eligible && syncOutcome.toolsSynced && syncOutcome.windowInitialized;
|
|
1237
1254
|
const { source } = resolved;
|
|
1238
1255
|
const warnings = resolved.issues.map((issue) => `${issue.path}: ${issue.code}`);
|
|
@@ -1291,10 +1308,17 @@ export default function registerCompactionExtension(
|
|
|
1291
1308
|
}
|
|
1292
1309
|
});
|
|
1293
1310
|
|
|
1294
|
-
pi.on("context", (event, ctx) =>
|
|
1311
|
+
pi.on("context", (event, ctx) => {
|
|
1312
|
+
manualCompact.guardRequest(ctx);
|
|
1313
|
+
return handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive);
|
|
1314
|
+
});
|
|
1295
1315
|
pi.on("session_before_compact", (event, ctx) => handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive));
|
|
1296
1316
|
pi.on("session_compact", (event, _ctx) => contextWindows.recordCompaction(event.compactionEntry.details));
|
|
1297
|
-
pi.on("
|
|
1317
|
+
pi.on("session_compact_failed", async (event, ctx) => {
|
|
1318
|
+
if (event.reason === "manual" && !manualCompact.isMaintenance) await manualCompact.compactFailed(ctx);
|
|
1319
|
+
});
|
|
1320
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1321
|
+
await manualCompact.shutdown(ctx);
|
|
1298
1322
|
contextWindowReady = false;
|
|
1299
1323
|
contextWindows.reset();
|
|
1300
1324
|
tools.reset();
|
|
@@ -1302,9 +1326,11 @@ export default function registerCompactionExtension(
|
|
|
1302
1326
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
1303
1327
|
// Idle: no retry, compaction, or queued continuation is left running, so this is the
|
|
1304
1328
|
// only safe place to spend a model call on the user's behalf.
|
|
1305
|
-
await
|
|
1329
|
+
if (await manualCompact.settled(ctx)) return;
|
|
1330
|
+
await handleWindowBulk(ctx, contextWindows, dependencies.loadConfig, "turn-end", undefined, () => manualCompact.beginMaintenance());
|
|
1306
1331
|
});
|
|
1307
1332
|
pi.on("model_select", async (event, ctx) => {
|
|
1333
|
+
manualCompact.modelSelected(event.model, ctx);
|
|
1308
1334
|
// Switching into a covered model mid-session must open the window
|
|
1309
1335
|
// lifecycle immediately: without an identity the request rewrite skips
|
|
1310
1336
|
// window metadata, the backend never ingests those turns, and the first
|
|
@@ -1314,12 +1340,24 @@ export default function registerCompactionExtension(
|
|
|
1314
1340
|
await syncTools(ctx, event.model, { notifyWindowFailure: true }, resolved);
|
|
1315
1341
|
// The switch itself is the moment the durable transcript changes consumer, and the
|
|
1316
1342
|
// session is idle here, so the close-out must happen now rather than mid-turn.
|
|
1317
|
-
await handleWindowBulk(ctx, contextWindows, dependencies.loadConfig, "model-switch", resolved);
|
|
1343
|
+
if (!manualCompact.busy) await handleWindowBulk(ctx, contextWindows, dependencies.loadConfig, "model-switch", resolved, () => manualCompact.beginMaintenance());
|
|
1318
1344
|
});
|
|
1319
1345
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
1320
1346
|
await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
|
|
1321
1347
|
});
|
|
1322
|
-
pi.on("
|
|
1348
|
+
pi.on("thinking_level_select", (event, ctx) => manualCompact.thinkingSelected(event.level, ctx));
|
|
1349
|
+
pi.on("message_start", (event) => manualCompact.messageStarted(event.message));
|
|
1350
|
+
pi.on("turn_end", (_event, ctx) => manualCompact.turnEnded(ctx));
|
|
1351
|
+
pi.on("tool_call", (event) => manualCompact.guardTool(event.toolName));
|
|
1352
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
1353
|
+
contextWindows.synchronize(ctx);
|
|
1354
|
+
await manualCompact.recover(ctx);
|
|
1355
|
+
await syncTools(ctx);
|
|
1356
|
+
});
|
|
1357
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
1358
|
+
manualCompact.guardRequest(ctx);
|
|
1359
|
+
return handleBeforeProviderRequest(event, ctx, dependencies.loadConfig, contextWindows, remoteContextActive);
|
|
1360
|
+
});
|
|
1323
1361
|
pi.on("before_provider_headers", async (event, ctx) => {
|
|
1324
1362
|
const resolved = contextOperation(dependencies.loadConfig, ctx);
|
|
1325
1363
|
requireContextPolicy(resolved, ctx);
|
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,
|
|
@@ -294,10 +294,12 @@ function createWebRunTool(deps: WebRunExecutorDependencies): ToolDefinition<
|
|
|
294
294
|
};
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
type StandaloneRegistrationState = "verified" | "excluded" | "unavailable";
|
|
298
|
+
|
|
297
299
|
function verifyStandaloneRegistration(
|
|
298
300
|
pi: ExtensionAPI,
|
|
299
301
|
definition: ToolDefinition<typeof STANDALONE_WEB_RUN_PARAMETERS, AlphaSearchDetails>,
|
|
300
|
-
):
|
|
302
|
+
): StandaloneRegistrationState {
|
|
301
303
|
try {
|
|
302
304
|
const api = pi as ExtensionAPI & {
|
|
303
305
|
getAllTools?: () => Array<{
|
|
@@ -307,14 +309,17 @@ function verifyStandaloneRegistration(
|
|
|
307
309
|
promptGuidelines?: string[];
|
|
308
310
|
}>;
|
|
309
311
|
};
|
|
310
|
-
if (typeof api.getAllTools !== "function") return
|
|
312
|
+
if (typeof api.getAllTools !== "function") return "unavailable";
|
|
311
313
|
const actual = api.getAllTools().find((tool) => tool.name === WEB_RUN_TOOL_NAME);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
+
// Pi publishes only tools permitted by --tools/--exclude-tools/--no-tools.
|
|
315
|
+
// After successful registration, an absent definition means this session
|
|
316
|
+
// cannot use search; it is not a reason to abort ordinary model requests.
|
|
317
|
+
if (actual === undefined) return "excluded";
|
|
318
|
+
return actual.description === definition.description &&
|
|
314
319
|
actual.parameters === definition.parameters &&
|
|
315
|
-
actual.promptGuidelines === definition.promptGuidelines;
|
|
320
|
+
actual.promptGuidelines === definition.promptGuidelines ? "verified" : "unavailable";
|
|
316
321
|
} catch {
|
|
317
|
-
return
|
|
322
|
+
return "unavailable";
|
|
318
323
|
}
|
|
319
324
|
}
|
|
320
325
|
|
|
@@ -399,8 +404,8 @@ export function registerWebSearchExtension(
|
|
|
399
404
|
standaloneRegistrationSucceeded = false;
|
|
400
405
|
}
|
|
401
406
|
|
|
402
|
-
function
|
|
403
|
-
if (!standaloneRegistrationSucceeded || !canManageActiveTools(pi)) return
|
|
407
|
+
function standaloneRegistrationState(): StandaloneRegistrationState {
|
|
408
|
+
if (!standaloneRegistrationSucceeded || !canManageActiveTools(pi)) return "unavailable";
|
|
404
409
|
// Recheck on every lifecycle boundary so a later extension cannot replace
|
|
405
410
|
// the definition after startup and leave a stale active name executable.
|
|
406
411
|
// Do not use getAllTools() during extension loading; lifecycle callbacks
|
|
@@ -408,6 +413,10 @@ export function registerWebSearchExtension(
|
|
|
408
413
|
return verifyStandaloneRegistration(pi, standaloneTool);
|
|
409
414
|
}
|
|
410
415
|
|
|
416
|
+
function standaloneReady(): boolean {
|
|
417
|
+
return standaloneRegistrationState() === "verified";
|
|
418
|
+
}
|
|
419
|
+
|
|
411
420
|
function readConfig(model: WebSearchModel | undefined, ctx?: ExtensionContext): ResolvedToolkitConfig {
|
|
412
421
|
const resolved = resolveToolkitConfig(loadConfig(), model);
|
|
413
422
|
if (ctx) notifyConfigIssues(ctx, resolved);
|
|
@@ -459,13 +468,15 @@ export function registerWebSearchExtension(
|
|
|
459
468
|
catch (error) { abortAndThrow(ctx, error instanceof Error ? error.message : "Invalid Web Search configuration."); }
|
|
460
469
|
const { config } = resolved;
|
|
461
470
|
const resolution = synchronizeResolved(ctx.model, resolved);
|
|
462
|
-
|
|
471
|
+
const registration = standaloneRegistrationState();
|
|
472
|
+
if (resolution.route === "standalone-alpha" && resolution.available && registration === "unavailable") {
|
|
463
473
|
abortAndThrow(ctx, `${WEB_RUN_TOOL_NAME} is not registered by the toolkit; standalone Web Search request aborted.`);
|
|
464
474
|
}
|
|
465
475
|
const transformed = transformWebSearchPayload({
|
|
466
476
|
model: ctx.model,
|
|
467
477
|
config: config.webSearch,
|
|
468
478
|
payload: event.payload,
|
|
479
|
+
standaloneToolExcluded: registration === "excluded",
|
|
469
480
|
});
|
|
470
481
|
if (transformed.fatal) {
|
|
471
482
|
abortAndThrow(ctx, transformed.errorMessage ?? "Web Search route request aborted.");
|
|
@@ -49,8 +49,9 @@ function unavailableRouteTransform(
|
|
|
49
49
|
function transformStandalonePayload(args: {
|
|
50
50
|
payload: JsonObject;
|
|
51
51
|
resolution: ReturnType<typeof resolveWebSearchRoute>;
|
|
52
|
+
standaloneToolExcluded?: boolean;
|
|
52
53
|
}): WebSearchPayloadTransform {
|
|
53
|
-
const { payload, resolution } = args;
|
|
54
|
+
const { payload, resolution, standaloneToolExcluded = false } = args;
|
|
54
55
|
const toolsValue = payload.tools;
|
|
55
56
|
if (toolsValue !== undefined && !Array.isArray(toolsValue)) {
|
|
56
57
|
return {
|
|
@@ -77,6 +78,7 @@ function transformStandalonePayload(args: {
|
|
|
77
78
|
const normalizedTools = tools.filter((tool) => {
|
|
78
79
|
if (isLocalWebSearchFunction(tool) || isNativeWebSearchTool(tool)) return false;
|
|
79
80
|
if (!isStandaloneWebRunFunction(tool)) return true;
|
|
81
|
+
if (standaloneToolExcluded) return false;
|
|
80
82
|
standaloneToolCount += 1;
|
|
81
83
|
return standaloneToolCount === 1;
|
|
82
84
|
});
|
|
@@ -84,7 +86,7 @@ function transformStandalonePayload(args: {
|
|
|
84
86
|
const normalizedInclude = include.filter((item) => item !== WEB_SEARCH_SOURCE_INCLUDE);
|
|
85
87
|
const toolsChanged = normalizedTools.length !== tools.length;
|
|
86
88
|
const includeChanged = normalizedInclude.length !== include.length;
|
|
87
|
-
if (toolsValue !== undefined && !normalizedTools.some(isStandaloneWebRunFunction)) {
|
|
89
|
+
if (!standaloneToolExcluded && toolsValue !== undefined && !normalizedTools.some(isStandaloneWebRunFunction)) {
|
|
88
90
|
return {
|
|
89
91
|
payload,
|
|
90
92
|
outcome: "unavailable-route",
|
|
@@ -159,6 +161,8 @@ export function transformWebSearchPayload(args: {
|
|
|
159
161
|
model: WebSearchModel | undefined;
|
|
160
162
|
config: WebSearchConfig;
|
|
161
163
|
payload: unknown;
|
|
164
|
+
/** The host filtered a successfully registered web_run out of this session's tool catalog. */
|
|
165
|
+
standaloneToolExcluded?: boolean;
|
|
162
166
|
}): WebSearchPayloadTransform {
|
|
163
167
|
const { model, config, payload } = args;
|
|
164
168
|
const resolution = resolveWebSearchRoute({ model, config });
|
|
@@ -195,7 +199,7 @@ export function transformWebSearchPayload(args: {
|
|
|
195
199
|
errorMessage: "Standalone Web Search requires an object provider payload.",
|
|
196
200
|
};
|
|
197
201
|
}
|
|
198
|
-
return transformStandalonePayload({ payload, resolution });
|
|
202
|
+
return transformStandalonePayload({ payload, resolution, standaloneToolExcluded: args.standaloneToolExcluded });
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
if (!isRecord(payload)) {
|