@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 +10 -2
- package/src/autoui-server.ts +80 -65
- package/src/index.ts +25 -6
- package/src/loop.ts +52 -33
- package/src/prompts/claude-prompt-builder.ts +81 -0
- package/src/prompts/gemma4-prompt-builder.ts +205 -0
- package/src/prompts/index.ts +55 -0
- package/src/prompts/mistral-prompt-builder.ts +90 -0
- package/src/prompts/qwen-prompt-builder.ts +90 -0
- package/src/prompts/tool-call-parsers.ts +322 -0
- package/src/prompts/tool-refs.ts +196 -0
- package/src/providers/factory.ts +34 -3
- package/src/providers/hawk-models.ts +22 -0
- package/src/providers/hawk.ts +181 -0
- package/src/providers/transformers-models.ts +143 -0
- package/src/providers/transformers-serialize.ts +81 -0
- package/src/providers/transformers.ts +329 -0
- package/src/providers/transformers.worker.ts +640 -0
- package/src/providers/wasm.ts +132 -332
- package/src/recipes/_generated.ts +306 -0
- package/src/recipes/hackathon-assemblee-nationale.md +111 -0
- package/src/recipes/notebook-playbook.md +193 -0
- package/src/server/hawkProxy.ts +54 -0
- package/src/server/index.ts +2 -0
- package/src/tool-layers.ts +7 -403
- package/src/trace-observer.ts +669 -0
- package/src/types.ts +17 -7
- package/src/util/opfs-cache.ts +364 -0
- package/src/util/storage-inventory.ts +195 -0
- package/tests/gemma-prompt.test.ts +472 -0
- package/tests/loop.test.ts +3 -3
- package/tests/transformers-serialize.test.ts +103 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webmcp-auto-ui/agent",
|
|
3
|
-
"version": "2.5.
|
|
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/
|
|
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
|
}
|
package/src/autoui-server.ts
CHANGED
|
@@ -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
|
|
993
|
-
|
|
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
|
|
7
|
-
export type { WasmProviderOptions, WasmStatus
|
|
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 './
|
|
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,
|
|
29
|
-
export type { ToolLayer, McpLayer, WebMcpLayer,
|
|
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,
|
|
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 './
|
|
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
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
655
|
+
// Remove orphaned tool_result blocks anywhere in history — strict 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
|
+
}
|