pi-openai-toolkit 0.14.6 → 0.14.8

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.
@@ -1,80 +1,523 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import { loadToolkitConfig } from "../config";
3
+ import { resolveResponsesEnvironment, type ResponsesEnvironmentResolution } from "../runtime";
4
+ import {
5
+ requestAlphaSearch,
6
+ normalizeStandaloneWebRunCommands,
7
+ sanitizeAlphaDiagnostic,
8
+ STANDALONE_WEB_RUN_PARAMETERS,
9
+ StandaloneWebRunError,
10
+ type AlphaSearchDetails,
11
+ type AlphaSearchClientResult,
12
+ type StandaloneWebRunParameters,
13
+ } from "./alpha-client";
3
14
  import { transformWebSearchPayload } from "./payload";
4
15
  import { appendWebSearchPrompt } from "./prompt";
5
- import { isWebSearchEnabledForModel, type WebSearchModel } from "./types";
16
+ import {
17
+ LOCAL_WEB_SEARCH_TOOL_NAME,
18
+ resolveWebSearchRoute,
19
+ WEB_RUN_TOOL_NAME,
20
+ WEB_SEARCH_CAPABLE_APIS,
21
+ type WebSearchModel,
22
+ type WebSearchRouteResolution,
23
+ } from "./types";
6
24
 
7
- function syncLocalWebSearchTool(
8
- pi: ExtensionAPI,
9
- model: WebSearchModel | undefined,
10
- config: Parameters<typeof isWebSearchEnabledForModel>[1],
11
- state: { wasActiveBeforeOwnership?: boolean; toolkitOwnsTool: boolean },
25
+ const registeredApis = new WeakSet<object>();
26
+
27
+ const WEB_RUN_LABEL = "web.run";
28
+ const WEB_RUN_DESCRIPTION =
29
+ "Run sequential standalone Web Search commands through the provider's CPA/Codex alpha-search endpoint. " +
30
+ "Use only when the current Web Search route explicitly selects standalone-alpha.";
31
+ const WEB_RUN_PROMPT_SNIPPET =
32
+ "Run search, image, page, finance, weather, sports, or time commands through CPA/Codex standalone Web Search.";
33
+ const WEB_RUN_PROMPT_GUIDELINES = [
34
+ "Use web.run only when the configured Web Search route is standalone-alpha; do not assume it is available for an unconfigured model.",
35
+ "Choose exactly the command fields needed for the current request and preserve ref_id values for open, click, find, and screenshot follow-ups.",
36
+ "Supported commands are search_query, image_query, open, click, find, screenshot, finance, weather, sports, and time; response_length may tune the result size.",
37
+ "The route requires a CPA/Codex gateway that exposes /alpha/search and supports standalone web search; a failed request is not retried or redirected to another search implementation.",
38
+ ];
39
+
40
+ const DEFAULT_TOOL_STATE = {
41
+ wasActiveBeforeOwnership: undefined as boolean | undefined,
42
+ toolkitOwnsTool: false,
43
+ };
44
+
45
+ type ToolOwnershipState = {
46
+ wasActiveBeforeOwnership?: boolean;
47
+ toolkitOwnsTool: boolean;
48
+ };
49
+
50
+ type WebRunExecutorDependencies = {
51
+ loadConfig: typeof loadToolkitConfig;
52
+ resolveRuntime: typeof resolveResponsesEnvironment;
53
+ requestSearch: typeof requestAlphaSearch;
54
+ isRegistered: () => boolean;
55
+ };
56
+
57
+ function cloneToolState(): ToolOwnershipState {
58
+ return { ...DEFAULT_TOOL_STATE };
59
+ }
60
+
61
+ function sanitizeMessage(message: string): string {
62
+ return sanitizeAlphaDiagnostic(message, "Standalone Web Search failed.");
63
+ }
64
+
65
+ function activeToolsApi(pi: ExtensionAPI): {
66
+ getActiveTools: () => string[];
67
+ setActiveTools: (names: string[]) => void;
68
+ } | undefined {
69
+ const api = pi as ExtensionAPI & {
70
+ getActiveTools?: () => string[];
71
+ setActiveTools?: (names: string[]) => void;
72
+ };
73
+ if (typeof api.getActiveTools !== "function" || typeof api.setActiveTools !== "function") return undefined;
74
+ return { getActiveTools: api.getActiveTools, setActiveTools: api.setActiveTools };
75
+ }
76
+
77
+ function canManageActiveTools(pi: ExtensionAPI): boolean {
78
+ const api = activeToolsApi(pi);
79
+ if (!api) return false;
80
+ try {
81
+ api.getActiveTools();
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ function isActiveTool(pi: ExtensionAPI, name: string): boolean | undefined {
89
+ const api = activeToolsApi(pi);
90
+ if (!api) return undefined;
91
+ try {
92
+ return api.getActiveTools().includes(name);
93
+ } catch {
94
+ return undefined;
95
+ }
96
+ }
97
+
98
+ function setActiveToolsIfChanged(
99
+ api: { getActiveTools: () => string[]; setActiveTools: (names: string[]) => void },
100
+ current: string[],
101
+ next: string[],
12
102
  ): void {
13
- const eligible = isWebSearchEnabledForModel(model, config);
14
- const activeTools = pi.getActiveTools();
103
+ if (current.length === next.length && current.every((name, index) => name === next[index])) return;
104
+ api.setActiveTools(next);
105
+ }
15
106
 
16
- if (eligible) {
107
+ function claimAndRemove(
108
+ pi: ExtensionAPI,
109
+ name: string,
110
+ state: ToolOwnershipState,
111
+ ): void {
112
+ const api = activeToolsApi(pi);
113
+ if (!api) return;
114
+ try {
115
+ const current = api.getActiveTools();
17
116
  if (!state.toolkitOwnsTool) {
18
- state.wasActiveBeforeOwnership = activeTools.includes("web_search");
117
+ state.wasActiveBeforeOwnership = current.includes(name);
19
118
  state.toolkitOwnsTool = true;
20
119
  }
120
+ const next = current.filter((toolName) => toolName !== name);
121
+ setActiveToolsIfChanged(api, current, next);
122
+ } catch {
123
+ // An unavailable active-tool API is handled as an unavailable route by the
124
+ // execution guard; it must never trigger a different search implementation.
125
+ }
126
+ }
21
127
 
22
- if (activeTools.includes("web_search")) {
23
- pi.setActiveTools(activeTools.filter((name) => name !== "web_search"));
128
+ function activateOwned(pi: ExtensionAPI, name: string, state: ToolOwnershipState): void {
129
+ const api = activeToolsApi(pi);
130
+ if (!api) return;
131
+ try {
132
+ const current = api.getActiveTools();
133
+ if (!state.toolkitOwnsTool) {
134
+ state.wasActiveBeforeOwnership = current.includes(name);
135
+ state.toolkitOwnsTool = true;
24
136
  }
25
- return;
137
+ let keptName = false;
138
+ const next = current.filter((toolName) => {
139
+ if (toolName !== name) return true;
140
+ if (keptName) return false;
141
+ keptName = true;
142
+ return true;
143
+ });
144
+ if (!keptName) next.push(name);
145
+ setActiveToolsIfChanged(api, current, next);
146
+ } catch {
147
+ // The tool-call guard remains fail closed if active-tool mutation fails.
26
148
  }
149
+ }
27
150
 
151
+ function releaseOwnership(
152
+ pi: ExtensionAPI,
153
+ name: string,
154
+ state: ToolOwnershipState,
155
+ ): void {
28
156
  if (!state.toolkitOwnsTool) return;
29
-
30
- if (state.wasActiveBeforeOwnership && !activeTools.includes("web_search")) {
31
- pi.setActiveTools([...activeTools, "web_search"]);
157
+ const api = activeToolsApi(pi);
158
+ if (!api) {
159
+ state.wasActiveBeforeOwnership = undefined;
160
+ state.toolkitOwnsTool = false;
161
+ return;
162
+ }
163
+ try {
164
+ const current = api.getActiveTools();
165
+ const withoutOwnedName = current.filter((toolName) => toolName !== name);
166
+ const next = state.wasActiveBeforeOwnership ? [...withoutOwnedName, name] : withoutOwnedName;
167
+ setActiveToolsIfChanged(api, current, next);
168
+ } catch {
169
+ // Do not claim that a failed release restored a third-party tool.
32
170
  }
33
171
  state.wasActiveBeforeOwnership = undefined;
34
172
  state.toolkitOwnsTool = false;
35
173
  }
36
174
 
175
+ function describeRouteFailure(resolution: WebSearchRouteResolution): string {
176
+ if (resolution.route === "none") {
177
+ return resolution.reason === "disabled"
178
+ ? "Web Search is disabled."
179
+ : "No Web Search route is configured for the current provider/model-id.";
180
+ }
181
+ return `Web Search route "${resolution.route}" is unavailable${resolution.reason ? ` (${resolution.reason})` : ""}.`;
182
+ }
183
+
184
+ function abortAndThrow(ctx: ExtensionContext, message: string): never {
185
+ try {
186
+ ctx.abort();
187
+ } catch {
188
+ // Test harnesses and older hosts may omit abort; throwing still prevents
189
+ // Pi from sending a payload that violates the selected route.
190
+ }
191
+ throw new Error(sanitizeMessage(message));
192
+ }
193
+
194
+ function standaloneRuntimeFailure(resolution: ResponsesEnvironmentResolution): Error {
195
+ if (resolution.ok) return new Error("Unexpected standalone Web Search runtime state.");
196
+ const reason = resolution.reason;
197
+ switch (reason) {
198
+ case "missing-api-key":
199
+ case "auth-resolution-failed":
200
+ return new Error(
201
+ sanitizeMessage(
202
+ resolution.errorMessage ??
203
+ "Standalone Web Search authentication could not be resolved for the current model.",
204
+ ),
205
+ );
206
+ case "missing-base-url":
207
+ return new Error("Standalone Web Search has no usable provider base URL.");
208
+ case "missing-session-id":
209
+ return new Error("Standalone Web Search requires a session identity for this gateway route.");
210
+ case "unsupported-api":
211
+ return new Error("Standalone Web Search requires an openai-responses or openai-codex-responses model.");
212
+ default:
213
+ return new Error(`Standalone Web Search runtime is unavailable (${reason}).`);
214
+ }
215
+ }
216
+
217
+ export async function executeStandaloneWebRun(args: {
218
+ params: unknown;
219
+ signal?: AbortSignal;
220
+ ctx: ExtensionContext;
221
+ loadConfig?: typeof loadToolkitConfig;
222
+ resolveRuntime?: typeof resolveResponsesEnvironment;
223
+ requestSearch?: typeof requestAlphaSearch;
224
+ isRegistered?: () => boolean;
225
+ }): Promise<{ text: string; details: AlphaSearchDetails }> {
226
+ const commands = normalizeStandaloneWebRunCommands(args.params);
227
+ if (args.signal?.aborted) throw new StandaloneWebRunError("web.run was cancelled.");
228
+
229
+ const loadConfig = args.loadConfig ?? loadToolkitConfig;
230
+ const resolveRuntime = args.resolveRuntime ?? resolveResponsesEnvironment;
231
+ const requestSearch = args.requestSearch ?? requestAlphaSearch;
232
+ if (args.isRegistered && !args.isRegistered()) {
233
+ throw new Error("web.run is not registered by the toolkit; standalone Web Search is unavailable.");
234
+ }
235
+
236
+ const { config } = loadConfig();
237
+ const route = resolveWebSearchRoute({ model: args.ctx.model, config: config.webSearch });
238
+ if (route.route !== "standalone-alpha" || !route.available) {
239
+ throw new Error(describeRouteFailure(route));
240
+ }
241
+
242
+ const runtime = await resolveRuntime(args.ctx, {
243
+ enabled: config.webSearch.enabled,
244
+ responsesApis: WEB_SEARCH_CAPABLE_APIS,
245
+ codexGatewayModels: config.compaction.gatewayContextModels,
246
+ });
247
+ if (!runtime.ok) throw standaloneRuntimeFailure(runtime);
248
+
249
+ const result: AlphaSearchClientResult = await requestSearch({
250
+ runtime: runtime.runtime,
251
+ commands,
252
+ signal: args.signal,
253
+ });
254
+ if (!result.ok) throw new Error(sanitizeMessage(result.errorMessage));
255
+ return { text: result.output, details: result.details };
256
+ }
257
+
258
+ function createWebRunTool(deps: WebRunExecutorDependencies): ToolDefinition<
259
+ typeof STANDALONE_WEB_RUN_PARAMETERS,
260
+ AlphaSearchDetails
261
+ > {
262
+ return {
263
+ name: WEB_RUN_TOOL_NAME,
264
+ label: WEB_RUN_LABEL,
265
+ description: WEB_RUN_DESCRIPTION,
266
+ promptSnippet: WEB_RUN_PROMPT_SNIPPET,
267
+ promptGuidelines: WEB_RUN_PROMPT_GUIDELINES,
268
+ parameters: STANDALONE_WEB_RUN_PARAMETERS,
269
+ executionMode: "sequential",
270
+ async execute(_toolCallId, params: StandaloneWebRunParameters, signal, _onUpdate, ctx) {
271
+ try {
272
+ const result = await executeStandaloneWebRun({
273
+ params,
274
+ signal,
275
+ ctx,
276
+ loadConfig: deps.loadConfig,
277
+ resolveRuntime: deps.resolveRuntime,
278
+ requestSearch: deps.requestSearch,
279
+ isRegistered: deps.isRegistered,
280
+ });
281
+ return {
282
+ content: [{ type: "text", text: result.text }],
283
+ details: result.details,
284
+ };
285
+ } catch (error) {
286
+ const message = error instanceof Error ? error.message : String(error);
287
+ throw new Error(sanitizeMessage(message));
288
+ }
289
+ },
290
+ };
291
+ }
292
+
293
+ function verifyStandaloneRegistration(
294
+ pi: ExtensionAPI,
295
+ definition: ToolDefinition<typeof STANDALONE_WEB_RUN_PARAMETERS, AlphaSearchDetails>,
296
+ ): boolean {
297
+ try {
298
+ const api = pi as ExtensionAPI & {
299
+ getAllTools?: () => Array<{
300
+ name: string;
301
+ description: string;
302
+ parameters: unknown;
303
+ promptGuidelines?: string[];
304
+ }>;
305
+ };
306
+ if (typeof api.getAllTools !== "function") return false;
307
+ const actual = api.getAllTools().find((tool) => tool.name === WEB_RUN_TOOL_NAME);
308
+ return actual !== undefined &&
309
+ actual.description === definition.description &&
310
+ actual.parameters === definition.parameters &&
311
+ actual.promptGuidelines === definition.promptGuidelines;
312
+ } catch {
313
+ return false;
314
+ }
315
+ }
316
+
317
+ function syncWebSearchRoute(
318
+ pi: ExtensionAPI,
319
+ model: WebSearchModel | undefined,
320
+ config: Parameters<typeof resolveWebSearchRoute>[0]["config"],
321
+ states: { webSearch: ToolOwnershipState; webRun: ToolOwnershipState },
322
+ standaloneReady: boolean,
323
+ ): WebSearchRouteResolution {
324
+ const resolution = resolveWebSearchRoute({ model, config });
325
+ if (resolution.route === "local" && resolution.available) {
326
+ releaseOwnership(pi, LOCAL_WEB_SEARCH_TOOL_NAME, states.webSearch);
327
+ claimAndRemove(pi, WEB_RUN_TOOL_NAME, states.webRun);
328
+ return resolution;
329
+ }
330
+ if (resolution.route === "standalone-alpha") {
331
+ claimAndRemove(pi, LOCAL_WEB_SEARCH_TOOL_NAME, states.webSearch);
332
+ if (resolution.available && standaloneReady) {
333
+ activateOwned(pi, WEB_RUN_TOOL_NAME, states.webRun);
334
+ } else {
335
+ claimAndRemove(pi, WEB_RUN_TOOL_NAME, states.webRun);
336
+ }
337
+ return resolution;
338
+ }
339
+ if (resolution.route === "hosted") {
340
+ claimAndRemove(pi, LOCAL_WEB_SEARCH_TOOL_NAME, states.webSearch);
341
+ claimAndRemove(pi, WEB_RUN_TOOL_NAME, states.webRun);
342
+ return resolution;
343
+ }
344
+
345
+ // `none`, and an explicit local route with no model, are fail-closed states:
346
+ // release any toolkit ownership, but do not activate another implementation.
347
+ if (resolution.route === "none" || (resolution.route === "local" && !resolution.available)) {
348
+ releaseOwnership(pi, LOCAL_WEB_SEARCH_TOOL_NAME, states.webSearch);
349
+ releaseOwnership(pi, WEB_RUN_TOOL_NAME, states.webRun);
350
+ } else {
351
+ claimAndRemove(pi, LOCAL_WEB_SEARCH_TOOL_NAME, states.webSearch);
352
+ claimAndRemove(pi, WEB_RUN_TOOL_NAME, states.webRun);
353
+ }
354
+ return resolution;
355
+ }
356
+
357
+ function routeReadyForPrompt(
358
+ resolution: WebSearchRouteResolution,
359
+ standaloneReady: boolean,
360
+ ): boolean {
361
+ return resolution.route !== "none" && resolution.available &&
362
+ (resolution.route !== "standalone-alpha" || standaloneReady);
363
+ }
364
+
37
365
  export function registerWebSearchExtension(
38
366
  pi: ExtensionAPI,
39
367
  loadConfig: typeof loadToolkitConfig = loadToolkitConfig,
368
+ requestSearch: typeof requestAlphaSearch = requestAlphaSearch,
369
+ resolveRuntime: typeof resolveResponsesEnvironment = resolveResponsesEnvironment,
40
370
  ): void {
41
- const toolState = { wasActiveBeforeOwnership: undefined as boolean | undefined, toolkitOwnsTool: false };
371
+ if (registeredApis.has(pi)) return;
372
+ registeredApis.add(pi);
42
373
 
43
- pi.on("session_start", (_event, ctx) => {
374
+ const states = { webSearch: cloneToolState(), webRun: cloneToolState() };
375
+ let standaloneRegistrationSucceeded = false;
376
+
377
+ const standaloneTool = createWebRunTool({
378
+ loadConfig,
379
+ resolveRuntime,
380
+ requestSearch,
381
+ isRegistered: () => standaloneReady(),
382
+ });
383
+
384
+ try {
385
+ const api = pi as ExtensionAPI & { registerTool?: (tool: typeof standaloneTool) => void };
386
+ if (typeof api.registerTool === "function") {
387
+ api.registerTool(standaloneTool);
388
+ standaloneRegistrationSucceeded = true;
389
+ // Pi initially activates newly registered extension tools. Treat that
390
+ // activation as toolkit-owned rather than as a third-party preference;
391
+ // otherwise an unconfigured route would leak web.run into every prompt.
392
+ states.webRun = { wasActiveBeforeOwnership: false, toolkitOwnsTool: true };
393
+ }
394
+ } catch {
395
+ standaloneRegistrationSucceeded = false;
396
+ }
397
+
398
+ function standaloneReady(): boolean {
399
+ if (!standaloneRegistrationSucceeded || !canManageActiveTools(pi)) return false;
400
+ // Recheck on every lifecycle boundary so a later extension cannot replace
401
+ // the definition after startup and leave a stale active name executable.
402
+ // Do not use getAllTools() during extension loading; lifecycle callbacks
403
+ // are the first point where Pi's action methods are bound.
404
+ return verifyStandaloneRegistration(pi, standaloneTool);
405
+ }
406
+
407
+ function synchronize(model: WebSearchModel | undefined, ctx?: ExtensionContext): WebSearchRouteResolution {
44
408
  const { config } = loadConfig();
45
- syncLocalWebSearchTool(pi, ctx.model, config.webSearch, toolState);
409
+ const resolution = syncWebSearchRoute(
410
+ pi,
411
+ model,
412
+ config.webSearch,
413
+ states,
414
+ standaloneReady(),
415
+ );
416
+ if (ctx && resolution.route === "standalone-alpha" && resolution.available && !standaloneReady()) {
417
+ // Keep the current route selected for diagnostics, but expose no tool.
418
+ return resolution;
419
+ }
420
+ return resolution;
421
+ }
422
+
423
+ pi.on("session_start", (_event, ctx) => {
424
+ synchronize(ctx.model, ctx);
46
425
  });
47
426
 
48
427
  pi.on("model_select", (event) => {
49
- const { config } = loadConfig();
50
- syncLocalWebSearchTool(pi, event.model, config.webSearch, toolState);
428
+ synchronize(event.model);
51
429
  });
52
430
 
53
431
  pi.on("before_agent_start", (event, ctx) => {
54
432
  const { config } = loadConfig();
55
- syncLocalWebSearchTool(pi, ctx.model, config.webSearch, toolState);
433
+ const resolution = syncWebSearchRoute(
434
+ pi,
435
+ ctx.model,
436
+ config.webSearch,
437
+ states,
438
+ standaloneReady(),
439
+ );
440
+ const localToolIsActive = resolution.route === "local"
441
+ ? isActiveTool(pi, LOCAL_WEB_SEARCH_TOOL_NAME)
442
+ : undefined;
443
+ const promptRouteReady = routeReadyForPrompt(resolution, standaloneReady()) &&
444
+ (resolution.route !== "local" || localToolIsActive === true);
56
445
  const systemPrompt = appendWebSearchPrompt({
57
446
  model: ctx.model,
58
447
  config: config.webSearch,
59
448
  systemPrompt: event.systemPrompt,
449
+ ...(promptRouteReady ? {} : { routeAvailable: false }),
60
450
  });
61
-
62
451
  return systemPrompt === event.systemPrompt ? undefined : { systemPrompt };
63
452
  });
64
453
 
65
454
  pi.on("before_provider_request", (event, ctx) => {
66
455
  const { config } = loadConfig();
67
- syncLocalWebSearchTool(pi, ctx.model, config.webSearch, toolState);
456
+ const resolution = syncWebSearchRoute(
457
+ pi,
458
+ ctx.model,
459
+ config.webSearch,
460
+ states,
461
+ standaloneReady(),
462
+ );
463
+ if (resolution.route === "standalone-alpha" && resolution.available && !standaloneReady()) {
464
+ abortAndThrow(ctx, "web.run is not registered by the toolkit; standalone Web Search request aborted.");
465
+ }
68
466
  const transformed = transformWebSearchPayload({
69
467
  model: ctx.model,
70
468
  config: config.webSearch,
71
469
  payload: event.payload,
72
470
  });
73
-
471
+ if (transformed.fatal) {
472
+ abortAndThrow(ctx, transformed.errorMessage ?? "Web Search route request aborted.");
473
+ }
74
474
  return transformed.changed ? transformed.payload : undefined;
75
475
  });
476
+
477
+ pi.on("tool_call", (event, ctx) => {
478
+ if (event.toolName !== LOCAL_WEB_SEARCH_TOOL_NAME && event.toolName !== WEB_RUN_TOOL_NAME) {
479
+ return undefined;
480
+ }
481
+ const { config } = loadConfig();
482
+ const resolution = syncWebSearchRoute(
483
+ pi,
484
+ ctx.model,
485
+ config.webSearch,
486
+ states,
487
+ standaloneReady(),
488
+ );
489
+ if (event.toolName === LOCAL_WEB_SEARCH_TOOL_NAME) {
490
+ if (resolution.route === "none") return undefined;
491
+ if (resolution.route === "local" && resolution.available) {
492
+ if (isActiveTool(pi, LOCAL_WEB_SEARCH_TOOL_NAME) === true) return undefined;
493
+ return {
494
+ block: true,
495
+ reason: "The local web_search tool is not active for the selected local Web Search route; the call was blocked instead of falling back.",
496
+ };
497
+ }
498
+ return {
499
+ block: true,
500
+ reason: `${describeRouteFailure(resolution)} The local web_search call was blocked instead of falling back.`,
501
+ };
502
+ }
503
+ if (resolution.route === "none") return undefined;
504
+ if (resolution.route === "standalone-alpha" && resolution.available && standaloneReady()) {
505
+ return undefined;
506
+ }
507
+ return {
508
+ block: true,
509
+ reason: `${describeRouteFailure(resolution)} The web.run call was blocked instead of falling back.`,
510
+ };
511
+ });
76
512
  }
77
513
 
78
514
  export default function webSearchExtension(pi: ExtensionAPI): void {
79
515
  registerWebSearchExtension(pi);
80
516
  }
517
+
518
+ export const _extensionTest = {
519
+ createWebRunTool,
520
+ verifyStandaloneRegistration,
521
+ syncWebSearchRoute,
522
+ standaloneRuntimeFailure,
523
+ };