@webmcp-auto-ui/agent 2.5.26 → 2.5.28

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.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Shared Hawk proxy handler — used by apps' /api/hawk/+server.ts
3
+ * Hawk = OpenAI-compatible endpoint at https://hawk.hyperskills.net/v1
4
+ * Bearer token lives server-side in HAWK_API_KEY env var.
5
+ */
6
+ function sanitizeId(id: string | undefined): string {
7
+ if (!id) return 'x';
8
+ const clean = id.replace(/[^a-zA-Z0-9]/g, '');
9
+ return clean || 'x';
10
+ }
11
+
12
+ /**
13
+ * Some llama-cpp chat templates (Qwen, Mistral family) enforce alphanumeric
14
+ * tool_call IDs via Jinja raise_exception. Gemma's template drops tool_use_id
15
+ * entirely (cf. gemma4-prompt-builder.ts:194) — safe passthrough.
16
+ * Mirrors the convention of sanitizeServerName (tool-layers.ts:24).
17
+ */
18
+ function needsIdSanitize(model: string | null | undefined): boolean {
19
+ if (!model) return false;
20
+ return /^(qwen|mistral|ministral|devstral|codestral|bielik)/.test(model);
21
+ }
22
+
23
+ export async function hawkProxy(
24
+ body: Record<string, unknown>,
25
+ apiKey: string,
26
+ model?: string | null,
27
+ ): Promise<Response> {
28
+ if (!apiKey) {
29
+ return new Response('HAWK_API_KEY missing', { status: 500 });
30
+ }
31
+ const m = model ?? 'qwen35-4b';
32
+ const cloned = JSON.parse(JSON.stringify(body)) as Record<string, unknown>;
33
+ if (needsIdSanitize(m) && Array.isArray(cloned.messages)) {
34
+ for (const msg of cloned.messages as Array<Record<string, unknown>>) {
35
+ if (msg.role === 'assistant' && Array.isArray(msg.tool_calls)) {
36
+ for (const tc of msg.tool_calls as Array<Record<string, unknown>>) {
37
+ tc.id = sanitizeId(tc.id as string | undefined);
38
+ }
39
+ } else if (msg.role === 'tool' && typeof msg.tool_call_id === 'string') {
40
+ msg.tool_call_id = sanitizeId(msg.tool_call_id);
41
+ }
42
+ }
43
+ }
44
+ const res = await fetch('https://hawk.hyperskills.net/v1/chat/completions', {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ 'Authorization': `Bearer ${apiKey}`,
49
+ },
50
+ body: JSON.stringify({ ...cloned, model: m }),
51
+ });
52
+ if (!res.ok) return new Response(await res.text(), { status: res.status });
53
+ return Response.json(await res.json());
54
+ }
@@ -0,0 +1,2 @@
1
+ export { llmProxy } from './llmProxy.js';
2
+ export { hawkProxy } from './hawkProxy.js';
@@ -33,11 +33,7 @@ export function sanitizeServerName(name: string): string {
33
33
  return result || 'mcp';
34
34
  }
35
35
 
36
- // ── Discovery pseudo-tool descriptions (shared between prompt and tool schemas) ──
37
- const shortSearchToolsDesc = (serverName: string) =>
38
- `Search tools by keyword on the ${serverName} server.`;
39
- const shortListToolsDesc = (serverName: string) =>
40
- `List ALL tools on the ${serverName} server.`;
36
+ // ── Discovery pseudo-tool descriptions (long form, used in tool schemas) ──
41
37
  const longSearchToolsDesc = (serverName: string) =>
42
38
  `Search tools by keyword on the ${serverName} server. Use this when you need to find a specific data-fetching or action tool but don't know its exact name. Pass a keyword related to the task (e.g. "weather", "search", "create") and get back matching tool names with descriptions and input schemas. This is more targeted than list_tools — prefer it when you have a clear idea of what you're looking for. Returns an array of {name, description, inputSchema} objects.`;
43
39
  const longListToolsDesc = (serverName: string) =>
@@ -66,110 +62,10 @@ export type ToolLayer = McpLayer | WebMcpLayer;
66
62
 
67
63
  /** Which provider syntax to emit in the system prompt.
68
64
  * - `generic`: `tool_name()` / `tool_name(arg1, arg2)` — Claude, Ollama, most providers.
69
- * - `gemma`: `<|tool_call>call:tool_name{}<tool_call|>` — Gemma 4 native format. */
70
- export type ProviderKind = 'generic' | 'gemma';
71
-
72
- /** Extract the first parameter name from a ProviderTool's input_schema.
73
- * Priority: first required field > first property > fallback.
74
- * Used to emit real parameter names in tool references for better prompting. */
75
- function firstParamName(tool?: ProviderTool, fallback = 'query'): string {
76
- if (!tool?.input_schema) return fallback;
77
- const schema = tool.input_schema as Record<string, unknown>;
78
- const required = (schema.required as string[] | undefined) ?? [];
79
- if (required.length > 0) return required[0];
80
- const props = (schema.properties as Record<string, unknown> | undefined) ?? {};
81
- return Object.keys(props)[0] ?? fallback;
82
- }
83
-
84
- /** Format a tool reference for inclusion in the system prompt.
85
- * Generic (Claude/Ollama/etc): `name()` or `name(arg1, arg2)`.
86
- * Gemma: emits the full `<|tool>declaration:...<tool|>` block inline when a tool is provided,
87
- * so declarations appear in context at each step of the workflow (no appendix).
88
- * Falls back to a plain backtick reference if no tool is provided.
89
- */
90
- function fmtToolRef(
91
- prefixedName: string,
92
- args: string[] = [],
93
- kind: ProviderKind = 'generic',
94
- tool?: ProviderTool,
95
- ): string {
96
- if (kind === 'gemma' && tool) {
97
- // Inline: full Gemma declaration with canonical prefixed name but real schema
98
- return formatGemmaToolDeclaration({ ...tool, name: prefixedName });
99
- }
100
- if (kind === 'gemma') {
101
- return args.length ? `\`${prefixedName}(${args.join(', ')})\`` : `\`${prefixedName}\``;
102
- }
103
- return args.length ? `${prefixedName}(${args.join(', ')})` : `${prefixedName}()`;
104
- }
105
-
106
- /**
107
- * Format a value for Gemma 4 native tool syntax.
108
- * Strings use <|"|> delimiters, numbers/booleans/null are bare.
109
- */
110
- export function gemmaValue(v: unknown): string {
111
- const q = '<|"|>';
112
- if (v === null || v === undefined) return 'null';
113
- if (typeof v === 'number' || typeof v === 'boolean') return String(v);
114
- if (Array.isArray(v)) return `[${v.map(i => gemmaValue(i)).join(',')}]`;
115
- if (typeof v === 'object') {
116
- const entries = Object.entries(v as Record<string, unknown>)
117
- .map(([k, val]) => `${k}:${gemmaValue(val)}`);
118
- return `{${entries.join(',')}}`;
119
- }
120
- return `${q}${String(v)}${q}`;
121
- }
122
-
123
- /**
124
- * Format a tool declaration in Gemma 4 native syntax.
125
- * Emitted in the system prompt tail so Gemma sees tool schemas alongside the
126
- * STEP-by-STEP instructions.
127
- */
128
- export function formatGemmaToolDeclaration(tool: ProviderTool): string {
129
- const q = '<|"|>';
130
- let decl = `<|tool>declaration:${tool.name}{\n`;
131
- decl += ` description:${q}${tool.description}${q}`;
132
-
133
- const schema = tool.input_schema;
134
- if (schema?.properties) {
135
- const props = schema.properties as Record<string, { description?: string; type?: string; enum?: string[]; format?: string; default?: unknown }>;
136
- decl += `,\n parameters:{\n properties:{\n`;
137
-
138
- const propEntries = Object.entries(props);
139
- for (let i = 0; i < propEntries.length; i++) {
140
- const [key, val] = propEntries[i];
141
- decl += ` ${key}:{`;
142
- const parts: string[] = [];
143
- if (val.description) parts.push(`description:${q}${val.description}${q}`);
144
- // If no type specified, infer OBJECT for params-like fields to avoid
145
- // Gemma wrapping the value in <|"|>...<|"|> (treating it as a string)
146
- let inferredType = val.type;
147
- if (!inferredType) {
148
- const descLower = (val.description ?? '').toLowerCase();
149
- if (descLower.includes('objet') || descLower.includes('object') || descLower.includes('parameter') || descLower.includes('paramètre') || key === 'params') {
150
- inferredType = 'object';
151
- } else {
152
- inferredType = 'string';
153
- }
154
- }
155
- parts.push(`type:${q}${inferredType.toUpperCase()}${q}`);
156
- if (val.enum) parts.push(`enum:[${val.enum.map(e => `${q}${e}${q}`).join(',')}]`);
157
- if (val.format) parts.push(`format:${q}${val.format}${q}`);
158
- if (val.default !== undefined) parts.push(`default:${gemmaValue(val.default)}`);
159
- decl += parts.join(',');
160
- decl += `}${i < propEntries.length - 1 ? ',' : ''}\n`;
161
- }
162
-
163
- decl += ` }`;
164
- if (schema.required && Array.isArray(schema.required)) {
165
- decl += `,\n required:[${(schema.required as string[]).map(r => `${q}${r}${q}`).join(',')}]`;
166
- }
167
- decl += `,\n type:${q}OBJECT${q}\n }`;
168
- }
169
-
170
- decl += `\n}<tool|>`;
171
- return decl;
172
- }
65
+ * - `gemma`: `<|tool_call>call:tool_name{}<tool_call|>` — Gemma 4 native format.
66
+ * - `qwen`: `<tool_call>\n{"name":...,"arguments":...}\n</tool_call>` Qwen 3/3.5 ChatML.
67
+ * - `mistral`: `[TOOL_CALLS][{"name":...,"arguments":...}]` — Mistral/Ministral. */
68
+ export type ProviderKind = 'generic' | 'gemma' | 'qwen' | 'mistral';
173
69
 
174
70
  /** Options controlling how tool schemas are transformed before sending to the LLM */
175
71
  export interface SchemaTransformOptions {
@@ -465,300 +361,8 @@ export function buildToolsFromLayers(layers: ToolLayer[], schemaOptions?: Schema
465
361
  return { tools: Array.from(seen.values()), pathMaps: localPathMaps };
466
362
  }
467
363
 
468
- /** Result of buildSystemPromptWithAliases prompt text + per-call alias map */
469
- export interface SystemPromptResult {
470
- prompt: string;
471
- aliasMap: Map<string, string>;
472
- }
473
-
474
- /**
475
- * Build system prompt with a local alias map (parallel-safe).
476
- * Prefer this over buildSystemPrompt() when running multiple agent loops.
477
- *
478
- * The `providerKind` option controls the syntax of tool references in the prompt:
479
- * - `'generic'` (default): `tool_name()` / `tool_name(arg)` — for Claude, Ollama, etc.
480
- * - `'gemma'`: `<|tool_call>call:tool_name{}<tool_call|>` — Gemma 4 native format.
481
- */
482
- export function buildSystemPromptWithAliases(
483
- layers: ToolLayer[],
484
- options: { providerKind?: ProviderKind } = {},
485
- ): SystemPromptResult {
486
- const kind = options.providerKind ?? 'generic';
487
- const mcpLayers = layers.filter((l): l is McpLayer => l.protocol === 'mcp');
488
- const webmcpLayers = layers.filter((l): l is WebMcpLayer => l.protocol === 'webmcp');
489
-
490
- // DISPLAY servers = WebMCP layers that expose widget_display (can render on canvas).
491
- // DATA servers = everything else (MCP servers + WebMCP without widget_display).
492
- const displayLayers = webmcpLayers.filter(l => l.tools.some(t => t.name === 'widget_display'));
493
- const dataLayers = layers.filter(l => !displayLayers.includes(l as WebMcpLayer));
494
-
495
- // Pre-build an index of prefixed tool name → ProviderTool so we can emit
496
- // real param names (and, for Gemma, inline declarations) at each call site.
497
- const providerToolsByName = new Map<string, ProviderTool>(
498
- buildToolsFromLayers(layers, { sanitize: true }).tools.map(t => [t.name, t]),
499
- );
500
-
501
- const aliasMap = new Map<string, string>();
502
-
503
- // ── Collect search_recipes / list_recipes / get_recipe from all layers ──
504
- const searchRecipes: string[] = [];
505
- const listRecipes: string[] = [];
506
- const getRecipes: string[] = [];
507
- const searchTools: string[] = [];
508
- const listTools: string[] = [];
509
-
510
- // WebMCP layers: always exact match (we control the naming)
511
- for (const l of webmcpLayers) {
512
- const prefix = `${sanitizeServerName(l.serverName)}_ui_`;
513
- for (const t of l.tools) {
514
- if (t.name === 'search_recipes') {
515
- const name = `${prefix}search_recipes`;
516
- const toolDef = providerToolsByName.get(name);
517
- searchRecipes.push(fmtToolRef(name, [firstParamName(toolDef, 'query')], kind, toolDef));
518
- }
519
- if (t.name === 'list_recipes') {
520
- const name = `${prefix}list_recipes`;
521
- const toolDef = providerToolsByName.get(name);
522
- listRecipes.push(fmtToolRef(name, [], kind, toolDef));
523
- }
524
- if (t.name === 'get_recipe') {
525
- const name = `${prefix}get_recipe`;
526
- const toolDef = providerToolsByName.get(name);
527
- getRecipes.push(fmtToolRef(name, [firstParamName(toolDef, 'id')], kind, toolDef));
528
- }
529
- }
530
- // Pseudo-tools for tool discovery on WebMCP servers — not in providerToolsByName,
531
- // so we build synthetic ProviderTools for inline declarations.
532
- const searchToolsPseudo: ProviderTool = {
533
- name: `${prefix}search_tools`,
534
- description: shortSearchToolsDesc(l.serverName),
535
- input_schema: { type: 'object', properties: { query: { type: 'string', description: 'Keyword to search for.' } }, required: ['query'] },
536
- };
537
- searchTools.push(fmtToolRef(`${prefix}search_tools`, ['query'], kind, searchToolsPseudo));
538
- const listToolsPseudo: ProviderTool = {
539
- name: `${prefix}list_tools`,
540
- description: shortListToolsDesc(l.serverName),
541
- input_schema: { type: 'object', properties: {} },
542
- };
543
- listTools.push(fmtToolRef(`${prefix}list_tools`, [], kind, listToolsPseudo));
544
- }
545
-
546
- // MCP layers: 4-layer matching + alias registration
547
- for (const l of mcpLayers) {
548
- const prefix = `${sanitizeServerName(l.serverName)}_data_`;
549
- const matches = resolveCanonicalTools(l.tools);
550
-
551
- for (const m of matches) {
552
- const canonicalPrefixed = `${prefix}${m.role}`;
553
- const realPrefixed = `${prefix}${m.realToolName}`;
554
-
555
- // Register alias only if names differ
556
- if (m.role !== m.realToolName) {
557
- aliasMap.set(canonicalPrefixed, realPrefixed);
558
- }
559
-
560
- // Look up the REAL tool (by real prefixed name) to get the actual schema
561
- const realToolDef = providerToolsByName.get(realPrefixed);
562
-
563
- if (m.role === 'search_recipes') {
564
- searchRecipes.push(fmtToolRef(canonicalPrefixed, [firstParamName(realToolDef, 'query')], kind, realToolDef));
565
- }
566
- if (m.role === 'list_recipes') {
567
- listRecipes.push(fmtToolRef(canonicalPrefixed, [], kind, realToolDef));
568
- }
569
- if (m.role === 'get_recipe') {
570
- getRecipes.push(fmtToolRef(canonicalPrefixed, [firstParamName(realToolDef, 'id')], kind, realToolDef));
571
- }
572
- }
573
-
574
- // Pseudo-tools for tool discovery on all MCP servers
575
- const searchToolsPseudo: ProviderTool = {
576
- name: `${prefix}search_tools`,
577
- description: shortSearchToolsDesc(l.serverName),
578
- input_schema: { type: 'object', properties: { query: { type: 'string', description: 'Keyword to search for.' } }, required: ['query'] },
579
- };
580
- searchTools.push(fmtToolRef(`${prefix}search_tools`, ['query'], kind, searchToolsPseudo));
581
- const listToolsPseudo: ProviderTool = {
582
- name: `${prefix}list_tools`,
583
- description: shortListToolsDesc(l.serverName),
584
- input_schema: { type: 'object', properties: {} },
585
- };
586
- listTools.push(fmtToolRef(`${prefix}list_tools`, [], kind, listToolsPseudo));
587
- }
588
-
589
- // ── WebMCP action tools (widget_display, canvas, recall) ──
590
- // Iterate in canonical order (widget_display, canvas, recall) so the prompt
591
- // always lists them in the same sequence regardless of tool definition order.
592
- const actionTools: string[] = [];
593
- const ACTION_NAMES = ['widget_display', 'canvas', 'recall'];
594
- for (const l of webmcpLayers) {
595
- const prefix = `${sanitizeServerName(l.serverName)}_ui_`;
596
- for (const actionName of ACTION_NAMES) {
597
- if (l.tools.some(t => t.name === actionName)) {
598
- const prefixedName = `${prefix}${actionName}`;
599
- const toolDef = providerToolsByName.get(prefixedName);
600
- const args = actionName === 'widget_display' ? ['name', 'params'] : [];
601
- actionTools.push(fmtToolRef(prefixedName, args, kind, toolDef));
602
- }
603
- }
604
- }
605
-
606
- // Same refs, grouped by DATA vs DISPLAY category, for the gemma-minimalist template.
607
- const dataPrefixes = new Set(dataLayers.map(l => `${sanitizeServerName(l.serverName)}_${protocolToken(l.protocol)}_`));
608
- const displayPrefixes = new Set(displayLayers.map(l => `${sanitizeServerName(l.serverName)}_ui_`));
609
-
610
- function splitByCategory(refs: string[]): { data: string[]; display: string[] } {
611
- // Refs may be backticked names, full declarations, or tool_name(arg) — in all cases,
612
- // the prefixed name (e.g. `tricoteuses_data_search_recipes`) is detectable by substring match.
613
- const data: string[] = [];
614
- const display: string[] = [];
615
- for (const ref of refs) {
616
- const isDisplay = [...displayPrefixes].some(p => ref.includes(p));
617
- if (isDisplay) display.push(ref);
618
- else data.push(ref);
619
- }
620
- return { data, display };
621
- }
622
-
623
- const listRecipesByCat = splitByCategory(listRecipes);
624
- const searchRecipesByCat = splitByCategory(searchRecipes);
625
- const getRecipesByCat = splitByCategory(getRecipes);
626
- // Suppress unused-variable warnings — dataPrefixes is referenced indirectly via dataLayers.
627
- void dataPrefixes;
628
-
629
- // ── Build prompt ──
630
- let prompt: string;
631
-
632
- if (kind === 'gemma') {
633
- // ── Minimalist template for Gemma (4B/E4B), inline declarations included ──
634
- const dataListSearch = [
635
- ...listRecipesByCat.data,
636
- ...searchRecipesByCat.data,
637
- ].join('\n');
638
- const displayListSearch = [
639
- ...listRecipesByCat.display,
640
- ...searchRecipesByCat.display,
641
- ].join('\n');
642
- const allGetRecipes = [
643
- ...getRecipesByCat.data,
644
- ...getRecipesByCat.display,
645
- ].join('\n');
646
-
647
- prompt = `Route: DATA (fetch) or DISPLAY (render). Greetings → chat.
648
-
649
- STEP 1 — List or search a recipe.
650
- DATA:
651
- ${dataListSearch}
652
- DISPLAY:
653
- ${displayListSearch}
654
- The tool results are for you, not for the user. Pick the best match and go to STEP 2. Never ask the user to choose.
655
-
656
- STEP 2 — Fetch the recipe.
657
- ${allGetRecipes}
658
-
659
- STEP 3 — Execute using the schema from STEP 2.
660
- - Data: follow the recipe (SQL / FTS / script).
661
- - Display: call widget_display(name, params).
662
- ${actionTools.join('\n')}
663
- If no recipe fits, use a tool directly:
664
- ${listTools.join('\n')}
665
- ${searchTools.join('\n')}
666
- Only use data returned by tools or given by the user. Never fabricate.
667
-
668
- Reply: one-line summary + result.`;
669
- } else {
670
- // ── Existing generic template for Claude/remote — DO NOT MODIFY ──
671
- const reasoningRule = 'Do not narrate your process in the response. Internal reasoning is permitted but must not appear in the final output. For trivial conversational messages such as greetings or small talk, skip directly to STEP 5.';
672
-
673
- prompt = `You are an AI assistant that helps users by answering their questions and completing tasks using recipes (also called skills) — instructions for an AI agent with scripts, schemas, and information. If no recipe or tool fits, fall back to a traditional chat (STEP 5).
674
-
675
- There are two kinds of servers: MCP servers expose DATA (recipes, instructions, tools) AND WebMCP servers expose UI tools (widget_display, canvas, recall) to render DATA on the canvas.
676
-
677
- You MUST NOT skip steps.
678
-
679
- CRITICAL RULE: ${reasoningRule}
680
-
681
- STEP 1 — List all recipes
682
-
683
- Look for a relevant recipe among these:
684
-
685
- ${listRecipes.join('\n')}
686
-
687
- If at least one relevant recipe is found → go to STEP 2.
688
- If no results → go to STEP 1b.
689
-
690
- STEP 1b — Search recipes
691
-
692
- No recipe found by listing. Search with keyword(s) extracted from the request:
693
-
694
- ${searchRecipes.join('\n')}
695
-
696
- Pick the most relevant recipe for the request.
697
- If a recipe matches → go to STEP 2.
698
- If no recipe is available or relevant → go to STEP 1c.
699
-
700
- STEP 1c — List tools
701
-
702
- No applicable recipe. List a relevant tool:
703
-
704
- ${listTools.join('\n')}
705
-
706
- If a relevant tool is found → use it directly to respond (go to STEP 3).
707
- If no results → go to STEP 1d.
708
-
709
- STEP 1d — Search tools
710
-
711
- ${searchTools.join('\n')}
712
-
713
- Pick the most relevant tool(s) and use them to respond (go to STEP 3).
714
-
715
- STEP 2 — Read the recipe
716
-
717
- ${getRecipes.join('\n')}
718
- The id comes from the result of list_recipes (STEP 1) or search_recipes (STEP 1b), whichever was called.
719
-
720
- Read the full instructions of the selected recipe.
721
-
722
- STEP 3 — Execute
723
-
724
- Prefer recipes over direct tool calls when a recipe matches the task. Use low-level instructions (DB queries, schema introspection, raw scripts) only when invoked from within a recipe's instructions.
725
-
726
- Follow the recipe instructions exactly if you have one. Otherwise use the tools with their schemas.
727
-
728
- Output format: (1) a one-sentence summary of the action performed, then (2) the result. Nothing else.
729
-
730
- STEP 4 — UI display
731
-
732
- Unless a recipe specifies otherwise, use these tools to display your responses on the canvas:
733
-
734
- ${actionTools.join('\n')}
735
-
736
- widget_display may ONLY be called with data returned by a non-autoui DATA tool actually invoked in the current session. Fabricating IDs, URLs, names, dates, or any content not returned by a tool is a critical violation. If no DATA tool has been called yet, go back to STEP 1.
737
-
738
- STEP 5 — Fallback
739
-
740
- If previous steps failed, fall back to a classic chat without tool calling.`;
741
- }
742
-
743
- // Note: for Gemma (kind === 'gemma'), tool declarations are emitted INLINE at each
744
- // STEP via `fmtToolRef(..., kind, tool)` — no appendix is appended here.
745
-
746
- return { prompt, aliasMap };
747
- }
748
-
749
- /** Build system prompt — backward-compatible wrapper that returns a plain string.
750
- * Also populates the deprecated global toolAliasMap for legacy consumers.
751
- * For parallel-safe usage, use buildSystemPromptWithAliases() instead.
752
- */
753
- export function buildSystemPrompt(layers: ToolLayer[], options?: { providerKind?: ProviderKind }): string {
754
- const { prompt, aliasMap } = buildSystemPromptWithAliases(layers, options);
755
-
756
- // Populate deprecated global singleton for backward compat
757
- toolAliasMap.clear();
758
- for (const [k, v] of aliasMap) toolAliasMap.set(k, v);
759
-
760
- return prompt;
761
- }
364
+ // buildSystemPromptWithAliases / buildSystemPrompt / SystemPromptResult now
365
+ // live in ./prompts/index.ts and are re-exported from src/index.ts.
762
366
 
763
367
  /** Result of buildDiscoveryToolsWithAliases */
764
368
  export interface DiscoveryToolsResult {