@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.
Files changed (188) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/bundle/chunks/anthropic-messages-GYG6YZY5.js +10 -0
  3. package/dist/bundle/chunks/{app-server-command-NHDOL4HQ.js → app-server-command-OL6PPUOA.js} +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-EJF2XJ7Q.js → azure-openai-responses-GU6FOSJR.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-ZABLYCHT.js → chunk-46JEQXD7.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-VE3D74HN.js → chunk-55VKYA5V.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-7UNN3YHW.js → chunk-5AVFHHDF.js} +230 -221
  8. package/dist/bundle/chunks/{chunk-2JE2DOIF.js → chunk-5DPTM5E2.js} +1 -1
  9. package/dist/bundle/chunks/chunk-5UPUSGP5.js +2 -0
  10. package/dist/bundle/chunks/chunk-5VMRMY6N.js +16 -0
  11. package/dist/bundle/chunks/{chunk-7X3VRAM3.js → chunk-7P3QPVTF.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-T6IYKWIH.js → chunk-BUGZZVQW.js} +1 -1
  13. package/dist/bundle/chunks/chunk-DL7EFVT2.js +12 -0
  14. package/dist/bundle/chunks/chunk-E5YUVGLQ.js +11 -0
  15. package/dist/bundle/chunks/{chunk-RLFQICEY.js → chunk-EQJY2B5F.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-VYKBN4MR.js → chunk-GC2RU5TS.js} +1 -1
  17. package/dist/bundle/chunks/chunk-GPKEJOTQ.js +202 -0
  18. package/dist/bundle/chunks/{chunk-KCVSSFOV.js → chunk-GQ5PWY6P.js} +4 -3
  19. package/dist/bundle/chunks/{chunk-4GPJOGXZ.js → chunk-I4AEC6MH.js} +1 -1
  20. package/dist/bundle/chunks/chunk-JXSFGRBE.js +8 -0
  21. package/dist/bundle/chunks/{chunk-YGCMN2CX.js → chunk-NAY5LB53.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-EBOD66AS.js → chunk-PBN2CATL.js} +2 -2
  23. package/dist/bundle/chunks/chunk-PFNBY43W.js +2 -0
  24. package/dist/bundle/chunks/{chunk-CY2B4QRR.js → chunk-PUWEQK2W.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-5DBPUQSX.js → chunk-SF6AUNQZ.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-G2XAU4BX.js → chunk-TMTT5CLA.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-2JKDZXIY.js → chunk-VWFKLW73.js} +2 -2
  28. package/dist/bundle/chunks/chunk-WZMSBRTW.js +2 -0
  29. package/dist/bundle/chunks/{chunk-55HLKZSB.js → chunk-XVJRRLGN.js} +1 -1
  30. package/dist/bundle/chunks/chunk-Z2PWWBBD.js +31 -0
  31. package/dist/bundle/chunks/cli-main-RSIYAR4J.js +3 -0
  32. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-SWVU2AXJ.js → cloudflare-workers-ai-system-one-LVWRKOUD.js} +1 -1
  33. package/dist/bundle/chunks/{google-generative-ai-XG4YX6FQ.js → google-generative-ai-JMGYPTXL.js} +1 -1
  34. package/dist/bundle/chunks/{google-vertex-BBOZ6OUB.js → google-vertex-CI4FPIU7.js} +1 -1
  35. package/dist/bundle/chunks/{help-fast-path-EZ3KEW6V.js → help-fast-path-M7KMBM6K.js} +1 -1
  36. package/dist/bundle/chunks/host-command-6UR3DSFR.js +2 -0
  37. package/dist/bundle/chunks/host-lifecycle-2BKCCK5X.js +3 -0
  38. package/dist/bundle/chunks/host-lifecycle.js +10 -7
  39. package/dist/bundle/chunks/host-session-command-A2MZHEBP.js +19 -0
  40. package/dist/bundle/chunks/interactive-mode-EJBMTJOH.js +2 -0
  41. package/dist/bundle/chunks/{list-tips-WE2YGQM6.js → list-tips-BEQAOA4N.js} +1 -1
  42. package/dist/bundle/chunks/{mistral-conversations-RB3ZBCGE.js → mistral-conversations-5OD7662T.js} +1 -1
  43. package/dist/bundle/chunks/{multi-session-host-V4CUQLV7.js → multi-session-host-YYMLHCUA.js} +4 -4
  44. package/dist/bundle/chunks/{openai-codex-responses-DBTPUXRW.js → openai-codex-responses-FN3UPAFI.js} +1 -1
  45. package/dist/bundle/chunks/{openai-completions-QAJQC4BY.js → openai-completions-KMUQ3T4M.js} +1 -1
  46. package/dist/bundle/chunks/{openai-responses-UBJWCEHG.js → openai-responses-V4Q5GVLO.js} +1 -1
  47. package/dist/bundle/chunks/{package-manager-cli-WTRDT4ZH.js → package-manager-cli-6A5ZP26E.js} +1 -1
  48. package/dist/bundle/chunks/rotation-stream-PT6LLBMJ.js +2 -0
  49. package/dist/bundle/chunks/rpc-mode-XNYJPYUM.js +2 -0
  50. package/dist/bundle/chunks/runtime-session-ZBHFMT65.js +2 -0
  51. package/dist/bundle/chunks/{schedule-command-PJVRKAKD.js → schedule-command-FYQG2MFX.js} +1 -1
  52. package/dist/bundle/chunks/session-control-endpoint-7TX4XHN4.js +3 -0
  53. package/dist/bundle/chunks/session-picker-4RNB27SM.js +2 -0
  54. package/dist/bundle/chunks/session-worker.js +418 -412
  55. package/dist/bundle/chunks/{typesafe-system-one-B5TW3FHM.js → typesafe-system-one-WUHW7HR4.js} +1 -1
  56. package/dist/bundle/cli.js +1 -1
  57. package/dist/bundle/index.js +1 -1
  58. package/dist/bundle/rpc-entry.js +1 -1
  59. package/dist/bundle/runtime-manifest.json +1 -1
  60. package/dist/core/agent-session.d.ts +11 -2
  61. package/dist/core/agent-session.js +152 -43
  62. package/dist/core/compaction/compaction.js +2 -0
  63. package/dist/core/extensions/builtin/anthropic-web-search/index.js +22 -14
  64. package/dist/core/extensions/builtin/cache-keepalive/index.js +8 -4
  65. package/dist/core/extensions/builtin/cache-keepalive/session-prewarm.d.ts +2 -1
  66. package/dist/core/extensions/builtin/cache-keepalive/session-prewarm.js +55 -7
  67. package/dist/core/extensions/builtin/cache-keepalive/startup-activity-gate.d.ts +8 -0
  68. package/dist/core/extensions/builtin/cache-keepalive/startup-activity-gate.js +25 -0
  69. package/dist/core/extensions/builtin/compaction/index.js +13 -5
  70. package/dist/core/extensions/builtin/compaction/speculative.d.ts +1 -0
  71. package/dist/core/extensions/builtin/compaction/speculative.js +8 -4
  72. package/dist/core/extensions/builtin/moved-path-guard/tool-classes.js +1 -0
  73. package/dist/core/extensions/builtin/openai-web-search/index.d.ts +4 -1
  74. package/dist/core/extensions/builtin/openai-web-search/index.js +54 -17
  75. package/dist/core/extensions/builtin/server-search-fallback.d.ts +12 -0
  76. package/dist/core/extensions/builtin/server-search-fallback.js +50 -0
  77. package/dist/core/extensions/builtin/terminal/runtime-session.d.ts +10 -1
  78. package/dist/core/extensions/builtin/terminal/runtime-session.js +48 -4
  79. package/dist/core/extensions/builtin/websearch/index.d.ts +1 -1
  80. package/dist/core/extensions/builtin/websearch/index.js +119 -18
  81. package/dist/core/extensions/builtin/websearch/server-search.d.ts +11 -0
  82. package/dist/core/extensions/builtin/websearch/server-search.js +230 -0
  83. package/dist/core/extensions/builtin/websearch/websearch/native.d.ts +2 -0
  84. package/dist/core/extensions/builtin/websearch/websearch/search-model.js +15 -0
  85. package/dist/core/extensions/runner.d.ts +8 -1
  86. package/dist/core/extensions/runner.js +56 -7
  87. package/dist/core/extensions/types.d.ts +12 -2
  88. package/dist/core/extensions/user-activity.d.ts +10 -0
  89. package/dist/core/extensions/user-activity.js +20 -0
  90. package/dist/core/model-config-schema.d.ts +34 -0
  91. package/dist/core/model-config-schema.js +2 -0
  92. package/dist/core/model-config.js +1 -0
  93. package/dist/core/reserved-keywords.d.ts +28 -0
  94. package/dist/core/reserved-keywords.js +81 -0
  95. package/dist/core/retry-fallback/candidates.d.ts +1 -1
  96. package/dist/core/retry-fallback/candidates.js +1 -12
  97. package/dist/core/retry-fallback/controller.d.ts +3 -1
  98. package/dist/core/retry-fallback/controller.js +11 -5
  99. package/dist/core/retry-fallback/cooldown.d.ts +2 -1
  100. package/dist/core/retry-fallback/cooldown.js +21 -12
  101. package/dist/core/retry-fallback/unusable.d.ts +2 -0
  102. package/dist/core/retry-fallback/unusable.js +14 -2
  103. package/dist/core/retry-fallback/usage-limit.d.ts +10 -1
  104. package/dist/core/retry-fallback/usage-limit.js +39 -2
  105. package/dist/core/session-title-generator.js +2 -0
  106. package/dist/index.d.ts +2 -1
  107. package/dist/index.js +1 -0
  108. package/dist/modes/index.d.ts +1 -1
  109. package/dist/modes/interactive/components/assistant-render-descriptors.js +7 -1
  110. package/dist/modes/interactive/composer-user-activity.d.ts +6 -0
  111. package/dist/modes/interactive/composer-user-activity.js +30 -0
  112. package/dist/modes/interactive/grok/chrome.js +2 -1
  113. package/dist/modes/interactive/interactive-mode.js +15 -6
  114. package/dist/modes/interactive/session-control-registry.js +8 -1
  115. package/dist/modes/interactive/theme/reserved-keyword-rainbow.d.ts +11 -0
  116. package/dist/modes/interactive/theme/reserved-keyword-rainbow.js +50 -0
  117. package/dist/modes/interactive/theme/theme.d.ts +3 -1
  118. package/dist/modes/interactive/theme/theme.js +9 -0
  119. package/dist/modes/print-mode.js +7 -2
  120. package/dist/modes/rpc/connection-handler.js +43 -11
  121. package/dist/modes/rpc/custom-capability.d.ts +2 -0
  122. package/dist/modes/rpc/custom-capability.js +2 -0
  123. package/dist/modes/rpc/host-admission-proxy.d.ts +9 -0
  124. package/dist/modes/rpc/host-admission-proxy.js +211 -0
  125. package/dist/modes/rpc/host-admission.d.ts +21 -0
  126. package/dist/modes/rpc/host-admission.js +59 -0
  127. package/dist/modes/rpc/host-capabilities.js +2 -1
  128. package/dist/modes/rpc/host-client-occupancy.d.ts +2 -1
  129. package/dist/modes/rpc/host-client-occupancy.js +7 -0
  130. package/dist/modes/rpc/host-decision.d.ts +2 -2
  131. package/dist/modes/rpc/host-decision.js +4 -0
  132. package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
  133. package/dist/modes/rpc/host-ensure.js +31 -3
  134. package/dist/modes/rpc/host-lifecycle-drain.d.ts +2 -1
  135. package/dist/modes/rpc/host-lifecycle-drain.js +3 -1
  136. package/dist/modes/rpc/host-lifecycle-proxy.d.ts +3 -2
  137. package/dist/modes/rpc/host-lifecycle-proxy.js +10 -19
  138. package/dist/modes/rpc/host-lifecycle-shutdown.d.ts +2 -0
  139. package/dist/modes/rpc/host-lifecycle-shutdown.js +3 -1
  140. package/dist/modes/rpc/host-lifecycle-ticker.d.ts +11 -0
  141. package/dist/modes/rpc/host-lifecycle-ticker.js +27 -0
  142. package/dist/modes/rpc/host-lifecycle.js +15 -23
  143. package/dist/modes/rpc/host-outcome.js +1 -0
  144. package/dist/modes/rpc/host-protocol-info.d.ts +2 -0
  145. package/dist/modes/rpc/host-protocol-info.js +1 -0
  146. package/dist/modes/rpc/host-session-runner.js +22 -1
  147. package/dist/modes/rpc/host-status-all.js +1 -0
  148. package/dist/modes/rpc/host-status.d.ts +1 -0
  149. package/dist/modes/rpc/host-status.js +5 -1
  150. package/dist/modes/rpc/rpc-client.d.ts +7 -1
  151. package/dist/modes/rpc/rpc-client.js +12 -0
  152. package/dist/modes/rpc/rpc-types.d.ts +49 -1
  153. package/dist/modes/rpc/rpc-types.js +5 -0
  154. package/dist/modes/rpc/session-command-activity.js +2 -0
  155. package/dist/modes/rpc/session-held.js +3 -0
  156. package/docs/extensions.md +13 -1
  157. package/docs/providers.md +88 -0
  158. package/docs/rpc.md +150 -5
  159. package/docs/web-search.md +3 -1
  160. package/package.json +7 -7
  161. package/dist/bundle/chunks/anthropic-messages-5QQJVQPZ.js +0 -9
  162. package/dist/bundle/chunks/chunk-2A3MS2Z7.js +0 -8
  163. package/dist/bundle/chunks/chunk-6SBY4ZWC.js +0 -2
  164. package/dist/bundle/chunks/chunk-6WCOSBI7.js +0 -2
  165. package/dist/bundle/chunks/chunk-E75RO54O.js +0 -14
  166. package/dist/bundle/chunks/chunk-JMYO34RK.js +0 -2
  167. package/dist/bundle/chunks/chunk-KRNXUTZJ.js +0 -2
  168. package/dist/bundle/chunks/chunk-L4HVJWIF.js +0 -11
  169. package/dist/bundle/chunks/chunk-N5WLLHUE.js +0 -2
  170. package/dist/bundle/chunks/chunk-NEFNNQSV.js +0 -9
  171. package/dist/bundle/chunks/chunk-PWIYVLIA.js +0 -9
  172. package/dist/bundle/chunks/chunk-Q3R44CGT.js +0 -5
  173. package/dist/bundle/chunks/chunk-QIARZN7G.js +0 -31
  174. package/dist/bundle/chunks/chunk-QKU6JOA4.js +0 -2
  175. package/dist/bundle/chunks/chunk-QYWXDE47.js +0 -2
  176. package/dist/bundle/chunks/chunk-XHELSLN3.js +0 -188
  177. package/dist/bundle/chunks/chunk-YDJ4HGWO.js +0 -16
  178. package/dist/bundle/chunks/chunk-ZJHRDQCH.js +0 -2
  179. package/dist/bundle/chunks/cli-main-TUBNBZNK.js +0 -3
  180. package/dist/bundle/chunks/host-command-WO7VKJBJ.js +0 -2
  181. package/dist/bundle/chunks/host-lifecycle-X2J5FTJL.js +0 -3
  182. package/dist/bundle/chunks/host-session-command-ICBOH5RP.js +0 -19
  183. package/dist/bundle/chunks/interactive-mode-UGDNCGQ5.js +0 -2
  184. package/dist/bundle/chunks/rotation-stream-OODURUGZ.js +0 -2
  185. package/dist/bundle/chunks/rpc-mode-22MCT32O.js +0 -2
  186. package/dist/bundle/chunks/runtime-session-I4NDNGC5.js +0 -2
  187. package/dist/bundle/chunks/session-control-endpoint-SFPRQBDO.js +0 -3
  188. 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
- remoteCompaction = await runOpenAiRemoteCompaction(ctx, event, (data) => {
578
- reportRemoteCompactionTimeout(ctx, event.reason, event.signal, data);
579
- pi.events.emit(SENPI_COMPACTION_EVENT, data);
580
- }, remoteCompactionDependencies);
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 auth = await context.modelRegistry?.getApiKeyAndHeaders(snapshot.model);
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 "${snapshot.model.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
- ? { ...snapshot, model: { ...snapshot.model, baseUrl: auth.baseUrl } }
156
- : snapshot;
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
- const OPENAI_RESPONSES_APIS = new Set(["openai-responses", "azure-openai-responses"]);
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
- if (model.api === "azure-openai-responses") {
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
- sanitized.tools = sanitizedTools;
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
- return {
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({ type: NATIVE_OPENAI_WEB_SEARCH_TYPE });
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
- return addOpenAiWebSearchToPayload(ctx.model, event.payload);
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 readonly screen;
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
- this.ownScreenWrite(this.screen.feed(text));
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
- return this.screen.snapshot();
150
+ if (this.screen)
151
+ return this.screen.snapshot();
152
+ return this.finalScreen ?? RELEASED_SCREEN;
111
153
  }
112
154
  resizeScreen(cols, rows) {
113
- this.ownScreenWrite(this.screen.resize(cols, rows));
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.dispose();
168
+ this.screen?.dispose();
169
+ this.screen = null;
126
170
  }
127
171
  }
128
172
  //# sourceMappingURL=runtime-session.js.map
@@ -1,3 +1,3 @@
1
- import type { ExtensionAPI } from "../../types.ts";
1
+ import { type ExtensionAPI } from "../../types.ts";
2
2
  export default function (pi: ExtensionAPI): void;
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,9 @@
1
- import { isAnthropicWebSearchEnabled, supportsNativeAnthropicWebSearch } from "../anthropic-web-search/index.js";
2
- import { isOpenaiWebSearchEnabled, supportsNativeOpenAiWebSearch } from "../openai-web-search/index.js";
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
- pi.registerTool(createWebSearchTool(() => state, {
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 = isProviderNativeBypass(model)
83
- ? { ok: false, reason: "provider_native_bypass", message: NATIVE_BYPASS_MESSAGE }
84
- : await loadWebsearchConfig({ cwd: ctx.cwd });
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