@code-yeongyu/senpi 2026.10.11 → 2026.10.12
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/CHANGELOG.md +46 -0
- package/dist/bundle/chunks/anthropic-messages-GYG6YZY5.js +10 -0
- package/dist/bundle/chunks/{app-server-command-NHDOL4HQ.js → app-server-command-OL6PPUOA.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-EJF2XJ7Q.js → azure-openai-responses-GU6FOSJR.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZABLYCHT.js → chunk-46JEQXD7.js} +1 -1
- package/dist/bundle/chunks/{chunk-VE3D74HN.js → chunk-55VKYA5V.js} +1 -1
- package/dist/bundle/chunks/{chunk-7UNN3YHW.js → chunk-5AVFHHDF.js} +230 -221
- package/dist/bundle/chunks/{chunk-2JE2DOIF.js → chunk-5DPTM5E2.js} +1 -1
- package/dist/bundle/chunks/chunk-5UPUSGP5.js +2 -0
- package/dist/bundle/chunks/chunk-5VMRMY6N.js +16 -0
- package/dist/bundle/chunks/{chunk-7X3VRAM3.js → chunk-7P3QPVTF.js} +1 -1
- package/dist/bundle/chunks/{chunk-T6IYKWIH.js → chunk-BUGZZVQW.js} +1 -1
- package/dist/bundle/chunks/chunk-DL7EFVT2.js +12 -0
- package/dist/bundle/chunks/chunk-E5YUVGLQ.js +11 -0
- package/dist/bundle/chunks/{chunk-RLFQICEY.js → chunk-EQJY2B5F.js} +1 -1
- package/dist/bundle/chunks/{chunk-VYKBN4MR.js → chunk-GC2RU5TS.js} +1 -1
- package/dist/bundle/chunks/chunk-GPKEJOTQ.js +202 -0
- package/dist/bundle/chunks/{chunk-KCVSSFOV.js → chunk-GQ5PWY6P.js} +4 -3
- package/dist/bundle/chunks/{chunk-4GPJOGXZ.js → chunk-I4AEC6MH.js} +1 -1
- package/dist/bundle/chunks/chunk-JXSFGRBE.js +8 -0
- package/dist/bundle/chunks/{chunk-YGCMN2CX.js → chunk-NAY5LB53.js} +1 -1
- package/dist/bundle/chunks/{chunk-EBOD66AS.js → chunk-PBN2CATL.js} +2 -2
- package/dist/bundle/chunks/chunk-PFNBY43W.js +2 -0
- package/dist/bundle/chunks/{chunk-CY2B4QRR.js → chunk-PUWEQK2W.js} +1 -1
- package/dist/bundle/chunks/{chunk-5DBPUQSX.js → chunk-SF6AUNQZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-G2XAU4BX.js → chunk-TMTT5CLA.js} +1 -1
- package/dist/bundle/chunks/{chunk-2JKDZXIY.js → chunk-VWFKLW73.js} +2 -2
- package/dist/bundle/chunks/chunk-WZMSBRTW.js +2 -0
- package/dist/bundle/chunks/{chunk-55HLKZSB.js → chunk-XVJRRLGN.js} +1 -1
- package/dist/bundle/chunks/chunk-Z2PWWBBD.js +31 -0
- package/dist/bundle/chunks/cli-main-RSIYAR4J.js +3 -0
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-SWVU2AXJ.js → cloudflare-workers-ai-system-one-LVWRKOUD.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-XG4YX6FQ.js → google-generative-ai-JMGYPTXL.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-BBOZ6OUB.js → google-vertex-CI4FPIU7.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-EZ3KEW6V.js → help-fast-path-M7KMBM6K.js} +1 -1
- package/dist/bundle/chunks/host-command-6UR3DSFR.js +2 -0
- package/dist/bundle/chunks/host-lifecycle-2BKCCK5X.js +3 -0
- package/dist/bundle/chunks/host-lifecycle.js +10 -7
- package/dist/bundle/chunks/host-session-command-A2MZHEBP.js +19 -0
- package/dist/bundle/chunks/interactive-mode-EJBMTJOH.js +2 -0
- package/dist/bundle/chunks/{list-tips-WE2YGQM6.js → list-tips-BEQAOA4N.js} +1 -1
- package/dist/bundle/chunks/{mistral-conversations-RB3ZBCGE.js → mistral-conversations-5OD7662T.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-V4CUQLV7.js → multi-session-host-YYMLHCUA.js} +4 -4
- package/dist/bundle/chunks/{openai-codex-responses-DBTPUXRW.js → openai-codex-responses-FN3UPAFI.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-QAJQC4BY.js → openai-completions-KMUQ3T4M.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-UBJWCEHG.js → openai-responses-V4Q5GVLO.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-WTRDT4ZH.js → package-manager-cli-6A5ZP26E.js} +1 -1
- package/dist/bundle/chunks/rotation-stream-PT6LLBMJ.js +2 -0
- package/dist/bundle/chunks/rpc-mode-XNYJPYUM.js +2 -0
- package/dist/bundle/chunks/runtime-session-ZBHFMT65.js +2 -0
- package/dist/bundle/chunks/{schedule-command-PJVRKAKD.js → schedule-command-FYQG2MFX.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-7TX4XHN4.js +3 -0
- package/dist/bundle/chunks/session-picker-4RNB27SM.js +2 -0
- package/dist/bundle/chunks/session-worker.js +418 -412
- package/dist/bundle/chunks/{typesafe-system-one-B5TW3FHM.js → typesafe-system-one-WUHW7HR4.js} +1 -1
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.d.ts +11 -2
- package/dist/core/agent-session.js +152 -43
- package/dist/core/compaction/compaction.js +2 -0
- package/dist/core/extensions/builtin/anthropic-web-search/index.js +22 -14
- package/dist/core/extensions/builtin/cache-keepalive/index.js +8 -4
- package/dist/core/extensions/builtin/cache-keepalive/session-prewarm.d.ts +2 -1
- package/dist/core/extensions/builtin/cache-keepalive/session-prewarm.js +55 -7
- package/dist/core/extensions/builtin/cache-keepalive/startup-activity-gate.d.ts +8 -0
- package/dist/core/extensions/builtin/cache-keepalive/startup-activity-gate.js +25 -0
- package/dist/core/extensions/builtin/compaction/index.js +13 -5
- package/dist/core/extensions/builtin/compaction/speculative.d.ts +1 -0
- package/dist/core/extensions/builtin/compaction/speculative.js +8 -4
- package/dist/core/extensions/builtin/moved-path-guard/tool-classes.js +1 -0
- package/dist/core/extensions/builtin/openai-web-search/index.d.ts +4 -1
- package/dist/core/extensions/builtin/openai-web-search/index.js +54 -17
- package/dist/core/extensions/builtin/server-search-fallback.d.ts +12 -0
- package/dist/core/extensions/builtin/server-search-fallback.js +50 -0
- package/dist/core/extensions/builtin/terminal/runtime-session.d.ts +10 -1
- package/dist/core/extensions/builtin/terminal/runtime-session.js +48 -4
- package/dist/core/extensions/builtin/websearch/index.d.ts +1 -1
- package/dist/core/extensions/builtin/websearch/index.js +119 -18
- package/dist/core/extensions/builtin/websearch/server-search.d.ts +11 -0
- package/dist/core/extensions/builtin/websearch/server-search.js +230 -0
- package/dist/core/extensions/builtin/websearch/websearch/native.d.ts +2 -0
- package/dist/core/extensions/builtin/websearch/websearch/search-model.js +15 -0
- package/dist/core/extensions/runner.d.ts +8 -1
- package/dist/core/extensions/runner.js +56 -7
- package/dist/core/extensions/types.d.ts +12 -2
- package/dist/core/extensions/user-activity.d.ts +10 -0
- package/dist/core/extensions/user-activity.js +20 -0
- package/dist/core/model-config-schema.d.ts +34 -0
- package/dist/core/model-config-schema.js +2 -0
- package/dist/core/model-config.js +1 -0
- package/dist/core/reserved-keywords.d.ts +28 -0
- package/dist/core/reserved-keywords.js +81 -0
- package/dist/core/retry-fallback/candidates.d.ts +1 -1
- package/dist/core/retry-fallback/candidates.js +1 -12
- package/dist/core/retry-fallback/controller.d.ts +3 -1
- package/dist/core/retry-fallback/controller.js +11 -5
- package/dist/core/retry-fallback/cooldown.d.ts +2 -1
- package/dist/core/retry-fallback/cooldown.js +21 -12
- package/dist/core/retry-fallback/unusable.d.ts +2 -0
- package/dist/core/retry-fallback/unusable.js +14 -2
- package/dist/core/retry-fallback/usage-limit.d.ts +10 -1
- package/dist/core/retry-fallback/usage-limit.js +39 -2
- package/dist/core/session-title-generator.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/modes/index.d.ts +1 -1
- package/dist/modes/interactive/components/assistant-render-descriptors.js +7 -1
- package/dist/modes/interactive/composer-user-activity.d.ts +6 -0
- package/dist/modes/interactive/composer-user-activity.js +30 -0
- package/dist/modes/interactive/grok/chrome.js +2 -1
- package/dist/modes/interactive/interactive-mode.js +15 -6
- package/dist/modes/interactive/session-control-registry.js +8 -1
- package/dist/modes/interactive/theme/reserved-keyword-rainbow.d.ts +11 -0
- package/dist/modes/interactive/theme/reserved-keyword-rainbow.js +50 -0
- package/dist/modes/interactive/theme/theme.d.ts +3 -1
- package/dist/modes/interactive/theme/theme.js +9 -0
- package/dist/modes/print-mode.js +7 -2
- package/dist/modes/rpc/connection-handler.js +43 -11
- package/dist/modes/rpc/custom-capability.d.ts +2 -0
- package/dist/modes/rpc/custom-capability.js +2 -0
- package/dist/modes/rpc/host-admission-proxy.d.ts +9 -0
- package/dist/modes/rpc/host-admission-proxy.js +211 -0
- package/dist/modes/rpc/host-admission.d.ts +21 -0
- package/dist/modes/rpc/host-admission.js +59 -0
- package/dist/modes/rpc/host-capabilities.js +2 -1
- package/dist/modes/rpc/host-client-occupancy.d.ts +2 -1
- package/dist/modes/rpc/host-client-occupancy.js +7 -0
- package/dist/modes/rpc/host-decision.d.ts +2 -2
- package/dist/modes/rpc/host-decision.js +4 -0
- package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
- package/dist/modes/rpc/host-ensure.js +31 -3
- package/dist/modes/rpc/host-lifecycle-drain.d.ts +2 -1
- package/dist/modes/rpc/host-lifecycle-drain.js +3 -1
- package/dist/modes/rpc/host-lifecycle-proxy.d.ts +3 -2
- package/dist/modes/rpc/host-lifecycle-proxy.js +10 -19
- package/dist/modes/rpc/host-lifecycle-shutdown.d.ts +2 -0
- package/dist/modes/rpc/host-lifecycle-shutdown.js +3 -1
- package/dist/modes/rpc/host-lifecycle-ticker.d.ts +11 -0
- package/dist/modes/rpc/host-lifecycle-ticker.js +27 -0
- package/dist/modes/rpc/host-lifecycle.js +15 -23
- package/dist/modes/rpc/host-outcome.js +1 -0
- package/dist/modes/rpc/host-protocol-info.d.ts +2 -0
- package/dist/modes/rpc/host-protocol-info.js +1 -0
- package/dist/modes/rpc/host-session-runner.js +22 -1
- package/dist/modes/rpc/host-status-all.js +1 -0
- package/dist/modes/rpc/host-status.d.ts +1 -0
- package/dist/modes/rpc/host-status.js +5 -1
- package/dist/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/modes/rpc/rpc-client.js +12 -0
- package/dist/modes/rpc/rpc-types.d.ts +49 -1
- package/dist/modes/rpc/rpc-types.js +5 -0
- package/dist/modes/rpc/session-command-activity.js +2 -0
- package/dist/modes/rpc/session-held.js +3 -0
- package/docs/extensions.md +13 -1
- package/docs/providers.md +88 -0
- package/docs/rpc.md +150 -5
- package/docs/web-search.md +3 -1
- package/package.json +7 -7
- package/dist/bundle/chunks/anthropic-messages-5QQJVQPZ.js +0 -9
- package/dist/bundle/chunks/chunk-2A3MS2Z7.js +0 -8
- package/dist/bundle/chunks/chunk-6SBY4ZWC.js +0 -2
- package/dist/bundle/chunks/chunk-6WCOSBI7.js +0 -2
- package/dist/bundle/chunks/chunk-E75RO54O.js +0 -14
- package/dist/bundle/chunks/chunk-JMYO34RK.js +0 -2
- package/dist/bundle/chunks/chunk-KRNXUTZJ.js +0 -2
- package/dist/bundle/chunks/chunk-L4HVJWIF.js +0 -11
- package/dist/bundle/chunks/chunk-N5WLLHUE.js +0 -2
- package/dist/bundle/chunks/chunk-NEFNNQSV.js +0 -9
- package/dist/bundle/chunks/chunk-PWIYVLIA.js +0 -9
- package/dist/bundle/chunks/chunk-Q3R44CGT.js +0 -5
- package/dist/bundle/chunks/chunk-QIARZN7G.js +0 -31
- package/dist/bundle/chunks/chunk-QKU6JOA4.js +0 -2
- package/dist/bundle/chunks/chunk-QYWXDE47.js +0 -2
- package/dist/bundle/chunks/chunk-XHELSLN3.js +0 -188
- package/dist/bundle/chunks/chunk-YDJ4HGWO.js +0 -16
- package/dist/bundle/chunks/chunk-ZJHRDQCH.js +0 -2
- package/dist/bundle/chunks/cli-main-TUBNBZNK.js +0 -3
- package/dist/bundle/chunks/host-command-WO7VKJBJ.js +0 -2
- package/dist/bundle/chunks/host-lifecycle-X2J5FTJL.js +0 -3
- package/dist/bundle/chunks/host-session-command-ICBOH5RP.js +0 -19
- package/dist/bundle/chunks/interactive-mode-UGDNCGQ5.js +0 -2
- package/dist/bundle/chunks/rotation-stream-OODURUGZ.js +0 -2
- package/dist/bundle/chunks/rpc-mode-22MCT32O.js +0 -2
- package/dist/bundle/chunks/runtime-session-I4NDNGC5.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-SFPRQBDO.js +0 -3
- package/dist/bundle/chunks/session-picker-3ZQPV7Z3.js +0 -2
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Restored history is not admission; real activity or a completed turn is. */
|
|
2
|
+
export function createStartupActivityGate() {
|
|
3
|
+
let waiting = false;
|
|
4
|
+
let unsubscribe;
|
|
5
|
+
function release() {
|
|
6
|
+
waiting = false;
|
|
7
|
+
unsubscribe?.();
|
|
8
|
+
unsubscribe = undefined;
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
get waiting() {
|
|
12
|
+
return waiting;
|
|
13
|
+
},
|
|
14
|
+
start(ctx, onActivity) {
|
|
15
|
+
release();
|
|
16
|
+
waiting = true;
|
|
17
|
+
unsubscribe = ctx.onUserActivity?.(() => {
|
|
18
|
+
release();
|
|
19
|
+
onActivity();
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
release,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=startup-activity-gate.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createWarmAnchorSnapshot, isWarmSummaryAnchorValid } from "../../../compaction/warm-anchor.js";
|
|
2
|
+
import { AccountUsageLimitError } from "../../../retry-fallback/usage-limit.js";
|
|
2
3
|
import * as checkpointState from "./checkpoint-state.js";
|
|
3
4
|
import * as breaker from "./circuit-breaker.js";
|
|
4
5
|
import { registerContextReductionLifecycle } from "./context-reduction-lifecycle.js";
|
|
@@ -368,7 +369,7 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
|
368
369
|
let feedbackSignal = ctx.beginCompaction?.({ reason: "extension" });
|
|
369
370
|
try {
|
|
370
371
|
let remoteFallbackReason;
|
|
371
|
-
if (isOpenAiRemoteCompactionModel(ctx.model)) {
|
|
372
|
+
if (isOpenAiRemoteCompactionModel(ctx.model) && (ctx.getCompactionModel?.() ?? ctx.model) === ctx.model) {
|
|
372
373
|
const remoteGeneration = speculativeGeneration + 1;
|
|
373
374
|
const remoteSnapshot = createSpeculativeCompactionSnapshot(ctx, {
|
|
374
375
|
generation: remoteGeneration,
|
|
@@ -574,12 +575,19 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
|
574
575
|
}
|
|
575
576
|
let remoteCompaction;
|
|
576
577
|
try {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
578
|
+
// Native remote compaction is tied to the active provider's replay origin.
|
|
579
|
+
// A different summary model must use the bounded local summary route.
|
|
580
|
+
remoteCompaction =
|
|
581
|
+
(ctx.getCompactionModel?.() ?? model) !== model
|
|
582
|
+
? undefined
|
|
583
|
+
: await runOpenAiRemoteCompaction(ctx, event, (data) => {
|
|
584
|
+
reportRemoteCompactionTimeout(ctx, event.reason, event.signal, data);
|
|
585
|
+
pi.events.emit(SENPI_COMPACTION_EVENT, data);
|
|
586
|
+
}, remoteCompactionDependencies);
|
|
581
587
|
}
|
|
582
588
|
catch (error) {
|
|
589
|
+
if (error instanceof AccountUsageLimitError)
|
|
590
|
+
return { cancel: true, reason: error.message };
|
|
583
591
|
// The remote route deliberately rethrows once its signal aborts; the
|
|
584
592
|
// handler converts that into a silent stand-down so the abort is not
|
|
585
593
|
// reported as an extension error with a stack (issue #886).
|
|
@@ -14,6 +14,7 @@ export interface SpeculativeCompactionContext {
|
|
|
14
14
|
modelRegistry?: ModelRegistry;
|
|
15
15
|
getContextUsage(): ContextUsage | undefined;
|
|
16
16
|
getCompactionSettings?(): CompactionPreparation["settings"];
|
|
17
|
+
getCompactionModel?(): Model<any> | undefined;
|
|
17
18
|
getMessageRevision(): number;
|
|
18
19
|
getSystemPrompt?(): string;
|
|
19
20
|
prepareProviderRequest?(messages: AgentMessage[]): Promise<ProviderRequestPreparation>;
|
|
@@ -140,7 +140,11 @@ export function createSpeculativeCompactionSnapshot(context, options) {
|
|
|
140
140
|
export async function runExtensionCompaction(context, snapshot, signal, onProgress) {
|
|
141
141
|
if (signal?.aborted)
|
|
142
142
|
return undefined;
|
|
143
|
-
const
|
|
143
|
+
const selectedModel = context.getCompactionModel?.() ?? snapshot.model;
|
|
144
|
+
const selectedSnapshot = selectedModel === snapshot.model
|
|
145
|
+
? snapshot
|
|
146
|
+
: { ...snapshot, model: selectedModel, contextWindow: selectedModel.contextWindow };
|
|
147
|
+
const auth = await context.modelRegistry?.getApiKeyAndHeaders(selectedModel);
|
|
144
148
|
if (signal?.aborted)
|
|
145
149
|
return undefined;
|
|
146
150
|
// A provider is authenticated for summarization by a resolved key, a credential
|
|
@@ -148,12 +152,12 @@ export async function runExtensionCompaction(context, snapshot, signal, onProgre
|
|
|
148
152
|
// same provider resolution normal turns use; an unconfigured keyed provider still
|
|
149
153
|
// has none of these and is rejected before a request.
|
|
150
154
|
if (!auth?.ok || !(auth.apiKey || hasCredentialHeaders(auth.headers) || auth.ambient)) {
|
|
151
|
-
const detail = auth && !auth.ok ? auth.error : `no credentials resolved for provider "${
|
|
155
|
+
const detail = auth && !auth.ok ? auth.error : `no credentials resolved for provider "${selectedModel.provider}"`;
|
|
152
156
|
throw new SummaryGenerationError("auth", `summarization credentials unavailable: ${detail}`);
|
|
153
157
|
}
|
|
154
158
|
const requestSnapshot = auth.baseUrl
|
|
155
|
-
? { ...
|
|
156
|
-
:
|
|
159
|
+
? { ...selectedSnapshot, model: { ...selectedModel, baseUrl: auth.baseUrl } }
|
|
160
|
+
: selectedSnapshot;
|
|
157
161
|
const prompt = buildPrompt({
|
|
158
162
|
variant: requestSnapshot.promptVariant,
|
|
159
163
|
previousSummary: requestSnapshot.preparation.previousSummary,
|
|
@@ -21,6 +21,7 @@ export const MOVED_PATH_TOOL_CLASSES = {
|
|
|
21
21
|
ask_user_question: { kind: "none" },
|
|
22
22
|
todo: { kind: "none" },
|
|
23
23
|
web_search: { kind: "none" },
|
|
24
|
+
$web_search: { kind: "none" },
|
|
24
25
|
tool_search: { kind: "none" },
|
|
25
26
|
webfetch: { kind: "none" },
|
|
26
27
|
create_goal: { kind: "none" },
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ExtensionAPI } from "../../types.ts";
|
|
3
|
-
type OpenAiWebSearchModel = Pick<Model<Api>, "api" | "baseUrl" | "compat"
|
|
3
|
+
type OpenAiWebSearchModel = Pick<Model<Api>, "api" | "baseUrl" | "compat"> & {
|
|
4
|
+
provider?: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
};
|
|
4
7
|
type OpenAiWebSearchTarget = Api | OpenAiWebSearchModel | undefined;
|
|
5
8
|
export declare function supportsNativeOpenAiWebSearch(target: OpenAiWebSearchTarget): boolean;
|
|
6
9
|
export declare function addOpenAiWebSearchToPayload(target: OpenAiWebSearchTarget, payload: unknown): unknown;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { hasServerSearchFailed, isServerSearchDisabled, isServerSearchFallback, markServerSearchFallback, } from "../server-search-fallback.js";
|
|
2
|
+
const OPENAI_RESPONSES_APIS = new Set([
|
|
3
|
+
"openai-responses",
|
|
4
|
+
"azure-openai-responses",
|
|
5
|
+
"openai-codex-responses",
|
|
6
|
+
]);
|
|
2
7
|
const ENABLE_ENV = "PI_OPENAI_WEB_SEARCH";
|
|
8
|
+
// OpenAI still supports this legacy server-tool name; Codex and xAI use web_search.
|
|
9
|
+
// https://developers.openai.com/api/docs/guides/tools-web-search
|
|
3
10
|
const NATIVE_OPENAI_WEB_SEARCH_TYPE = "web_search_preview";
|
|
4
11
|
const WEB_SEARCH_SOURCES_INCLUDE = "web_search_call.action.sources";
|
|
5
12
|
const STATUS_KEY = "openai-web-search";
|
|
@@ -34,27 +41,42 @@ function resolveTarget(target) {
|
|
|
34
41
|
}
|
|
35
42
|
return target;
|
|
36
43
|
}
|
|
37
|
-
function isOpenAiResponsesNativeEndpoint(model) {
|
|
38
|
-
try {
|
|
39
|
-
return new URL(model.baseUrl || "https://api.openai.com/v1").hostname === "api.openai.com";
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
44
|
export function supportsNativeOpenAiWebSearch(target) {
|
|
46
45
|
const model = resolveTarget(target);
|
|
47
46
|
if (!isOpenAiResponsesApi(model?.api)) {
|
|
48
47
|
return false;
|
|
49
48
|
}
|
|
50
|
-
|
|
49
|
+
const compat = model.compat;
|
|
50
|
+
// Codex/ChatGPT plans support web_search in their main Responses request.
|
|
51
|
+
// https://developers.openai.com/codex/config-basic/#web-search-mode
|
|
52
|
+
// Real subscription search and output-item replay were verified for senpi#3122.
|
|
53
|
+
if (model.api === "openai-codex-responses") {
|
|
54
|
+
if (model.id?.includes("-spark"))
|
|
55
|
+
return false;
|
|
56
|
+
try {
|
|
57
|
+
return compat?.supportsWebSearchPreview ?? new URL(model.baseUrl).hostname === "chatgpt.com";
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// xAI documents web_search on Responses, not its Chat Completions endpoint.
|
|
64
|
+
// Compatible/custom Responses endpoints still need an explicit capability opt-in.
|
|
65
|
+
// https://docs.x.ai/developers/tools/web-search
|
|
66
|
+
// Preserve main's shipped Azure and first-party OpenAI defaults.
|
|
67
|
+
if (model.api === "azure-openai-responses")
|
|
51
68
|
return true;
|
|
69
|
+
if (compat?.supportsWebSearchPreview !== undefined)
|
|
70
|
+
return compat.supportsWebSearchPreview;
|
|
71
|
+
try {
|
|
72
|
+
return new URL(model.baseUrl || "https://api.openai.com/v1").hostname === "api.openai.com";
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
52
76
|
}
|
|
53
|
-
const compat = model.compat;
|
|
54
|
-
return compat?.supportsWebSearchPreview ?? isOpenAiResponsesNativeEndpoint(model);
|
|
55
77
|
}
|
|
56
78
|
function isNativeOpenAiWebSearchType(value) {
|
|
57
|
-
return value === "web_search_preview" || value === "web_search_preview_2025_03_11";
|
|
79
|
+
return value === "web_search" || value === "web_search_preview" || value === "web_search_preview_2025_03_11";
|
|
58
80
|
}
|
|
59
81
|
function isUnsupportedWebSearchType(value) {
|
|
60
82
|
return (typeof value === "string" &&
|
|
@@ -75,7 +97,10 @@ function stripNativeOpenAiWebSearch(payload) {
|
|
|
75
97
|
const sanitizedTools = tools.filter((tool) => !(isRecord(tool) && isNativeOpenAiWebSearchType(tool.type)));
|
|
76
98
|
if (sanitizedTools.length !== tools.length) {
|
|
77
99
|
changed = true;
|
|
78
|
-
|
|
100
|
+
if (sanitizedTools.length)
|
|
101
|
+
sanitized.tools = sanitizedTools;
|
|
102
|
+
else
|
|
103
|
+
delete sanitized.tools;
|
|
79
104
|
}
|
|
80
105
|
}
|
|
81
106
|
const include = payload.include;
|
|
@@ -119,6 +144,8 @@ function includeWebSearchSources(payload) {
|
|
|
119
144
|
return include.includes(WEB_SEARCH_SOURCES_INCLUDE) ? include : [...include, WEB_SEARCH_SOURCES_INCLUDE];
|
|
120
145
|
}
|
|
121
146
|
export function addOpenAiWebSearchToPayload(target, payload) {
|
|
147
|
+
if (isServerSearchDisabled(payload) || isServerSearchFallback(payload))
|
|
148
|
+
return stripNativeOpenAiWebSearch(payload);
|
|
122
149
|
const model = resolveTarget(target);
|
|
123
150
|
if (!isOpenAiResponsesApi(model?.api)) {
|
|
124
151
|
// Defense in depth. `web_search_preview` is an OpenAI Responses-only tool
|
|
@@ -149,14 +176,20 @@ export function addOpenAiWebSearchToPayload(target, payload) {
|
|
|
149
176
|
if (!nativeStripped && !passiveSanitized.changed) {
|
|
150
177
|
return strippedPayload;
|
|
151
178
|
}
|
|
152
|
-
|
|
179
|
+
const result = {
|
|
153
180
|
...strippedPayload,
|
|
154
181
|
tools: passiveSanitized.tools,
|
|
155
182
|
};
|
|
183
|
+
if (!passiveSanitized.tools.length)
|
|
184
|
+
delete result.tools;
|
|
185
|
+
return result;
|
|
156
186
|
}
|
|
157
187
|
const hasNativeWebSearch = sanitizedTools.some((tool) => isNativeOpenAiWebSearchType(tool.type));
|
|
158
188
|
if (!hasNativeWebSearch) {
|
|
159
|
-
sanitizedTools.push({
|
|
189
|
+
sanitizedTools.push({
|
|
190
|
+
type: model.compat?.webSearchToolType ??
|
|
191
|
+
(model.api === "openai-codex-responses" ? "web_search" : NATIVE_OPENAI_WEB_SEARCH_TYPE),
|
|
192
|
+
});
|
|
160
193
|
}
|
|
161
194
|
return {
|
|
162
195
|
...payload,
|
|
@@ -185,7 +218,11 @@ Prefer web search over guessing when freshness matters.
|
|
|
185
218
|
`;
|
|
186
219
|
export default function openaiWebSearchExtension(pi) {
|
|
187
220
|
pi.on("before_provider_request", (event, ctx) => {
|
|
188
|
-
|
|
221
|
+
const model = event.model ?? ctx.model;
|
|
222
|
+
const payload = hasServerSearchFailed(ctx.sessionManager, model)
|
|
223
|
+
? markServerSearchFallback(event.payload)
|
|
224
|
+
: event.payload;
|
|
225
|
+
return addOpenAiWebSearchToPayload(model, payload);
|
|
189
226
|
});
|
|
190
227
|
pi.on("session_start", async (_event, ctx) => {
|
|
191
228
|
syncUi(ctx);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Api, Model, SimpleStreamOptions } from "@earendil-works/pi-ai";
|
|
2
|
+
export declare const SERVER_SEARCH_FALLBACK: unique symbol;
|
|
3
|
+
export declare const SERVER_SEARCH_DISABLED: unique symbol;
|
|
4
|
+
export declare function resetServerSearchFailures(session: object): void;
|
|
5
|
+
export declare function rememberServerSearchFailure(session: object, model: Model<Api>): void;
|
|
6
|
+
export declare function hasServerSearchFailed(session: object, model: Model<Api> | undefined): boolean;
|
|
7
|
+
export declare function markServerSearchFallback(payload: unknown): unknown;
|
|
8
|
+
export declare function disableServerSearch(payload: unknown): unknown;
|
|
9
|
+
export declare function withoutServerSearch(onPayload: SimpleStreamOptions["onPayload"], cacheRetention?: SimpleStreamOptions["cacheRetention"]): SimpleStreamOptions["onPayload"];
|
|
10
|
+
export declare function isServerSearchDisabled(payload: unknown): boolean;
|
|
11
|
+
export declare function isServerSearchFallback(payload: unknown): boolean;
|
|
12
|
+
//# sourceMappingURL=server-search-fallback.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Request-local metadata: Symbols survive hook-to-hook object spreads, but are
|
|
2
|
+
// omitted by JSON.stringify and never reach the provider or stored transcript.
|
|
3
|
+
export const SERVER_SEARCH_FALLBACK = Symbol.for("senpi.serverSearchFallback");
|
|
4
|
+
export const SERVER_SEARCH_DISABLED = Symbol.for("senpi.serverSearchDisabled");
|
|
5
|
+
const failedModels = new WeakMap();
|
|
6
|
+
function modelKey(model) {
|
|
7
|
+
return JSON.stringify([model.api, model.provider, model.id, model.baseUrl]);
|
|
8
|
+
}
|
|
9
|
+
export function resetServerSearchFailures(session) {
|
|
10
|
+
failedModels.delete(session);
|
|
11
|
+
}
|
|
12
|
+
export function rememberServerSearchFailure(session, model) {
|
|
13
|
+
let failures = failedModels.get(session);
|
|
14
|
+
if (!failures) {
|
|
15
|
+
failures = new Set();
|
|
16
|
+
failedModels.set(session, failures);
|
|
17
|
+
}
|
|
18
|
+
failures.add(modelKey(model));
|
|
19
|
+
}
|
|
20
|
+
export function hasServerSearchFailed(session, model) {
|
|
21
|
+
return model !== undefined && failedModels.get(session)?.has(modelKey(model)) === true;
|
|
22
|
+
}
|
|
23
|
+
export function markServerSearchFallback(payload) {
|
|
24
|
+
return payload !== null && typeof payload === "object" ? { ...payload, [SERVER_SEARCH_FALLBACK]: true } : payload;
|
|
25
|
+
}
|
|
26
|
+
export function disableServerSearch(payload) {
|
|
27
|
+
return payload !== null && typeof payload === "object" ? { ...payload, [SERVER_SEARCH_DISABLED]: true } : payload;
|
|
28
|
+
}
|
|
29
|
+
export function withoutServerSearch(onPayload, cacheRetention = "none") {
|
|
30
|
+
// Cache-friendly summaries must keep the main-turn hook and tool prefix verbatim.
|
|
31
|
+
if (cacheRetention !== "none")
|
|
32
|
+
return onPayload;
|
|
33
|
+
return async (payload, model, request) => {
|
|
34
|
+
const disabled = disableServerSearch(payload);
|
|
35
|
+
return (await onPayload?.(disabled, model, request)) ?? disabled;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function isServerSearchDisabled(payload) {
|
|
39
|
+
return (payload !== null &&
|
|
40
|
+
typeof payload === "object" &&
|
|
41
|
+
SERVER_SEARCH_DISABLED in payload &&
|
|
42
|
+
payload[SERVER_SEARCH_DISABLED] === true);
|
|
43
|
+
}
|
|
44
|
+
export function isServerSearchFallback(payload) {
|
|
45
|
+
return (payload !== null &&
|
|
46
|
+
typeof payload === "object" &&
|
|
47
|
+
SERVER_SEARCH_FALLBACK in payload &&
|
|
48
|
+
payload[SERVER_SEARCH_FALLBACK] === true);
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=server-search-fallback.js.map
|
|
@@ -11,13 +11,19 @@ export interface DeltaRead {
|
|
|
11
11
|
* A live terminal session: a pi-pty {@link TerminalSession} plus an xterm screen model
|
|
12
12
|
* and a bounded decoded-output buffer with a per-consumer read cursor. Reads are pure
|
|
13
13
|
* non-blocking peeks; watchers subscribe through `onOutput` (the monitor path).
|
|
14
|
+
*
|
|
15
|
+
* Once the process exits, the screen model is replaced by its final visible screen: an exited session
|
|
16
|
+
* stays listed until the registry needs its slot, and a live xterm with its scrollback and replay
|
|
17
|
+
* history costs tens of MB that nothing reads after exit (senpi#3171).
|
|
14
18
|
*/
|
|
15
19
|
export declare class TerminalRuntimeSession {
|
|
16
20
|
readonly session: TerminalSession;
|
|
17
21
|
readonly command: string;
|
|
18
22
|
readonly startedAtMs: number;
|
|
19
23
|
private readonly argv;
|
|
20
|
-
private
|
|
24
|
+
private screen;
|
|
25
|
+
private finalScreen;
|
|
26
|
+
private readonly screenRelease;
|
|
21
27
|
private readonly decoder;
|
|
22
28
|
private buffer;
|
|
23
29
|
private droppedChars;
|
|
@@ -25,6 +31,9 @@ export declare class TerminalRuntimeSession {
|
|
|
25
31
|
private readonly outputListeners;
|
|
26
32
|
private unsubscribeData;
|
|
27
33
|
constructor(command: string, options: TerminalRuntimeOptions);
|
|
34
|
+
/** Resolves once an exited session has kept its final visible screen and dropped the screen model. */
|
|
35
|
+
screenReleased(): Promise<void>;
|
|
36
|
+
private releaseScreen;
|
|
28
37
|
get backend(): string | null;
|
|
29
38
|
/** The spawned child as a later process can recognise it; undefined when the backend exposes no pid. */
|
|
30
39
|
identity(): ChildProcessIdentity | undefined;
|
|
@@ -2,13 +2,25 @@ import { processBootAtMs } from "./process-identity.js";
|
|
|
2
2
|
import { loadPty } from "./pty.lazy.js";
|
|
3
3
|
import { DEFAULT_SCROLLBACK, MAX_SESSION_OUTPUT_CHARS } from "./shared.js";
|
|
4
4
|
const pty = await loadPty();
|
|
5
|
+
const RELEASED_SCREEN = {
|
|
6
|
+
cols: 0,
|
|
7
|
+
rows: 0,
|
|
8
|
+
visibleGrid: [],
|
|
9
|
+
scrollback: [],
|
|
10
|
+
cursor: { x: 0, y: 0 },
|
|
11
|
+
};
|
|
5
12
|
/**
|
|
6
13
|
* A live terminal session: a pi-pty {@link TerminalSession} plus an xterm screen model
|
|
7
14
|
* and a bounded decoded-output buffer with a per-consumer read cursor. Reads are pure
|
|
8
15
|
* non-blocking peeks; watchers subscribe through `onOutput` (the monitor path).
|
|
16
|
+
*
|
|
17
|
+
* Once the process exits, the screen model is replaced by its final visible screen: an exited session
|
|
18
|
+
* stays listed until the registry needs its slot, and a live xterm with its scrollback and replay
|
|
19
|
+
* history costs tens of MB that nothing reads after exit (senpi#3171).
|
|
9
20
|
*/
|
|
10
21
|
export class TerminalRuntimeSession {
|
|
11
22
|
constructor(command, options) {
|
|
23
|
+
this.finalScreen = null;
|
|
12
24
|
this.decoder = new TextDecoder("utf-8", { fatal: false });
|
|
13
25
|
this.buffer = "";
|
|
14
26
|
this.droppedChars = 0;
|
|
@@ -46,9 +58,36 @@ export class TerminalRuntimeSession {
|
|
|
46
58
|
}
|
|
47
59
|
}
|
|
48
60
|
});
|
|
61
|
+
this.screenRelease = new Promise((resolve) => {
|
|
62
|
+
this.session.onExit(() => {
|
|
63
|
+
// Best-effort like every screen write (#837): a failed release leaves an empty final screen, never an unhandled rejection.
|
|
64
|
+
void this.releaseScreen()
|
|
65
|
+
.catch(() => { })
|
|
66
|
+
.finally(resolve);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
49
69
|
this.session.start();
|
|
50
70
|
this.startedAtMs = Date.now();
|
|
51
71
|
}
|
|
72
|
+
/** Resolves once an exited session has kept its final visible screen and dropped the screen model. */
|
|
73
|
+
screenReleased() {
|
|
74
|
+
return this.screenRelease;
|
|
75
|
+
}
|
|
76
|
+
async releaseScreen() {
|
|
77
|
+
const screen = this.screen;
|
|
78
|
+
if (!screen)
|
|
79
|
+
return;
|
|
80
|
+
await screen.flush().catch(() => { });
|
|
81
|
+
if (this.screen !== screen)
|
|
82
|
+
return;
|
|
83
|
+
this.screen = null;
|
|
84
|
+
try {
|
|
85
|
+
this.finalScreen = { ...screen.snapshot(), scrollback: [] };
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
screen.dispose();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
52
91
|
get backend() {
|
|
53
92
|
return this.session.backend;
|
|
54
93
|
}
|
|
@@ -80,7 +119,8 @@ export class TerminalRuntimeSession {
|
|
|
80
119
|
const text = this.decoder.decode(chunk, { stream: true });
|
|
81
120
|
if (text.length === 0)
|
|
82
121
|
return text;
|
|
83
|
-
|
|
122
|
+
if (this.screen)
|
|
123
|
+
this.ownScreenWrite(this.screen.feed(text));
|
|
84
124
|
this.buffer += text;
|
|
85
125
|
if (this.buffer.length > MAX_SESSION_OUTPUT_CHARS) {
|
|
86
126
|
const overflow = this.buffer.length - MAX_SESSION_OUTPUT_CHARS;
|
|
@@ -107,10 +147,13 @@ export class TerminalRuntimeSession {
|
|
|
107
147
|
return this.buffer;
|
|
108
148
|
}
|
|
109
149
|
snapshot() {
|
|
110
|
-
|
|
150
|
+
if (this.screen)
|
|
151
|
+
return this.screen.snapshot();
|
|
152
|
+
return this.finalScreen ?? RELEASED_SCREEN;
|
|
111
153
|
}
|
|
112
154
|
resizeScreen(cols, rows) {
|
|
113
|
-
|
|
155
|
+
if (this.screen)
|
|
156
|
+
this.ownScreenWrite(this.screen.resize(cols, rows));
|
|
114
157
|
}
|
|
115
158
|
ownScreenWrite(written) {
|
|
116
159
|
if (written === this.lastScreenWrite)
|
|
@@ -122,7 +165,8 @@ export class TerminalRuntimeSession {
|
|
|
122
165
|
this.unsubscribeData?.();
|
|
123
166
|
this.unsubscribeData = null;
|
|
124
167
|
this.outputListeners.clear();
|
|
125
|
-
this.screen
|
|
168
|
+
this.screen?.dispose();
|
|
169
|
+
this.screen = null;
|
|
126
170
|
}
|
|
127
171
|
}
|
|
128
172
|
//# sourceMappingURL=runtime-session.js.map
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { defineTool } from "../../types.js";
|
|
3
|
+
import { ANTHROPIC_WEB_SEARCH_SECTION } from "../anthropic-web-search/index.js";
|
|
4
|
+
import { OPENAI_WEB_SEARCH_SECTION } from "../openai-web-search/index.js";
|
|
5
|
+
import { hasServerSearchFailed, rememberServerSearchFailure, resetServerSearchFailures, } from "../server-search-fallback.js";
|
|
6
|
+
import { additionalServerSearch, hasServerSearch, serverSearchFailure, serverSearchPayload, } from "./server-search.js";
|
|
3
7
|
import { loadWebsearchConfig } from "./websearch/config.js";
|
|
4
8
|
import { attemptRouteLabel } from "./websearch/route-attempts.js";
|
|
5
9
|
import { providerEntryLabel } from "./websearch/search.js";
|
|
@@ -8,18 +12,6 @@ import { createWebSearchTool } from "./websearch/tool.js";
|
|
|
8
12
|
const STATUS_KEY = "pi-websearch";
|
|
9
13
|
const WIDGET_KEY = "pi-websearch";
|
|
10
14
|
const NATIVE_BYPASS_MESSAGE = "Native provider web search is handled by the built-in provider extension.";
|
|
11
|
-
// Defer to the provider-native server-side web_search only when the builtin
|
|
12
|
-
// injecting extension will actually add it for this exact model (first-party
|
|
13
|
-
// endpoint or explicit compat opt-in, and not disabled via env). A provider id
|
|
14
|
-
// of "anthropic"/"openai" alone is not enough: proxied baseUrls (ccapi, quotio,
|
|
15
|
-
// …) never receive the native tool, so bypassing there leaves no web search.
|
|
16
|
-
function isProviderNativeBypass(model) {
|
|
17
|
-
if (supportsNativeAnthropicWebSearch(model))
|
|
18
|
-
return isAnthropicWebSearchEnabled();
|
|
19
|
-
if (supportsNativeOpenAiWebSearch(model))
|
|
20
|
-
return isOpenaiWebSearchEnabled();
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
15
|
export default function (pi) {
|
|
24
16
|
let state = {
|
|
25
17
|
ok: false,
|
|
@@ -50,6 +42,11 @@ export default function (pi) {
|
|
|
50
42
|
ctx.ui.notify(state.message, "error");
|
|
51
43
|
}
|
|
52
44
|
let lastServedBy;
|
|
45
|
+
let nativeFailed = false;
|
|
46
|
+
let fallbackPending = false;
|
|
47
|
+
let fallbackSent = false;
|
|
48
|
+
let userAborted = false;
|
|
49
|
+
let requestModel;
|
|
53
50
|
// senpi#2340: status names the model native search runs on, warns about an ignored nativeModel,
|
|
54
51
|
// and reports which route and model served the last search.
|
|
55
52
|
function nativeModelStatus(config, ctx) {
|
|
@@ -72,25 +69,129 @@ export default function (pi) {
|
|
|
72
69
|
}
|
|
73
70
|
return { text };
|
|
74
71
|
}
|
|
75
|
-
|
|
72
|
+
const searchTool = createWebSearchTool(() => state, {
|
|
76
73
|
onSearchComplete: (details) => {
|
|
77
74
|
if (!details.error)
|
|
78
75
|
lastServedBy = attemptRouteLabel(details);
|
|
79
76
|
},
|
|
77
|
+
});
|
|
78
|
+
pi.registerTool(searchTool);
|
|
79
|
+
pi.registerTool(defineTool({
|
|
80
|
+
name: "$web_search",
|
|
81
|
+
exposure: "search",
|
|
82
|
+
allowLazyActivation: false,
|
|
83
|
+
label: "Moonshot Server Search",
|
|
84
|
+
description: "Return the Moonshot built-in server search result to the same model.",
|
|
85
|
+
parameters: Type.Record(Type.String(), Type.Unknown()),
|
|
86
|
+
async execute(_id, params) {
|
|
87
|
+
// Official Moonshot flow: echo generated arguments, never make a
|
|
88
|
+
// separate request or substitute another model.
|
|
89
|
+
return { content: [{ type: "text", text: JSON.stringify(params) }], details: params };
|
|
90
|
+
},
|
|
80
91
|
}));
|
|
81
92
|
async function refreshState(model, ctx) {
|
|
82
|
-
state =
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
state =
|
|
94
|
+
!nativeFailed && hasServerSearch(model)
|
|
95
|
+
? { ok: false, reason: "provider_native_bypass", message: NATIVE_BYPASS_MESSAGE }
|
|
96
|
+
: await loadWebsearchConfig({ cwd: ctx.cwd });
|
|
97
|
+
const active = pi.getActiveTools();
|
|
98
|
+
const withoutMoonshot = active.filter((name) => name !== "$web_search");
|
|
99
|
+
const moonshotEnabled = !nativeFailed && additionalServerSearch(model) === "moonshot";
|
|
100
|
+
if (moonshotEnabled !== active.includes("$web_search")) {
|
|
101
|
+
pi.setActiveTools(moonshotEnabled ? [...withoutMoonshot, "$web_search"] : withoutMoonshot);
|
|
102
|
+
}
|
|
85
103
|
updateUi(ctx);
|
|
86
104
|
}
|
|
87
105
|
pi.on("session_start", async (_event, ctx) => {
|
|
88
106
|
lastServedBy = undefined;
|
|
107
|
+
resetServerSearchFailures(ctx.sessionManager);
|
|
108
|
+
nativeFailed = false;
|
|
109
|
+
fallbackPending = false;
|
|
110
|
+
fallbackSent = false;
|
|
111
|
+
userAborted = false;
|
|
89
112
|
await refreshState(ctx.model, ctx);
|
|
90
113
|
});
|
|
91
114
|
pi.on("model_select", async (event, ctx) => {
|
|
115
|
+
nativeFailed = hasServerSearchFailed(ctx.sessionManager, event.model);
|
|
116
|
+
fallbackPending = false;
|
|
117
|
+
fallbackSent = false;
|
|
92
118
|
await refreshState(event.model, ctx);
|
|
93
119
|
});
|
|
120
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
121
|
+
if (!event.preview && event.trigger !== "extension") {
|
|
122
|
+
nativeFailed = hasServerSearchFailed(ctx.sessionManager, ctx.model);
|
|
123
|
+
fallbackPending = false;
|
|
124
|
+
fallbackSent = false;
|
|
125
|
+
userAborted = false;
|
|
126
|
+
await refreshState(ctx.model, ctx);
|
|
127
|
+
}
|
|
128
|
+
if (!hasServerSearch(ctx.model))
|
|
129
|
+
return undefined;
|
|
130
|
+
const systemPrompt = event.systemPrompt
|
|
131
|
+
.replace(ANTHROPIC_WEB_SEARCH_SECTION, "")
|
|
132
|
+
.replace(OPENAI_WEB_SEARCH_SECTION, "");
|
|
133
|
+
return {
|
|
134
|
+
systemPrompt: `${systemPrompt}\n## Web Search\n${hasServerSearchFailed(ctx.sessionManager, ctx.model)
|
|
135
|
+
? "Use the standalone web_search tool when current information is needed. Native search failed for this session model."
|
|
136
|
+
: "Use the provider's in-turn server web search when current information is needed."}`,
|
|
137
|
+
};
|
|
138
|
+
}, { previewSafe: true });
|
|
139
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
140
|
+
requestModel = event.model ?? ctx.model;
|
|
141
|
+
return serverSearchPayload(requestModel, event.payload, hasServerSearchFailed(ctx.sessionManager, requestModel));
|
|
142
|
+
});
|
|
143
|
+
pi.on("message_end", async (event, ctx) => {
|
|
144
|
+
if (event.message.role !== "assistant")
|
|
145
|
+
return undefined;
|
|
146
|
+
const message = event.message;
|
|
147
|
+
const model = requestModel?.api === message.api &&
|
|
148
|
+
requestModel.provider === message.provider &&
|
|
149
|
+
requestModel.id === message.model
|
|
150
|
+
? requestModel
|
|
151
|
+
: ctx.model?.api === message.api &&
|
|
152
|
+
ctx.model.provider === message.provider &&
|
|
153
|
+
ctx.model.id === message.model
|
|
154
|
+
? ctx.model
|
|
155
|
+
: ctx.modelRegistry.find(message.provider, message.model);
|
|
156
|
+
if (!model || hasServerSearchFailed(ctx.sessionManager, model) || !hasServerSearch(model))
|
|
157
|
+
return undefined;
|
|
158
|
+
const reason = serverSearchFailure(message, model);
|
|
159
|
+
if (!reason)
|
|
160
|
+
return undefined;
|
|
161
|
+
nativeFailed = true;
|
|
162
|
+
rememberServerSearchFailure(ctx.sessionManager, model);
|
|
163
|
+
// A provider can recover from a failed search and still finish its answer.
|
|
164
|
+
// Enable fallback for another request, but never duplicate a completed answer.
|
|
165
|
+
fallbackPending =
|
|
166
|
+
event.message.stopReason === "error" ||
|
|
167
|
+
!event.message.content.some((block) => block.type === "text" && block.text.trim().length > 0);
|
|
168
|
+
await refreshState(model, ctx);
|
|
169
|
+
pi.appendEntry("websearch-native-fallback", { reason });
|
|
170
|
+
if (ctx.hasUI)
|
|
171
|
+
ctx.ui.notify(`Web search fallback: ${reason}.`, "warning");
|
|
172
|
+
return undefined;
|
|
173
|
+
});
|
|
174
|
+
pi.on("agent_end", (event) => {
|
|
175
|
+
userAborted = event.abortSource === "user";
|
|
176
|
+
// The core retry already continues the original request with the restored function tool.
|
|
177
|
+
if (event.willRetry || userAborted)
|
|
178
|
+
fallbackPending = false;
|
|
179
|
+
});
|
|
180
|
+
pi.on("session_abort", () => {
|
|
181
|
+
userAborted = true;
|
|
182
|
+
fallbackPending = false;
|
|
183
|
+
});
|
|
184
|
+
pi.on("agent_settled", () => {
|
|
185
|
+
if (!fallbackPending || fallbackSent || userAborted)
|
|
186
|
+
return;
|
|
187
|
+
fallbackPending = false;
|
|
188
|
+
fallbackSent = true;
|
|
189
|
+
pi.sendMessage({
|
|
190
|
+
customType: "websearch-native-fallback",
|
|
191
|
+
content: "Continue the original request using the standalone web_search tool. Do not retry native server search on this turn.",
|
|
192
|
+
display: false,
|
|
193
|
+
}, { triggerTurn: true });
|
|
194
|
+
});
|
|
94
195
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
95
196
|
clearUi(ctx);
|
|
96
197
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
export type SearchModel = Pick<Model<Api>, "api" | "provider" | "id" | "baseUrl" | "compat">;
|
|
3
|
+
type AdditionalServerSearch = "google" | "openrouter" | "perplexity" | "moonshot" | "zai";
|
|
4
|
+
export declare function additionalServerSearch(model: SearchModel | undefined): AdditionalServerSearch | undefined;
|
|
5
|
+
export declare function hasServerSearch(model: SearchModel | undefined): boolean;
|
|
6
|
+
/** Preserve adapter-built function search on fallback; never reconstruct its normalized schema. */
|
|
7
|
+
export declare function serverSearchPayload(model: SearchModel | undefined, payload: unknown, failed?: boolean): unknown;
|
|
8
|
+
/** Only search-specific errors trigger fallback; cancellation and unrelated provider failures do not. */
|
|
9
|
+
export declare function serverSearchFailure(message: Pick<AssistantMessage, "stopReason" | "content" | "errorMessage">, model?: SearchModel): string | undefined;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=server-search.d.ts.map
|