@webmcp-auto-ui/agent 2.5.36 → 2.5.38
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 +1 -1
- package/src/autoui-server.ts +204 -23
- package/src/discovery-cache.ts +16 -8
- package/src/index.ts +3 -3
- package/src/loop.ts +73 -34
- package/src/tool-layers.ts +2 -3
- package/src/trace-observer.ts +78 -1
package/package.json
CHANGED
package/src/autoui-server.ts
CHANGED
|
@@ -54,6 +54,40 @@ When the user wants to browse, search, or pick a recipe — for example "show me
|
|
|
54
54
|
Call widget_display({name: "recipe-browser", params: {recipes: [...], layout: "list"}}). The widget emits a bubbling 'widget:interact' CustomEvent with detail={action:"pick", payload: recipe} when the user clicks Pick.
|
|
55
55
|
`;
|
|
56
56
|
|
|
57
|
+
// Inline recipe for tool-browser (real vanilla widget)
|
|
58
|
+
const toolBrowserRecipe = `---
|
|
59
|
+
widget: tool-browser
|
|
60
|
+
description: Interactive tool browser with search/filters/preview. Use when the user wants to browse, search, or pick a tool from connected servers.
|
|
61
|
+
group: rich
|
|
62
|
+
schema:
|
|
63
|
+
type: object
|
|
64
|
+
required:
|
|
65
|
+
- tools
|
|
66
|
+
properties:
|
|
67
|
+
tools:
|
|
68
|
+
type: array
|
|
69
|
+
description: List of BrowsableTool objects (name, description, server, inputSchema, ...).
|
|
70
|
+
items:
|
|
71
|
+
type: object
|
|
72
|
+
filters:
|
|
73
|
+
type: object
|
|
74
|
+
description: Initial filters
|
|
75
|
+
properties:
|
|
76
|
+
q:
|
|
77
|
+
type: string
|
|
78
|
+
layout:
|
|
79
|
+
type: string
|
|
80
|
+
enum: [list, grid]
|
|
81
|
+
description: Default layout (default list)
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## When to use
|
|
85
|
+
When the user wants to browse, search, or pick a tool — for example "show me the available tools".
|
|
86
|
+
|
|
87
|
+
## How to use
|
|
88
|
+
Call widget_display({name: "tool-browser", params: {tools: [...]}}). The widget emits a bubbling 'widget:interact' CustomEvent on user actions.
|
|
89
|
+
`;
|
|
90
|
+
|
|
57
91
|
// ---------------------------------------------------------------------------
|
|
58
92
|
// Inline recipes (frontmatter + body)
|
|
59
93
|
// ---------------------------------------------------------------------------
|
|
@@ -306,24 +340,37 @@ schema:
|
|
|
306
340
|
title:
|
|
307
341
|
type: string
|
|
308
342
|
columns:
|
|
343
|
+
description: Column definitions. Also accepts plain strings ["A","B"]. Alias "headers" is also accepted.
|
|
309
344
|
type: array
|
|
310
345
|
items:
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
346
|
+
oneOf:
|
|
347
|
+
- type: string
|
|
348
|
+
- type: object
|
|
349
|
+
required:
|
|
350
|
+
- key
|
|
351
|
+
- label
|
|
352
|
+
properties:
|
|
353
|
+
key:
|
|
354
|
+
type: string
|
|
355
|
+
label:
|
|
356
|
+
type: string
|
|
357
|
+
align:
|
|
358
|
+
type: string
|
|
359
|
+
enum: [left, center, right]
|
|
360
|
+
headers:
|
|
361
|
+
description: Alias for columns. Accepts plain strings ["A","B"] or full column objects.
|
|
362
|
+
type: array
|
|
363
|
+
items:
|
|
364
|
+
oneOf:
|
|
365
|
+
- type: string
|
|
366
|
+
- type: object
|
|
323
367
|
rows:
|
|
368
|
+
description: Array of row objects OR array of arrays (positionally mapped to columns/headers keys).
|
|
324
369
|
type: array
|
|
325
370
|
items:
|
|
326
|
-
|
|
371
|
+
oneOf:
|
|
372
|
+
- type: object
|
|
373
|
+
- type: array
|
|
327
374
|
---
|
|
328
375
|
|
|
329
376
|
## When to use
|
|
@@ -331,6 +378,7 @@ Display structured data in a table with column sorting.
|
|
|
331
378
|
|
|
332
379
|
## How to use
|
|
333
380
|
Call widget_display({name: "data-table", params: {columns: [{key:"name",label:"Nom"}], rows: [{name:"Alice"}]}}).
|
|
381
|
+
Rows can also be arrays: {headers:["name","age"], rows:[["Alice",30],["Bob",25]]}.
|
|
334
382
|
`,
|
|
335
383
|
|
|
336
384
|
// ── timeline ─────────────────────────────────────────────────────────────
|
|
@@ -545,8 +593,6 @@ schema:
|
|
|
545
593
|
type: array
|
|
546
594
|
items:
|
|
547
595
|
type: object
|
|
548
|
-
required:
|
|
549
|
-
- values
|
|
550
596
|
properties:
|
|
551
597
|
label:
|
|
552
598
|
type: string
|
|
@@ -554,6 +600,9 @@ schema:
|
|
|
554
600
|
type: array
|
|
555
601
|
items:
|
|
556
602
|
type: number
|
|
603
|
+
value:
|
|
604
|
+
type: number
|
|
605
|
+
description: Scalar shorthand — treated as values:[value]. Use for single-point series.
|
|
557
606
|
color:
|
|
558
607
|
type: string
|
|
559
608
|
---
|
|
@@ -563,6 +612,7 @@ Pour des graphiques multi-series (barres, lignes, aires, camembert, donut).
|
|
|
563
612
|
|
|
564
613
|
## How to use
|
|
565
614
|
Call widget_display({name: "chart-rich", params: {type: "bar", labels: ["Q1","Q2"], data: [{label:"Ventes", values:[10,20]}]}}).
|
|
615
|
+
Single-point shorthand: data: [{label:"A", value:5}, {label:"B", value:8}].
|
|
566
616
|
`,
|
|
567
617
|
|
|
568
618
|
// ── cards ────────────────────────────────────────────────────────────────
|
|
@@ -571,13 +621,30 @@ widget: cards
|
|
|
571
621
|
description: Card grid (results, records, entities).
|
|
572
622
|
schema:
|
|
573
623
|
type: object
|
|
574
|
-
required:
|
|
575
|
-
- cards
|
|
576
624
|
properties:
|
|
577
625
|
title:
|
|
578
626
|
type: string
|
|
579
627
|
cards:
|
|
580
628
|
type: array
|
|
629
|
+
description: List of cards (alias: items)
|
|
630
|
+
items:
|
|
631
|
+
type: object
|
|
632
|
+
required:
|
|
633
|
+
- title
|
|
634
|
+
properties:
|
|
635
|
+
title:
|
|
636
|
+
type: string
|
|
637
|
+
description:
|
|
638
|
+
type: string
|
|
639
|
+
subtitle:
|
|
640
|
+
type: string
|
|
641
|
+
tags:
|
|
642
|
+
type: array
|
|
643
|
+
items:
|
|
644
|
+
type: string
|
|
645
|
+
items:
|
|
646
|
+
type: array
|
|
647
|
+
description: Alias for cards — use either cards or items
|
|
581
648
|
items:
|
|
582
649
|
type: object
|
|
583
650
|
required:
|
|
@@ -773,12 +840,11 @@ Call widget_display({name: "carousel", params: {slides: [{src: "https://...", ti
|
|
|
773
840
|
// ── stat-card ────────────────────────────────────────────────────────────
|
|
774
841
|
`---
|
|
775
842
|
widget: stat-card
|
|
776
|
-
description:
|
|
843
|
+
description: >
|
|
844
|
+
Enriched KPI card with unit, delta, icon, and colored variant.
|
|
845
|
+
Supports single card OR a grid of N cards via the "items" array.
|
|
777
846
|
schema:
|
|
778
847
|
type: object
|
|
779
|
-
required:
|
|
780
|
-
- label
|
|
781
|
-
- value
|
|
782
848
|
properties:
|
|
783
849
|
label:
|
|
784
850
|
type: string
|
|
@@ -787,6 +853,11 @@ schema:
|
|
|
787
853
|
unit:
|
|
788
854
|
type: string
|
|
789
855
|
description: Unite affichee apres la valeur (ex "%", "km")
|
|
856
|
+
icon:
|
|
857
|
+
type: string
|
|
858
|
+
description: >
|
|
859
|
+
Emoji, unicode symbol, or keyword (e.g. "fire", "trending-up", "star").
|
|
860
|
+
Unknown keywords fall back silently to ℹ.
|
|
790
861
|
delta:
|
|
791
862
|
type: string
|
|
792
863
|
description: Variation affichee (ex "+12%")
|
|
@@ -798,13 +869,47 @@ schema:
|
|
|
798
869
|
variant:
|
|
799
870
|
type: string
|
|
800
871
|
enum: [default, success, warning, error, info]
|
|
872
|
+
items:
|
|
873
|
+
type: array
|
|
874
|
+
description: >
|
|
875
|
+
Grid mode: pass an array of stat items to render N cards side by side.
|
|
876
|
+
When present, top-level label/value/icon are ignored.
|
|
877
|
+
items:
|
|
878
|
+
type: object
|
|
879
|
+
required:
|
|
880
|
+
- label
|
|
881
|
+
- value
|
|
882
|
+
properties:
|
|
883
|
+
label:
|
|
884
|
+
type: string
|
|
885
|
+
value:
|
|
886
|
+
type: string
|
|
887
|
+
unit:
|
|
888
|
+
type: string
|
|
889
|
+
icon:
|
|
890
|
+
type: string
|
|
891
|
+
delta:
|
|
892
|
+
type: string
|
|
893
|
+
trend:
|
|
894
|
+
type: string
|
|
895
|
+
enum: [up, down, flat]
|
|
896
|
+
previousValue:
|
|
897
|
+
type: string
|
|
898
|
+
variant:
|
|
899
|
+
type: string
|
|
900
|
+
enum: [default, success, warning, error, info]
|
|
801
901
|
---
|
|
802
902
|
|
|
803
903
|
## When to use
|
|
804
|
-
Pour un KPI enrichi avec delta, unite et variante de couleur.
|
|
904
|
+
Pour un KPI enrichi avec delta, unite, icone et variante de couleur.
|
|
905
|
+
Pour plusieurs KPIs cote a cote, utiliser le champ "items".
|
|
805
906
|
|
|
806
907
|
## How to use
|
|
807
|
-
|
|
908
|
+
Single card:
|
|
909
|
+
Call widget_display({name: "stat-card", params: {label: "Uptime", value: "99.9", unit: "%", trend: "up", variant: "success", icon: "🟢"}}).
|
|
910
|
+
|
|
911
|
+
Grid of cards:
|
|
912
|
+
Call widget_display({name: "stat-card", params: {items: [{label: "CPU", value: "42", unit: "%", icon: "💻", variant: "info"}, {label: "RAM", value: "8", unit: "GB", icon: "🗄", variant: "warning"}]}}).
|
|
808
913
|
`,
|
|
809
914
|
|
|
810
915
|
// ── grid-data ────────────────────────────────────────────────────────────
|
|
@@ -893,6 +998,78 @@ Pour des visualisations custom, animations, ou prototypes interactifs en JS pur.
|
|
|
893
998
|
|
|
894
999
|
## How to use
|
|
895
1000
|
Call widget_display({name: "js-sandbox", params: {code: "document.getElementById('root').innerHTML = '<h1>Hello</h1>'"}}).
|
|
1001
|
+
`,
|
|
1002
|
+
|
|
1003
|
+
// ── map ─────────────────────────────────────────────────────────────────
|
|
1004
|
+
`---
|
|
1005
|
+
widget: map
|
|
1006
|
+
description: Interactive geospatial map (MapLibre GL, light theme) with markers and/or GeoJSON overlay. Auto-fits view to data via Turf bbox.
|
|
1007
|
+
group: rich
|
|
1008
|
+
schema:
|
|
1009
|
+
type: object
|
|
1010
|
+
properties:
|
|
1011
|
+
markers:
|
|
1012
|
+
type: array
|
|
1013
|
+
description: Points to display as map markers. Aliases tolerated per marker — \`lng\` accepted for \`lon\`, \`popup\` accepted for \`label\`.
|
|
1014
|
+
items:
|
|
1015
|
+
type: object
|
|
1016
|
+
required:
|
|
1017
|
+
- lat
|
|
1018
|
+
properties:
|
|
1019
|
+
lat:
|
|
1020
|
+
type: number
|
|
1021
|
+
lon:
|
|
1022
|
+
type: number
|
|
1023
|
+
description: Longitude. Alias \`lng\` also accepted.
|
|
1024
|
+
label:
|
|
1025
|
+
type: string
|
|
1026
|
+
description: Popup text shown when the marker is clicked. Alias \`popup\` also accepted.
|
|
1027
|
+
color:
|
|
1028
|
+
type: string
|
|
1029
|
+
description: CSS color for the marker pin (defaults to blue).
|
|
1030
|
+
geojson:
|
|
1031
|
+
type: object
|
|
1032
|
+
description: GeoJSON Feature or FeatureCollection rendered as fill/line/circle layers.
|
|
1033
|
+
center:
|
|
1034
|
+
description: "Initial center. Either \`[lon, lat]\` array (MapLibre/Turf convention) OR object \`{lat, lon}\` / \`{lat, lng}\`. If omitted, computed from data via Turf bbox."
|
|
1035
|
+
zoom:
|
|
1036
|
+
type: number
|
|
1037
|
+
description: Initial zoom. If omitted, derived by fitting the bbox of the data.
|
|
1038
|
+
height:
|
|
1039
|
+
type: string
|
|
1040
|
+
description: CSS height of the map container (default "400px").
|
|
1041
|
+
cluster:
|
|
1042
|
+
type: boolean
|
|
1043
|
+
description: When true, group nearby markers into clusters (MapLibre native clustering, clusterRadius 50). Click a cluster to zoom in.
|
|
1044
|
+
tileLayers:
|
|
1045
|
+
type: array
|
|
1046
|
+
description: Raster tile overlays drawn on top of the base style (e.g. NASA GIBS imagery).
|
|
1047
|
+
items:
|
|
1048
|
+
type: object
|
|
1049
|
+
required:
|
|
1050
|
+
- url
|
|
1051
|
+
properties:
|
|
1052
|
+
name:
|
|
1053
|
+
type: string
|
|
1054
|
+
description: Optional human-readable layer name.
|
|
1055
|
+
url:
|
|
1056
|
+
type: string
|
|
1057
|
+
description: Tile URL template with \`{z}/{x}/{y}\` placeholders.
|
|
1058
|
+
opacity:
|
|
1059
|
+
type: number
|
|
1060
|
+
description: Layer opacity 0..1 (default 1).
|
|
1061
|
+
required: []
|
|
1062
|
+
---
|
|
1063
|
+
|
|
1064
|
+
## When to use
|
|
1065
|
+
Pour visualiser des donnees geospatiales : positions, itineraires, polygones (zones, regions). Choisir cette carte pour points + popups + overlays GeoJSON sur fond clair (Carto Positron).
|
|
1066
|
+
|
|
1067
|
+
## How to use
|
|
1068
|
+
Call widget_display({name: "map", params: {markers: [{lat: 48.85, lon: 2.35, label: "Paris"}]}}).
|
|
1069
|
+
|
|
1070
|
+
## Common mistakes
|
|
1071
|
+
- Coordonnees au format [lon, lat] (PAS [lat, lon]) pour le champ \`center\` et pour les Point GeoJSON — convention Turf/MapLibre.
|
|
1072
|
+
- Ne PAS fournir un \`zoom\` sans \`center\` si tu veux l'auto-fit : laisse les deux vides pour que la vue s'adapte automatiquement aux donnees.
|
|
896
1073
|
`,
|
|
897
1074
|
|
|
898
1075
|
`---
|
|
@@ -930,6 +1107,7 @@ Call widget_display({name: "chat-input", params: {placeholder: "Your reply..."}}
|
|
|
930
1107
|
const _NOTEBOOK_RECIPE_SOURCES: string[] = [
|
|
931
1108
|
notebookRecipe as string,
|
|
932
1109
|
recipeBrowserRecipe,
|
|
1110
|
+
toolBrowserRecipe,
|
|
933
1111
|
];
|
|
934
1112
|
export const NATIVE_WIDGET_NAMES = [...RECIPES, ..._NOTEBOOK_RECIPE_SOURCES].map(r => {
|
|
935
1113
|
const match = r.match(/widget:\s*(\S+)/);
|
|
@@ -955,6 +1133,9 @@ autoui.registerWidget(notebookRecipe as string, renderNotebook as any);
|
|
|
955
1133
|
// Recipe browser — resolved by WidgetRenderer as <auto-recipe-browser> custom element
|
|
956
1134
|
autoui.registerWidget(recipeBrowserRecipe, undefined);
|
|
957
1135
|
|
|
1136
|
+
// Tool browser — resolved by WidgetRenderer as <auto-tool-browser> custom element
|
|
1137
|
+
autoui.registerWidget(toolBrowserRecipe, undefined);
|
|
1138
|
+
|
|
958
1139
|
// Register flow recipes (multi-step procedures) from the global recipe registry
|
|
959
1140
|
// that declare this server (autoui) in their frontmatter.
|
|
960
1141
|
for (const [key, rawMd] of Object.entries(RAW_RECIPES)) {
|
package/src/discovery-cache.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ProviderTool } from './types.js';
|
|
8
8
|
import type { PipelineTrace } from './pipeline-trace.js';
|
|
9
|
+
import { sanitizeServerName } from './tool-layers.js';
|
|
9
10
|
|
|
10
11
|
/** Tool names that are resolved locally from cache — hidden from user-facing browsers. */
|
|
11
12
|
export const DISCOVERY_TOOL_NAMES = new Set(['list_recipes', 'search_recipes', 'get_recipe', 'list_tools', 'search_tools']);
|
|
@@ -30,14 +31,21 @@ export interface ServerCache {
|
|
|
30
31
|
export class DiscoveryCache {
|
|
31
32
|
private servers = new Map<string, ServerCache>();
|
|
32
33
|
|
|
34
|
+
/** Normalize a server prefix so register/lookup share the same key shape
|
|
35
|
+
* regardless of casing, spaces, or other artifacts. Mirrors the
|
|
36
|
+
* sanitization applied to tool name prefixes by tool-layers. */
|
|
37
|
+
private norm(serverPrefix: string): string {
|
|
38
|
+
return sanitizeServerName(serverPrefix);
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
/** Register a server's recipes and tools */
|
|
34
42
|
register(serverPrefix: string, data: ServerCache): void {
|
|
35
|
-
this.servers.set(serverPrefix, data);
|
|
43
|
+
this.servers.set(this.norm(serverPrefix), data);
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
/** Check if we have cached data for a server prefix */
|
|
39
47
|
has(serverPrefix: string): boolean {
|
|
40
|
-
return this.servers.has(serverPrefix);
|
|
48
|
+
return this.servers.has(this.norm(serverPrefix));
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
/** Clear all cached data */
|
|
@@ -45,7 +53,7 @@ export class DiscoveryCache {
|
|
|
45
53
|
this.servers.clear();
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
/** All registered server prefixes */
|
|
56
|
+
/** All registered server prefixes (normalized) */
|
|
49
57
|
serverPrefixes(): string[] {
|
|
50
58
|
return [...this.servers.keys()];
|
|
51
59
|
}
|
|
@@ -70,22 +78,22 @@ export class DiscoveryCache {
|
|
|
70
78
|
|
|
71
79
|
/** Recipe count for a specific server */
|
|
72
80
|
recipeCount(serverPrefix: string): number {
|
|
73
|
-
return this.servers.get(serverPrefix)?.recipes.length ?? 0;
|
|
81
|
+
return this.servers.get(this.norm(serverPrefix))?.recipes.length ?? 0;
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
/** Get the cached recipes for a specific server prefix. */
|
|
77
85
|
recipesFor(serverPrefix: string): CachedRecipe[] {
|
|
78
|
-
return this.servers.get(serverPrefix)?.recipes ?? [];
|
|
86
|
+
return this.servers.get(this.norm(serverPrefix))?.recipes ?? [];
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
/** Tool count for a specific server */
|
|
82
90
|
toolCount(serverPrefix: string): number {
|
|
83
|
-
return this.servers.get(serverPrefix)?.tools.length ?? 0;
|
|
91
|
+
return this.servers.get(this.norm(serverPrefix))?.tools.length ?? 0;
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
/** Tool count excluding discovery tools (hidden from user-facing browsers) */
|
|
87
95
|
browsableToolCount(serverPrefix: string): number {
|
|
88
|
-
const tools = this.servers.get(serverPrefix)?.tools ?? [];
|
|
96
|
+
const tools = this.servers.get(this.norm(serverPrefix))?.tools ?? [];
|
|
89
97
|
return tools.filter(t => !DISCOVERY_TOOL_NAMES.has(t.name)).length;
|
|
90
98
|
}
|
|
91
99
|
|
|
@@ -103,7 +111,7 @@ export class DiscoveryCache {
|
|
|
103
111
|
params: Record<string, unknown>,
|
|
104
112
|
trace?: PipelineTrace,
|
|
105
113
|
): string | null {
|
|
106
|
-
const cache = this.servers.get(serverPrefix);
|
|
114
|
+
const cache = this.servers.get(this.norm(serverPrefix));
|
|
107
115
|
if (!cache) return null;
|
|
108
116
|
|
|
109
117
|
switch (realToolName) {
|
package/src/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ export { GemmaProvider } from './providers/gemma.js';
|
|
|
32
32
|
export type { GemmaProviderOptions, GemmaStatus } from './providers/gemma.js';
|
|
33
33
|
|
|
34
34
|
// Agent loop
|
|
35
|
-
export { runAgentLoop, toProviderTools, fromMcpTools, trimConversationHistory } from './loop.js';
|
|
35
|
+
export { runAgentLoop, toProviderTools, fromMcpTools, trimConversationHistory, pruneOrphanToolResults } from './loop.js';
|
|
36
36
|
export { buildSystemPrompt, buildSystemPromptWithAliases } from './prompts/index.js';
|
|
37
37
|
export type { SystemPromptResult } from './prompts/index.js';
|
|
38
38
|
export type { AgentLoopOptions } from './loop.js';
|
|
@@ -41,7 +41,7 @@ export type { AgentLoopOptions } from './loop.js';
|
|
|
41
41
|
export { autoui, NATIVE_WIDGET_NAMES } from './autoui-server.js';
|
|
42
42
|
|
|
43
43
|
// Tool layers
|
|
44
|
-
export { buildToolsFromLayers, buildDiscoveryTools, buildDiscoveryToolsWithAliases, activateServerTools, resolveCanonicalTools, toolAliasMap, flattenPathMaps, buildDiscoveryCache } from './tool-layers.js';
|
|
44
|
+
export { buildToolsFromLayers, buildDiscoveryTools, buildDiscoveryToolsWithAliases, activateServerTools, resolveCanonicalTools, toolAliasMap, flattenPathMaps, buildDiscoveryCache, sanitizeServerName } from './tool-layers.js';
|
|
45
45
|
export type { ToolLayer, McpLayer, WebMcpLayer, DiscoveryToolsResult, SchemaTransformOptions, BuildToolsResult, ProviderKind } from './tool-layers.js';
|
|
46
46
|
|
|
47
47
|
// Discovery cache
|
|
@@ -88,7 +88,7 @@ export {
|
|
|
88
88
|
} from './ort-version.js';
|
|
89
89
|
|
|
90
90
|
// Trace observer — live visual trace for runAgentLoop
|
|
91
|
-
export { createTraceObserver, type TraceObserver, type TraceObserverContext, type RoundTripDetail } from './trace-observer.js';
|
|
91
|
+
export { createTraceObserver, type TraceObserver, type TraceObserverContext, type RoundTripDetail, type WidgetLineage } from './trace-observer.js';
|
|
92
92
|
|
|
93
93
|
// Nano-RAG — context compaction
|
|
94
94
|
export { ContextRAG, type ContextRAGOptions } from './nano-rag/mod.js';
|
package/src/loop.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { ToolLayer, SchemaTransformOptions } from './tool-layers.js';
|
|
|
12
12
|
import { buildToolsFromLayers, buildDiscoveryToolsWithAliases, activateServerTools, toProviderTools, sanitizeServerName } from './tool-layers.js';
|
|
13
13
|
import { buildSystemPromptWithAliases } from './prompts/index.js';
|
|
14
14
|
import type { DiscoveryCache } from './discovery-cache.js';
|
|
15
|
+
import { DISCOVERY_TOOL_NAMES } from './discovery-cache.js';
|
|
15
16
|
import { unflattenParams, validateJsonSchema } from '@webmcp-auto-ui/core';
|
|
16
17
|
import type { JsonSchema } from '@webmcp-auto-ui/core';
|
|
17
18
|
import { autoRepairParams } from './auto-repair.js';
|
|
@@ -22,12 +23,6 @@ export { buildSystemPrompt } from './prompts/index.js';
|
|
|
22
23
|
|
|
23
24
|
const MAX_RESULT_LEN = 10_000;
|
|
24
25
|
|
|
25
|
-
/** Tool names (after prefix strip) that indicate discovery/exploration */
|
|
26
|
-
const DISCOVERY_TOOL_NAMES = new Set([
|
|
27
|
-
'search_recipes', 'get_recipe', 'list_recipes', 'list_tables', 'describe_table',
|
|
28
|
-
'get_json_schemas', 'get_typescript_types', 'recall',
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
26
|
function isDiscoveryTool(prefixedName: string): boolean {
|
|
32
27
|
const match = prefixedName.match(/^.+?_(data|ui)_(.+)$/);
|
|
33
28
|
if (!match) return false;
|
|
@@ -275,6 +270,10 @@ export async function runAgentLoop(
|
|
|
275
270
|
}
|
|
276
271
|
}
|
|
277
272
|
|
|
273
|
+
// Defensive: strip any orphan tool_result blocks before sending — strict
|
|
274
|
+
// providers (Anthropic) reject these with a 400.
|
|
275
|
+
pruneOrphanToolResults(messages);
|
|
276
|
+
|
|
278
277
|
callbacks.onLLMRequest?.(messages, iterationTools);
|
|
279
278
|
const t0 = performance.now();
|
|
280
279
|
let streamingText = '';
|
|
@@ -360,14 +359,28 @@ export async function runAgentLoop(
|
|
|
360
359
|
}
|
|
361
360
|
|
|
362
361
|
// ── Intercept list_tools / search_tools (local pseudo-tools) ──
|
|
363
|
-
//
|
|
362
|
+
// The agent inspecting a server's tools is a strong signal of intent to use them.
|
|
363
|
+
// Activate the server now so the next iteration can invoke its data tools directly,
|
|
364
|
+
// matching the system-prompt promise that "list_tools → use it directly in STEP 3".
|
|
364
365
|
if (toolMatch && (toolMatch[3] === 'list_tools' || toolMatch[3] === 'search_tools')) {
|
|
365
366
|
const [, serverName, token, pseudoTool] = toolMatch;
|
|
366
367
|
const protocol = tokenToProtocol(token);
|
|
367
368
|
const layer = (options.layers ?? []).find(l => sanitizeServerName(l.serverName) === serverName && l.protocol === protocol);
|
|
368
369
|
if (!layer) {
|
|
369
370
|
result = 'Error: server not found';
|
|
370
|
-
} else
|
|
371
|
+
} else {
|
|
372
|
+
const serverKey = `${serverName}_${token}`;
|
|
373
|
+
if (!activatedServers.has(serverKey)) {
|
|
374
|
+
try {
|
|
375
|
+
const act = activateServerTools(activeTools, layer, schemaOptions, trace);
|
|
376
|
+
activeTools = act.tools;
|
|
377
|
+
for (const [k, v] of act.pathMaps) localPathMaps.set(k, v);
|
|
378
|
+
activatedServers.add(serverKey);
|
|
379
|
+
} catch (e) {
|
|
380
|
+
trace.push('activate', name, `activation failed for ${serverKey}: ${e instanceof Error ? e.message : String(e)}`, 'error');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (pseudoTool === 'list_tools') {
|
|
371
384
|
const tools = layer.tools.map(t => ({ name: t.name, description: t.description, inputSchema: (t as any).inputSchema }));
|
|
372
385
|
result = JSON.stringify(tools, null, 2);
|
|
373
386
|
} else {
|
|
@@ -384,6 +397,7 @@ export async function runAgentLoop(
|
|
|
384
397
|
result = JSON.stringify(tools, null, 2);
|
|
385
398
|
}
|
|
386
399
|
}
|
|
400
|
+
}
|
|
387
401
|
} else {
|
|
388
402
|
// ── Normal tool dispatch — activate server on first contact ──
|
|
389
403
|
|
|
@@ -456,10 +470,18 @@ export async function runAgentLoop(
|
|
|
456
470
|
if (!client) {
|
|
457
471
|
result = `Error: no MCP client available for tool ${name}`;
|
|
458
472
|
} else {
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
473
|
+
const mcpLayer = (options.layers ?? []).find(
|
|
474
|
+
l => sanitizeServerName(l.serverName) === serverName && l.protocol === 'mcp'
|
|
475
|
+
) as { serverUrl?: string } | undefined;
|
|
476
|
+
const serverUrl = mcpLayer?.serverUrl;
|
|
477
|
+
if (!serverUrl) {
|
|
478
|
+
result = `Error: no serverUrl resolved for tool "${name}" (server "${serverName}"). Pass serverUrl on the matching ToolLayer.`;
|
|
479
|
+
} else {
|
|
480
|
+
const mcpResult = await (client as unknown as { callToolOn: (u: string, n: string, a: unknown) => Promise<{ content?: { type: string; text?: string }[] }> }).callToolOn(serverUrl, realToolName, toolInput);
|
|
481
|
+
const textContent = mcpResult.content?.find((c: { type: string }) => c.type === 'text') as { text?: string } | undefined;
|
|
482
|
+
const rawResult = textContent?.text ?? JSON.stringify(mcpResult);
|
|
483
|
+
result = truncateResults ? truncateResult(rawResult, maxResultLength) : rawResult;
|
|
484
|
+
}
|
|
463
485
|
}
|
|
464
486
|
} else if (protocol === 'webmcp') {
|
|
465
487
|
// Intercept recall BEFORE hitting executeTool — use the local resultBuffer directly
|
|
@@ -539,6 +561,15 @@ export async function runAgentLoop(
|
|
|
539
561
|
const c = messages[i].content;
|
|
540
562
|
if (Array.isArray(c) && c.length === 0) messages.splice(i, 1);
|
|
541
563
|
}
|
|
564
|
+
// Also drop pending tool_results in the current iteration whose
|
|
565
|
+
// tool_use was just stripped from the assistant turn — otherwise
|
|
566
|
+
// they become orphans when pushed at the end of the loop.
|
|
567
|
+
for (let j = toolResults.length - 1; j >= 0; j--) {
|
|
568
|
+
const b = toolResults[j];
|
|
569
|
+
if (b.type === 'tool_result' && strippedIds.has((b as { tool_use_id: string }).tool_use_id)) {
|
|
570
|
+
toolResults.splice(j, 1);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
542
573
|
}
|
|
543
574
|
hasRendered = false;
|
|
544
575
|
}
|
|
@@ -663,18 +694,39 @@ export function trimConversationHistory(history: ChatMessage[], maxTokens: numbe
|
|
|
663
694
|
|
|
664
695
|
// Remove orphaned tool_result blocks anywhere in history — strict providers
|
|
665
696
|
// (Anthropic, etc.) reject tool_result blocks whose tool_use_id does not
|
|
666
|
-
// correspond to an earlier assistant tool_use.
|
|
667
|
-
|
|
697
|
+
// correspond to an earlier assistant tool_use.
|
|
698
|
+
pruneOrphanToolResults(trimmed);
|
|
699
|
+
|
|
700
|
+
// Ensure the first non-system message is role=user (API requirement)
|
|
701
|
+
while (trimmed.length > 0) {
|
|
702
|
+
const firstNonSystem = trimmed.findIndex(m => m.role !== 'system');
|
|
703
|
+
if (firstNonSystem >= 0 && trimmed[firstNonSystem].role === 'assistant') {
|
|
704
|
+
trimmed.splice(firstNonSystem, 1);
|
|
705
|
+
} else {
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return trimmed;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Strip orphan tool_result blocks (whose tool_use_id has no matching tool_use
|
|
715
|
+
* in any preceding assistant message) and drop user messages that become
|
|
716
|
+
* empty as a result. Mutates `messages` in place.
|
|
717
|
+
*
|
|
718
|
+
* Strict providers (Anthropic API) reject orphans with a 400. This guard
|
|
719
|
+
* runs immediately before each provider call.
|
|
720
|
+
*/
|
|
721
|
+
export function pruneOrphanToolResults(messages: ChatMessage[]): void {
|
|
668
722
|
const validToolUseIds = new Set<string>();
|
|
669
|
-
for (let i = 0; i <
|
|
670
|
-
const msg =
|
|
671
|
-
// Collect tool_use ids from assistant messages seen so far
|
|
723
|
+
for (let i = 0; i < messages.length; i++) {
|
|
724
|
+
const msg = messages[i];
|
|
672
725
|
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
673
726
|
for (const b of msg.content as any[]) {
|
|
674
727
|
if (b?.type === 'tool_use' && typeof b.id === 'string') validToolUseIds.add(b.id);
|
|
675
728
|
}
|
|
676
729
|
}
|
|
677
|
-
// Filter out orphan tool_result blocks in user messages
|
|
678
730
|
if (msg.role === 'user' && Array.isArray(msg.content)) {
|
|
679
731
|
msg.content = (msg.content as any[]).filter(b => {
|
|
680
732
|
if (b?.type !== 'tool_result') return true;
|
|
@@ -682,21 +734,8 @@ export function trimConversationHistory(history: ChatMessage[], maxTokens: numbe
|
|
|
682
734
|
}) as any;
|
|
683
735
|
}
|
|
684
736
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (Array.isArray(c) && c.length === 0) trimmed.splice(i, 1);
|
|
737
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
738
|
+
const c = messages[i].content;
|
|
739
|
+
if (Array.isArray(c) && c.length === 0) messages.splice(i, 1);
|
|
689
740
|
}
|
|
690
|
-
|
|
691
|
-
// Ensure the first non-system message is role=user (API requirement)
|
|
692
|
-
while (trimmed.length > 0) {
|
|
693
|
-
const firstNonSystem = trimmed.findIndex(m => m.role !== 'system');
|
|
694
|
-
if (firstNonSystem >= 0 && trimmed[firstNonSystem].role === 'assistant') {
|
|
695
|
-
trimmed.splice(firstNonSystem, 1);
|
|
696
|
-
} else {
|
|
697
|
-
break;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
return trimmed;
|
|
702
741
|
}
|
package/src/tool-layers.ts
CHANGED
|
@@ -261,10 +261,9 @@ export function toProviderTools(tools: McpToolDef[], schemaOptions?: SchemaTrans
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
|
-
|
|
265
|
-
// Ensure root schema has additionalProperties for strict mode
|
|
264
|
+
let schemaObj = schema as Record<string, unknown>;
|
|
266
265
|
if (schemaObj.type === 'object' && !('additionalProperties' in schemaObj)) {
|
|
267
|
-
schemaObj
|
|
266
|
+
schemaObj = { ...schemaObj, additionalProperties: false };
|
|
268
267
|
}
|
|
269
268
|
return {
|
|
270
269
|
name: t.name,
|
package/src/trace-observer.ts
CHANGED
|
@@ -42,6 +42,19 @@ export interface RoundTripDetail {
|
|
|
42
42
|
originRecipe?: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** Lineage of a widget mount: tool_calls in the same iteration that produced it. */
|
|
46
|
+
export interface WidgetLineage {
|
|
47
|
+
widgetType: string;
|
|
48
|
+
widgetParams: Record<string, unknown>;
|
|
49
|
+
toolCalls: Array<{
|
|
50
|
+
serverName: string;
|
|
51
|
+
toolName: string;
|
|
52
|
+
args: Record<string, unknown>;
|
|
53
|
+
resultPreview?: string;
|
|
54
|
+
}>;
|
|
55
|
+
originRecipe?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
export interface TraceObserver {
|
|
46
59
|
/** Partial AgentCallbacks to merge into runAgentLoop callbacks. */
|
|
47
60
|
callbacks: Partial<AgentCallbacks>;
|
|
@@ -57,6 +70,8 @@ export interface TraceObserver {
|
|
|
57
70
|
getLoadedRecipes: () => Map<string, string>;
|
|
58
71
|
/** Current recipe context: name of the most recently loaded recipe via get_recipe, or undefined. */
|
|
59
72
|
getCurrentRecipeContext: () => string | undefined;
|
|
73
|
+
/** Resolve the chain of tool_calls in the same iteration that produced a widget. */
|
|
74
|
+
getWidgetLineage: (blockIdOrTraceNodeId: string) => WidgetLineage | null;
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
type NodeKind =
|
|
@@ -552,11 +567,12 @@ export function createTraceObserver(ctx: TraceObserverContext): TraceObserver {
|
|
|
552
567
|
scheduleFlush();
|
|
553
568
|
},
|
|
554
569
|
|
|
555
|
-
onWidget: (type: string,
|
|
570
|
+
onWidget: (type: string, data: Record<string, unknown>, serverName?: string): void => {
|
|
556
571
|
const node = addNode('widget', `${serverName ?? '?'}/${type}`, {
|
|
557
572
|
iterationId: currentIterationId,
|
|
558
573
|
type,
|
|
559
574
|
serverName,
|
|
575
|
+
data,
|
|
560
576
|
});
|
|
561
577
|
const parent = lastToolCallId ?? currentLlmRespId;
|
|
562
578
|
if (parent) {
|
|
@@ -654,5 +670,66 @@ export function createTraceObserver(ctx: TraceObserverContext): TraceObserver {
|
|
|
654
670
|
getCurrentRecipeContext(): string | undefined {
|
|
655
671
|
return currentRecipeContext;
|
|
656
672
|
},
|
|
673
|
+
getWidgetLineage(blockIdOrTraceNodeId: string): WidgetLineage | null {
|
|
674
|
+
let widgetNode = nodes.find(
|
|
675
|
+
(n) => n.kind === 'widget' && n.id === blockIdOrTraceNodeId,
|
|
676
|
+
);
|
|
677
|
+
if (!widgetNode) {
|
|
678
|
+
widgetNode = nodes.find((n) => {
|
|
679
|
+
if (n.kind !== 'widget') return false;
|
|
680
|
+
for (const v of Object.values(n.meta)) {
|
|
681
|
+
if (v === blockIdOrTraceNodeId) return true;
|
|
682
|
+
if (v && typeof v === 'object') {
|
|
683
|
+
for (const vv of Object.values(v as Record<string, unknown>)) {
|
|
684
|
+
if (vv === blockIdOrTraceNodeId) return true;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return false;
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (!widgetNode) return null;
|
|
692
|
+
const iter = iterationAncestor(widgetNode);
|
|
693
|
+
if (!iter) {
|
|
694
|
+
return {
|
|
695
|
+
widgetType: (widgetNode.meta.type as string) ?? '',
|
|
696
|
+
widgetParams: ((widgetNode.meta.data as Record<string, unknown>) ?? {}) as Record<string, unknown>,
|
|
697
|
+
toolCalls: [],
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
const callNodes = nodes
|
|
701
|
+
.filter(
|
|
702
|
+
(n) =>
|
|
703
|
+
n.kind === 'tool_call' &&
|
|
704
|
+
n.meta.iterationId === iter.id &&
|
|
705
|
+
n.startMs <= widgetNode!.startMs,
|
|
706
|
+
)
|
|
707
|
+
.sort((a, b) => a.startMs - b.startMs);
|
|
708
|
+
const toolCalls: WidgetLineage['toolCalls'] = [];
|
|
709
|
+
let originRecipe: string | undefined;
|
|
710
|
+
for (const c of callNodes) {
|
|
711
|
+
const detail = nodeDetails.get(c.id);
|
|
712
|
+
const toolName = detail?.toolName ?? '';
|
|
713
|
+
const serverName = (c.meta.serverName as string | undefined)
|
|
714
|
+
?? (toolName.includes('_') ? toolName.split('_')[0]! : '');
|
|
715
|
+
const args = ((c.meta.args as Record<string, unknown>) ?? {}) as Record<string, unknown>;
|
|
716
|
+
let resultPreview: string | undefined;
|
|
717
|
+
if (detail?.toolResult !== undefined) {
|
|
718
|
+
resultPreview = detail.toolResult.slice(0, 4096);
|
|
719
|
+
}
|
|
720
|
+
toolCalls.push({ serverName, toolName, args, resultPreview });
|
|
721
|
+
if (toolName === 'get_recipe' || toolName.endsWith('_get_recipe')) {
|
|
722
|
+
const r = (args.recipe_name ?? args.name ?? args.id) as unknown;
|
|
723
|
+
if (typeof r === 'string') originRecipe = r;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const lineage: WidgetLineage = {
|
|
727
|
+
widgetType: (widgetNode.meta.type as string) ?? '',
|
|
728
|
+
widgetParams: ((widgetNode.meta.data as Record<string, unknown>) ?? {}) as Record<string, unknown>,
|
|
729
|
+
toolCalls,
|
|
730
|
+
};
|
|
731
|
+
if (originRecipe) lineage.originRecipe = originRecipe;
|
|
732
|
+
return lineage;
|
|
733
|
+
},
|
|
657
734
|
};
|
|
658
735
|
}
|