@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 +2 -1
- package/src/auto-repair.ts +2 -2
- package/src/diagnostics.ts +5 -1
- package/src/index.ts +4 -1
- package/src/loop.ts +73 -11
- package/src/nano-rag/mod.ts +1 -1
- package/src/nano-rag/quantizer-bridge.ts +4 -0
- package/src/pipeline-trace.ts +32 -0
- package/src/providers/wasm.ts +30 -5
- package/src/recipe-registry.ts +5 -6
- package/src/recipes/_generated.ts +7 -7
- package/src/recipes/afficher-oeuvres-art-collection-musee.md +5 -5
- package/src/recipes/gallery-images.md +2 -2
- package/src/recipes/widgets/cards.md +2 -1
- package/src/recipes/widgets/gallery.md +1 -0
- package/src/token-tracker.ts +2 -1
- package/src/tool-layers.ts +82 -42
- package/src/types.ts +5 -2
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webmcp-auto-ui/agent",
|
|
3
|
-
"version": "2.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
|
".": {
|
package/src/auto-repair.ts
CHANGED
|
@@ -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
|
}
|
package/src/diagnostics.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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.
|
|
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 =
|
|
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.
|
|
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':
|
|
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
|
|
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.
|
|
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)_(.+)$/);
|
package/src/nano-rag/mod.ts
CHANGED
|
@@ -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';
|
|
@@ -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
|
+
}
|
package/src/providers/wasm.ts
CHANGED
|
@@ -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 {
|
|
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)
|
|
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
|
|
540
|
-
else if (ch
|
|
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
|
package/src/recipe-registry.ts
CHANGED
|
@@ -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
|
|
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': ['
|
|
30
|
-
'
|
|
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 -
|
|
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 ("
|
|
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.
|
|
52
|
+
.filter(o => o.primaryImageSmall)
|
|
53
53
|
.map(o => ({
|
|
54
|
-
src: o.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
49
|
+
.filter(o => o.primaryImageSmall)
|
|
50
50
|
.map(o => ({
|
|
51
|
-
src: o.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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é
|
package/src/token-tracker.ts
CHANGED
|
@@ -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 =
|
|
75
|
+
this._metrics.isWasm = isWasm;
|
|
75
76
|
|
|
76
77
|
// Rolling per-minute rates
|
|
77
78
|
const oneMinAgo = now - 60_000;
|
package/src/tool-layers.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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):
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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 —
|
|
399
|
+
ÉTAPE 1 — Liste toutes les recettes
|
|
356
400
|
|
|
357
|
-
Cherche une recette pertinente
|
|
401
|
+
Cherche une recette pertinente parmi celles-ci :
|
|
358
402
|
|
|
359
|
-
${
|
|
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 —
|
|
408
|
+
ÉTAPE 1b — Recherche de recettes
|
|
365
409
|
|
|
366
|
-
Aucune recette trouvée par
|
|
410
|
+
Aucune recette trouvée par liste. Recherche avec un ou des mot-clé(s) extrait de la demande :
|
|
367
411
|
|
|
368
|
-
${
|
|
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 —
|
|
418
|
+
ÉTAPE 1c — Liste des outils
|
|
375
419
|
|
|
376
|
-
Aucune recette applicable.
|
|
420
|
+
Aucune recette applicable. Liste un outil pertinent :
|
|
377
421
|
|
|
378
|
-
${
|
|
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 —
|
|
427
|
+
ÉTAPE 1d — Recherche d'outils
|
|
384
428
|
|
|
385
|
-
${
|
|
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
|
|
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
|
-
|
|
449
|
+
ÉTAPE 5 — Fallback
|
|
406
450
|
|
|
407
|
-
|
|
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
|
-
|
|
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;
|