@webmcp-auto-ui/agent 2.5.26 → 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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/autoui-server.ts +27 -0
  3. package/src/index.ts +20 -6
  4. package/src/loop.ts +4 -12
  5. package/src/notebook-widgets/compact.ts +312 -0
  6. package/src/notebook-widgets/document.ts +372 -0
  7. package/src/notebook-widgets/editorial.ts +348 -0
  8. package/src/notebook-widgets/recipes/compact.md +104 -0
  9. package/src/notebook-widgets/recipes/document.md +100 -0
  10. package/src/notebook-widgets/recipes/editorial.md +104 -0
  11. package/src/notebook-widgets/recipes/workspace.md +94 -0
  12. package/src/notebook-widgets/shared.ts +1064 -0
  13. package/src/notebook-widgets/workspace.ts +328 -0
  14. package/src/prompts/claude-prompt-builder.ts +81 -0
  15. package/src/prompts/gemma4-prompt-builder.ts +205 -0
  16. package/src/prompts/index.ts +55 -0
  17. package/src/prompts/mistral-prompt-builder.ts +90 -0
  18. package/src/prompts/qwen-prompt-builder.ts +90 -0
  19. package/src/prompts/tool-call-parsers.ts +322 -0
  20. package/src/prompts/tool-refs.ts +196 -0
  21. package/src/providers/factory.ts +20 -3
  22. package/src/providers/transformers-models.ts +143 -0
  23. package/src/providers/transformers-serialize.ts +81 -0
  24. package/src/providers/transformers.ts +329 -0
  25. package/src/providers/transformers.worker.ts +667 -0
  26. package/src/providers/wasm.ts +132 -332
  27. package/src/recipes/_generated.ts +242 -0
  28. package/src/recipes/hackathon-assemblee-nationale.md +111 -0
  29. package/src/recipes/notebook-playbook.md +129 -0
  30. package/src/tool-layers.ts +7 -403
  31. package/src/trace-observer.ts +669 -0
  32. package/src/types.ts +17 -7
  33. package/src/util/opfs-cache.ts +265 -0
  34. package/tests/gemma-prompt.test.ts +472 -0
  35. package/tests/loop.test.ts +3 -3
  36. package/tests/transformers-serialize.test.ts +103 -0
package/src/types.ts CHANGED
@@ -6,11 +6,22 @@ export type { Recipe, McpRecipe } from './recipes/types.js';
6
6
  // Short model IDs for remote (Anthropic-compatible) providers
7
7
  export type RemoteModelId = 'haiku' | 'sonnet' | 'opus' | string;
8
8
 
9
- // Model IDs for in-browser WASM providers
9
+ // Model IDs for in-browser WASM providers (MediaPipe/LiteRT)
10
10
  export type WasmModelId = 'gemma-e2b' | 'gemma-e4b' | string;
11
11
 
12
+ // Model IDs for in-browser transformers.js providers (ONNX + WebGPU)
13
+ // Canonical list in ./providers/transformers-models.ts.
14
+ export type TransformersModelId =
15
+ | 'transformers-gemma-4-e2b'
16
+ | 'transformers-gemma-4-e4b'
17
+ | 'transformers-qwen-3-4b'
18
+ | 'transformers-qwen-3.5-2b'
19
+ | 'transformers-qwen-3.5-4b'
20
+ | 'transformers-ministral-3-3b'
21
+ | string;
22
+
12
23
  // Union of all LLM IDs used by canvas.llm and LLMSelector
13
- export type LLMId = RemoteModelId | WasmModelId;
24
+ export type LLMId = RemoteModelId | WasmModelId | TransformersModelId;
14
25
 
15
26
  // Backward compat alias
16
27
  export type ModelId = LLMId;
@@ -22,6 +33,7 @@ export interface ChatMessage {
22
33
 
23
34
  export type ContentBlock =
24
35
  | { type: 'text'; text: string }
36
+ | { type: 'image'; data: string; mediaType: string }
25
37
  | { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> }
26
38
  | { type: 'tool_result'; tool_use_id: string; content: string };
27
39
 
@@ -50,9 +62,9 @@ export interface LLMProvider {
50
62
  readonly model: string;
51
63
  /** Hint for system prompt builders: which syntax this provider expects for tool
52
64
  * references. `undefined` → treated as `'generic'`. Providers using a non-standard
53
- * native call syntax (e.g. Gemma) should set this so the agent loop can build
54
- * the prompt with the correct formatting. */
55
- readonly promptKind?: 'generic' | 'gemma';
65
+ * native call syntax (e.g. Gemma, Qwen ChatML, Mistral [INST]) should set this so
66
+ * the agent loop can build the prompt with the correct formatting. */
67
+ readonly promptKind?: 'generic' | 'gemma' | 'qwen' | 'mistral';
56
68
  chat(
57
69
  messages: ChatMessage[],
58
70
  tools: ProviderTool[],
@@ -67,8 +79,6 @@ export interface ToolCall {
67
79
  result?: string;
68
80
  error?: string;
69
81
  elapsed?: number;
70
- /** true if this call was preceded by a discovery tool (search_recipes, get_recipe, etc.) */
71
- guided?: boolean;
72
82
  }
73
83
 
74
84
  export interface AgentMetrics {
@@ -0,0 +1,265 @@
1
+ /**
2
+ * OPFS model cache — download & cache N files per model repo in the
3
+ * Origin Private File System, returning a streamable Uint8Array for each file.
4
+ *
5
+ * Designed for large-model scenarios (MediaPipe .task, transformers.js ONNX
6
+ * multi-file bundles, tokenizers, configs). Each file is streamed from network
7
+ * with `tee()` so the consumer and the OPFS writer share a single download.
8
+ *
9
+ * Cache validation strategy: for every file we write a sibling `<file>.complete`
10
+ * marker containing the total size in bytes. A cache is considered valid only
11
+ * when the file exists AND the marker exists AND their sizes match (and if the
12
+ * caller provided `expectedSize`, that too). This avoids serving half-written
13
+ * files when a tab is closed mid-download.
14
+ */
15
+
16
+ export interface ModelFileSpec {
17
+ /** Path relative to the repo root (e.g. "onnx/model.onnx", "tokenizer.json") */
18
+ path: string;
19
+ /** Expected byte size if known — enables exact match cache validation */
20
+ expectedSize?: number;
21
+ }
22
+
23
+ export interface CacheProgress {
24
+ /** 0-1 progress of the file currently being processed */
25
+ fileProgress: number;
26
+ /** 0-1 aggregate progress across all files */
27
+ totalProgress: number;
28
+ status: 'cached' | 'downloading' | 'initializing' | 'error';
29
+ currentFile?: string;
30
+ /** Bytes loaded for current file */
31
+ loaded: number;
32
+ /** Total bytes expected for current file */
33
+ total: number;
34
+ }
35
+
36
+ /** Sanitize a repo id (e.g. "owner/name") into a valid OPFS folder name. */
37
+ function sanitizeRepoKey(repo: string): string {
38
+ return repo.replace(/\//g, '__').replace(/[^a-zA-Z0-9_.-]/g, '_');
39
+ }
40
+
41
+ /**
42
+ * Walk nested directory segments, creating subdirectories as needed.
43
+ * Returns `{ parent, filename }` where `parent` is the deepest directory
44
+ * handle and `filename` is the leaf name.
45
+ */
46
+ async function resolveFileParent(
47
+ repoDir: FileSystemDirectoryHandle,
48
+ relPath: string,
49
+ ): Promise<{ parent: FileSystemDirectoryHandle; filename: string }> {
50
+ const segments = relPath.split('/').filter((s) => s.length > 0);
51
+ if (segments.length === 0) throw new Error(`Invalid file path: ${relPath}`);
52
+ let parent = repoDir;
53
+ for (let i = 0; i < segments.length - 1; i++) {
54
+ parent = await parent.getDirectoryHandle(segments[i], { create: true });
55
+ }
56
+ return { parent, filename: segments[segments.length - 1] };
57
+ }
58
+
59
+ /**
60
+ * Removes legacy cache entries: pre-refactor the helper stored model files
61
+ * directly under `webmcp-models/<filename>`. The new layout nests them under
62
+ * `webmcp-models/<repo-key>/<filename>`, so the old top-level files are
63
+ * orphaned and can each weigh several GB.
64
+ * Runs once per process.
65
+ */
66
+ let legacyCleanupDone = false;
67
+ async function cleanupLegacyModelFiles(modelsDir: FileSystemDirectoryHandle): Promise<void> {
68
+ if (legacyCleanupDone) return;
69
+ legacyCleanupDone = true;
70
+ try {
71
+ const dir = modelsDir as unknown as {
72
+ entries: () => AsyncIterable<[string, FileSystemHandle]>;
73
+ };
74
+ for await (const [name, handle] of dir.entries()) {
75
+ if (handle.kind === 'file') {
76
+ try { await modelsDir.removeEntry(name); } catch { /* best-effort */ }
77
+ }
78
+ }
79
+ } catch { /* iteration unsupported or blocked — skip silently */ }
80
+ }
81
+
82
+ /**
83
+ * Load every requested file from the OPFS cache if valid, otherwise stream it
84
+ * from the HuggingFace repo and cache it in the background.
85
+ *
86
+ * The returned Map is keyed by the original `file.path` (including any
87
+ * subdirectory prefix) and contains a `ReadableStream<Uint8Array>` per file.
88
+ * Consumers are responsible for consuming the streams.
89
+ */
90
+ export async function loadOrDownloadModel(
91
+ repo: string,
92
+ files: ModelFileSpec[],
93
+ onProgress?: (progress: CacheProgress) => void,
94
+ ): Promise<Map<string, ReadableStream<Uint8Array>>> {
95
+ const root = await navigator.storage.getDirectory();
96
+ const modelsDir = await root.getDirectoryHandle('webmcp-models', { create: true });
97
+ await cleanupLegacyModelFiles(modelsDir);
98
+ const repoKey = sanitizeRepoKey(repo);
99
+ const repoDir = await modelsDir.getDirectoryHandle(repoKey, { create: true });
100
+
101
+ const totalExpected = files.reduce((s, f) => s + (f.expectedSize ?? 0), 0);
102
+ let totalLoaded = 0;
103
+ const result = new Map<string, ReadableStream<Uint8Array>>();
104
+
105
+ for (const file of files) {
106
+ const { parent, filename } = await resolveFileParent(repoDir, file.path);
107
+ const markerName = `${filename}.complete`;
108
+
109
+ // Clean orphan .crswap files (Chrome WritableStream leftovers).
110
+ try { await parent.removeEntry(`${filename}.crswap`); } catch { /* no swap — OK */ }
111
+
112
+ // ── Cache hit attempt ───────────────────────────────────────────
113
+ let cacheHitSize: number | null = null;
114
+ try {
115
+ const fileHandle = await parent.getFileHandle(filename);
116
+ const fileObj = await fileHandle.getFile();
117
+ let expectedFromMarker: number | null = null;
118
+ try {
119
+ const markerHandle = await parent.getFileHandle(markerName);
120
+ const markerText = await (await markerHandle.getFile()).text();
121
+ expectedFromMarker = Number(markerText.trim());
122
+ } catch {
123
+ // Marker missing — try backfill via HEAD request
124
+ try {
125
+ const head = await fetch(`https://huggingface.co/${repo}/resolve/main/${file.path}`, { method: 'HEAD' });
126
+ if (head.ok) {
127
+ const headerSize = Number(head.headers.get('content-length'));
128
+ if (Number.isFinite(headerSize) && headerSize > 0 && fileObj.size === headerSize) {
129
+ // Backfill marker
130
+ const markerHandle = await parent.getFileHandle(markerName, { create: true });
131
+ const markerWritable = await markerHandle.createWritable();
132
+ await markerWritable.write(String(headerSize));
133
+ await markerWritable.close();
134
+ expectedFromMarker = headerSize;
135
+ } else {
136
+ // Size mismatch — drop cached file
137
+ try { await parent.removeEntry(filename); } catch {}
138
+ }
139
+ }
140
+ } catch { /* network/HEAD failed — treat as cache miss */ }
141
+ }
142
+
143
+ if (
144
+ expectedFromMarker !== null
145
+ && fileObj.size === expectedFromMarker
146
+ && (file.expectedSize === undefined || file.expectedSize === fileObj.size)
147
+ ) {
148
+ cacheHitSize = fileObj.size;
149
+ onProgress?.({
150
+ fileProgress: 1,
151
+ totalProgress: totalExpected > 0 ? (totalLoaded + fileObj.size) / totalExpected : 1,
152
+ status: 'cached',
153
+ currentFile: file.path,
154
+ loaded: fileObj.size,
155
+ total: fileObj.size,
156
+ });
157
+ totalLoaded += fileObj.size;
158
+ result.set(file.path, fileObj.stream() as ReadableStream<Uint8Array>);
159
+ }
160
+ } catch {
161
+ // Cache miss — fall through to download
162
+ }
163
+
164
+ if (cacheHitSize !== null) continue;
165
+
166
+ // ── Network download (retry on 503) ─────────────────────────────
167
+ const url = `https://huggingface.co/${repo}/resolve/main/${file.path}`;
168
+ let response: Response | null = null;
169
+ for (let attempt = 0; attempt < 3; attempt++) {
170
+ response = await fetch(url);
171
+ if (response.ok) break;
172
+ if (response.status === 503 && attempt < 2) {
173
+ const wait = (attempt + 1) * 5000;
174
+ onProgress?.({
175
+ fileProgress: 0,
176
+ totalProgress: totalExpected > 0 ? totalLoaded / totalExpected : 0,
177
+ status: 'downloading',
178
+ currentFile: file.path,
179
+ loaded: 0,
180
+ total: file.expectedSize ?? 0,
181
+ });
182
+ await new Promise((r) => setTimeout(r, wait));
183
+ continue;
184
+ }
185
+ throw new Error(`Download failed for ${file.path}: ${response.status} ${response.statusText}`);
186
+ }
187
+ if (!response || !response.ok) throw new Error(`Download failed for ${file.path} after retries`);
188
+ if (!response.body) throw new Error(`Response body is null for ${file.path}`);
189
+
190
+ const headerTotal = Number(response.headers.get('content-length'));
191
+ const total = Number.isFinite(headerTotal) && headerTotal > 0
192
+ ? headerTotal
193
+ : (file.expectedSize ?? 0);
194
+
195
+ const [streamForConsumer, streamForCache] = response.body.tee();
196
+
197
+ // Background OPFS cache (fire-and-forget). Marker is written AFTER close
198
+ // succeeds, so a crashed tab will leave a file without a marker, which is
199
+ // detected as "invalid cache" on the next load.
200
+ (async () => {
201
+ try {
202
+ const handle = await parent.getFileHandle(filename, { create: true });
203
+ const writable = await handle.createWritable();
204
+ await streamForCache.pipeTo(writable);
205
+ const markerHandle = await parent.getFileHandle(markerName, { create: true });
206
+ const markerWritable = await markerHandle.createWritable();
207
+ await markerWritable.write(String(total));
208
+ await markerWritable.close();
209
+ } catch {
210
+ try { await parent.removeEntry(filename); } catch {}
211
+ try { await parent.removeEntry(markerName); } catch {}
212
+ }
213
+ })();
214
+
215
+ // Capture outer variables for the transform stream closure
216
+ const filePath = file.path;
217
+ const baselineTotalLoaded = totalLoaded;
218
+ const totalExpectedLocal = totalExpected;
219
+ let loaded = 0;
220
+ const progressTransform = new TransformStream<Uint8Array, Uint8Array>({
221
+ transform(chunk, controller) {
222
+ loaded += chunk.length;
223
+ const denom = totalExpectedLocal || total || 1;
224
+ onProgress?.({
225
+ fileProgress: total > 0 ? loaded / total : 0,
226
+ totalProgress: (baselineTotalLoaded + loaded) / denom,
227
+ status: 'downloading',
228
+ currentFile: filePath,
229
+ loaded,
230
+ total,
231
+ });
232
+ controller.enqueue(chunk);
233
+ },
234
+ flush() {
235
+ // No-op: totalLoaded is advanced eagerly below so that subsequent
236
+ // files reflect the contribution of the current file even if the
237
+ // consumer is still draining the stream.
238
+ },
239
+ });
240
+
241
+ // Eagerly advance the baseline for subsequent files: by the time we
242
+ // process the next file we assume this one will complete (or fail — in
243
+ // which case progress is moot anyway).
244
+ totalLoaded += total;
245
+
246
+ result.set(file.path, streamForConsumer.pipeThrough(progressTransform));
247
+ }
248
+
249
+ return result;
250
+ }
251
+
252
+ /**
253
+ * Remove every cached file for a given repo. Silently no-ops if the repo
254
+ * directory does not exist.
255
+ */
256
+ export async function clearModelCache(repo: string): Promise<void> {
257
+ try {
258
+ const root = await navigator.storage.getDirectory();
259
+ const modelsDir = await root.getDirectoryHandle('webmcp-models', { create: false });
260
+ const repoKey = sanitizeRepoKey(repo);
261
+ await modelsDir.removeEntry(repoKey, { recursive: true });
262
+ } catch {
263
+ // Nothing to clear
264
+ }
265
+ }