@webmcp-auto-ui/agent 2.5.31 → 2.5.33

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmcp-auto-ui/agent",
3
- "version": "2.5.31",
3
+ "version": "2.5.33",
4
4
  "description": "LLM agent loop + remote/WASM/local providers + MCP wrapper",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -12,7 +12,6 @@ import notebookRecipe from '@webmcp-auto-ui/ui/widgets/notebook/recipes/notebook
12
12
  // Notebook widget renderer (vanilla JS) — import via subpath to avoid pulling
13
13
  // the .svelte exports of the ui package root through tsc.
14
14
  import { render as renderNotebook } from '@webmcp-auto-ui/ui/widgets/notebook/notebook.js';
15
- import { render as renderRecipeBrowser } from '@webmcp-auto-ui/ui/widgets/notebook/recipe-browser.js';
16
15
 
17
16
  // Inline recipe for recipe-browser (real vanilla widget)
18
17
  const recipeBrowserRecipe = `---
@@ -143,7 +142,7 @@ Call widget_display({name: "list", params: {items: ["A", "B", "C"]}}).
143
142
  // ── chart ────────────────────────────────────────────────────────────────
144
143
  `---
145
144
  widget: chart
146
- description: Simple bar chart. Labels + numeric values.
145
+ description: Simple bar chart. bars is an array of tuples [label: string, value: number]. Each entry MUST be an array of length exactly 2, NOT an object.
147
146
  schema:
148
147
  type: object
149
148
  required:
@@ -153,8 +152,11 @@ schema:
153
152
  type: string
154
153
  bars:
155
154
  type: array
155
+ description: Array of tuples [label, value]. Each item is an array [string, number] of length exactly 2. Do NOT use objects like {label, value}.
156
156
  items:
157
157
  type: array
158
+ minItems: 2
159
+ maxItems: 2
158
160
  ---
159
161
 
160
162
  ## When to use
@@ -162,6 +164,7 @@ Pour un graphique a barres simple avec des labels et valeurs numeriques.
162
164
 
163
165
  ## How to use
164
166
  Call widget_display({name: "chart", params: {bars: [["Jan", 10], ["Fev", 20]]}}).
167
+ Each bar is a tuple array of exactly 2 elements: [label: string, value: number]. NEVER use objects.
165
168
  `,
166
169
 
167
170
  // ── alert ────────────────────────────────────────────────────────────────
@@ -765,57 +768,6 @@ Call widget_display({name: "carousel", params: {slides: [{src: "https://...", ti
765
768
 
766
769
  ## Common mistakes
767
770
  - NEVER fabricate image URLs for src — only use those returned by MCP tools
768
- `,
769
-
770
- // ── map ──────────────────────────────────────────────────────────────────
771
- `---
772
- widget: map
773
- description: Interactive Leaflet map with markers. Dark CARTO basemap.
774
- schema:
775
- type: object
776
- properties:
777
- title:
778
- type: string
779
- center:
780
- type: object
781
- description: Centre de la carte
782
- required:
783
- - lat
784
- - lng
785
- properties:
786
- lat:
787
- type: number
788
- lng:
789
- type: number
790
- zoom:
791
- type: number
792
- description: Niveau de zoom (1-18)
793
- height:
794
- type: string
795
- description: Hauteur CSS de la carte (ex "400px")
796
- markers:
797
- type: array
798
- items:
799
- type: object
800
- required:
801
- - lat
802
- - lng
803
- properties:
804
- lat:
805
- type: number
806
- lng:
807
- type: number
808
- label:
809
- type: string
810
- color:
811
- type: string
812
- ---
813
-
814
- ## When to use
815
- Display a geographic map with markers.
816
-
817
- ## How to use
818
- Call widget_display({name: "map", params: {center: {lat: 48.8, lng: 2.3}, zoom: 12, markers: [{lat: 48.8, lng: 2.3, label: "Paris"}]}}).
819
771
  `,
820
772
 
821
773
  // ── stat-card ────────────────────────────────────────────────────────────
@@ -882,9 +834,10 @@ schema:
882
834
  type: string
883
835
  rows:
884
836
  type: array
885
- description: Tableau de tableaux de valeurs (row-major)
837
+ description: Array of rows, row-major. Each row is an array of primitive values (string/number/boolean) with length equal to the number of columns. Do NOT use objects as rows.
886
838
  items:
887
839
  type: array
840
+ minItems: 1
888
841
  highlights:
889
842
  type: array
890
843
  description: Cellules a coloriser
@@ -907,34 +860,6 @@ Pour des grilles de donnees avec mise en valeur de cellules (heatmap, comparaiso
907
860
 
908
861
  ## How to use
909
862
  Call widget_display({name: "grid-data", params: {columns: [{key:"a",label:"A"}], rows: [[1,2],[3,4]], highlights: [{row:0,col:1,color:"#ff0"}]}}).
910
- `,
911
-
912
- // ── d3 ───────────────────────────────────────────────────────────────────
913
- `---
914
- widget: d3
915
- description: D3.js visualization (hex-heatmap, radial, treemap, force graph).
916
- schema:
917
- type: object
918
- required:
919
- - preset
920
- - data
921
- properties:
922
- title:
923
- type: string
924
- preset:
925
- type: string
926
- enum: [hex-heatmap, radial, treemap, force]
927
- data:
928
- type: object
929
- config:
930
- type: object
931
- ---
932
-
933
- ## When to use
934
- Pour des visualisations avancees D3.js (heatmap hexagonale, radial, treemap, graphe de force).
935
-
936
- ## How to use
937
- Call widget_display({name: "d3", params: {preset: "treemap", data: {name: "root", children: [...]}}}).
938
863
  `,
939
864
 
940
865
  // ── js-sandbox ───────────────────────────────────────────────────────────
@@ -970,6 +895,31 @@ Pour des visualisations custom, animations, ou prototypes interactifs en JS pur.
970
895
  Call widget_display({name: "js-sandbox", params: {code: "document.getElementById('root').innerHTML = '<h1>Hello</h1>'"}}).
971
896
  `,
972
897
 
898
+ `---
899
+ widget: chat-input
900
+ description: Minimal inline chat bar with a text input and stop button. Use when the agent needs to solicit a short free-form user reply inside the current canvas (e.g. "explain this result").
901
+ group: rich
902
+ schema:
903
+ type: object
904
+ properties:
905
+ placeholder:
906
+ type: string
907
+ description: Placeholder text shown in the input (default "Your reply...").
908
+ value:
909
+ type: string
910
+ description: Optional initial value.
911
+ disabled:
912
+ type: boolean
913
+ description: Disable the input (while the agent is processing).
914
+ ---
915
+
916
+ ## When to use
917
+ When you need the user to reply with a short text — clarifying questions, follow-ups, free-form input mid-canvas. Prefer this over a modal for lightweight conversation inside a widget layout.
918
+
919
+ ## How to use
920
+ Call widget_display({name: "chat-input", params: {placeholder: "Your reply..."}}). The widget emits a bubbling 'widget:interact' CustomEvent with detail={action: "submit", payload: {text}} when the user submits, and detail={action: "stop"} when the stop button is pressed.
921
+ `,
922
+
973
923
  ];
974
924
 
975
925
  // ---------------------------------------------------------------------------
@@ -999,14 +949,11 @@ for (const recipe of RECIPES) {
999
949
  autoui.registerWidget(recipe, undefined);
1000
950
  }
1001
951
 
1002
- // Notebook widget — vanilla renderer (resolved via WidgetRenderer vanilla path)
1003
- const NOTEBOOK_WIDGETS: Array<[string, (container: HTMLElement, data: any) => any]> = [
1004
- [notebookRecipe as string, renderNotebook],
1005
- [recipeBrowserRecipe, renderRecipeBrowser],
1006
- ];
1007
- for (const [recipe, renderer] of NOTEBOOK_WIDGETS) {
1008
- autoui.registerWidget(recipe, renderer as any);
1009
- }
952
+ // Notebook widget — vanilla renderer (wrapped by <auto-notebook> custom element)
953
+ autoui.registerWidget(notebookRecipe as string, renderNotebook as any);
954
+
955
+ // Recipe browser — resolved by WidgetRenderer as <auto-recipe-browser> custom element
956
+ autoui.registerWidget(recipeBrowserRecipe, undefined);
1010
957
 
1011
958
  // Register flow recipes (multi-step procedures) from the global recipe registry
1012
959
  // that declare this server (autoui) in their frontmatter.
package/src/loop.ts CHANGED
@@ -10,7 +10,7 @@ import type {
10
10
  } from './types.js';
11
11
  import type { ToolLayer, SchemaTransformOptions } from './tool-layers.js';
12
12
  import { buildToolsFromLayers, buildDiscoveryToolsWithAliases, activateServerTools, toProviderTools, sanitizeServerName } from './tool-layers.js';
13
- import { buildSystemPromptWithAliases, buildSystemPrompt } from './prompts/index.js';
13
+ import { buildSystemPromptWithAliases } from './prompts/index.js';
14
14
  import type { DiscoveryCache } from './discovery-cache.js';
15
15
  import { unflattenParams, validateJsonSchema } from '@webmcp-auto-ui/core';
16
16
  import type { JsonSchema } from '@webmcp-auto-ui/core';
@@ -395,13 +395,19 @@ export async function runAgentLoop(
395
395
  const protocol = tokenToProtocol(token);
396
396
  const serverKey = `${serverName}_${token}`;
397
397
  if (!activatedServers.has(serverKey)) {
398
- activatedServers.add(serverKey);
399
398
  const layer = (options.layers ?? []).find(l => sanitizeServerName(l.serverName) === serverName && l.protocol === protocol);
400
399
  if (layer) {
401
- const act = activateServerTools(activeTools, layer, schemaOptions, trace);
402
- activeTools = act.tools;
403
- // Merge new pathMaps so unflattenParams works for lazily-activated tools.
404
- for (const [k, v] of act.pathMaps) localPathMaps.set(k, v);
400
+ try {
401
+ const act = activateServerTools(activeTools, layer, schemaOptions, trace);
402
+ activeTools = act.tools;
403
+ // Merge new pathMaps so unflattenParams works for lazily-activated tools.
404
+ for (const [k, v] of act.pathMaps) localPathMaps.set(k, v);
405
+ activatedServers.add(serverKey);
406
+ } catch (e) {
407
+ trace.push('activate', name, `activation failed for ${serverKey}: ${e instanceof Error ? e.message : String(e)}`, 'error');
408
+ }
409
+ } else {
410
+ trace.push('activate', name, `layer not found for server="${serverName}" protocol="${protocol}"`, 'warn');
405
411
  }
406
412
  }
407
413
  }
@@ -43,7 +43,12 @@ export function buildSystemPromptWithAliases(
43
43
  return { prompt, aliasMap: refs.aliasMap };
44
44
  }
45
45
 
46
- /** Backward-compat wrapper — also populates the deprecated global toolAliasMap. */
46
+ /**
47
+ * @deprecated Use `buildSystemPromptWithAliases()` instead and pass the returned
48
+ * `aliasMap` to the agent loop explicitly. This wrapper still populates the
49
+ * deprecated global `toolAliasMap` singleton as a side-effect, which is not
50
+ * parallel-safe across concurrent agent loops.
51
+ */
47
52
  export function buildSystemPrompt(
48
53
  layers: ToolLayer[],
49
54
  options?: { providerKind?: ProviderKind },
@@ -1,12 +1,84 @@
1
1
  // Recipe loader — imports auto-generated .md strings, parses them, exports ready-to-use recipes
2
2
 
3
- export type { Recipe, McpRecipe } from './types.js';
4
- export { parseRecipe, parseRecipes } from './parser.js';
5
-
3
+ import { parseFrontmatter } from '@webmcp-auto-ui/core';
4
+ import type { Recipe } from './types.js';
6
5
  import { RAW_RECIPES } from './_generated.js';
7
- import { parseRecipes } from './parser.js';
8
6
  import { registerRecipes } from '../recipe-registry.js';
9
7
 
8
+ export type { Recipe, McpRecipe } from './types.js';
9
+
10
+ /**
11
+ * Parse a single recipe from its raw markdown string.
12
+ *
13
+ * Supports two formats:
14
+ * - **Structured**: YAML frontmatter between `---` delimiters + markdown body
15
+ * - **Freeform**: plain markdown without frontmatter (id derived from fileKey)
16
+ *
17
+ * @param raw - The raw markdown string
18
+ * @param fileKey - Optional file key (e.g. "gallery-images") used as fallback id for freeform recipes
19
+ */
20
+ export function parseRecipe(raw: string, fileKey?: string): Recipe {
21
+ const { frontmatter, body } = parseFrontmatter(raw);
22
+
23
+ // No frontmatter found → freeform recipe
24
+ if (Object.keys(frontmatter).length === 0) {
25
+ return parseRecipeFreeform(raw, fileKey);
26
+ }
27
+
28
+ return {
29
+ id: (frontmatter.id as string) ?? fileKey ?? '',
30
+ name: (frontmatter.name as string) ?? (frontmatter.id as string) ?? fileKey ?? '',
31
+ description: frontmatter.description as string | undefined,
32
+ components_used: parseStringArray(frontmatter.components_used),
33
+ layout: frontmatter.layout as Recipe['layout'] | undefined,
34
+ interactions: parseInteractions(frontmatter.interactions),
35
+ when: (frontmatter.when as string) ?? '',
36
+ servers: parseStringArray(frontmatter.servers),
37
+ body: body.trim(),
38
+ };
39
+ }
40
+
41
+ /** Parse a freeform .md recipe (no frontmatter). Extracts id from fileKey, name from first heading. */
42
+ function parseRecipeFreeform(raw: string, fileKey?: string): Recipe {
43
+ const body = raw.trim();
44
+ const headingMatch = body.match(/^#+ +(.+)$/m);
45
+ const name = headingMatch?.[1] ?? fileKey ?? 'untitled';
46
+ const id = fileKey ?? name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
47
+
48
+ const whenMatch = body.match(/##\s*Quand[^\n]*\n([\s\S]*?)(?=\n##|\n$|$)/i);
49
+ const when = whenMatch?.[1]?.trim().split('\n')[0] ?? '';
50
+
51
+ return { id, name, when, body };
52
+ }
53
+
54
+ /** Parse all raw recipe strings into Recipe objects. Skips invalid ones with a warning. */
55
+ export function parseRecipes(raws: Record<string, string>): Recipe[] {
56
+ const recipes: Recipe[] = [];
57
+ for (const [key, raw] of Object.entries(raws)) {
58
+ try {
59
+ recipes.push(parseRecipe(raw, key));
60
+ } catch (e) {
61
+ console.warn(`[recipes] Failed to parse "${key}":`, e);
62
+ }
63
+ }
64
+ return recipes;
65
+ }
66
+
67
+ function parseStringArray(val: unknown): string[] | undefined {
68
+ if (!val) return undefined;
69
+ if (Array.isArray(val)) return val.map(String);
70
+ if (typeof val === 'string') return val.split(',').map(s => s.trim()).filter(Boolean);
71
+ return undefined;
72
+ }
73
+
74
+ function parseInteractions(val: unknown): Recipe['interactions'] | undefined {
75
+ if (!Array.isArray(val)) return undefined;
76
+ return val.filter(
77
+ (v): v is { source: string; target: string; event: string; action: string } =>
78
+ typeof v === 'object' && v !== null && 'source' in v && 'target' in v,
79
+ );
80
+ }
81
+
10
82
  /** All built-in WebMCP UI recipes, parsed and ready to use */
11
83
  export const WEBMCP_RECIPES = parseRecipes(RAW_RECIPES);
12
84
 
@@ -1,182 +0,0 @@
1
- // Frontmatter parser for recipe .md files
2
- // Parses YAML-like frontmatter + markdown body into a Recipe object
3
-
4
- import type { Recipe } from './types.js';
5
-
6
- /**
7
- * Parse a single recipe from its raw markdown string.
8
- *
9
- * Supports two formats:
10
- * - **Structured**: YAML-like frontmatter between `---` delimiters + markdown body
11
- * - **Freeform**: plain markdown without frontmatter (id derived from fileKey)
12
- *
13
- * @param raw - The raw markdown string
14
- * @param fileKey - Optional file key (e.g. "gallery-images") used as fallback id for freeform recipes
15
- */
16
- export function parseRecipe(raw: string, fileKey?: string): Recipe {
17
- const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
18
-
19
- if (!match) {
20
- // Freeform recipe: no frontmatter — derive metadata from content
21
- return parseRecipeFreeform(raw, fileKey);
22
- }
23
-
24
- const frontmatter = parseFrontmatter(match[1]);
25
- const body = match[2].trim();
26
-
27
- return {
28
- id: frontmatter.id as string ?? fileKey ?? '',
29
- name: frontmatter.name as string ?? frontmatter.id as string ?? fileKey ?? '',
30
- description: frontmatter.description as string | undefined,
31
- components_used: parseStringArray(frontmatter.components_used),
32
- layout: frontmatter.layout as Recipe['layout'] | undefined,
33
- interactions: parseInteractions(frontmatter.interactions),
34
- when: frontmatter.when as string ?? '',
35
- servers: parseStringArray(frontmatter.servers),
36
- body,
37
- };
38
- }
39
-
40
- /** Parse a freeform .md recipe (no frontmatter). Extracts id from fileKey, name from first heading. */
41
- function parseRecipeFreeform(raw: string, fileKey?: string): Recipe {
42
- const body = raw.trim();
43
- // Try to extract name from first markdown heading
44
- const headingMatch = body.match(/^#+ +(.+)$/m);
45
- const name = headingMatch?.[1] ?? fileKey ?? 'untitled';
46
- const id = fileKey ?? name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
47
-
48
- // Try to extract a "when" hint from the first paragraph or a "## Quand" section
49
- const whenMatch = body.match(/##\s*Quand[^\n]*\n([\s\S]*?)(?=\n##|\n$|$)/i);
50
- const when = whenMatch?.[1]?.trim().split('\n')[0] ?? '';
51
-
52
- return { id, name, when, body };
53
- }
54
-
55
- /** Parse all raw recipe strings into Recipe objects. Skips invalid ones with a warning. */
56
- export function parseRecipes(raws: Record<string, string>): Recipe[] {
57
- const recipes: Recipe[] = [];
58
- for (const [key, raw] of Object.entries(raws)) {
59
- try {
60
- recipes.push(parseRecipe(raw, key));
61
- } catch (e) {
62
- console.warn(`[recipes] Failed to parse "${key}":`, e);
63
- }
64
- }
65
- return recipes;
66
- }
67
-
68
- // ── Internal helpers ──────────────────────────────────────────────────────────
69
-
70
- function parseFrontmatter(raw: string): Record<string, unknown> {
71
- const result: Record<string, unknown> = {};
72
- let currentKey = '';
73
- let currentArray: unknown[] | null = null;
74
- let currentObj: Record<string, unknown> | null = null;
75
- let inObjectArray = false;
76
-
77
- for (const line of raw.split('\n')) {
78
- // Array item: " - value" or " - key: value" (under a key)
79
- if (/^\s+-\s/.test(line) && currentKey) {
80
- const itemRaw = line.replace(/^\s+-\s*/, '').trim();
81
- if (inObjectArray && currentArray) {
82
- // Object item in array: " - source: gallery"
83
- // Collect key: value pairs into a single object until next " -" or new top-level key
84
- const obj = parseInlineObject(itemRaw);
85
- if (obj) {
86
- currentArray.push(obj);
87
- } else {
88
- // Simple string in array
89
- if (!currentArray) currentArray = [];
90
- currentArray.push(itemRaw);
91
- }
92
- } else {
93
- if (!currentArray) currentArray = [];
94
- // Check if it looks like "key: value" (object item)
95
- if (itemRaw.includes(': ')) {
96
- inObjectArray = true;
97
- currentArray.push(parseInlineObject(itemRaw) ?? itemRaw);
98
- } else {
99
- currentArray.push(itemRaw);
100
- }
101
- }
102
- continue;
103
- }
104
-
105
- // Nested key: " key: value" (under a parent key with object value)
106
- if (/^\s+\w/.test(line) && currentKey && !currentArray && currentObj !== null) {
107
- const m = line.match(/^\s+(\w+):\s*(.*)$/);
108
- if (m) {
109
- const val = m[2].trim();
110
- currentObj[m[1]] = isNumeric(val) ? Number(val) : val;
111
- continue;
112
- }
113
- }
114
-
115
- // Flush pending array/object
116
- if (currentKey && (currentArray || currentObj)) {
117
- result[currentKey] = currentArray ?? currentObj;
118
- currentArray = null;
119
- currentObj = null;
120
- inObjectArray = false;
121
- }
122
-
123
- // Top-level key: "key: value" or "key:"
124
- const topMatch = line.match(/^(\w[\w_]*)\s*:\s*(.*)$/);
125
- if (topMatch) {
126
- currentKey = topMatch[1];
127
- const val = topMatch[2].trim();
128
-
129
- if (val === '' || val === '|') {
130
- // Next lines are nested (object or array)
131
- currentObj = {};
132
- continue;
133
- }
134
- if (val.startsWith('[') && val.endsWith(']')) {
135
- // Inline array: [gallery, carousel]
136
- result[currentKey] = val.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
137
- currentKey = '';
138
- continue;
139
- }
140
- // Simple scalar
141
- result[currentKey] = isNumeric(val) ? Number(val) : val;
142
- currentKey = '';
143
- }
144
- }
145
-
146
- // Flush last pending
147
- if (currentKey && (currentArray || currentObj)) {
148
- result[currentKey] = currentArray ?? currentObj;
149
- }
150
-
151
- return result;
152
- }
153
-
154
- function parseStringArray(val: unknown): string[] | undefined {
155
- if (!val) return undefined;
156
- if (Array.isArray(val)) return val.map(String);
157
- if (typeof val === 'string') return val.split(',').map(s => s.trim()).filter(Boolean);
158
- return undefined;
159
- }
160
-
161
- function parseInteractions(val: unknown): Recipe['interactions'] | undefined {
162
- if (!Array.isArray(val)) return undefined;
163
- return val.filter(
164
- (v): v is { source: string; target: string; event: string; action: string } =>
165
- typeof v === 'object' && v !== null && 'source' in v && 'target' in v
166
- );
167
- }
168
-
169
- function parseInlineObject(raw: string): Record<string, unknown> | null {
170
- if (!raw.includes(': ')) return null;
171
- const obj: Record<string, unknown> = {};
172
- // Split on ", " but not inside values — simple heuristic
173
- for (const part of raw.split(/,\s+/)) {
174
- const m = part.match(/^(\w+)\s*:\s*(.+)$/);
175
- if (m) obj[m[1]] = isNumeric(m[2].trim()) ? Number(m[2].trim()) : m[2].trim();
176
- }
177
- return Object.keys(obj).length > 0 ? obj : null;
178
- }
179
-
180
- function isNumeric(val: string): boolean {
181
- return val !== '' && !isNaN(Number(val));
182
- }