@webmcp-auto-ui/agent 2.5.5 → 2.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@webmcp-auto-ui/agent",
3
- "version": "2.5.5",
3
+ "version": "2.5.7",
4
4
  "description": "LLM agent loop + Anthropic/Gemma LiteRT providers + MCP wrapper",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
7
+ "sideEffects": true,
7
8
  "main": "./src/index.ts",
8
9
  "exports": {
9
10
  ".": {
@@ -82,8 +82,8 @@ export function autoRepairParams(
82
82
  params[req] = propSchema.default;
83
83
  fixes.push(`${req}: missing required → default ${JSON.stringify(propSchema.default)}`);
84
84
  }
85
- // If enum with single value, use it
86
- if (propSchema.enum && (propSchema.enum as unknown[]).length === 1) {
85
+ // If enum with single value, use it (only if not already filled by default above)
86
+ else if (propSchema.enum && (propSchema.enum as unknown[]).length === 1) {
87
87
  params[req] = (propSchema.enum as unknown[])[0];
88
88
  fixes.push(`${req}: missing required → single enum ${JSON.stringify(params[req])}`);
89
89
  }
@@ -23,6 +23,8 @@ export function runDiagnostics(
23
23
  tools: ProviderTool[],
24
24
  systemPrompt: string,
25
25
  schemaOptions?: { sanitize?: boolean; flatten?: boolean },
26
+ /** Original (pre-sanitize) tools — used for check #5 to detect patchable schemas */
27
+ rawTools?: ProviderTool[],
26
28
  ): Diagnostic[] {
27
29
  const diagnostics: Diagnostic[] = [];
28
30
 
@@ -102,7 +104,9 @@ export function runDiagnostics(
102
104
  }
103
105
 
104
106
  // 5. Strict mode — schemas that were auto-patched
105
- for (const tool of tools) {
107
+ // Must run on raw (pre-sanitize) schemas; sanitized tools will never show patches.
108
+ const checkTools = rawTools ?? tools;
109
+ for (const tool of checkTools) {
106
110
  const { patches } = sanitizeSchemaWithReport(tool.input_schema as JsonSchema);
107
111
  if (patches.length > 0) {
108
112
  diagnostics.push({
package/src/index.ts CHANGED
@@ -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 } from './tool-layers.js';
29
+ export type { ToolLayer, McpLayer, WebMcpLayer, SystemPromptResult, DiscoveryToolsResult, SchemaTransformOptions, BuildToolsResult } from './tool-layers.js';
30
30
 
31
31
  // Discovery cache
32
32
  export { DiscoveryCache } from './discovery-cache.js';
@@ -55,6 +55,9 @@ export type { Diagnostic } from './diagnostics.js';
55
55
  export { autoRepairParams } from './auto-repair.js';
56
56
  export type { RepairResult } from './auto-repair.js';
57
57
 
58
+ // Pipeline trace
59
+ export { PipelineTrace, type TraceEntry } from './pipeline-trace.js';
60
+
58
61
  // Nano-RAG — context compaction
59
62
  export { ContextRAG, type ContextRAGOptions } from './nano-rag/mod.js';
60
63
 
package/src/loop.ts CHANGED
@@ -9,11 +9,12 @@ import type {
9
9
  LLMProvider, ProviderTool, McpToolDef, AgentCallbacks,
10
10
  } from './types.js';
11
11
  import type { ToolLayer, SchemaTransformOptions } from './tool-layers.js';
12
- import { buildToolsFromLayers, buildSystemPromptWithAliases, buildDiscoveryToolsWithAliases, buildSystemPrompt, buildDiscoveryTools, activateServerTools, toProviderTools, sanitizeServerName, flattenPathMaps } from './tool-layers.js';
12
+ import { buildToolsFromLayers, buildSystemPromptWithAliases, buildDiscoveryToolsWithAliases, buildSystemPrompt, activateServerTools, toProviderTools, sanitizeServerName, flattenPathMaps } from './tool-layers.js';
13
13
  import type { DiscoveryCache } from './discovery-cache.js';
14
14
  import { unflattenParams, validateJsonSchema } from '@webmcp-auto-ui/core';
15
15
  import type { JsonSchema } from '@webmcp-auto-ui/core';
16
16
  import { autoRepairParams } from './auto-repair.js';
17
+ import { PipelineTrace } from './pipeline-trace.js';
17
18
 
18
19
  // Re-export buildSystemPrompt for backward compat
19
20
  export { buildSystemPrompt } from './tool-layers.js';
@@ -160,11 +161,19 @@ export async function runAgentLoop(
160
161
  // Use local alias maps (parallel-safe — no global singleton)
161
162
  const activatedServers = new Set<string>();
162
163
  const localAliasMap = new Map<string, string>();
164
+ const trace = new PipelineTrace();
163
165
 
164
- const disc = buildDiscoveryToolsWithAliases(options.layers ?? [], schemaOptions);
166
+ const disc = buildDiscoveryToolsWithAliases(options.layers ?? [], schemaOptions, trace);
165
167
  let activeTools: ProviderTool[] = disc.tools;
166
168
  for (const [k, v] of disc.aliasMap) localAliasMap.set(k, v);
167
169
 
170
+ // Log any trace warnings from initial tool build
171
+ const initTraceSummary = trace.summary();
172
+ if (initTraceSummary) {
173
+ callbacks.onTrace?.(`[pipeline-trace] init\n${initTraceSummary}`);
174
+ trace.clear();
175
+ }
176
+
168
177
  let baseSystemPrompt: string;
169
178
  if (options.systemPrompt) {
170
179
  baseSystemPrompt = options.systemPrompt;
@@ -238,7 +247,7 @@ export async function runAgentLoop(
238
247
  try {
239
248
  const ragResults = await contextRAG.query(queryText);
240
249
  if (ragResults.length > 0) {
241
- callbacks.onText?.(`[nano-rag] query "${queryText.slice(0, 40)}${queryText.length > 40 ? '…' : ''}" → ${ragResults.length} results (${ragResults.map(r => r.score.toFixed(2)).join(', ')})`);
250
+ callbacks.onTrace?.(`[nano-rag] query "${queryText.slice(0, 40)}${queryText.length > 40 ? '…' : ''}" → ${ragResults.length} results (${ragResults.map(r => r.score.toFixed(2)).join(', ')})`);
242
251
  }
243
252
  const ragContext = await contextRAG.buildContext(queryText);
244
253
  if (ragContext) {
@@ -371,7 +380,7 @@ export async function runAgentLoop(
371
380
 
372
381
  // Parse tool name to extract server — activate on first contact
373
382
  {
374
- const activateMatch = block.name.match(/^(.+?)_(mcp|webmcp)_(.+)$/);
383
+ const activateMatch = resolvedName.match(/^(.+?)_(mcp|webmcp)_(.+)$/);
375
384
  if (activateMatch) {
376
385
  const [, serverName, protocol] = activateMatch;
377
386
  const serverKey = `${serverName}_${protocol}`;
@@ -379,12 +388,13 @@ export async function runAgentLoop(
379
388
  activatedServers.add(serverKey);
380
389
  const layer = (options.layers ?? []).find(l => sanitizeServerName(l.serverName) === serverName && l.protocol === protocol);
381
390
  if (layer) {
382
- activeTools = activateServerTools(activeTools, layer, schemaOptions);
391
+ activeTools = activateServerTools(activeTools, layer, schemaOptions, trace);
383
392
  }
384
393
  }
385
394
  }
386
395
  }
387
396
  if (!toolMatch) {
397
+ trace.push('dispatch', name, `unknown tool format, expected {source}_{protocol}_{tool}`, 'error');
388
398
  result = `Error: unknown tool format "${name}". Expected {source}_{protocol}_{tool}.`;
389
399
  } else {
390
400
  const [, serverName, protocol, realToolName] = toolMatch;
@@ -396,12 +406,16 @@ export async function runAgentLoop(
396
406
  const repair = autoRepairParams(toolInput, toolDef.input_schema, realToolName);
397
407
  if (repair.fixes.length > 0) {
398
408
  toolInput = repair.params;
399
- callbacks.onText?.(`[auto-repair] ${repair.fixes.join(', ')}`);
409
+ callbacks.onTrace?.(`[auto-repair] ${repair.fixes.join(', ')}`);
410
+ for (const fix of repair.fixes) {
411
+ trace.push('repair', name, fix, 'warn');
412
+ }
400
413
  }
401
414
  // Validate after repair
402
415
  const validation = validateJsonSchema(toolInput, toolDef.input_schema as JsonSchema);
403
416
  if (!validation.valid) {
404
417
  const errors = validation.errors?.map((e: { message?: string }) => e.message ?? String(e)).join(', ') ?? 'unknown';
418
+ trace.push('validate', name, errors, 'error');
405
419
  result = `Validation error: ${errors}. Expected schema: ${JSON.stringify(toolDef.input_schema)}`;
406
420
  // Push error as tool_result and skip to next block
407
421
  toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: result });
@@ -449,7 +463,7 @@ export async function runAgentLoop(
449
463
  if (realToolName === 'widget_display' && typeof toolResult === 'object' && toolResult !== null) {
450
464
  const wr = toolResult as Record<string, unknown>;
451
465
  if (wr.widget && wr.data && !wr.error) {
452
- const widgetResult = callbacks.onWidget?.(wr.widget as string, wr.data as Record<string, unknown>);
466
+ const widgetResult = callbacks.onWidget?.(wr.widget as string, wr.data as Record<string, unknown>, serverName);
453
467
  if (widgetResult?.id) {
454
468
  result = JSON.stringify({ ...wr, id: widgetResult.id });
455
469
  }
@@ -462,7 +476,49 @@ export async function runAgentLoop(
462
476
  const id = (block.input as Record<string, unknown>).id as string;
463
477
  const actionParams = (block.input as Record<string, unknown>).params as Record<string, unknown> | undefined;
464
478
  switch (action) {
465
- case 'clear': callbacks.onClear?.(); break;
479
+ case 'clear':
480
+ callbacks.onClear?.();
481
+ // Strip old widget_display / render_* tool calls AND their
482
+ // matching tool_result blocks from the messages array so the
483
+ // LLM no longer sees them in context and cannot re-create
484
+ // the cleared widgets.
485
+ {
486
+ const strippedIds = new Set<string>();
487
+ // Pass 1: collect IDs of widget tool_use blocks
488
+ for (const msg of messages) {
489
+ if (msg.role !== 'assistant' || !Array.isArray(msg.content)) continue;
490
+ for (const b of msg.content as ContentBlock[]) {
491
+ if (b.type !== 'tool_use') continue;
492
+ const tu = b as { type: 'tool_use'; id: string; name: string };
493
+ if (tu.name.includes('widget_display') || tu.name.startsWith('render_')) {
494
+ strippedIds.add(tu.id);
495
+ }
496
+ }
497
+ }
498
+ // Pass 2: strip tool_use and matching tool_result blocks
499
+ if (strippedIds.size > 0) {
500
+ for (const msg of messages) {
501
+ if (!Array.isArray(msg.content)) continue;
502
+ msg.content = (msg.content as ContentBlock[]).filter(b => {
503
+ if (b.type === 'tool_use') {
504
+ return !strippedIds.has((b as { type: 'tool_use'; id: string }).id);
505
+ }
506
+ if (b.type === 'tool_result') {
507
+ return !strippedIds.has((b as { type: 'tool_result'; tool_use_id: string }).tool_use_id);
508
+ }
509
+ return true;
510
+ });
511
+ }
512
+ // Remove messages with empty content arrays (orphaned after stripping)
513
+ let i = messages.length;
514
+ while (i-- > 0) {
515
+ const c = messages[i].content;
516
+ if (Array.isArray(c) && c.length === 0) messages.splice(i, 1);
517
+ }
518
+ }
519
+ hasRendered = false;
520
+ }
521
+ break;
466
522
  case 'update': callbacks.onUpdate?.(id, actionParams ?? {}); break;
467
523
  case 'move': callbacks.onMove?.(id, (actionParams?.x ?? (block.input as any).x) as number, (actionParams?.y ?? (block.input as any).y) as number); break;
468
524
  case 'resize': callbacks.onResize?.(id, (actionParams?.width ?? (block.input as any).width) as number, (actionParams?.height ?? (block.input as any).height) as number); break;
@@ -482,8 +538,7 @@ export async function runAgentLoop(
482
538
 
483
539
  // Nano-RAG: ingest tool result for future context retrieval
484
540
  if (contextRAG && result) {
485
- const toolMatch2 = resolvedName.match(/^(.+?)_(mcp|webmcp)_(.+)$/);
486
- const realName = toolMatch2 ? toolMatch2[3] : block.name;
541
+ const realName = toolMatch ? toolMatch[3] : block.name;
487
542
  const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
488
543
  // Find the last user message for contextual embeddings
489
544
  const lastUserText = [...messages].reverse()
@@ -493,7 +548,7 @@ export async function runAgentLoop(
493
548
  : (lastUserText as any[])?.find((b: any) => b.type === 'text')?.text ?? '';
494
549
  contextRAG.ingest(realName, block.id, resultStr, userQuery).then((chunkCount) => {
495
550
  if (chunkCount > 0) {
496
- callbacks.onText?.(`[nano-rag] ingested ${chunkCount} chunks from ${realName} (${resultStr.length} chars)`);
551
+ callbacks.onTrace?.(`[nano-rag] ingested ${chunkCount} chunks from ${realName} (${resultStr.length} chars)`);
497
552
  }
498
553
  }).catch(() => {});
499
554
  }
@@ -513,6 +568,13 @@ export async function runAgentLoop(
513
568
 
514
569
  messages.push({ role: 'user', content: toolResults });
515
570
 
571
+ // Flush pipeline trace warnings to agent console
572
+ const traceSummary = trace.summary();
573
+ if (traceSummary) {
574
+ callbacks.onTrace?.(`[pipeline-trace]\n${traceSummary}`);
575
+ trace.clear();
576
+ }
577
+
516
578
  // Track iterations without render — widget_display means a render happened
517
579
  const renderedThisIteration = toolBlocks.some(b => {
518
580
  const match = b.name.match(/^.+?_(mcp|webmcp)_(.+)$/);
@@ -2,4 +2,4 @@ export { ContextRAG, type ContextRAGOptions } from './context-rag.js';
2
2
  export { chunkToolResult, type Chunk } from './chunker.js';
3
3
  export { Embedder, EMBEDDING_DIMS } from './embedder.js';
4
4
  export { VectorIndex, type SearchResult, type IndexEntry } from './index.js';
5
- export { loadQuantizer, type QuantizerInstance } from './quantizer-bridge.js';
5
+ export { loadQuantizer, resetQuantizer, type QuantizerInstance } from './quantizer-bridge.js';
@@ -113,3 +113,7 @@ export async function loadQuantizer(): Promise<QuantizerInstance> {
113
113
 
114
114
  return instance;
115
115
  }
116
+
117
+ export function resetQuantizer(): void {
118
+ instance = null;
119
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * PipelineTrace — lightweight tracing for the tool pipeline.
3
+ * Accumulates what happens at each step (sanitize, flatten, validate, repair, parse, dispatch).
4
+ * When everything is fine → nothing logged. When a step degrades data → warning visible in agent console.
5
+ */
6
+
7
+ export interface TraceEntry {
8
+ step: string; // "sanitize", "flatten", "validate", "repair", "parse", "dispatch"
9
+ tool: string; // tool name
10
+ detail: string; // human-readable description
11
+ level: 'ok' | 'warn' | 'error';
12
+ }
13
+
14
+ export class PipelineTrace {
15
+ entries: TraceEntry[] = [];
16
+
17
+ push(step: string, tool: string, detail: string, level: 'ok' | 'warn' | 'error' = 'ok') {
18
+ this.entries.push({ step, tool, detail, level });
19
+ }
20
+
21
+ get warnings() { return this.entries.filter(e => e.level !== 'ok'); }
22
+
23
+ hasErrors() { return this.entries.some(e => e.level === 'error'); }
24
+
25
+ summary(): string {
26
+ const w = this.warnings;
27
+ if (w.length === 0) return '';
28
+ return w.map(e => `[${e.step}] ${e.tool}: ${e.detail}`).join('\n');
29
+ }
30
+
31
+ clear() { this.entries.length = 0; }
32
+ }
@@ -4,6 +4,7 @@
4
4
  * Uses dynamic import() to avoid bundling MediaPipe when only Claude is used.
5
5
  */
6
6
  import type { LLMProvider, LLMResponse, ChatMessage, AnthropicTool, WasmModelId, ContentBlock } from '../types.js';
7
+ import type { PipelineTrace } from '../pipeline-trace.js';
7
8
 
8
9
  export type WasmStatus = 'idle' | 'loading' | 'ready' | 'error';
9
10
 
@@ -23,6 +24,9 @@ export class WasmProvider implements LLMProvider {
23
24
  readonly name = 'wasm';
24
25
  readonly model: string;
25
26
 
27
+ /** Optional pipeline trace — set externally to trace parsing strategy fallbacks */
28
+ trace?: PipelineTrace;
29
+
26
30
  private inference: any = null; // LlmInference
27
31
  private status: WasmStatus = 'idle';
28
32
  private opts: WasmProviderOptions;
@@ -222,7 +226,7 @@ export class WasmProvider implements LLMProvider {
222
226
  while (messages.length > MAX_MESSAGES) {
223
227
  messages = messages.slice(1);
224
228
  }
225
- prompt = this.buildPrompt(messages, tools, options?.system);
229
+ prompt = this.buildPrompt(messages, tools, options?.system, options?.maxTools);
226
230
 
227
231
  // Token-based clipping: if prompt is still too large, drop oldest messages
228
232
  const maxPromptTokens = (this.opts.contextSize ?? 4096) - 512;
@@ -235,6 +239,15 @@ export class WasmProvider implements LLMProvider {
235
239
  // sizeInTokens not available — skip clipping
236
240
  }
237
241
 
242
+ // Count input tokens for usage reporting (TokenBubble Ctx ratio)
243
+ let inputTokenCount = 0;
244
+ try {
245
+ inputTokenCount = this.inference.sizeInTokens(prompt);
246
+ } catch {
247
+ // sizeInTokens not available — estimate from char count
248
+ inputTokenCount = Math.round(prompt.length / 4);
249
+ }
250
+
238
251
  // Generate
239
252
  const t0 = performance.now();
240
253
  let fullText = '';
@@ -389,7 +402,10 @@ export class WasmProvider implements LLMProvider {
389
402
  // Strategy 2: If no pairs found, try simple replacement + JSON.parse
390
403
  if (Object.keys(toolArgs).length === 0) {
391
404
  const argsStr = rawArgs.replace(/<\|"\|>/g, '"');
392
- try { toolArgs = JSON.parse(argsStr); } catch {
405
+ try {
406
+ toolArgs = JSON.parse(argsStr);
407
+ this.trace?.push('parse', toolName, 'fell back to quote replacement strategy', 'warn');
408
+ } catch {
393
409
  // Strategy 3: regex key:value extraction on replaced string
394
410
  try {
395
411
  const obj: Record<string, unknown> = {};
@@ -402,7 +418,10 @@ export class WasmProvider implements LLMProvider {
402
418
  else if (arrVal !== undefined) { try { obj[k] = JSON.parse(arrVal); } catch { obj[k] = arrVal; } }
403
419
  else if (litVal !== undefined) obj[k] = JSON.parse(litVal);
404
420
  }
405
- if (Object.keys(obj).length > 0) toolArgs = obj;
421
+ if (Object.keys(obj).length > 0) {
422
+ toolArgs = obj;
423
+ this.trace?.push('parse', toolName, 'fell back to regex key:value strategy', 'warn');
424
+ }
406
425
  } catch {}
407
426
  }
408
427
  }
@@ -492,6 +511,10 @@ export class WasmProvider implements LLMProvider {
492
511
  totalTokens: realTokenCount,
493
512
  latencyMs,
494
513
  },
514
+ usage: {
515
+ input_tokens: inputTokenCount,
516
+ output_tokens: realTokenCount,
517
+ },
495
518
  };
496
519
  }
497
520
 
@@ -536,8 +559,10 @@ export class WasmProvider implements LLMProvider {
536
559
  let i = m.index + m[0].length;
537
560
  while (i < raw.length && depth > 0) {
538
561
  const ch = raw[i];
539
- if (ch === opener || ch === '{' || ch === '[') depth++;
540
- else if (ch === closer || ch === '}' || ch === ']') depth--;
562
+ if (ch === opener) depth++;
563
+ else if (ch !== opener && (ch === '{' || ch === '[')) depth++;
564
+ else if (ch === closer) depth--;
565
+ else if (ch !== closer && (ch === '}' || ch === ']')) depth--;
541
566
  i++;
542
567
  }
543
568
  const fragment = raw.slice(m.index + m[0].length - 1, i); // includes opener and closer
@@ -22,13 +22,12 @@ export function registerRecipes(recipes: Recipe[]): void {
22
22
  /**
23
23
  * Filter recipes that match at least one of the given server names.
24
24
  * Uses substring matching: recipe server "tricoteuses" matches connected server
25
- * "Tricoteuses - Moulineuse" or "tricoteuses-mcp".
25
+ * "Tricoteuses" or "tricoteuses-mcp".
26
26
  */
27
27
  // Known aliases: servers that are the same but have different names
28
28
  const SERVER_ALIASES: Record<string, string[]> = {
29
- 'tricoteuses': ['moulineuse', 'code4code', 'mcp.code4code.eu'],
30
- 'moulineuse': ['tricoteuses', 'code4code', 'mcp.code4code.eu'],
31
- 'code4code': ['tricoteuses', 'moulineuse', 'mcp.code4code.eu'],
29
+ 'tricoteuses': ['code4code', 'mcp.code4code.eu'],
30
+ 'code4code': ['tricoteuses', 'mcp.code4code.eu'],
32
31
  };
33
32
 
34
33
  export function filterRecipesByServer(recipes: Recipe[], serverNames: string[]): Recipe[] {
@@ -39,7 +38,7 @@ export function filterRecipesByServer(recipes: Recipe[], serverNames: string[]):
39
38
  const lower = name.toLowerCase();
40
39
  expanded.add(lower);
41
40
  for (const alias of SERVER_ALIASES[lower] ?? []) expanded.add(alias);
42
- // Also add parts of compound names ("Tricoteuses - Moulineuse" → tricoteuses, moulineuse)
41
+ // Also add parts of compound names ("Tricoteuses - code4code" → tricoteuses, code4code)
43
42
  for (const part of lower.split(/[\s,\-–]+/)) {
44
43
  if (part.length > 2) {
45
44
  expanded.add(part);
@@ -80,7 +79,7 @@ export function formatMcpRecipesForPrompt(recipes: McpRecipe[]): string {
80
79
  return recipes.map(r => {
81
80
  const id = r.name ?? '';
82
81
  const desc = r.description ?? '';
83
- // If name is missing or looks like a prefix ("moulineuse: undefined"), use description as display
82
+ // If name is missing or looks like a prefix ("tricoteuses: undefined"), use description as display
84
83
  if (!id || id.includes('undefined')) {
85
84
  return `- ${desc}`;
86
85
  }
@@ -49,9 +49,9 @@ Le serveur Met Museum donne acces a la collection du Metropolitan Museum of Art
49
49
  \`\`\`
50
50
  component("gallery", {
51
51
  images: objects
52
- .filter(o => o.primaryImage)
52
+ .filter(o => o.primaryImageSmall)
53
53
  .map(o => ({
54
- src: o.primaryImage,
54
+ src: o.primaryImageSmall,
55
55
  alt: o.title + " — " + o.artistDisplayName,
56
56
  caption: o.objectDate + " | " + o.medium
57
57
  }))
@@ -97,7 +97,7 @@ objectIDs.slice(0, 8).forEach(id => get_object({objectID: id}))
97
97
 
98
98
  // 3. Rendu
99
99
  component("stat-card", {label: "Oeuvres de Van Gogh", value: "8", icon: "palette"})
100
- component("gallery", {images: vanGoghWorks.map(w => ({src: w.primaryImage, alt: w.title, caption: w.objectDate}))})
100
+ component("gallery", {images: vanGoghWorks.map(w => ({src: w.primaryImageSmall, alt: w.title, caption: w.objectDate}))})
101
101
  component("cards", {items: vanGoghWorks.map(w => ({title: w.title, subtitle: w.objectDate, image: w.primaryImageSmall, body: w.medium}))})
102
102
  \`\`\`
103
103
 
@@ -107,7 +107,7 @@ component("cards", {items: vanGoghWorks.map(w => ({title: w.title, subtitle: w.o
107
107
  search_objects({query: "egypt pharaoh", departmentId: 10, hasImages: true})
108
108
 
109
109
  // 2. Rendu avec metadonnees culturelles
110
- component("gallery", {images: egyptWorks.map(w => ({src: w.primaryImage, alt: w.title}))})
110
+ component("gallery", {images: egyptWorks.map(w => ({src: w.primaryImageSmall, alt: w.title}))})
111
111
  component("table", {columns: ["Titre", "Periode", "Culture", "Medium"], rows: egyptDetails})
112
112
  component("kv", {pairs: [["Departement", "Egyptian Art"], ["Source", "Met Museum — Open Access"]]})
113
113
  \`\`\`
@@ -116,7 +116,7 @@ component("kv", {pairs: [["Departement", "Egyptian Art"], ["Source", "Met Museum
116
116
 
117
117
  - **Trop d'appels \`get_object\`** : la recherche retourne parfois des centaines d'IDs — limiter a 5-10 appels detail pour la performance
118
118
  - **Oeuvres sans image** : beaucoup d'objets Met n'ont pas de \`primaryImage\` — toujours filtrer avec \`hasImages: true\` dans la recherche ou verifier le champ
119
- - **Images basse resolution** : utiliser \`primaryImage\` (haute resolution) pour la galerie, \`primaryImageSmall\` pour les cards/thumbnails
119
+ - **Images haute resolution cassees** : utiliser \`primaryImageSmall\` (web-large) pour la galerie et les cards — les URLs \`primaryImage\` (original) retournent souvent des 404
120
120
  - **Oublier la licence** : les oeuvres en domaine public (\`isPublicDomain: true\`) peuvent etre affichees librement, les autres ont un champ \`rights\` a respecter
121
121
  - **Artiste inconnu** : beaucoup d'oeuvres anciennes n'ont pas d'\`artistDisplayName\` — afficher "Artiste inconnu" ou la culture/periode a la place
122
122
  `,
@@ -715,7 +715,7 @@ Etape 2 : pour chaque ID, \`get_object({objectID: id})\` → \`{primaryImage, ti
715
715
  \`\`\`
716
716
  component("gallery", {
717
717
  images: objects.map(obj => ({
718
- src: obj.primaryImage,
718
+ src: obj.primaryImageSmall,
719
719
  alt: obj.title + " — " + obj.artistDisplayName,
720
720
  caption: obj.objectDate
721
721
  }))
@@ -740,7 +740,7 @@ component("gallery", {
740
740
 
741
741
  ## Erreurs courantes
742
742
 
743
- - **Inventer des URLs placeholder** (\`https://example.com/image.jpg\`) — strictement INTERDIT
743
+ - **Inventer des URLs placeholder** (\`https://example.com/image.jpg\`, \`via.placeholder.com\`, \`placehold.co\`, \`dummyimage.com\`, \`?text=...\`) — strictement INTERDIT. Si aucune image réelle n'est retournée par l'API, ne PAS afficher de galerie.
744
744
  - **Oublier de verifier** que le champ image existe dans les donnees retournees (certains objets Met Museum n'ont pas de \`primaryImage\`)
745
745
  - **Utiliser \`text\` pour afficher des URLs** au lieu de \`gallery\` — les images doivent etre rendues visuellement
746
746
  - **Ne pas adapter la taille** : iNaturalist retourne des thumbnails "square" par defaut, remplacer par "medium" ou "large" dans l'URL
@@ -46,9 +46,9 @@ Le serveur Met Museum donne acces a la collection du Metropolitan Museum of Art
46
46
  ```
47
47
  component("gallery", {
48
48
  images: objects
49
- .filter(o => o.primaryImage)
49
+ .filter(o => o.primaryImageSmall)
50
50
  .map(o => ({
51
- src: o.primaryImage,
51
+ src: o.primaryImageSmall,
52
52
  alt: o.title + " — " + o.artistDisplayName,
53
53
  caption: o.objectDate + " | " + o.medium
54
54
  }))
@@ -94,7 +94,7 @@ objectIDs.slice(0, 8).forEach(id => get_object({objectID: id}))
94
94
 
95
95
  // 3. Rendu
96
96
  component("stat-card", {label: "Oeuvres de Van Gogh", value: "8", icon: "palette"})
97
- component("gallery", {images: vanGoghWorks.map(w => ({src: w.primaryImage, alt: w.title, caption: w.objectDate}))})
97
+ component("gallery", {images: vanGoghWorks.map(w => ({src: w.primaryImageSmall, alt: w.title, caption: w.objectDate}))})
98
98
  component("cards", {items: vanGoghWorks.map(w => ({title: w.title, subtitle: w.objectDate, image: w.primaryImageSmall, body: w.medium}))})
99
99
  ```
100
100
 
@@ -104,7 +104,7 @@ component("cards", {items: vanGoghWorks.map(w => ({title: w.title, subtitle: w.o
104
104
  search_objects({query: "egypt pharaoh", departmentId: 10, hasImages: true})
105
105
 
106
106
  // 2. Rendu avec metadonnees culturelles
107
- component("gallery", {images: egyptWorks.map(w => ({src: w.primaryImage, alt: w.title}))})
107
+ component("gallery", {images: egyptWorks.map(w => ({src: w.primaryImageSmall, alt: w.title}))})
108
108
  component("table", {columns: ["Titre", "Periode", "Culture", "Medium"], rows: egyptDetails})
109
109
  component("kv", {pairs: [["Departement", "Egyptian Art"], ["Source", "Met Museum — Open Access"]]})
110
110
  ```
@@ -113,6 +113,6 @@ component("kv", {pairs: [["Departement", "Egyptian Art"], ["Source", "Met Museum
113
113
 
114
114
  - **Trop d'appels `get_object`** : la recherche retourne parfois des centaines d'IDs — limiter a 5-10 appels detail pour la performance
115
115
  - **Oeuvres sans image** : beaucoup d'objets Met n'ont pas de `primaryImage` — toujours filtrer avec `hasImages: true` dans la recherche ou verifier le champ
116
- - **Images basse resolution** : utiliser `primaryImage` (haute resolution) pour la galerie, `primaryImageSmall` pour les cards/thumbnails
116
+ - **Images haute resolution cassees** : utiliser `primaryImageSmall` (web-large) pour la galerie et les cards — les URLs `primaryImage` (original) retournent souvent des 404
117
117
  - **Oublier la licence** : les oeuvres en domaine public (`isPublicDomain: true`) peuvent etre affichees librement, les autres ont un champ `rights` a respecter
118
118
  - **Artiste inconnu** : beaucoup d'oeuvres anciennes n'ont pas d'`artistDisplayName` — afficher "Artiste inconnu" ou la culture/periode a la place
@@ -55,7 +55,7 @@ Etape 2 : pour chaque ID, `get_object({objectID: id})` → `{primaryImage, title
55
55
  ```
56
56
  component("gallery", {
57
57
  images: objects.map(obj => ({
58
- src: obj.primaryImage,
58
+ src: obj.primaryImageSmall,
59
59
  alt: obj.title + " — " + obj.artistDisplayName,
60
60
  caption: obj.objectDate
61
61
  }))
@@ -80,7 +80,7 @@ component("gallery", {
80
80
 
81
81
  ## Erreurs courantes
82
82
 
83
- - **Inventer des URLs placeholder** (`https://example.com/image.jpg`) — strictement INTERDIT
83
+ - **Inventer des URLs placeholder** (`https://example.com/image.jpg`, `via.placeholder.com`, `placehold.co`, `dummyimage.com`, `?text=...`) — strictement INTERDIT. Si aucune image réelle n'est retournée par l'API, ne PAS afficher de galerie.
84
84
  - **Oublier de verifier** que le champ image existe dans les donnees retournees (certains objets Met Museum n'ont pas de `primaryImage`)
85
85
  - **Utiliser `text` pour afficher des URLs** au lieu de `gallery` — les images doivent etre rendues visuellement
86
86
  - **Ne pas adapter la taille** : iNaturalist retourne des thumbnails "square" par defaut, remplacer par "medium" ou "large" dans l'URL
@@ -36,5 +36,6 @@ Pour afficher une collection d'éléments riches — produits, articles, projets
36
36
  2. Appeler `autoui_webmcp_widget_display('cards', { title: 'Projets actifs', cards: [{ title: 'Refonte UI', description: 'Migration vers Svelte 5', subtitle: 'Q2 2024', tags: ['frontend', 'priorité haute'] }] })`
37
37
 
38
38
  ## Erreurs courantes
39
- - Ne JAMAIS inventer d'URLs d'images pour le champ `image`. Utiliser UNIQUEMENT les URLs retournées par les outils MCP (query_sql, search_recipes, etc.). Si aucune URL n'est disponible, ne pas inclure de champ image.
39
+ - Ne JAMAIS inventer d'URLs d'images pour le champ `image`. Utiliser UNIQUEMENT les URLs retournées par les outils MCP. Si aucune URL n'est disponible, ne pas inclure de champ image — le widget s'affiche correctement sans.
40
+ - STRICTEMENT INTERDIT : URLs placeholder (`via.placeholder.com`, `placehold.co`, `dummyimage.com`, `?text=...`). Omettre le champ `image` plutôt que de mettre un placeholder.
40
41
  - Toujours fournir un `title` pour chaque carte
@@ -35,4 +35,5 @@ Pour afficher une collection d'images en grille — galerie photo, résultats de
35
35
 
36
36
  ## Erreurs courantes
37
37
  - Ne JAMAIS fabriquer d'URLs d'images — utiliser uniquement celles retournées par les outils MCP
38
+ - STRICTEMENT INTERDIT : URLs placeholder (`via.placeholder.com`, `placehold.co`, `dummyimage.com`, `?text=...`, `example.com/image.jpg`). Si aucune image réelle n'est disponible, ne PAS afficher de galerie — utiliser un widget `text` ou `cards` sans image à la place
38
39
  - Toujours fournir un `alt` pour l'accessibilité
@@ -51,6 +51,7 @@ export class TokenTracker {
51
51
  record(
52
52
  usage: { input_tokens: number; output_tokens: number; cache_read_input_tokens?: number },
53
53
  latencyMs?: number,
54
+ isWasm = false,
54
55
  ): void {
55
56
  const now = Date.now();
56
57
  const event: TokenEvent = {
@@ -71,7 +72,7 @@ export class TokenTracker {
71
72
  this._metrics.lastCacheReadTokens = event.cacheRead;
72
73
  this._metrics.lastLatencyMs = latencyMs ?? 0;
73
74
  this._metrics.totalCachedGB = this._metrics.totalCacheReadTokens * 4 / 1e9;
74
- this._metrics.isWasm = false;
75
+ this._metrics.isWasm = isWasm;
75
76
 
76
77
  // Rolling per-minute rates
77
78
  const oneMinAgo = now - 60_000;
@@ -6,6 +6,7 @@ import type { WebMcpToolDef } from '@webmcp-auto-ui/core';
6
6
  import { sanitizeSchema, sanitizeSchemaWithReport, flattenSchema } from '@webmcp-auto-ui/core';
7
7
  import type { SchemaPatch } from '@webmcp-auto-ui/core';
8
8
  import { DiscoveryCache, type ServerCache } from './discovery-cache.js';
9
+ import type { PipelineTrace } from './pipeline-trace.js';
9
10
 
10
11
  /** Sanitize a server name for use in tool name prefixes.
11
12
  * Returns a clean underscore-separated identifier with no "mcp"/"server" noise.
@@ -54,9 +55,20 @@ export interface SchemaTransformOptions {
54
55
  onSchemaPatch?: (toolName: string, patches: SchemaPatch[]) => void;
55
56
  }
56
57
 
57
- /** Path maps for flattened schemas — keyed by prefixed tool name */
58
+ /**
59
+ * @deprecated Use the `pathMaps` field returned by `buildToolsFromLayers()` instead.
60
+ * This singleton is NOT parallel-safe — concurrent agent loops will clobber each other.
61
+ * Kept for backward compatibility only; populated as a side-effect of `buildToolsFromLayers`.
62
+ */
58
63
  export const flattenPathMaps = new Map<string, Record<string, string[]>>();
59
64
 
65
+ /** Result of buildToolsFromLayers — tools + per-call path maps (parallel-safe) */
66
+ export interface BuildToolsResult {
67
+ tools: ProviderTool[];
68
+ /** Path maps for flattened schemas, keyed by prefixed tool name. Empty if flatten is off. */
69
+ pathMaps: Map<string, Record<string, string[]>>;
70
+ }
71
+
60
72
  // ── Canonical tool resolution (4-layer matching) ─────────────────────
61
73
  //
62
74
  // MCP servers expose tools with arbitrary names. We need to identify which
@@ -189,7 +201,7 @@ function isStrictCompatible(schema: Record<string, unknown>): boolean {
189
201
  }
190
202
 
191
203
  /** Convert McpToolDef[] to ProviderTool[] */
192
- export function toProviderTools(tools: McpToolDef[], schemaOptions?: SchemaTransformOptions): ProviderTool[] {
204
+ export function toProviderTools(tools: McpToolDef[], schemaOptions?: SchemaTransformOptions, trace?: PipelineTrace): ProviderTool[] {
193
205
  return tools.map(t => {
194
206
  let schema = (t.inputSchema ?? { type: 'object', properties: {}, additionalProperties: false }) as import('@webmcp-auto-ui/core').JsonSchema;
195
207
  if (schemaOptions?.sanitize !== false) {
@@ -197,6 +209,14 @@ export function toProviderTools(tools: McpToolDef[], schemaOptions?: SchemaTrans
197
209
  schema = report.schema;
198
210
  if (report.patches.length > 0) {
199
211
  schemaOptions?.onSchemaPatch?.(t.name, report.patches);
212
+ if (trace) {
213
+ for (const p of report.patches) {
214
+ const msg = p.type === 'additionalProperties'
215
+ ? `added additionalProperties:false at ${p.path}`
216
+ : `removed ${p.keyword} at ${p.path}`;
217
+ trace.push('sanitize', t.name, msg, 'warn');
218
+ }
219
+ }
200
220
  }
201
221
  }
202
222
  const schemaObj = schema as Record<string, unknown>;
@@ -214,14 +234,22 @@ export function toProviderTools(tools: McpToolDef[], schemaOptions?: SchemaTrans
214
234
  }
215
235
 
216
236
  /** Convert WebMcpToolDef[] to ProviderTool[] (Fix 12: sanitize schemas) */
217
- function webmcpToProviderTools(tools: WebMcpToolDef[], schemaOptions?: SchemaTransformOptions): ProviderTool[] {
237
+ function webmcpToProviderTools(tools: WebMcpToolDef[], schemaOptions?: SchemaTransformOptions, trace?: PipelineTrace): ProviderTool[] {
218
238
  return tools.map(t => {
219
- let schema = (t.inputSchema ?? { type: 'object', properties: {} }) as import('@webmcp-auto-ui/core').JsonSchema;
239
+ let schema = (t.inputSchema ?? { type: 'object', properties: {}, additionalProperties: false }) as import('@webmcp-auto-ui/core').JsonSchema;
220
240
  if (schemaOptions?.sanitize !== false) {
221
241
  const report = sanitizeSchemaWithReport(schema);
222
242
  schema = report.schema;
223
243
  if (report.patches.length > 0) {
224
244
  schemaOptions?.onSchemaPatch?.(t.name, report.patches);
245
+ if (trace) {
246
+ for (const p of report.patches) {
247
+ const msg = p.type === 'additionalProperties'
248
+ ? `added additionalProperties:false at ${p.path}`
249
+ : `removed ${p.keyword} at ${p.path}`;
250
+ trace.push('sanitize', t.name, msg, 'warn');
251
+ }
252
+ }
225
253
  }
226
254
  }
227
255
  const schemaObj = schema as Record<string, unknown>;
@@ -236,19 +264,20 @@ function webmcpToProviderTools(tools: WebMcpToolDef[], schemaOptions?: SchemaTra
236
264
 
237
265
  /** Build ProviderTool[] from structured layers.
238
266
  * ALL tools are prefixed: {serverName}_{protocol}_{toolName}
267
+ * Returns { tools, pathMaps } — use pathMaps instead of the deprecated flattenPathMaps singleton.
239
268
  */
240
- export function buildToolsFromLayers(layers: ToolLayer[], schemaOptions?: SchemaTransformOptions): ProviderTool[] {
269
+ export function buildToolsFromLayers(layers: ToolLayer[], schemaOptions?: SchemaTransformOptions, trace?: PipelineTrace): BuildToolsResult {
241
270
  const tools: ProviderTool[] = [];
242
271
 
243
272
  for (const layer of layers) {
244
273
  const prefix = `${sanitizeServerName(layer.serverName)}_${layer.protocol}_`;
245
274
 
246
275
  if (layer.protocol === 'mcp') {
247
- for (const tool of toProviderTools(layer.tools, schemaOptions)) {
276
+ for (const tool of toProviderTools(layer.tools, schemaOptions, trace)) {
248
277
  tools.push({ ...tool, name: `${prefix}${tool.name}` });
249
278
  }
250
279
  } else {
251
- for (const tool of webmcpToProviderTools(layer.tools, schemaOptions)) {
280
+ for (const tool of webmcpToProviderTools(layer.tools, schemaOptions, trace)) {
252
281
  tools.push({ ...tool, name: `${prefix}${tool.name}` });
253
282
  }
254
283
  }
@@ -260,21 +289,34 @@ export function buildToolsFromLayers(layers: ToolLayer[], schemaOptions?: Schema
260
289
  seen.set(tool.name, tool);
261
290
  }
262
291
 
292
+ const localPathMaps = new Map<string, Record<string, string[]>>();
293
+
263
294
  // Apply flatten if requested
264
295
  if (schemaOptions?.flatten) {
265
- flattenPathMaps.clear();
266
296
  const result = Array.from(seen.values());
267
297
  for (const tool of result) {
268
298
  const { schema: flatSchema, pathMap } = flattenSchema(tool.input_schema as import('@webmcp-auto-ui/core').JsonSchema);
269
299
  if (Object.keys(pathMap).length > 0) {
270
300
  tool.input_schema = flatSchema as Record<string, unknown>;
271
- flattenPathMaps.set(tool.name, pathMap);
301
+ localPathMaps.set(tool.name, pathMap);
302
+ }
303
+ if (trace) {
304
+ const flattenedCount = Object.keys(pathMap).filter(k => pathMap[k].length > 1).length;
305
+ const totalProps = Object.keys((tool.input_schema as Record<string, unknown>).properties ?? {}).length;
306
+ if (flattenedCount === 0 && totalProps > 0) {
307
+ trace.push('flatten', tool.name, `0/${totalProps} properties flattened`, 'warn');
308
+ }
272
309
  }
273
310
  }
274
- return result;
311
+
312
+ // Populate deprecated singleton for backward compat
313
+ flattenPathMaps.clear();
314
+ for (const [k, v] of localPathMaps) flattenPathMaps.set(k, v);
315
+
316
+ return { tools: result, pathMaps: localPathMaps };
275
317
  }
276
318
 
277
- return Array.from(seen.values());
319
+ return { tools: Array.from(seen.values()), pathMaps: localPathMaps };
278
320
  }
279
321
 
280
322
  /** Result of buildSystemPromptWithAliases — prompt text + per-call alias map */
@@ -347,42 +389,44 @@ export function buildSystemPromptWithAliases(layers: ToolLayer[]): SystemPromptR
347
389
  }
348
390
  }
349
391
 
350
- // ── Build prompt (cascade: search recipes → list recipes → search tools → list tools) ──
351
- let prompt = `Tu es un assistant IA qui aide les utilisateurs en répondant à leurs questions et en accomplissant des tâches à l'aide de recettes. Tu NE DOIS PAS sauter d'étapes.
392
+ // ── Build prompt (cascade: list recipes → search recipes → list tools → search tools) ──
393
+ let prompt = `Tu es un assistant IA qui aide les utilisateurs en répondant à leurs questions et en accomplissant des tâches à l'aide de recettes (appelées aussi skills ou recipes). Ces recettes ne sont pas des recettes de cuisine mais des instructions pour un agent IA avec des scripts, des schémas et des informations pour l'aider. Si tu ne trouve pas de recette ou d'outils appropriés, tu peux fallback sur un chat traditionnel sans tool calling (ETAPE 5).
394
+
395
+ Tu NE DOIS PAS sauter d'étapes.
352
396
 
353
397
  RÈGLE CRITIQUE : Tu DOIS exécuter toutes les étapes en silence. Ne génère AUCUNE réflexion interne, raisonnement ou texte intermédiaire.
354
398
 
355
- ÉTAPE 1 — Recherche de recette
399
+ ÉTAPE 1 — Liste toutes les recettes
356
400
 
357
- Cherche une recette pertinente avec un mot-clé extrait de la demande :
401
+ Cherche une recette pertinente parmi celles-ci :
358
402
 
359
- ${searchRecipes.join('\n')}
403
+ ${listRecipes.join('\n')}
360
404
 
361
405
  Si au moins une recette pertinente est trouvée → passe à l'ÉTAPE 2.
362
406
  Si aucun résultat → passe à l'ÉTAPE 1b.
363
407
 
364
- ÉTAPE 1b — Liste des recettes
408
+ ÉTAPE 1b — Recherche de recettes
365
409
 
366
- Aucune recette trouvée par recherche. Liste toutes les recettes disponibles :
410
+ Aucune recette trouvée par liste. Recherche avec un ou des mot-clé(s) extrait de la demande :
367
411
 
368
- ${listRecipes.join('\n')}
412
+ ${searchRecipes.join('\n')}
369
413
 
370
414
  Choisis la recette la plus pertinente par rapport à la demande.
371
415
  Si une recette correspond → passe à l'ÉTAPE 2.
372
416
  Si aucune recette disponible ou pertinente → passe à l'ÉTAPE 1c.
373
417
 
374
- ÉTAPE 1c — Recherche d'outils
418
+ ÉTAPE 1c — Liste des outils
375
419
 
376
- Aucune recette applicable. Cherche un outil pertinent :
420
+ Aucune recette applicable. Liste un outil pertinent :
377
421
 
378
- ${searchTools.join('\n')}
422
+ ${listTools.join('\n')}
379
423
 
380
424
  Si un outil pertinent est trouvé → utilise-le directement pour répondre (passe à l'ÉTAPE 3).
381
425
  Si aucun résultat → passe à l'ÉTAPE 1d.
382
426
 
383
- ÉTAPE 1d — Liste des outils
427
+ ÉTAPE 1d — Recherche d'outils
384
428
 
385
- ${listTools.join('\n')}
429
+ ${searchTools.join('\n')}
386
430
 
387
431
  Choisis le ou les outils les plus pertinents et utilise-les pour répondre (passe à l'ÉTAPE 3).
388
432
 
@@ -394,26 +438,17 @@ Lis les instructions complètes de la recette sélectionnée.
394
438
 
395
439
  ÉTAPE 3 — Exécution
396
440
 
397
- Suis les instructions de la recette exactement si tu en as une. Sinon utilise les outils directement. Produis UNIQUEMENT le résultat final : un résumé en une phrase de l'action effectuée, ainsi que le résultat.
441
+ Suis les instructions de la recette exactement si tu en as une. Sinon utilise les outils directement. Produis UNIQUEMENT le résultat final, un résumé en une phrase de l'action effectuée, ainsi que le résultat.
398
442
 
399
443
  ÉTAPE 4 — Affichage UI
400
444
 
401
- Sauf indication contraire d'une recette, utilise ces outils :
445
+ Sauf indication UI contraire d'une recette, utilise ces outils pour afficher tes réponses sur le canvas :
402
446
 
403
447
  ${actionTools.join('\n')}
404
448
 
405
- GESTION DES ERREURS D'OUTILS (RÈGLE PRIORITAIRE)
449
+ ÉTAPE 5 Fallback
406
450
 
407
- Si un outil retourne une erreur (validation, schéma, type, paramètres invalides ou rejet explicite), tu DOIS d'abord traiter cette erreur avant toute autre action.
408
-
409
- 1. Analyse uniquement le message d'erreur et le schéma attendu.
410
- 2. Corrige l'appel en respectant STRICTEMENT le schéma (pas de champs supplémentaires, types respectés).
411
- 3. Si une valeur est mal formée (ex: JSON sérialisé en string), convertis-la sans changer le contenu métier.
412
- 4. Tu n'as PAS le droit d'inventer de nouveaux formats, champs ou structures.
413
- 5. Ne change PAS de recette ou de widget tant que l'appel n'a pas été retenté au moins une fois.
414
- 6. Après deux échecs consécutifs identiques, tu peux chercher une autre recette.
415
-
416
- Ne fabrique jamais d'URLs d'images — utilise uniquement celles retournées par les outils.`;
451
+ En cas d'échec des étapes précédentes, fallback sur un chat classique sans tool calling.`;
417
452
 
418
453
  return { prompt, aliasMap };
419
454
  }
@@ -442,7 +477,7 @@ export interface DiscoveryToolsResult {
442
477
  * Build discovery-only tools with a local alias map (parallel-safe).
443
478
  * Prefer this over buildDiscoveryTools() when running multiple agent loops.
444
479
  */
445
- export function buildDiscoveryToolsWithAliases(layers: ToolLayer[], schemaOptions?: SchemaTransformOptions): DiscoveryToolsResult {
480
+ export function buildDiscoveryToolsWithAliases(layers: ToolLayer[], schemaOptions?: SchemaTransformOptions, trace?: PipelineTrace): DiscoveryToolsResult {
446
481
  const tools: ProviderTool[] = [];
447
482
  const aliasMap = new Map<string, string>();
448
483
 
@@ -450,7 +485,7 @@ export function buildDiscoveryToolsWithAliases(layers: ToolLayer[], schemaOption
450
485
  const prefix = `${sanitizeServerName(layer.serverName)}_${layer.protocol}_`;
451
486
 
452
487
  if (layer.protocol === 'mcp') {
453
- const allProviderTools = toProviderTools(layer.tools, schemaOptions);
488
+ const allProviderTools = toProviderTools(layer.tools, schemaOptions, trace);
454
489
  const matches = resolveCanonicalTools(layer.tools);
455
490
 
456
491
  for (const m of matches) {
@@ -481,7 +516,7 @@ export function buildDiscoveryToolsWithAliases(layers: ToolLayer[], schemaOption
481
516
  });
482
517
  } else {
483
518
  // WebMCP: search_recipes, list_recipes, get_recipe, plus action tools (widget_display, canvas, recall)
484
- for (const tool of webmcpToProviderTools(layer.tools, schemaOptions)) {
519
+ for (const tool of webmcpToProviderTools(layer.tools, schemaOptions, trace)) {
485
520
  if (tool.name === 'search_recipes' || tool.name === 'list_recipes' || tool.name === 'get_recipe' ||
486
521
  tool.name === 'widget_display' || tool.name === 'canvas' || tool.name === 'recall') {
487
522
  tools.push({ ...tool, name: `${prefix}${tool.name}` });
@@ -532,14 +567,15 @@ export function activateServerTools(
532
567
  currentTools: ProviderTool[],
533
568
  layer: ToolLayer,
534
569
  schemaOptions?: SchemaTransformOptions,
570
+ trace?: PipelineTrace,
535
571
  ): ProviderTool[] {
536
572
  const prefix = `${sanitizeServerName(layer.serverName)}_${layer.protocol}_`;
537
573
  const existing = new Set(currentTools.map(t => t.name));
538
574
  const newTools = [...currentTools];
539
575
 
540
576
  const layerTools = layer.protocol === 'mcp'
541
- ? toProviderTools(layer.tools, schemaOptions)
542
- : webmcpToProviderTools(layer.tools, schemaOptions);
577
+ ? toProviderTools(layer.tools, schemaOptions, trace)
578
+ : webmcpToProviderTools(layer.tools, schemaOptions, trace);
543
579
 
544
580
  for (const tool of layerTools) {
545
581
  const prefixed = `${prefix}${tool.name}`;
@@ -559,6 +595,10 @@ export function buildDiscoveryCache(layers: ToolLayer[]): DiscoveryCache {
559
595
  const cache = new DiscoveryCache();
560
596
 
561
597
  for (const layer of layers) {
598
+ // Skip WebMCP layers — their discovery tools (list_recipes, etc.) are local
599
+ // closures over the widgets Map and must be executed directly, not cached.
600
+ if (layer.protocol === 'webmcp') continue;
601
+
562
602
  const prefix = sanitizeServerName(layer.serverName);
563
603
  const serverCache: ServerCache = {
564
604
  recipes: [],
package/src/types.ts CHANGED
@@ -93,11 +93,14 @@ export interface AgentCallbacks {
93
93
  onLLMRequest?: (messages: ChatMessage[], tools: ProviderTool[]) => void;
94
94
  onLLMResponse?: (response: LLMResponse, latencyMs: number, tokens?: { input: number; output: number }) => void;
95
95
  onToolCall?: (call: ToolCall) => void;
96
- /** Called when a widget_display renders a widget. Return { id } so the LLM knows the block id. */
97
- onWidget?: (type: string, data: Record<string, unknown>) => { id: string } | void;
96
+ /** Called when a widget_display renders a widget. Return { id } so the LLM knows the block id.
97
+ * @param serverName - the WebMCP server that produced the widget (e.g. "chartjs", "d3") */
98
+ onWidget?: (type: string, data: Record<string, unknown>, serverName?: string) => { id: string } | void;
98
99
  onClear?: () => void;
99
100
  onText?: (text: string) => void;
100
101
  onToken?: (token: string) => void;
102
+ /** Called for pipeline trace / auto-repair / nano-rag diagnostics (not streamed to UI) */
103
+ onTrace?: (message: string) => void;
101
104
  onDone?: (metrics: AgentMetrics) => void;
102
105
  // Canvas mutation tools
103
106
  onUpdate?: (id: string, data: Record<string, unknown>) => void;