@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.
- package/package.json +1 -1
- package/src/autoui-server.ts +27 -0
- package/src/index.ts +20 -6
- package/src/loop.ts +4 -12
- package/src/notebook-widgets/compact.ts +312 -0
- package/src/notebook-widgets/document.ts +372 -0
- package/src/notebook-widgets/editorial.ts +348 -0
- package/src/notebook-widgets/recipes/compact.md +104 -0
- package/src/notebook-widgets/recipes/document.md +100 -0
- package/src/notebook-widgets/recipes/editorial.md +104 -0
- package/src/notebook-widgets/recipes/workspace.md +94 -0
- package/src/notebook-widgets/shared.ts +1064 -0
- package/src/notebook-widgets/workspace.ts +328 -0
- 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 +20 -3
- 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 +667 -0
- package/src/providers/wasm.ts +132 -332
- package/src/recipes/_generated.ts +242 -0
- package/src/recipes/hackathon-assemblee-nationale.md +111 -0
- package/src/recipes/notebook-playbook.md +129 -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 +265 -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/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
|
|
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
|
+
}
|