@webmcp-auto-ui/agent 2.5.26 → 2.5.28

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.26",
3
+ "version": "2.5.28",
4
4
  "description": "LLM agent loop + remote/WASM/local providers + MCP wrapper",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  "import": "./src/index.ts"
12
12
  },
13
13
  "./server": {
14
- "import": "./src/server/llmProxy.ts"
14
+ "import": "./src/server/index.ts"
15
15
  }
16
16
  },
17
17
  "scripts": {
@@ -27,5 +27,13 @@
27
27
  "@webmcp-auto-ui/core": "file:../core",
28
28
  "onnxruntime-web": "^1.24.3",
29
29
  "typescript": "^5.0.0"
30
+ },
31
+ "peerDependencies": {
32
+ "vega-embed": "^6.24.0"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "vega-embed": {
36
+ "optional": true
37
+ }
30
38
  }
31
39
  }
@@ -5,6 +5,65 @@
5
5
  import { createWebMcpServer, parseFrontmatter } from '@webmcp-auto-ui/core';
6
6
  import { RAW_RECIPES } from './recipes/_generated.js';
7
7
 
8
+ // Notebook widget recipes (vanilla renderers) — moved to @webmcp-auto-ui/ui
9
+ // @ts-ignore — Vite raw imports, not resolved by tsc
10
+ import compactRecipe from '@webmcp-auto-ui/ui/widgets/notebook/recipes/compact.md?raw';
11
+ // @ts-ignore
12
+ import workspaceRecipe from '@webmcp-auto-ui/ui/widgets/notebook/recipes/workspace.md?raw';
13
+ // @ts-ignore
14
+ import documentRecipe from '@webmcp-auto-ui/ui/widgets/notebook/recipes/document.md?raw';
15
+ // @ts-ignore
16
+ import editorialRecipe from '@webmcp-auto-ui/ui/widgets/notebook/recipes/editorial.md?raw';
17
+
18
+ // Notebook widget renderers (vanilla JS) — import via subpath to avoid pulling
19
+ // the .svelte exports of the ui package root through tsc.
20
+ import { render as renderCompact } from '@webmcp-auto-ui/ui/widgets/notebook/compact.js';
21
+ import { render as renderWorkspace } from '@webmcp-auto-ui/ui/widgets/notebook/workspace.js';
22
+ import { render as renderDocument } from '@webmcp-auto-ui/ui/widgets/notebook/document.js';
23
+ import { render as renderEditorial } from '@webmcp-auto-ui/ui/widgets/notebook/editorial.js';
24
+ import { render as renderRecipeBrowser } from '@webmcp-auto-ui/ui/widgets/notebook/recipe-browser.js';
25
+
26
+ // Inline recipe for recipe-browser (real vanilla widget)
27
+ const recipeBrowserRecipe = `---
28
+ widget: recipe-browser
29
+ description: Interactive recipe browser with search, kind/tag filters, preview and pick. Use when the user wants to browse, search, or select recipes from connected servers.
30
+ group: rich
31
+ schema:
32
+ type: object
33
+ required:
34
+ - recipes
35
+ properties:
36
+ recipes:
37
+ type: array
38
+ description: List of Recipe objects (id, name, description, body, servers, ...).
39
+ items:
40
+ type: object
41
+ filters:
42
+ type: object
43
+ description: Initial filters
44
+ properties:
45
+ q:
46
+ type: string
47
+ kind:
48
+ type: string
49
+ enum: [all, webmcp, mcp]
50
+ tags:
51
+ type: array
52
+ items:
53
+ type: string
54
+ layout:
55
+ type: string
56
+ enum: [list, grid]
57
+ description: Default layout (default list)
58
+ ---
59
+
60
+ ## When to use
61
+ When the user wants to browse, search, or pick a recipe — for example "show me the available recipes" or "let me choose a recipe".
62
+
63
+ ## How to use
64
+ 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.
65
+ `;
66
+
8
67
  // ---------------------------------------------------------------------------
9
68
  // Inline recipes (frontmatter + body)
10
69
  // ---------------------------------------------------------------------------
@@ -920,77 +979,21 @@ Pour des visualisations custom, animations, ou prototypes interactifs en JS pur.
920
979
  Call widget_display({name: "js-sandbox", params: {code: "document.getElementById('root').innerHTML = '<h1>Hello</h1>'"}}).
921
980
  `,
922
981
 
923
- // ── recipe-browser ──────────────────────────────────────────────────────
924
- `---
925
- widget: recipe-browser
926
- description: Displays available recipes as interactive cards and allows browsing each recipe's details.
927
- group: rich
928
- schema:
929
- type: object
930
- required:
931
- - cards
932
- properties:
933
- title:
934
- type: string
935
- cards:
936
- type: array
937
- items:
938
- type: object
939
- required:
940
- - title
941
- properties:
942
- title:
943
- type: string
944
- description:
945
- type: string
946
- tags:
947
- type: array
948
- items:
949
- type: string
950
- meta:
951
- type: object
952
- properties:
953
- recipe_name:
954
- type: string
955
- server:
956
- type: string
957
- interactive:
958
- type: boolean
959
- ---
960
-
961
- ## When to use
962
- Quand l'utilisateur veut voir les recettes disponibles, explorer les possibilites du serveur, ou comprendre comment utiliser un widget specifique.
963
-
964
- ## Comment
965
-
966
- ### Etape 1 — Lister les recettes
967
- Appelle search_recipes() sur chaque serveur connecte (MCP et WebMCP) pour obtenir la liste des recettes.
968
-
969
- ### Etape 2 — Afficher en cartes interactives
970
- Utilise widget_display({name: "cards", params: {...}}) avec le parametre interactive: true pour rendre les cartes cliquables :
971
- widget_display({name: "cards", params: {title: "Recettes disponibles", cards: [{title: "Nom", description: "Description", tags: ["serveur"], meta: {recipe_name: "nom_technique", server: "nom_serveur"}}], interactive: true}})
972
-
973
- Le champ meta est important : il sera renvoye dans l'evenement d'interaction quand l'utilisateur clique sur la carte.
974
-
975
- ### Etape 3 — Reagir au clic
976
- Quand l'utilisateur clique sur une carte, tu recevras un message d'interaction contenant les donnees de meta. Utilise meta.recipe_name et meta.server pour :
977
- 1. Appeler get_recipe(meta.recipe_name) sur le bon serveur
978
- 2. Afficher le contenu dans un widget code avec lang: 'markdown'
979
- 3. Lier les deux widgets : reutiliser le widget detail existant via canvas('update', ...) au lieu d'en creer un nouveau a chaque clic.
980
-
981
- ## Common mistakes
982
- - Ne pas oublier interactive: true dans les cartes — sans ca, les clics ne remontent pas
983
- - Ne pas creer un nouveau widget detail a chaque clic — reutiliser l'existant via canvas('update', ...)
984
- - Les recettes MCP et WebMCP ont des noms de serveur differents — utiliser le bon prefixe pour get_recipe()
985
- `,
986
982
  ];
987
983
 
988
984
  // ---------------------------------------------------------------------------
989
985
  // Native widget names — derived from RECIPES frontmatter
990
986
  // ---------------------------------------------------------------------------
991
987
 
992
- /** Derived from RECIPES frontmatter — always in sync with registered widgets */
993
- export const NATIVE_WIDGET_NAMES = RECIPES.map(r => {
988
+ /** Derived from RECIPES + notebook widget recipes — always in sync with registered widgets */
989
+ const _NOTEBOOK_RECIPE_SOURCES: string[] = [
990
+ compactRecipe as string,
991
+ workspaceRecipe as string,
992
+ documentRecipe as string,
993
+ editorialRecipe as string,
994
+ recipeBrowserRecipe,
995
+ ];
996
+ export const NATIVE_WIDGET_NAMES = [...RECIPES, ..._NOTEBOOK_RECIPE_SOURCES].map(r => {
994
997
  const match = r.match(/widget:\s*(\S+)/);
995
998
  return match ? match[1] : '';
996
999
  }).filter(Boolean) as string[];
@@ -1008,6 +1011,18 @@ for (const recipe of RECIPES) {
1008
1011
  autoui.registerWidget(recipe, undefined);
1009
1012
  }
1010
1013
 
1014
+ // Notebook widgets — vanilla renderers (resolved via WidgetRenderer vanilla path)
1015
+ const NOTEBOOK_WIDGETS: Array<[string, (container: HTMLElement, data: any) => any]> = [
1016
+ [compactRecipe as string, renderCompact],
1017
+ [workspaceRecipe as string, renderWorkspace],
1018
+ [documentRecipe as string, renderDocument],
1019
+ [editorialRecipe as string, renderEditorial],
1020
+ [recipeBrowserRecipe, renderRecipeBrowser],
1021
+ ];
1022
+ for (const [recipe, renderer] of NOTEBOOK_WIDGETS) {
1023
+ autoui.registerWidget(recipe, renderer as any);
1024
+ }
1025
+
1011
1026
  // Register flow recipes (multi-step procedures) from the global recipe registry
1012
1027
  // that declare this server (autoui) in their frontmatter.
1013
1028
  for (const [key, rawMd] of Object.entries(RAW_RECIPES)) {
package/src/index.ts CHANGED
@@ -3,10 +3,25 @@
3
3
  // Providers
4
4
  export { RemoteLLMProvider } from './providers/remote.js';
5
5
  export type { RemoteLLMProviderOptions } from './providers/remote.js';
6
- export { WasmProvider, buildGemmaPrompt } from './providers/wasm.js';
7
- export type { WasmProviderOptions, WasmStatus, BuildGemmaPromptInput } from './providers/wasm.js';
6
+ export { WasmProvider } from './providers/wasm.js';
7
+ export type { WasmProviderOptions, WasmStatus } from './providers/wasm.js';
8
+ export { TransformersProvider } from './providers/transformers.js';
9
+ export type { TransformersProviderOptions, TransformersStatus } from './providers/transformers.js';
10
+ export { TRANSFORMERS_MODELS, getTransformersModel, listTransformersModels } from './providers/transformers-models.js';
11
+ export type { TransformersModelEntry, TransformersFamily, ToolCallFormat } from './providers/transformers-models.js';
12
+ export { parseToolCalls } from './prompts/tool-call-parsers.js';
13
+ export type { ParseResult } from './prompts/tool-call-parsers.js';
14
+ export { loadOrDownloadModel, clearModelCache, listCachedModels, clearAllModelCaches, walkDirectoryStats } from './util/opfs-cache.js';
15
+ export type { ModelFileSpec, CacheProgress, CachedModelInfo } from './util/opfs-cache.js';
16
+ export { listAllStorage, deleteStorageEntry, clearAllStorage } from './util/storage-inventory.js';
17
+ export type { StorageEntry, StorageSource } from './util/storage-inventory.js';
18
+ export { buildGemmaPrompt } from './prompts/index.js';
19
+ export type { BuildGemmaPromptInput } from './prompts/index.js';
8
20
  export { LocalLLMProvider } from './providers/local.js';
9
21
  export type { LocalLLMProviderOptions, LocalBackend } from './providers/local.js';
22
+ export { HawkProvider } from './providers/hawk.js';
23
+ export type { HawkLLMProviderOptions } from './providers/hawk.js';
24
+ export { HAWK_MODELS, listHawkModels, type HawkModelEntry } from './providers/hawk-models.js';
10
25
  export { createProvider } from './providers/factory.js';
11
26
  export type { LLMConfig } from './providers/factory.js';
12
27
 
@@ -18,15 +33,16 @@ export type { GemmaProviderOptions, GemmaStatus } from './providers/gemma.js';
18
33
 
19
34
  // Agent loop
20
35
  export { runAgentLoop, toProviderTools, fromMcpTools, trimConversationHistory } from './loop.js';
21
- export { buildSystemPrompt } from './tool-layers.js';
36
+ export { buildSystemPrompt, buildSystemPromptWithAliases } from './prompts/index.js';
37
+ export type { SystemPromptResult } from './prompts/index.js';
22
38
  export type { AgentLoopOptions } from './loop.js';
23
39
 
24
40
  // autoui — built-in WebMCP server
25
41
  export { autoui, NATIVE_WIDGET_NAMES } from './autoui-server.js';
26
42
 
27
43
  // Tool layers
28
- export { buildToolsFromLayers, buildDiscoveryTools, buildDiscoveryToolsWithAliases, activateServerTools, resolveCanonicalTools, toolAliasMap, buildSystemPromptWithAliases, flattenPathMaps, buildDiscoveryCache } from './tool-layers.js';
29
- export type { ToolLayer, McpLayer, WebMcpLayer, SystemPromptResult, DiscoveryToolsResult, SchemaTransformOptions, BuildToolsResult, ProviderKind } from './tool-layers.js';
44
+ export { buildToolsFromLayers, buildDiscoveryTools, buildDiscoveryToolsWithAliases, activateServerTools, resolveCanonicalTools, toolAliasMap, flattenPathMaps, buildDiscoveryCache } from './tool-layers.js';
45
+ export type { ToolLayer, McpLayer, WebMcpLayer, DiscoveryToolsResult, SchemaTransformOptions, BuildToolsResult, ProviderKind } from './tool-layers.js';
30
46
 
31
47
  // Discovery cache
32
48
  export { DiscoveryCache, DISCOVERY_TOOL_NAMES } from './discovery-cache.js';
@@ -63,12 +79,15 @@ export type { RepairResult } from './auto-repair.js';
63
79
  // Pipeline trace
64
80
  export { PipelineTrace, type TraceEntry } from './pipeline-trace.js';
65
81
 
82
+ // Trace observer — live visual trace for runAgentLoop
83
+ export { createTraceObserver, type TraceObserver, type TraceObserverContext, type RoundTripDetail } from './trace-observer.js';
84
+
66
85
  // Nano-RAG — context compaction
67
86
  export { ContextRAG, type ContextRAGOptions } from './nano-rag/mod.js';
68
87
 
69
88
  // Types
70
89
  export type {
71
- RemoteModelId, WasmModelId, LLMId, ModelId,
90
+ RemoteModelId, WasmModelId, TransformersModelId, LLMId, ModelId,
72
91
  ChatMessage, ContentBlock, McpToolDef, ProviderTool,
73
92
  LLMProvider, LLMResponse, ToolCall, AgentMetrics, AgentResult, AgentCallbacks,
74
93
  Recipe, McpRecipe,
package/src/loop.ts CHANGED
@@ -9,7 +9,8 @@ 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, activateServerTools, toProviderTools, sanitizeServerName, flattenPathMaps } from './tool-layers.js';
12
+ import { buildToolsFromLayers, buildDiscoveryToolsWithAliases, activateServerTools, toProviderTools, sanitizeServerName, flattenPathMaps } from './tool-layers.js';
13
+ import { buildSystemPromptWithAliases, buildSystemPrompt } from './prompts/index.js';
13
14
  import type { DiscoveryCache } from './discovery-cache.js';
14
15
  import { unflattenParams, validateJsonSchema } from '@webmcp-auto-ui/core';
15
16
  import type { JsonSchema } from '@webmcp-auto-ui/core';
@@ -17,7 +18,7 @@ import { autoRepairParams } from './auto-repair.js';
17
18
  import { PipelineTrace } from './pipeline-trace.js';
18
19
 
19
20
  // Re-export buildSystemPrompt for backward compat
20
- export { buildSystemPrompt } from './tool-layers.js';
21
+ export { buildSystemPrompt } from './prompts/index.js';
21
22
 
22
23
  const MAX_RESULT_LEN = 10_000;
23
24
 
@@ -122,7 +123,7 @@ export interface AgentLoopOptions {
122
123
  }
123
124
 
124
125
  export async function runAgentLoop(
125
- userMessage: string,
126
+ userMessage: string | ContentBlock[],
126
127
  options: AgentLoopOptions
127
128
  ): Promise<AgentResult> {
128
129
  const {
@@ -167,6 +168,9 @@ export async function runAgentLoop(
167
168
  // Use local alias maps (parallel-safe — no global singleton)
168
169
  const activatedServers = new Set<string>();
169
170
  const localAliasMap = new Map<string, string>();
171
+ // Snapshot pathMaps locally (parallel-safe). Reading the global flattenPathMaps
172
+ // singleton at dispatch-time races when two loops run concurrently.
173
+ const localPathMaps = new Map<string, Record<string, string[]>>(flattenPathMaps);
170
174
  const trace = new PipelineTrace();
171
175
 
172
176
  const disc = buildDiscoveryToolsWithAliases(options.layers ?? [], schemaOptions, trace);
@@ -209,7 +213,6 @@ export async function runAgentLoop(
209
213
  const allToolCalls: ToolCall[] = [];
210
214
  let lastText = '';
211
215
  let finishedNormally = false;
212
- let discoveryPhase = false;
213
216
  let iterationsWithoutRender = 0;
214
217
  let nudgedOnce = false;
215
218
  let hasRendered = false;
@@ -228,17 +231,24 @@ export async function runAgentLoop(
228
231
  // After 5+ iterations without render, inject a nudge message (once)
229
232
  // Merge into existing user message if the last message is already role=user (to avoid consecutive user messages)
230
233
  if (iterationsWithoutRender >= 5 && !hasRendered && !nudgedOnce) {
231
- nudgedOnce = true;
232
234
  const nudgeText = 'STOP exploration. Use the data you already collected. Call widget_display() NOW to display results.';
233
235
  const lastMsg = messages[messages.length - 1];
234
- if (lastMsg && lastMsg.role === 'user') {
235
- if (typeof lastMsg.content === 'string') {
236
- lastMsg.content = [{ type: 'text', text: lastMsg.content }, { type: 'text', text: nudgeText }];
237
- } else if (Array.isArray(lastMsg.content)) {
238
- (lastMsg.content as ContentBlock[]).push({ type: 'text', text: nudgeText });
236
+ // Skip if last turn carries tool_result blocks — mixing raw text with tool_response
237
+ // in one turn violates Gemma spec §7 (the serializer would emit text + <|tool_response|>
238
+ // together). Defer the nudge to a later iteration where the turn is pure-user.
239
+ const lastHasToolResult = lastMsg && Array.isArray(lastMsg.content)
240
+ && (lastMsg.content as ContentBlock[]).some(b => b.type === 'tool_result');
241
+ if (!lastHasToolResult) {
242
+ nudgedOnce = true;
243
+ if (lastMsg && lastMsg.role === 'user') {
244
+ if (typeof lastMsg.content === 'string') {
245
+ lastMsg.content = [{ type: 'text', text: lastMsg.content }, { type: 'text', text: nudgeText }];
246
+ } else if (Array.isArray(lastMsg.content)) {
247
+ (lastMsg.content as ContentBlock[]).push({ type: 'text', text: nudgeText });
248
+ }
249
+ } else {
250
+ messages.push({ role: 'user', content: nudgeText });
239
251
  }
240
- } else {
241
- messages.push({ role: 'user', content: nudgeText });
242
252
  }
243
253
  }
244
254
 
@@ -320,14 +330,6 @@ export async function runAgentLoop(
320
330
  const toolResults: ContentBlock[] = [];
321
331
  for (const block of toolBlocks) {
322
332
  const call: ToolCall = { id: block.id, name: block.name, args: block.input };
323
- const wasDiscovering = discoveryPhase;
324
- if (isDiscoveryTool(block.name)) {
325
- discoveryPhase = true;
326
- call.guided = false;
327
- } else {
328
- call.guided = wasDiscovering;
329
- discoveryPhase = false;
330
- }
331
333
  const t1 = performance.now();
332
334
 
333
335
  try {
@@ -409,8 +411,11 @@ export async function runAgentLoop(
409
411
  const protocol = tokenToProtocol(token);
410
412
 
411
413
  // Auto-repair + validate params before dispatch
414
+ // Resolve from the full activeTools (not iterationTools, which may be filtered
415
+ // to strip discovery tools after 4 iterations — would make toolDef undefined
416
+ // and silently skip auto-repair + schema validation).
412
417
  let toolInput = block.input as Record<string, unknown>;
413
- const toolDef = iterationTools.find(t => t.name === block.name);
418
+ const toolDef = activeTools.find(t => t.name === block.name);
414
419
  if (toolDef?.input_schema) {
415
420
  const repair = autoRepairParams(toolInput, toolDef.input_schema, realToolName);
416
421
  if (repair.fixes.length > 0) {
@@ -459,8 +464,9 @@ export async function runAgentLoop(
459
464
  result = `Error: no WebMCP server "${serverName}" found.`;
460
465
  } else {
461
466
  // Unflatten params if schema was flattened
467
+ // Use the local snapshot (parallel-safe) rather than the global singleton.
462
468
  if (schemaOptions?.flatten) {
463
- const pathMap = flattenPathMaps.get(block.name);
469
+ const pathMap = localPathMaps.get(block.name);
464
470
  if (pathMap) {
465
471
  toolInput = unflattenParams(toolInput, pathMap);
466
472
  }
@@ -646,18 +652,31 @@ export function trimConversationHistory(history: ChatMessage[], maxTokens: numbe
646
652
  total -= removed.reduce((s, m) => s + JSON.stringify(m).length, 0);
647
653
  }
648
654
 
649
- // Remove orphaned tool_result messages at the startthese reference
650
- // a tool_use in a message that was trimmed away, causing API errors.
651
- while (trimmed.length > 0) {
652
- const first = trimmed[0];
653
- if (first.role === 'system') break; // preserve system messages at the front
654
- const blocks = Array.isArray(first.content) ? first.content : [];
655
- const hasToolResult = blocks.some((b: any) => b.type === 'tool_result');
656
- if (hasToolResult) {
657
- trimmed.shift();
658
- } else {
659
- break;
655
+ // Remove orphaned tool_result blocks anywhere in historystrict providers
656
+ // (Anthropic, etc.) reject tool_result blocks whose tool_use_id does not
657
+ // correspond to an earlier assistant tool_use. Head-only pruning misses
658
+ // internal orphans caused by mid-history trims.
659
+ const validToolUseIds = new Set<string>();
660
+ for (let i = 0; i < trimmed.length; i++) {
661
+ const msg = trimmed[i];
662
+ // Collect tool_use ids from assistant messages seen so far
663
+ if (msg.role === 'assistant' && Array.isArray(msg.content)) {
664
+ for (const b of msg.content as any[]) {
665
+ if (b?.type === 'tool_use' && typeof b.id === 'string') validToolUseIds.add(b.id);
666
+ }
660
667
  }
668
+ // Filter out orphan tool_result blocks in user messages
669
+ if (msg.role === 'user' && Array.isArray(msg.content)) {
670
+ msg.content = (msg.content as any[]).filter(b => {
671
+ if (b?.type !== 'tool_result') return true;
672
+ return typeof b.tool_use_id === 'string' && validToolUseIds.has(b.tool_use_id);
673
+ }) as any;
674
+ }
675
+ }
676
+ // Drop user messages that became empty after orphan-pruning
677
+ for (let i = trimmed.length - 1; i >= 0; i--) {
678
+ const c = trimmed[i].content;
679
+ if (Array.isArray(c) && c.length === 0) trimmed.splice(i, 1);
661
680
  }
662
681
 
663
682
  // Ensure the first non-system message is role=user (API requirement)
@@ -0,0 +1,81 @@
1
+ // Claude / generic 5-STEP system prompt template.
2
+
3
+ import type { PromptRefs } from './tool-refs.js';
4
+
5
+ export function buildClaudePrompt(refs: PromptRefs): string {
6
+ const { listRecipes, searchRecipes, listTools, searchTools, getRecipes, actionTools } = refs;
7
+
8
+ return `You are FLEX, an AI assistant that helps users by answering their questions and completing tasks using recipes (also called skills) which are procedures containing instructions for AI agents to use tools (functions, scripts, schemas, and other relevant information) and tools. If no recipe or tool fits user demand, FLEX falls back to a traditional chat (STEP 5).
9
+
10
+ There are two kinds of servers: MCP servers exposing DATA (database, images, text, json) with tool calls and WebMCP servers exposing UI (widget_display, canvas, recall) with other tool calls to render DATA on the canvas. Both servers have recipes describing how to best use their tools.
11
+
12
+ CRITICAL RULE: FLEX does not narrate its process in the response. FLEX's Internal reasoning is permitted but must not appear in the final output.
13
+
14
+ FLEX follows a multi-step lazy-loading protocol:
15
+
16
+ STEP 1 — FLEX lists all recipes
17
+
18
+ FLEX tries to fetch a relevant DATA or UI recipe using these functions:
19
+
20
+ ${listRecipes.join('\n')}
21
+
22
+ If at least one relevant recipe is found → FLEX goes to STEP 2.
23
+ If no results → FLEX goes to STEP 1b.
24
+
25
+ STEP 1b — FLEX search recipes
26
+
27
+ If FLEX does not find appropriate recipe by listing, FLEX searches an appropriate DATA or UI recipe with keyword(s) extracted from the request with these functions:
28
+
29
+ ${searchRecipes.join('\n')}
30
+
31
+ FLEX picks the most relevant recipe for the request.
32
+ If a recipe matches → FLEX goes to STEP 2.
33
+ If no recipe is available or relevant → FLEX goes to STEP 1c.
34
+
35
+ STEP 1c — FLEX lists tools
36
+
37
+ If FLEX does not find any applicable recipe, FLEX lists relevant tools using these functions:
38
+
39
+ ${listTools.join('\n')}
40
+
41
+ If FLEX finds a relevant tool → FLEX uses it directly in STEP 3.
42
+ If FLEX does not find any relevant tools by listing them → FLEX goes to STEP 1d.
43
+
44
+ STEP 1d — FLEX searches tools using these functions:
45
+
46
+ ${searchTools.join('\n')}
47
+
48
+ FLEX picks the most relevant tool(s) and use it directly in STEP 3.
49
+
50
+ STEP 2 — FLEX ingests the recipe in its context
51
+
52
+ ${getRecipes.join('\n')}
53
+
54
+ FLEX knows tools functions arguments or schemas because they come from the result of list_recipes (STEP 1) or search_recipes (STEP 1b), whichever was called by FLEX. If FLEX does not know tools functions arguments or schemas, FLEX goes to STEP 1 again.
55
+
56
+ If the recipe references other recipes by name (e.g. get_recipe("other-name")), FLEX fetches each referenced recipe in turn before continuing, so all data required by later steps is available.
57
+
58
+ If FLEX knows tool functions arguments or schemas, FLEX also reads the full instructions of the selected recipe and executes them directly in STEP 3.
59
+
60
+ STEP 3 — FLEX executes tool functions
61
+
62
+ FLEX prefers recipes over direct tool calls when a recipe matches the task. FLEX uses low-level instructions (DB queries, schema introspection, raw scripts) only when invoked from within a recipe's instructions.
63
+
64
+ FLEX follows recipe instructions exactly if they are present. Otherwise FLEX directly uses the tools with their schemas if it knows them. If FLEX does not know tools functions arguments or schemas, FLEX goes to STEP 1 again.
65
+
66
+ Placeholder markers in recipes like <step 1>, <step 2>, <jsCode from step 2 verbatim> are slots: FLEX replaces them with the real values returned by earlier tool calls, keeping the original text verbatim where the recipe specifies.
67
+
68
+ Output format: (1) FLEX returns a one-sentence summary of the action performed, then (2) FLEX display the result usually as a UI element such as a widget in STEP 4.
69
+
70
+ STEP 4 — UI display
71
+
72
+ Unless a recipe specifies otherwise, FLEX uses these functions to display its responses on the canvas:
73
+
74
+ ${actionTools.join('\n')}
75
+
76
+ FLEX knows that widget_display may ONLY be called with data returned by a DATA tool actually invoked in the current session. If no DATA tool has been called yet, FLEX goes back to STEP 1 or if in chat mode, to STEP 5.
77
+
78
+ STEP 5 — Fallback
79
+
80
+ If previous steps failed, FLEX falls back to a classic chat without tool calling.`;
81
+ }