@webmcp-auto-ui/agent 2.5.25 → 2.5.27

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.
Files changed (73) hide show
  1. package/package.json +1 -1
  2. package/src/autoui-server.ts +44 -0
  3. package/src/diagnostics.ts +6 -6
  4. package/src/discovery-cache.ts +17 -3
  5. package/src/index.ts +18 -4
  6. package/src/loop.ts +31 -34
  7. package/src/notebook-widgets/compact.ts +312 -0
  8. package/src/notebook-widgets/document.ts +372 -0
  9. package/src/notebook-widgets/editorial.ts +348 -0
  10. package/src/notebook-widgets/recipes/compact.md +104 -0
  11. package/src/notebook-widgets/recipes/document.md +100 -0
  12. package/src/notebook-widgets/recipes/editorial.md +104 -0
  13. package/src/notebook-widgets/recipes/workspace.md +94 -0
  14. package/src/notebook-widgets/shared.ts +1064 -0
  15. package/src/notebook-widgets/workspace.ts +328 -0
  16. package/src/prompts/claude-prompt-builder.ts +81 -0
  17. package/src/prompts/gemma4-prompt-builder.ts +205 -0
  18. package/src/prompts/index.ts +55 -0
  19. package/src/prompts/mistral-prompt-builder.ts +90 -0
  20. package/src/prompts/qwen-prompt-builder.ts +90 -0
  21. package/src/prompts/tool-call-parsers.ts +322 -0
  22. package/src/prompts/tool-refs.ts +196 -0
  23. package/src/providers/factory.ts +20 -3
  24. package/src/providers/transformers-models.ts +143 -0
  25. package/src/providers/transformers-serialize.ts +81 -0
  26. package/src/providers/transformers.ts +329 -0
  27. package/src/providers/transformers.worker.ts +667 -0
  28. package/src/providers/wasm.ts +150 -510
  29. package/src/recipes/_generated.ts +515 -0
  30. package/src/recipes/canary-data.md +50 -0
  31. package/src/recipes/canary-display.md +99 -0
  32. package/src/recipes/canary-middle.md +32 -0
  33. package/src/recipes/hackathon-assemblee-nationale.md +111 -0
  34. package/src/recipes/hummingbird-data.md +32 -0
  35. package/src/recipes/hummingbird-display.md +36 -0
  36. package/src/recipes/hummingbird-middle.md +18 -0
  37. package/src/recipes/notebook-playbook.md +129 -0
  38. package/src/tool-layers.ts +33 -157
  39. package/src/trace-observer.ts +669 -0
  40. package/src/types.ts +20 -5
  41. package/src/util/opfs-cache.ts +265 -0
  42. package/tests/gemma-prompt.test.ts +472 -0
  43. package/tests/loop.test.ts +5 -5
  44. package/tests/transformers-serialize.test.ts +103 -0
  45. package/src/providers/gemma.worker.legacy.ts +0 -123
  46. package/src/providers/litert.worker.ts +0 -294
  47. package/src/recipes/widgets/actions.md +0 -28
  48. package/src/recipes/widgets/alert.md +0 -27
  49. package/src/recipes/widgets/cards.md +0 -41
  50. package/src/recipes/widgets/carousel.md +0 -39
  51. package/src/recipes/widgets/chart-rich.md +0 -51
  52. package/src/recipes/widgets/chart.md +0 -32
  53. package/src/recipes/widgets/code.md +0 -21
  54. package/src/recipes/widgets/d3.md +0 -36
  55. package/src/recipes/widgets/data-table.md +0 -46
  56. package/src/recipes/widgets/gallery.md +0 -39
  57. package/src/recipes/widgets/grid-data.md +0 -57
  58. package/src/recipes/widgets/hemicycle.md +0 -43
  59. package/src/recipes/widgets/js-sandbox.md +0 -32
  60. package/src/recipes/widgets/json-viewer.md +0 -27
  61. package/src/recipes/widgets/kv.md +0 -31
  62. package/src/recipes/widgets/list.md +0 -24
  63. package/src/recipes/widgets/log.md +0 -39
  64. package/src/recipes/widgets/map.md +0 -49
  65. package/src/recipes/widgets/profile.md +0 -49
  66. package/src/recipes/widgets/recipe-browser.md +0 -102
  67. package/src/recipes/widgets/sankey.md +0 -54
  68. package/src/recipes/widgets/stat-card.md +0 -43
  69. package/src/recipes/widgets/stat.md +0 -35
  70. package/src/recipes/widgets/tags.md +0 -30
  71. package/src/recipes/widgets/text.md +0 -19
  72. package/src/recipes/widgets/timeline.md +0 -38
  73. package/src/recipes/widgets/trombinoscope.md +0 -39
@@ -0,0 +1,328 @@
1
+ // @ts-nocheck
2
+ // ---------------------------------------------------------------------------
3
+ // notebook-workspace — dense analyst workspace (hex-like)
4
+ // Header bar + sidebar (sources + cells nav) + main cells area.
5
+ // ---------------------------------------------------------------------------
6
+
7
+ import {
8
+ createState, injectStyles, mountRunControls, mountHistoryPanel,
9
+ setupDnD, deleteCellWithConfirm, restoreCellFromSnapshot, addCell,
10
+ autosize, openShareModal, registerHistoryObserver,
11
+ buildServersButton,
12
+ type NotebookState, type NotebookCell,
13
+ } from './shared.js';
14
+
15
+ export async function render(container: HTMLElement, data: Record<string, unknown>): Promise<() => void> {
16
+ injectStyles();
17
+ injectLayoutStyles();
18
+
19
+ const state: NotebookState = createState({
20
+ id: data.id as string,
21
+ title: data.title as string ?? 'Untitled notebook',
22
+ mode: (data.mode as any) ?? 'edit',
23
+ cells: data.cells as any,
24
+ });
25
+ let activeCellId: string | null = state.cells.find((c) => c.type !== 'md')?.id ?? state.cells[0]?.id ?? null;
26
+
27
+ container.classList.add('nb-root');
28
+ container.classList.toggle('nb-view-mode', state.mode === 'view');
29
+
30
+ container.innerHTML = `
31
+ <div class="nbw-shell">
32
+ <div class="nbw-header">
33
+ <div class="nbw-logo"></div>
34
+ <input class="nbw-title-edit nb-title-edit" value="${escapeAttr(state.title)}">
35
+ <span class="nbw-tag">draft</span>
36
+ <div class="nbw-ctx">
37
+ <span class="nbw-source">no source connected</span>
38
+ <div class="nb-mode-switch">
39
+ <button class="nb-mode-edit nb-on">edit</button>
40
+ <button class="nb-mode-view">view</button>
41
+ </div>
42
+ <button class="nb-btn nbw-history-btn">⟲ history</button>
43
+ <span class="nbw-servers-slot"></span>
44
+ <button class="nb-btn">run all</button>
45
+ <button class="nb-btn nbw-share-btn">share</button>
46
+ <button class="nb-btn nb-btn-primary">publish</button>
47
+ </div>
48
+ </div>
49
+ <div class="nb-history-panel nbw-history-panel"></div>
50
+ <div class="nbw-body">
51
+ <aside class="nbw-sidebar">
52
+ <div class="nbw-section">sources</div>
53
+ <div class="nbw-item">◉ no source</div>
54
+ <div class="nbw-item nbw-indent nbw-dim">connect via mcp…</div>
55
+ <div class="nbw-section">cells</div>
56
+ <div class="nbw-cells-nav"></div>
57
+ <div class="nbw-add">
58
+ <button class="nb-btn nb-add-cell" data-add="md">+ md</button>
59
+ <button class="nb-btn nb-add-cell" data-add="sql">+ sql</button>
60
+ <button class="nb-btn nb-add-cell" data-add="js">+ js</button>
61
+ </div>
62
+ </aside>
63
+ <div class="nbw-cells"></div>
64
+ </div>
65
+ </div>`;
66
+
67
+ const shell = container.querySelector('.nbw-shell') as HTMLElement;
68
+ const cellsEl = shell.querySelector('.nbw-cells') as HTMLElement;
69
+ const navEl = shell.querySelector('.nbw-cells-nav') as HTMLElement;
70
+ const historyPanel = shell.querySelector('.nbw-history-panel') as HTMLElement;
71
+
72
+ function renderCells() {
73
+ cellsEl.innerHTML = '';
74
+ navEl.innerHTML = '';
75
+ state.cells.forEach((cell, idx) => {
76
+ const navItem = document.createElement('div');
77
+ navItem.className = 'nbw-item' + (cell.id === activeCellId ? ' nbw-active' : '');
78
+ navItem.textContent = `${idx + 1} · ${cell.name || cell.type}`;
79
+ navItem.addEventListener('click', () => { activeCellId = cell.id; rerender(); });
80
+ navEl.appendChild(navItem);
81
+
82
+ cellsEl.appendChild(renderCell(cell, idx, state, rerender));
83
+ });
84
+ }
85
+
86
+ function rerender() {
87
+ mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); });
88
+ renderCells();
89
+ }
90
+
91
+ shell.querySelectorAll<HTMLElement>('[data-add]').forEach((btn) => {
92
+ btn.addEventListener('click', () => {
93
+ const type = btn.dataset.add as any;
94
+ const name = type === 'md' ? 'note' : type === 'sql' ? 'query_' + (state.cells.length + 1) : 'cell_' + (state.cells.length + 1);
95
+ const cell = addCell(state, type, { name });
96
+ activeCellId = cell.id;
97
+ rerender();
98
+ });
99
+ });
100
+ (shell.querySelector('.nbw-history-btn') as HTMLElement).addEventListener('click', () => {
101
+ historyPanel.classList.toggle('nb-open');
102
+ });
103
+ (shell.querySelector('.nbw-share-btn') as HTMLElement).addEventListener('click', () => {
104
+ openShareModal(state, (fmt) => console.log('[notebook-workspace] share as', fmt, state));
105
+ });
106
+ (shell.querySelector('.nbw-title-edit') as HTMLInputElement).addEventListener('input', (e) => {
107
+ state.title = (e.target as HTMLInputElement).value;
108
+ });
109
+ const editBtn = shell.querySelector('.nb-mode-edit') as HTMLElement;
110
+ const viewBtn = shell.querySelector('.nb-mode-view') as HTMLElement;
111
+ editBtn.addEventListener('click', () => {
112
+ state.mode = 'edit';
113
+ container.classList.remove('nb-view-mode');
114
+ editBtn.classList.add('nb-on'); viewBtn.classList.remove('nb-on');
115
+ });
116
+ viewBtn.addEventListener('click', () => {
117
+ state.mode = 'view';
118
+ container.classList.add('nb-view-mode');
119
+ viewBtn.classList.add('nb-on'); editBtn.classList.remove('nb-on');
120
+ });
121
+
122
+ buildServersButton(state, shell.querySelector('.nbw-servers-slot') as HTMLElement, data, rerender);
123
+
124
+ setupDnD(cellsEl, state, rerender);
125
+ const unsubHistory = registerHistoryObserver(() => mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); }));
126
+
127
+ rerender();
128
+ return () => { unsubHistory(); };
129
+ }
130
+
131
+ function renderCell(cell: NotebookCell, idx: number, state: NotebookState, rerender: () => void): HTMLElement {
132
+ const wrap = document.createElement('div');
133
+ wrap.className = 'nb-cell-wrapper nbw-cell';
134
+ wrap.dataset.id = cell.id;
135
+
136
+ const inner = document.createElement('div');
137
+ inner.className = 'nb-cell';
138
+
139
+ const head = document.createElement('div');
140
+ head.className = 'nbw-cell-head';
141
+ const isCode = cell.type !== 'md';
142
+ head.innerHTML = `
143
+ <span class="nb-drag-handle" draggable="true" title="drag">⋮⋮</span>
144
+ ${isCode ? '<span class="nbw-run-controls"></span>' : ''}
145
+ <span class="nbw-type nbw-type-${cell.type}">${cell.type}</span>
146
+ <input class="nbw-cell-name-edit" value="${idx + 1} · ${escapeAttr(cell.name || '')}">
147
+ <div class="nbw-meta">
148
+ ${isCode ? `<span class="nbw-meta-info">${cell.lastMs != null ? cell.lastMs + 'ms' : '—'} · 4 rows</span>` : ''}
149
+ <button class="nb-icon-btn nb-toggle-src">${cell.hideSource ? '▸ src' : '◂ src'}</button>
150
+ ${isCode ? `<button class="nb-icon-btn nb-toggle-res">${cell.hideResult ? '▸ res' : '◂ res'}</button>` : ''}
151
+ <button class="nb-icon-btn nb-danger nbw-del">✕</button>
152
+ </div>`;
153
+ inner.appendChild(head);
154
+
155
+ if (isCode) {
156
+ mountRunControls(head.querySelector('.nbw-run-controls') as HTMLElement, cell, wrap, rerender);
157
+ }
158
+
159
+ const body = document.createElement('div');
160
+ body.className = 'nbw-cell-body' + (isCode ? ' nbw-code' : '') + (cell.hideSource ? ' nbw-hidden' : '');
161
+ const ta = document.createElement('textarea');
162
+ ta.className = isCode ? 'nb-code-edit' : 'nb-md-edit';
163
+ ta.value = cell.content;
164
+ ta.rows = 1;
165
+ ta.spellcheck = false;
166
+ ta.addEventListener('input', () => { cell.content = ta.value; autosize(ta); cell.status = 'stale'; });
167
+ body.appendChild(ta);
168
+ inner.appendChild(body);
169
+ setTimeout(() => autosize(ta), 0);
170
+
171
+ if (cell.type === 'sql' && !cell.hideResult) {
172
+ const resWrap = document.createElement('div');
173
+ resWrap.innerHTML = `
174
+ <table class="nbw-result-table">
175
+ <thead><tr><th>col_a</th><th>col_b</th><th>share</th></tr></thead>
176
+ <tbody>
177
+ <tr><td>row_1</td><td>42</td><td><div class="nbw-share-bar" style="width:100%"></div></td></tr>
178
+ <tr><td>row_2</td><td>29</td><td><div class="nbw-share-bar" style="width:69%"></div></td></tr>
179
+ <tr><td>row_3</td><td>22</td><td><div class="nbw-share-bar" style="width:52%"></div></td></tr>
180
+ <tr><td>row_4</td><td>9</td><td><div class="nbw-share-bar" style="width:22%"></div></td></tr>
181
+ </tbody>
182
+ </table>`;
183
+ inner.appendChild(resWrap);
184
+ } else if (cell.type === 'js' && !cell.hideResult) {
185
+ const chart = document.createElement('div');
186
+ chart.className = 'nbw-chart';
187
+ chart.innerHTML = `
188
+ <div class="nbw-bar" style="height:100%"></div>
189
+ <div class="nbw-bar" style="height:68%"></div>
190
+ <div class="nbw-bar" style="height:52%"></div>
191
+ <div class="nbw-bar" style="height:22%"></div>`;
192
+ inner.appendChild(chart);
193
+ }
194
+
195
+ (head.querySelector('.nb-toggle-src') as HTMLElement).addEventListener('click', () => { cell.hideSource = !cell.hideSource; rerender(); });
196
+ const togRes = head.querySelector('.nb-toggle-res') as HTMLElement | null;
197
+ if (togRes) togRes.addEventListener('click', () => { cell.hideResult = !cell.hideResult; rerender(); });
198
+ (head.querySelector('.nbw-del') as HTMLElement).addEventListener('click', () =>
199
+ deleteCellWithConfirm(state, cell, (c) => `${c.type} cell "${c.name}"`, rerender)
200
+ );
201
+ (head.querySelector('.nbw-cell-name-edit') as HTMLInputElement).addEventListener('input', (e) => {
202
+ const v = (e.target as HTMLInputElement).value;
203
+ const m = v.match(/^\d+\s*·\s*(.+)$/);
204
+ cell.name = m ? m[1] : v;
205
+ });
206
+
207
+ wrap.appendChild(inner);
208
+ return wrap;
209
+ }
210
+
211
+ function escapeAttr(s: string): string {
212
+ return (s ?? '').replace(/"/g, '&quot;');
213
+ }
214
+
215
+ function injectLayoutStyles(): void {
216
+ if (document.getElementById('nbw-styles')) return;
217
+ const style = document.createElement('style');
218
+ style.id = 'nbw-styles';
219
+ style.textContent = `
220
+ .nbw-shell {
221
+ background: var(--color-surface); border: 1px solid var(--color-border);
222
+ border-radius: 12px; overflow: hidden;
223
+ }
224
+ .nbw-header {
225
+ display: flex; align-items: center; padding: 10px 14px; gap: 12px;
226
+ border-bottom: 1px solid var(--color-border); background: var(--color-surface2);
227
+ }
228
+ .nbw-logo { width: 14px; height: 14px; background: var(--color-accent); border-radius: 3px; }
229
+ .nbw-title-edit {
230
+ font-size: 13px; font-weight: 500; color: var(--color-text1);
231
+ background: transparent; border: none; outline: none;
232
+ font-family: var(--font-sans, 'Syne', sans-serif);
233
+ width: 260px; padding: 2px 4px; border-radius: 3px;
234
+ }
235
+ .nbw-title-edit:focus { background: var(--color-bg); }
236
+ .nbw-tag {
237
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
238
+ font-size: 10px; color: var(--color-text2);
239
+ background: var(--color-bg); padding: 2px 7px; border-radius: 3px;
240
+ border: 1px solid var(--color-border);
241
+ }
242
+ .nbw-ctx {
243
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
244
+ font-size: 11px; color: var(--color-text2);
245
+ margin-left: auto; display: flex; gap: 8px; align-items: center;
246
+ }
247
+ .nbw-history-panel { margin: 0 14px; }
248
+ .nbw-body { display: grid; grid-template-columns: 180px 1fr; min-height: 380px; }
249
+ .nbw-sidebar {
250
+ border-right: 1px solid var(--color-border);
251
+ background: var(--color-surface2); padding: 14px 12px; font-size: 12px;
252
+ }
253
+ .nbw-section {
254
+ color: var(--color-text2);
255
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
256
+ font-size: 10px; letter-spacing: 0.1em;
257
+ margin: 0 0 8px; text-transform: uppercase;
258
+ }
259
+ .nbw-section:not(:first-child) { margin-top: 16px; }
260
+ .nbw-item {
261
+ padding: 4px 6px; color: var(--color-text2);
262
+ border-radius: 4px; cursor: pointer;
263
+ display: flex; align-items: center; gap: 6px;
264
+ }
265
+ .nbw-item:hover { background: var(--color-surface); color: var(--color-text1); }
266
+ .nbw-item.nbw-indent { padding-left: 18px; }
267
+ .nbw-item.nbw-dim { opacity: 0.5; }
268
+ .nbw-item.nbw-active {
269
+ background: var(--color-surface); color: var(--color-text1);
270
+ border-left: 2px solid var(--color-accent); border-radius: 0 4px 4px 0;
271
+ }
272
+ .nbw-add { margin-top: 10px; display: flex; gap: 4px; flex-wrap: wrap; }
273
+ .nbw-add .nb-btn { flex: 1; font-size: 10px; padding: 3px 4px; }
274
+
275
+ .nbw-cells { display: flex; flex-direction: column; }
276
+ .nbw-cell .nb-cell { border-bottom: 1px solid var(--color-border); position: relative; }
277
+ .nbw-cell:last-child .nb-cell { border-bottom: none; }
278
+ .nbw-cell-head {
279
+ padding: 8px 16px;
280
+ display: flex; align-items: center; gap: 8px;
281
+ background: var(--color-surface2);
282
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
283
+ font-size: 10px; color: var(--color-text2);
284
+ letter-spacing: 0.06em;
285
+ }
286
+ .nbw-type {
287
+ padding: 1px 7px; border-radius: 3px; font-weight: 500;
288
+ text-transform: uppercase; letter-spacing: 0.08em; font-size: 9.5px;
289
+ }
290
+ .nbw-type-md { background: rgba(160,160,184,0.15); color: var(--color-text2); }
291
+ .nbw-type-sql { background: rgba(124,109,250,0.18); color: var(--color-accent); }
292
+ .nbw-type-js { background: rgba(62,207,178,0.15); color: var(--color-teal); }
293
+ .nbw-cell-name-edit {
294
+ background: transparent; border: none; outline: none;
295
+ color: var(--color-text1);
296
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
297
+ font-size: 10px; width: 140px;
298
+ }
299
+ .nbw-meta { margin-left: auto; display: flex; gap: 8px; align-items: center; }
300
+ .nbw-cell-body { padding: 14px 16px; font-size: 13.5px; line-height: 1.6; }
301
+ .nbw-cell-body.nbw-code {
302
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
303
+ font-size: 12.5px; line-height: 1.65;
304
+ }
305
+ .nbw-hidden { display: none !important; }
306
+
307
+ .nbw-result-table { width: 100%; border-collapse: collapse; font-size: 12px; }
308
+ .nbw-result-table thead tr { background: var(--color-surface2); }
309
+ .nbw-result-table th {
310
+ text-align: left; padding: 7px 16px; font-weight: 500;
311
+ color: var(--color-text2);
312
+ font-family: var(--font-mono, 'IBM Plex Mono', monospace);
313
+ font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
314
+ border-top: 1px solid var(--color-border);
315
+ border-bottom: 1px solid var(--color-border);
316
+ }
317
+ .nbw-result-table td {
318
+ padding: 6px 16px; border-bottom: 1px solid var(--color-border);
319
+ font-variant-numeric: tabular-nums;
320
+ }
321
+ .nbw-result-table td:first-child { color: var(--color-text1); font-variant-numeric: normal; }
322
+ .nbw-share-bar { height: 8px; background: var(--color-accent); border-radius: 2px; }
323
+
324
+ .nbw-chart { padding: 16px; display: flex; align-items: flex-end; gap: 10px; height: 110px; }
325
+ .nbw-bar { flex: 1; background: var(--color-accent); border-radius: 2px 2px 0 0; }
326
+ `;
327
+ document.head.appendChild(style);
328
+ }
@@ -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
+ }
@@ -0,0 +1,205 @@
1
+ // Gemma 4 native format: inline tool declarations, tool-call / tool-response
2
+ // transport, and the 3-STEP minimalist system prompt.
3
+
4
+ import type { ChatMessage, ContentBlock, ProviderTool } from '../types.js';
5
+ import type { PromptRefs } from './tool-refs.js';
6
+
7
+ /** Format a value in Gemma 4 native syntax. */
8
+ export function gemmaValue(v: unknown): string {
9
+ const q = '<|"|>';
10
+ if (v === null || v === undefined) return 'null';
11
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v);
12
+ if (Array.isArray(v)) return `[${v.map(i => gemmaValue(i)).join(',')}]`;
13
+ if (typeof v === 'object') {
14
+ const entries = Object.entries(v as Record<string, unknown>)
15
+ .map(([k, val]) => `${k}:${gemmaValue(val)}`);
16
+ return `{${entries.join(',')}}`;
17
+ }
18
+ return `${q}${String(v)}${q}`;
19
+ }
20
+
21
+ /** Format a tool declaration in Gemma 4 native syntax. */
22
+ export function formatGemmaToolDeclaration(tool: ProviderTool): string {
23
+ const q = '<|"|>';
24
+ let decl = `<|tool>declaration:${tool.name}{\n`;
25
+ decl += ` description:${q}${tool.description}${q}`;
26
+
27
+ const schema = tool.input_schema;
28
+ if (schema?.properties) {
29
+ const props = schema.properties as Record<string, { description?: string; type?: string; enum?: string[]; format?: string; default?: unknown }>;
30
+ decl += `,\n parameters:{\n properties:{\n`;
31
+
32
+ const propEntries = Object.entries(props);
33
+ for (let i = 0; i < propEntries.length; i++) {
34
+ const [key, val] = propEntries[i];
35
+ decl += ` ${key}:{`;
36
+ const parts: string[] = [];
37
+ if (val.description) parts.push(`description:${q}${val.description}${q}`);
38
+ let inferredType = val.type;
39
+ if (!inferredType) {
40
+ const descLower = (val.description ?? '').toLowerCase();
41
+ if (descLower.includes('objet') || descLower.includes('object') || descLower.includes('parameter') || descLower.includes('paramètre') || key === 'params') {
42
+ inferredType = 'object';
43
+ } else {
44
+ inferredType = 'string';
45
+ }
46
+ }
47
+ parts.push(`type:${q}${inferredType.toUpperCase()}${q}`);
48
+ if (val.enum) parts.push(`enum:[${val.enum.map(e => `${q}${e}${q}`).join(',')}]`);
49
+ if (val.format) parts.push(`format:${q}${val.format}${q}`);
50
+ if (val.default !== undefined) parts.push(`default:${gemmaValue(val.default)}`);
51
+ decl += parts.join(',');
52
+ decl += `}${i < propEntries.length - 1 ? ',' : ''}\n`;
53
+ }
54
+
55
+ decl += ` }`;
56
+ if (schema.required && Array.isArray(schema.required)) {
57
+ decl += `,\n required:[${(schema.required as string[]).map(r => `${q}${r}${q}`).join(',')}]`;
58
+ }
59
+ decl += `,\n type:${q}OBJECT${q}\n }`;
60
+ }
61
+
62
+ decl += `\n}<tool|>`;
63
+ return decl;
64
+ }
65
+
66
+ /** Format a tool call in Gemma 4 native syntax. */
67
+ export function formatToolCall(name: string, input: Record<string, unknown>): string {
68
+ const entries = Object.entries(input ?? {})
69
+ .map(([k, v]) => `${k}:${gemmaValue(v)}`);
70
+ return `<|tool_call>call:${name}{${entries.join(',')}}<tool_call|>`;
71
+ }
72
+
73
+ /** Format a tool response in Gemma 4 native syntax. */
74
+ export function formatToolResponse(content: string): string {
75
+ try {
76
+ JSON.parse(content);
77
+ return `<|tool_response>response:${content}<tool_response|>`;
78
+ } catch {
79
+ return `<|tool_response>response:<|"|>${content}<|"|><tool_response|>`;
80
+ }
81
+ }
82
+
83
+ /** Build the Gemma FLEX system prompt (tool decls embedded inline via fmtToolRef). */
84
+ export function buildGemma4Prompt(refs: PromptRefs): string {
85
+ const { listRecipes, searchRecipes, listTools, searchTools, getRecipes, actionTools } = refs;
86
+
87
+ 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).
88
+
89
+ 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.
90
+
91
+ 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.
92
+
93
+ FLEX follows a multi-step lazy-loading protocol:
94
+
95
+ STEP 1 — FLEX lists all recipes
96
+
97
+ FLEX tries to fetch a relevant DATA or UI recipe using these functions:
98
+
99
+ ${listRecipes.join('\n')}
100
+
101
+ If at least one relevant recipe is found → FLEX goes to STEP 2.
102
+ If no results → FLEX goes to STEP 1b.
103
+
104
+ STEP 1b — FLEX search recipes
105
+
106
+ 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:
107
+
108
+ ${searchRecipes.join('\n')}
109
+
110
+ FLEX picks the most relevant recipe for the request.
111
+ If a recipe matches → FLEX goes to STEP 2.
112
+ If no recipe is available or relevant → FLEX goes to STEP 1c.
113
+
114
+ STEP 1c — FLEX lists tools
115
+
116
+ If FLEX does not find any applicable recipe, FLEX lists relevant tools using these functions:
117
+
118
+ ${listTools.join('\n')}
119
+
120
+ If FLEX finds a relevant tool → FLEX uses it directly in STEP 3.
121
+ If FLEX does not find any relevant tools by listing them → FLEX goes to STEP 1d.
122
+
123
+ STEP 1d — FLEX searches tools using these functions:
124
+
125
+ ${searchTools.join('\n')}
126
+
127
+ FLEX picks the most relevant tool(s) and use it directly in STEP 3.
128
+
129
+ STEP 2 — FLEX ingests the recipe in its context
130
+
131
+ ${getRecipes.join('\n')}
132
+
133
+ 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.
134
+
135
+ 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.
136
+
137
+ 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.
138
+
139
+ STEP 3 — FLEX executes tool functions
140
+
141
+ 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.
142
+
143
+ 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.
144
+
145
+ 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.
146
+
147
+ 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.
148
+
149
+ STEP 4 — UI display
150
+
151
+ Unless a recipe specifies otherwise, FLEX uses these functions to display its responses on the canvas:
152
+
153
+ ${actionTools.join('\n')}
154
+
155
+ 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.
156
+
157
+ STEP 5 — Fallback
158
+
159
+ If previous steps failed, FLEX falls back to a classic chat without tool calling.`;
160
+ }
161
+
162
+ export interface BuildGemmaPromptInput {
163
+ systemPrompt?: string;
164
+ messages?: ChatMessage[];
165
+ }
166
+
167
+ /** Build the final Gemma 4 wire-format prompt (turns + inline tool_call/response). */
168
+ export function buildGemmaPrompt(input: BuildGemmaPromptInput): string {
169
+ const { systemPrompt, messages = [] } = input;
170
+
171
+ const parts: string[] = [];
172
+
173
+ if (systemPrompt) {
174
+ parts.push(`<|turn>system\n${systemPrompt}\n<turn|>`);
175
+ }
176
+
177
+ for (const msg of messages) {
178
+ const role: 'model' | 'user' | 'system' =
179
+ msg.role === 'assistant' ? 'model' : msg.role === 'system' ? 'system' : 'user';
180
+
181
+ let segments: string[];
182
+ if (typeof msg.content === 'string') {
183
+ segments = [msg.content];
184
+ } else {
185
+ segments = [];
186
+ const blocks = msg.content as ContentBlock[];
187
+ for (const block of blocks) {
188
+ if (block.type === 'text') {
189
+ segments.push((block as { type: 'text'; text: string }).text);
190
+ } else if (block.type === 'tool_use') {
191
+ const b = block as { type: 'tool_use'; name: string; input: Record<string, unknown> };
192
+ segments.push(formatToolCall(b.name, b.input));
193
+ } else if (block.type === 'tool_result') {
194
+ const b = block as { type: 'tool_result'; tool_use_id: string; content: string };
195
+ segments.push(formatToolResponse(b.content));
196
+ }
197
+ }
198
+ }
199
+ if (segments.length === 0) continue;
200
+
201
+ parts.push(`<|turn>${role}\n${segments.join('\n')}<turn|>`);
202
+ }
203
+ parts.push('<|turn>model\n');
204
+ return parts.join('\n');
205
+ }
@@ -0,0 +1,55 @@
1
+ // Dispatcher: collect refs once, route to provider-specific template.
2
+
3
+ import { collectPromptRefs } from './tool-refs.js';
4
+ import { buildClaudePrompt } from './claude-prompt-builder.js';
5
+ import { buildGemma4Prompt } from './gemma4-prompt-builder.js';
6
+ import { buildQwenPrompt } from './qwen-prompt-builder.js';
7
+ import { buildMistralPrompt } from './mistral-prompt-builder.js';
8
+ import { toolAliasMap, type ProviderKind, type ToolLayer } from '../tool-layers.js';
9
+
10
+ export type { PromptRefs } from './tool-refs.js';
11
+ export { collectPromptRefs } from './tool-refs.js';
12
+ export { buildClaudePrompt } from './claude-prompt-builder.js';
13
+ export {
14
+ buildGemma4Prompt,
15
+ buildGemmaPrompt,
16
+ formatGemmaToolDeclaration,
17
+ formatToolCall,
18
+ formatToolResponse,
19
+ gemmaValue,
20
+ } from './gemma4-prompt-builder.js';
21
+ export type { BuildGemmaPromptInput } from './gemma4-prompt-builder.js';
22
+ export { buildQwenPrompt } from './qwen-prompt-builder.js';
23
+ export { buildMistralPrompt } from './mistral-prompt-builder.js';
24
+
25
+ export interface SystemPromptResult {
26
+ prompt: string;
27
+ aliasMap: Map<string, string>;
28
+ }
29
+
30
+ export function buildSystemPromptWithAliases(
31
+ layers: ToolLayer[],
32
+ options: { providerKind?: ProviderKind } = {},
33
+ ): SystemPromptResult {
34
+ const kind = options.providerKind ?? 'generic';
35
+ const refs = collectPromptRefs(layers, kind);
36
+ let prompt: string;
37
+ switch (kind) {
38
+ case 'gemma': prompt = buildGemma4Prompt(refs); break;
39
+ case 'qwen': prompt = buildQwenPrompt(refs); break;
40
+ case 'mistral': prompt = buildMistralPrompt(refs); break;
41
+ default: prompt = buildClaudePrompt(refs);
42
+ }
43
+ return { prompt, aliasMap: refs.aliasMap };
44
+ }
45
+
46
+ /** Backward-compat wrapper — also populates the deprecated global toolAliasMap. */
47
+ export function buildSystemPrompt(
48
+ layers: ToolLayer[],
49
+ options?: { providerKind?: ProviderKind },
50
+ ): string {
51
+ const { prompt, aliasMap } = buildSystemPromptWithAliases(layers, options);
52
+ toolAliasMap.clear();
53
+ for (const [k, v] of aliasMap) toolAliasMap.set(k, v);
54
+ return prompt;
55
+ }