@webmcp-auto-ui/agent 2.5.24 → 2.5.26

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 (46) hide show
  1. package/package.json +1 -1
  2. package/src/autoui-server.ts +17 -0
  3. package/src/diagnostics.ts +6 -6
  4. package/src/discovery-cache.ts +17 -3
  5. package/src/index.ts +3 -3
  6. package/src/loop.ts +27 -19
  7. package/src/providers/wasm.ts +184 -330
  8. package/src/recipes/_generated.ts +273 -0
  9. package/src/recipes/canary-data.md +50 -0
  10. package/src/recipes/canary-display.md +99 -0
  11. package/src/recipes/canary-middle.md +32 -0
  12. package/src/recipes/hummingbird-data.md +32 -0
  13. package/src/recipes/hummingbird-display.md +36 -0
  14. package/src/recipes/hummingbird-middle.md +18 -0
  15. package/src/tool-layers.ts +303 -31
  16. package/src/types.ts +6 -1
  17. package/tests/loop.test.ts +2 -2
  18. package/src/providers/gemma.worker.legacy.ts +0 -123
  19. package/src/providers/litert.worker.ts +0 -294
  20. package/src/recipes/widgets/actions.md +0 -28
  21. package/src/recipes/widgets/alert.md +0 -27
  22. package/src/recipes/widgets/cards.md +0 -41
  23. package/src/recipes/widgets/carousel.md +0 -39
  24. package/src/recipes/widgets/chart-rich.md +0 -51
  25. package/src/recipes/widgets/chart.md +0 -32
  26. package/src/recipes/widgets/code.md +0 -21
  27. package/src/recipes/widgets/d3.md +0 -36
  28. package/src/recipes/widgets/data-table.md +0 -46
  29. package/src/recipes/widgets/gallery.md +0 -39
  30. package/src/recipes/widgets/grid-data.md +0 -57
  31. package/src/recipes/widgets/hemicycle.md +0 -43
  32. package/src/recipes/widgets/js-sandbox.md +0 -32
  33. package/src/recipes/widgets/json-viewer.md +0 -27
  34. package/src/recipes/widgets/kv.md +0 -31
  35. package/src/recipes/widgets/list.md +0 -24
  36. package/src/recipes/widgets/log.md +0 -39
  37. package/src/recipes/widgets/map.md +0 -49
  38. package/src/recipes/widgets/profile.md +0 -49
  39. package/src/recipes/widgets/recipe-browser.md +0 -102
  40. package/src/recipes/widgets/sankey.md +0 -54
  41. package/src/recipes/widgets/stat-card.md +0 -43
  42. package/src/recipes/widgets/stat.md +0 -35
  43. package/src/recipes/widgets/tags.md +0 -30
  44. package/src/recipes/widgets/text.md +0 -19
  45. package/src/recipes/widgets/timeline.md +0 -38
  46. package/src/recipes/widgets/trombinoscope.md +0 -39
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmcp-auto-ui/agent",
3
- "version": "2.5.24",
3
+ "version": "2.5.26",
4
4
  "description": "LLM agent loop + remote/WASM/local providers + MCP wrapper",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -3,6 +3,7 @@
3
3
  // ---------------------------------------------------------------------------
4
4
 
5
5
  import { createWebMcpServer, parseFrontmatter } from '@webmcp-auto-ui/core';
6
+ import { RAW_RECIPES } from './recipes/_generated.js';
6
7
 
7
8
  // ---------------------------------------------------------------------------
8
9
  // Inline recipes (frontmatter + body)
@@ -1007,6 +1008,22 @@ for (const recipe of RECIPES) {
1007
1008
  autoui.registerWidget(recipe, undefined);
1008
1009
  }
1009
1010
 
1011
+ // Register flow recipes (multi-step procedures) from the global recipe registry
1012
+ // that declare this server (autoui) in their frontmatter.
1013
+ for (const [key, rawMd] of Object.entries(RAW_RECIPES)) {
1014
+ const { frontmatter } = parseFrontmatter(rawMd);
1015
+ const servers = (Array.isArray(frontmatter.servers) ? frontmatter.servers : []) as string[];
1016
+ if (servers.map((s) => s.toLowerCase()).includes('autoui')) {
1017
+ try {
1018
+ autoui.registerRecipe(rawMd);
1019
+ } catch (e) {
1020
+ // Skip malformed recipes (missing id/name) — log for debugging
1021
+ // eslint-disable-next-line no-console
1022
+ console.warn(`[autoui] Failed to register flow recipe "${key}":`, e);
1023
+ }
1024
+ }
1025
+ }
1026
+
1010
1027
  // Expose recipe summaries to the UI browser
1011
1028
  const parsedRecipes = RECIPES.map((md) => {
1012
1029
  const { frontmatter, body } = parseFrontmatter(md);
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { ToolLayer } from './tool-layers.js';
4
4
  import type { ProviderTool } from './types.js';
5
- import { sanitizeServerName } from './tool-layers.js';
5
+ import { sanitizeServerName, protocolToken } from './tool-layers.js';
6
6
  import { sanitizeSchemaWithReport } from '@webmcp-auto-ui/core';
7
7
  import type { JsonSchema, SchemaPatch } from '@webmcp-auto-ui/core';
8
8
 
@@ -41,8 +41,8 @@ export function runDiagnostics(
41
41
  diagnostics.push({
42
42
  severity: 'error',
43
43
  title: `Bruit dans le prefixe "${prefix}"`,
44
- detail: `Le serveur "${layer.serverName}" produit un prefixe contenant "${noise.join(', ')}". Les outils auront des noms ambigus comme ${prefix}_mcp_list_recipes.`,
45
- quickFix: `Ajoutez au system prompt:\n"Note: le serveur ${layer.serverName} utilise le prefixe '${prefix}'. Utilisez ${prefix}_mcp_<tool> ou ${prefix}_webmcp_<tool>."`,
44
+ detail: `Le serveur "${layer.serverName}" produit un prefixe contenant "${noise.join(', ')}". Les outils auront des noms ambigus comme ${prefix}_data_list_recipes.`,
45
+ quickFix: `Ajoutez au system prompt:\n"Note: le serveur ${layer.serverName} utilise le prefixe '${prefix}'. Utilisez ${prefix}_data_<tool> ou ${prefix}_ui_<tool>."`,
46
46
  codeFix: `Renommer le serveur MCP cote serveur pour ne pas inclure "mcp"/"server" dans le nom.`,
47
47
  });
48
48
  }
@@ -88,14 +88,14 @@ export function runDiagnostics(
88
88
  // Include discovery tools (search_tools, list_tools) which are generated dynamically
89
89
  const toolNames = new Set(tools.map(t => t.name));
90
90
  for (const layer of layers) {
91
- const prefix = `${sanitizeServerName(layer.serverName)}_${layer.protocol}_`;
91
+ const prefix = `${sanitizeServerName(layer.serverName)}_${protocolToken(layer.protocol)}_`;
92
92
  toolNames.add(`${prefix}search_tools`);
93
93
  toolNames.add(`${prefix}list_tools`);
94
94
  toolNames.add(`${prefix}search_recipes`);
95
95
  toolNames.add(`${prefix}list_recipes`);
96
96
  toolNames.add(`${prefix}get_recipe`);
97
97
  }
98
- const toolPattern = /\b([a-z][a-z0-9_]{2,})_(mcp|webmcp)_([a-z][a-z0-9_]+)\b/g;
98
+ const toolPattern = /\b([a-z][a-z0-9_]{2,})_(data|ui)_([a-z][a-z0-9_]+)\b/g;
99
99
  let match;
100
100
  while ((match = toolPattern.exec(systemPrompt)) !== null) {
101
101
  const fullName = match[0];
@@ -112,7 +112,7 @@ export function runDiagnostics(
112
112
  // 5. Strict mode — schemas that were auto-patched
113
113
  // Must run on raw (pre-sanitize) schemas; sanitized tools will never show patches.
114
114
  // Skipped for Gemma — it doesn't use additionalProperties/strict mode at all.
115
- if (!skipSchemaChecks) {
115
+ if (!skipSchemaChecks && schemaOptions?.strict) {
116
116
  const checkTools = rawTools ?? tools;
117
117
  for (const tool of checkTools) {
118
118
  const { patches } = sanitizeSchemaWithReport(tool.input_schema as JsonSchema);
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { ProviderTool } from './types.js';
8
+ import type { PipelineTrace } from './pipeline-trace.js';
8
9
 
9
10
  /** Tool names that are resolved locally from cache — hidden from user-facing browsers. */
10
11
  export const DISCOVERY_TOOL_NAMES = new Set(['list_recipes', 'search_recipes', 'get_recipe', 'list_tools', 'search_tools']);
@@ -100,13 +101,18 @@ export class DiscoveryCache {
100
101
  serverPrefix: string,
101
102
  realToolName: string,
102
103
  params: Record<string, unknown>,
104
+ trace?: PipelineTrace,
103
105
  ): string | null {
104
106
  const cache = this.servers.get(serverPrefix);
105
107
  if (!cache) return null;
106
108
 
107
109
  switch (realToolName) {
108
110
  case 'search_recipes': {
109
- const query = ((params.query ?? '') as string).toLowerCase();
111
+ const rawQuery = params.query ?? params.q ?? params.keyword ?? params.search;
112
+ const query = String(rawQuery ?? '').toLowerCase();
113
+ if (rawQuery === undefined && Object.keys(params).length > 0) {
114
+ trace?.push('discovery', realToolName, `unknown param keys: ${Object.keys(params).join(',')} — expected query/q/keyword/search`, 'warn');
115
+ }
110
116
  const results = query
111
117
  ? cache.recipes.filter(r =>
112
118
  r.name.toLowerCase().includes(query) ||
@@ -121,7 +127,11 @@ export class DiscoveryCache {
121
127
  }
122
128
 
123
129
  case 'get_recipe': {
124
- const key = String(params.name ?? params.id ?? '').toLowerCase();
130
+ const rawKey = params.name ?? params.id ?? params.recipe_id ?? params.key;
131
+ const key = String(rawKey ?? '').toLowerCase();
132
+ if (rawKey === undefined && Object.keys(params).length > 0) {
133
+ trace?.push('discovery', realToolName, `unknown param keys: ${Object.keys(params).join(',')} — expected name/id/recipe_id/key`, 'warn');
134
+ }
125
135
  const recipe = cache.recipes.find(r =>
126
136
  (r.name?.toLowerCase() === key) ||
127
137
  ((r as Record<string, unknown>).id as string | undefined)?.toLowerCase() === key
@@ -131,7 +141,11 @@ export class DiscoveryCache {
131
141
  }
132
142
 
133
143
  case 'search_tools': {
134
- const query = ((params.query ?? '') as string).toLowerCase();
144
+ const rawQuery = params.query ?? params.q ?? params.keyword ?? params.search;
145
+ const query = String(rawQuery ?? '').toLowerCase();
146
+ if (rawQuery === undefined && Object.keys(params).length > 0) {
147
+ trace?.push('discovery', realToolName, `unknown param keys: ${Object.keys(params).join(',')} — expected query/q/keyword/search`, 'warn');
148
+ }
135
149
  const results = query
136
150
  ? cache.tools.filter(t =>
137
151
  t.name.toLowerCase().includes(query) ||
package/src/index.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  // Providers
4
4
  export { RemoteLLMProvider } from './providers/remote.js';
5
5
  export type { RemoteLLMProviderOptions } from './providers/remote.js';
6
- export { WasmProvider } from './providers/wasm.js';
7
- export type { WasmProviderOptions, WasmStatus } from './providers/wasm.js';
6
+ export { WasmProvider, buildGemmaPrompt } from './providers/wasm.js';
7
+ export type { WasmProviderOptions, WasmStatus, BuildGemmaPromptInput } from './providers/wasm.js';
8
8
  export { LocalLLMProvider } from './providers/local.js';
9
9
  export type { LocalLLMProviderOptions, LocalBackend } from './providers/local.js';
10
10
  export { createProvider } from './providers/factory.js';
@@ -26,7 +26,7 @@ export { autoui, NATIVE_WIDGET_NAMES } from './autoui-server.js';
26
26
 
27
27
  // Tool layers
28
28
  export { buildToolsFromLayers, buildDiscoveryTools, buildDiscoveryToolsWithAliases, activateServerTools, resolveCanonicalTools, toolAliasMap, buildSystemPromptWithAliases, flattenPathMaps, buildDiscoveryCache } from './tool-layers.js';
29
- export type { ToolLayer, McpLayer, WebMcpLayer, SystemPromptResult, DiscoveryToolsResult, SchemaTransformOptions, BuildToolsResult } from './tool-layers.js';
29
+ export type { ToolLayer, McpLayer, WebMcpLayer, SystemPromptResult, DiscoveryToolsResult, SchemaTransformOptions, BuildToolsResult, ProviderKind } from './tool-layers.js';
30
30
 
31
31
  // Discovery cache
32
32
  export { DiscoveryCache, DISCOVERY_TOOL_NAMES } from './discovery-cache.js';
package/src/loop.ts CHANGED
@@ -28,11 +28,18 @@ const DISCOVERY_TOOL_NAMES = new Set([
28
28
  ]);
29
29
 
30
30
  function isDiscoveryTool(prefixedName: string): boolean {
31
- const match = prefixedName.match(/^.+?_(mcp|webmcp)_(.+)$/);
31
+ const match = prefixedName.match(/^.+?_(data|ui)_(.+)$/);
32
32
  if (!match) return false;
33
33
  return DISCOVERY_TOOL_NAMES.has(match[2]);
34
34
  }
35
35
 
36
+ /** Map a tool-name token (data/ui) back to its protocol discriminator (mcp/webmcp). */
37
+ function tokenToProtocol(token: string): 'mcp' | 'webmcp' | null {
38
+ if (token === 'data') return 'mcp';
39
+ if (token === 'ui') return 'webmcp';
40
+ return null;
41
+ }
42
+
36
43
  /**
37
44
  * Compress old tool_result blocks in conversation history.
38
45
  * IMPORTANT: mutates messages in-place intentionally to reduce memory usage.
@@ -91,7 +98,6 @@ export interface AgentLoopOptions {
91
98
  layers?: ToolLayer[];
92
99
  maxIterations?: number;
93
100
  maxTokens?: number;
94
- maxTools?: number;
95
101
  temperature?: number;
96
102
  topK?: number;
97
103
  cacheEnabled?: boolean;
@@ -124,7 +130,6 @@ export async function runAgentLoop(
124
130
  provider,
125
131
  maxIterations = 5,
126
132
  maxTokens,
127
- maxTools,
128
133
  temperature,
129
134
  topK,
130
135
  cacheEnabled = true,
@@ -179,14 +184,14 @@ export async function runAgentLoop(
179
184
  if (options.systemPrompt) {
180
185
  baseSystemPrompt = options.systemPrompt;
181
186
  } else {
182
- const sp = buildSystemPromptWithAliases(options.layers ?? []);
187
+ const sp = buildSystemPromptWithAliases(options.layers ?? [], {
188
+ providerKind: provider.promptKind ?? 'generic',
189
+ });
183
190
  baseSystemPrompt = sp.prompt;
184
191
  for (const [k, v] of sp.aliasMap) localAliasMap.set(k, v);
185
192
  }
186
193
 
187
- const systemPrompt = maxTokens
188
- ? `${baseSystemPrompt}\n\nIMPORTANT: Limit your responses to ${maxTokens} tokens.`
189
- : baseSystemPrompt;
194
+ const systemPrompt = baseSystemPrompt;
190
195
 
191
196
  const messages: ChatMessage[] = [
192
197
  ...initialMessages.map(m => ({
@@ -264,7 +269,7 @@ export async function runAgentLoop(
264
269
  const t0 = performance.now();
265
270
  let streamingText = '';
266
271
  const response = await provider.chat(messages, iterationTools, {
267
- signal, cacheEnabled, system: iterationSystemPrompt, maxTokens, maxTools, temperature, topK,
272
+ signal, cacheEnabled, system: iterationSystemPrompt, maxTokens, temperature, topK,
268
273
  onToken: callbacks.onToken ? (token) => {
269
274
  callbacks.onToken!(token);
270
275
  streamingText += token;
@@ -332,12 +337,12 @@ export async function runAgentLoop(
332
337
  // Resolve alias (canonical name → real tool name) if needed
333
338
  const resolvedName = localAliasMap.get(name) ?? name;
334
339
 
335
- // Parse tool name: {serverName}_{protocol}_{toolName}
336
- const toolMatch = resolvedName.match(/^(.+?)_(mcp|webmcp)_(.+)$/);
340
+ // Parse tool name: {serverName}_{token}_{toolName} where token ∈ {data, ui}
341
+ const toolMatch = resolvedName.match(/^(.+?)_(data|ui)_(.+)$/);
337
342
 
338
343
  // ── Discovery cache — resolve search/list/get locally if cached ──
339
344
  if (discoveryCache && toolMatch) {
340
- const cached = discoveryCache.resolve(toolMatch[1], toolMatch[3], block.input as Record<string, unknown>);
345
+ const cached = discoveryCache.resolve(toolMatch[1], toolMatch[3], block.input as Record<string, unknown>, trace);
341
346
  if (cached !== null) {
342
347
  result = cached;
343
348
  // Store + push result, then continue to next tool block
@@ -355,7 +360,8 @@ export async function runAgentLoop(
355
360
  // ── Intercept list_tools / search_tools (local pseudo-tools) ──
356
361
  // These are read-only discovery operations — do NOT activate the server.
357
362
  if (toolMatch && (toolMatch[3] === 'list_tools' || toolMatch[3] === 'search_tools')) {
358
- const [, serverName, protocol, pseudoTool] = toolMatch;
363
+ const [, serverName, token, pseudoTool] = toolMatch;
364
+ const protocol = tokenToProtocol(token);
359
365
  const layer = (options.layers ?? []).find(l => sanitizeServerName(l.serverName) === serverName && l.protocol === protocol);
360
366
  if (!layer) {
361
367
  result = 'Error: server not found';
@@ -381,10 +387,11 @@ export async function runAgentLoop(
381
387
 
382
388
  // Parse tool name to extract server — activate on first contact
383
389
  {
384
- const activateMatch = resolvedName.match(/^(.+?)_(mcp|webmcp)_(.+)$/);
390
+ const activateMatch = resolvedName.match(/^(.+?)_(data|ui)_(.+)$/);
385
391
  if (activateMatch) {
386
- const [, serverName, protocol] = activateMatch;
387
- const serverKey = `${serverName}_${protocol}`;
392
+ const [, serverName, token] = activateMatch;
393
+ const protocol = tokenToProtocol(token);
394
+ const serverKey = `${serverName}_${token}`;
388
395
  if (!activatedServers.has(serverKey)) {
389
396
  activatedServers.add(serverKey);
390
397
  const layer = (options.layers ?? []).find(l => sanitizeServerName(l.serverName) === serverName && l.protocol === protocol);
@@ -395,10 +402,11 @@ export async function runAgentLoop(
395
402
  }
396
403
  }
397
404
  if (!toolMatch) {
398
- trace.push('dispatch', name, `unknown tool format, expected {source}_{protocol}_{tool}`, 'error');
399
- result = `Error: unknown tool format "${name}". Expected {source}_{protocol}_{tool}.`;
405
+ trace.push('dispatch', name, `unknown tool format, expected {source}_{token}_{tool} where token in {data,ui}`, 'error');
406
+ result = `Error: unknown tool format "${name}". Expected {source}_{token}_{tool} where token in {data,ui}.`;
400
407
  } else {
401
- const [, serverName, protocol, realToolName] = toolMatch;
408
+ const [, serverName, token, realToolName] = toolMatch;
409
+ const protocol = tokenToProtocol(token);
402
410
 
403
411
  // Auto-repair + validate params before dispatch
404
412
  let toolInput = block.input as Record<string, unknown>;
@@ -584,7 +592,7 @@ export async function runAgentLoop(
584
592
 
585
593
  // Track iterations without render — widget_display means a render happened
586
594
  const renderedThisIteration = toolBlocks.some(b => {
587
- const match = b.name.match(/^.+?_(mcp|webmcp)_(.+)$/);
595
+ const match = b.name.match(/^.+?_(data|ui)_(.+)$/);
588
596
  return match && match[2] === 'widget_display';
589
597
  });
590
598
  if (renderedThisIteration) {