diff --git a/.github/workflows/browser.yml b/.github/workflows/browser.yml
index e3f6d86..d960778 100644
--- a/.github/workflows/browser.yml
+++ b/.github/workflows/browser.yml
@@ -21,11 +21,11 @@ jobs:
matrix:
browser: [chromium, firefox, webkit]
steps:
- - uses: actions/checkout@v7
- - uses: pnpm/action-setup@v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
run_install: false
- - uses: actions/setup-node@v7
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
@@ -33,7 +33,7 @@ jobs:
- run: pnpm exec playwright install --with-deps ${{ matrix.browser }}
- run: pnpm exec playwright test --project=${{ matrix.browser }}
- if: failure()
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: browser-evidence-${{ matrix.browser }}
path: |
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dd83cda..9acfc77 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,11 +29,11 @@ jobs:
- os: ubuntu-latest
node: 26
steps:
- - uses: actions/checkout@v7
- - uses: pnpm/action-setup@v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
run_install: false
- - uses: actions/setup-node@v7
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node }}
cache: pnpm
@@ -45,11 +45,11 @@ jobs:
name: Production dependency audit
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v7
- - uses: pnpm/action-setup@v6
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
run_install: false
- - uses: actions/setup-node@v7
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 76edec5..ab17c3d 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -16,8 +16,8 @@ jobs:
name: Analyze JavaScript and TypeScript
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v7
- - uses: github/codeql-action/init@v3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ - uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with:
languages: javascript-typescript
- - uses: github/codeql-action/analyze@v3
+ - uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a06ebad..67fa2e0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,13 +13,14 @@ jobs:
name: Verify and publish prerelease assets
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- - uses: pnpm/action-setup@v6
+ persist-credentials: false
+ - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
run_install: false
- - uses: actions/setup-node@v7
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
diff --git a/README.md b/README.md
index db86add..ff495b2 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,11 @@ pnpm add ./tabloom-0.3.0-alpha.2.tgz
Verify the adjacent `.sha256` asset before installing in a controlled delivery pipeline.
+For a complete Vite consumer using only public package exports, start with the
+[pinned WebLLM starter](examples/vite-webllm/README.md). Its page and
+SharedWorker share one configuration manifest and the repository package smoke
+builds it against a freshly packed TabLoom archive.
+
## Quick start
```ts
diff --git a/eslint.config.js b/eslint.config.js
index ec03a6b..1e4e057 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -44,6 +44,16 @@ export default tseslint.config(
],
},
},
+ {
+ files: ['examples/vite-webllm/**/*.ts'],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.starter-lint.json'],
+ projectService: false,
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ },
{
files: ['scripts/**/*.mjs'],
languageOptions: {
diff --git a/examples/vite-webllm/README.md b/examples/vite-webllm/README.md
new file mode 100644
index 0000000..0407f52
--- /dev/null
+++ b/examples/vite-webllm/README.md
@@ -0,0 +1,61 @@
+# TabLoom Vite + WebLLM starter
+
+This is a clean consumer of the released TabLoom package. The application and
+SharedWorker import only documented package exports; neither entry point reaches
+into the TabLoom source tree.
+
+## Pinned boundaries
+
+| Boundary | Pinned value | Change rule |
+| ---------------- | ---------------------------------------------------- | -------------------------------------------------------- |
+| TabLoom | Verified `0.3.0-alpha.2` GitHub release + lock SRI | Update `tabloomRelease`, lock, and all consumer gates |
+| WebLLM | `0.2.84` | Update the adapter identity and re-run a real-model gate |
+| Model | `SmolLM2-360M-Instruct-q4f16_1-MLC` at one HF commit | Update the commit and SRI values; expect a new download |
+| Runtime identity | Runtime, immutable artifact URLs, SRI, and policies | Never reuse a fingerprint after any behavior change |
+| Concurrency | One generation per owner | Keep at one for the current WebLLM adapter |
+
+`package.json` names the independently verified published TabLoom boundary; it
+is intentionally separate from the repository's next unreleased package
+version. The exact application policy lives in `src/runtime-config.ts`. Model
+weights are resolved at one immutable Hugging Face revision; the config,
+tokenizer, and WASM library additionally fail closed on SRI mismatch. Both the
+page and SharedWorker derive the same fingerprint from those artifact identities
+and the runtime policy, so incompatible deployments fail negotiation instead of
+silently sharing a runtime.
+
+## Run
+
+Requires Node.js 24+, pnpm 11.13.0, a WebGPU-capable browser, and HTTPS or a
+loopback development origin.
+
+```bash
+pnpm install --frozen-lockfile
+pnpm dev
+```
+
+Open `http://127.0.0.1:4173`. The first model load downloads provider assets and
+can take time. The adaptive policy prefers a SharedWorker where its lifecycle is
+portable; otherwise it selects the fenced page-owner path. In page-owner mode,
+open the sibling page before submitting a prompt.
+
+From the repository root, the opt-in live gate exercises the pinned URLs, SRI,
+owner handshake, streaming path, and one real generation in Chrome/WebGPU:
+
+```bash
+TABLOOM_WEBLLM_LIVE=1 pnpm test:live:starter
+```
+
+## Alpha contract
+
+- There is no automatic cloud fallback or provider routing.
+- Prompts stay in the selected local browser owner unless you add an external
+ adapter yourself.
+- A request may execute again after owner takeover; external side effects need
+ an application idempotency key.
+- Built-in telemetry excludes request and generated content, but application
+ logging remains your responsibility.
+- The checked-in model and provider pair is the verified boundary, not a claim
+ that every browser, GPU, model, or later provider version works.
+
+From the repository root, `pnpm package:smoke` packs TabLoom, installs that fresh
+archive into a temporary copy of this starter, and runs its production build.
diff --git a/examples/vite-webllm/index.html b/examples/vite-webllm/index.html
new file mode 100644
index 0000000..e45044f
--- /dev/null
+++ b/examples/vite-webllm/index.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+ TabLoom WebLLM starter
+
+
+
+
+ Verified consumer starter
+ One local model runtime, shared across sibling pages.
+
+ This Vite app consumes only TabLoom's public package exports and keeps
+ the model, runtime identity, broker policy, and generation defaults in
+ one replaceable configuration module.
+
+
+
+
+
+
+
Model
+ not loaded
+
+
+
Topology
+ starting
+
+
+
Role
+ starting
+
+
+
Readiness
+ starting
+
+
+
Epoch
+ 0
+
+
+
+ Checking browser capabilities.
+
+
+
+
+
+ Waiting for a request.
+
+
+
+ Alpha boundary: WebGPU, the pinned provider runtime, and the configured
+ model are required. Automatic cloud fallback is intentionally absent.
+
+
+
+
+
diff --git a/examples/vite-webllm/package.json b/examples/vite-webllm/package.json
new file mode 100644
index 0000000..eb739c7
--- /dev/null
+++ b/examples/vite-webllm/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "tabloom-vite-webllm-starter",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "engines": {
+ "node": ">=24"
+ },
+ "packageManager": "pnpm@11.13.0",
+ "tabloomRelease": {
+ "version": "0.3.0-alpha.2",
+ "integrity": "sha512-E9LQMl+dovCjAksjA6jrmfFemviu6uT45IwjUK2dcsvnINiUPPiZUzXHd8pwbTHQhPeIft0SrnFcdTFNdxL3hg==",
+ "webLlmVersion": "0.2.84"
+ },
+ "scripts": {
+ "build": "tsc --noEmit && vite build",
+ "dev": "vite",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@aantenore/tabloom": "https://github.com/aantenore/tabloom/releases/download/v0.3.0-alpha.2/tabloom-0.3.0-alpha.2.tgz",
+ "@mlc-ai/web-llm": "0.2.84"
+ },
+ "devDependencies": {
+ "typescript": "6.0.3",
+ "vite": "8.1.5"
+ }
+}
diff --git a/examples/vite-webllm/pnpm-lock.yaml b/examples/vite-webllm/pnpm-lock.yaml
new file mode 100644
index 0000000..f2298b5
--- /dev/null
+++ b/examples/vite-webllm/pnpm-lock.yaml
@@ -0,0 +1,552 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@aantenore/tabloom':
+ specifier: https://github.com/aantenore/tabloom/releases/download/v0.3.0-alpha.2/tabloom-0.3.0-alpha.2.tgz
+ version: https://github.com/aantenore/tabloom/releases/download/v0.3.0-alpha.2/tabloom-0.3.0-alpha.2.tgz(@mlc-ai/web-llm@0.2.84)
+ '@mlc-ai/web-llm':
+ specifier: 0.2.84
+ version: 0.2.84
+ devDependencies:
+ typescript:
+ specifier: 6.0.3
+ version: 6.0.3
+ vite:
+ specifier: 8.1.5
+ version: 8.1.5
+
+packages:
+
+ '@aantenore/tabloom@https://github.com/aantenore/tabloom/releases/download/v0.3.0-alpha.2/tabloom-0.3.0-alpha.2.tgz':
+ resolution: {integrity: sha512-E9LQMl+dovCjAksjA6jrmfFemviu6uT45IwjUK2dcsvnINiUPPiZUzXHd8pwbTHQhPeIft0SrnFcdTFNdxL3hg==, tarball: https://github.com/aantenore/tabloom/releases/download/v0.3.0-alpha.2/tabloom-0.3.0-alpha.2.tgz}
+ version: 0.3.0-alpha.2
+ engines: {node: '>=24'}
+ peerDependencies:
+ '@mlc-ai/web-llm': 0.2.84
+ peerDependenciesMeta:
+ '@mlc-ai/web-llm':
+ optional: true
+
+ '@emnapi/core@1.11.1':
+ resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
+
+ '@emnapi/runtime@1.11.1':
+ resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
+
+ '@emnapi/wasi-threads@1.2.2':
+ resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
+
+ '@mlc-ai/web-llm@0.2.84':
+ resolution: {integrity: sha512-hrOWzK4/nGNmgoRKT8pgVmZZ2oEPpbblIWQOwpqNyvK2dysHw3KVB1gNJOuRcQfKOPhucEhX1NJzXzgMDnwSCQ==}
+
+ '@napi-rs/wasm-runtime@1.1.6':
+ resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@oxc-project/types@0.139.0':
+ resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==}
+
+ '@rolldown/binding-android-arm64@1.1.5':
+ resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.1.5':
+ resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.1.5':
+ resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.1.5':
+ resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.1.5':
+ resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.1.5':
+ resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.1.5':
+ resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.1.5':
+ resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.1.5':
+ resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.1.5':
+ resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.1.5':
+ resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.1.5':
+ resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.1.5':
+ resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.1.5':
+ resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.1.5':
+ resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@tybys/wasm-util@0.10.3':
+ resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ idb@8.0.3:
+ resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==}
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ loglevel@1.9.2:
+ resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
+ engines: {node: '>= 0.6.0'}
+
+ nanoid@3.3.16:
+ resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.5:
+ resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.19:
+ resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ rolldown@1.1.5:
+ resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ vite@8.1.5:
+ resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.3.0
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ zod@4.4.3:
+ resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
+
+snapshots:
+
+ '@aantenore/tabloom@https://github.com/aantenore/tabloom/releases/download/v0.3.0-alpha.2/tabloom-0.3.0-alpha.2.tgz(@mlc-ai/web-llm@0.2.84)':
+ dependencies:
+ idb: 8.0.3
+ zod: 4.4.3
+ optionalDependencies:
+ '@mlc-ai/web-llm': 0.2.84
+
+ '@emnapi/core@1.11.1':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.2
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.11.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@mlc-ai/web-llm@0.2.84':
+ dependencies:
+ loglevel: 1.9.2
+
+ '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@tybys/wasm-util': 0.10.3
+ optional: true
+
+ '@oxc-project/types@0.139.0': {}
+
+ '@rolldown/binding-android-arm64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.1.5':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.1.5':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.1.5':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.1.5':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.1.5':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.1.5':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@tybys/wasm-util@0.10.3':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ detect-libc@2.1.2: {}
+
+ fdir@6.5.0(picomatch@4.0.5):
+ optionalDependencies:
+ picomatch: 4.0.5
+
+ fsevents@2.3.3:
+ optional: true
+
+ idb@8.0.3: {}
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
+ loglevel@1.9.2: {}
+
+ nanoid@3.3.16: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.5: {}
+
+ postcss@8.5.19:
+ dependencies:
+ nanoid: 3.3.16
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ rolldown@1.1.5:
+ dependencies:
+ '@oxc-project/types': 0.139.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.1.5
+ '@rolldown/binding-darwin-arm64': 1.1.5
+ '@rolldown/binding-darwin-x64': 1.1.5
+ '@rolldown/binding-freebsd-x64': 1.1.5
+ '@rolldown/binding-linux-arm-gnueabihf': 1.1.5
+ '@rolldown/binding-linux-arm64-gnu': 1.1.5
+ '@rolldown/binding-linux-arm64-musl': 1.1.5
+ '@rolldown/binding-linux-ppc64-gnu': 1.1.5
+ '@rolldown/binding-linux-s390x-gnu': 1.1.5
+ '@rolldown/binding-linux-x64-gnu': 1.1.5
+ '@rolldown/binding-linux-x64-musl': 1.1.5
+ '@rolldown/binding-openharmony-arm64': 1.1.5
+ '@rolldown/binding-wasm32-wasi': 1.1.5
+ '@rolldown/binding-win32-arm64-msvc': 1.1.5
+ '@rolldown/binding-win32-x64-msvc': 1.1.5
+
+ source-map-js@1.2.1: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.5)
+ picomatch: 4.0.5
+
+ tslib@2.8.1:
+ optional: true
+
+ typescript@6.0.3: {}
+
+ vite@8.1.5:
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.5
+ postcss: 8.5.19
+ rolldown: 1.1.5
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ zod@4.4.3: {}
diff --git a/examples/vite-webllm/src/main.ts b/examples/vite-webllm/src/main.ts
new file mode 100644
index 0000000..0f3a2d3
--- /dev/null
+++ b/examples/vite-webllm/src/main.ts
@@ -0,0 +1,170 @@
+import TabLoomWorker from './tabloom.worker?sharedworker';
+import { WebLlmInferenceAdapter } from '@aantenore/tabloom/adapters/webllm';
+import { createAdaptiveBrowserBroker } from '@aantenore/tabloom/shared-worker';
+import { resolveRuntimeConfig } from './runtime-config';
+import './styles.css';
+
+const elements = {
+ epoch: requiredElement('epoch'),
+ form: requiredElement('form'),
+ model: requiredElement('model'),
+ output: requiredElement('output'),
+ peer: requiredElement('peer'),
+ prompt: requiredElement('prompt'),
+ readiness: requiredElement('readiness'),
+ role: requiredElement('role'),
+ status: requiredElement('status'),
+ submit: requiredElement('submit'),
+ topology: requiredElement('topology'),
+};
+
+elements.peer.addEventListener('click', () => {
+ window.open(window.location.href, '_blank', 'noopener');
+});
+
+void start();
+
+async function start(): Promise {
+ if (!window.isSecureContext || !Reflect.has(navigator, 'gpu')) {
+ setStatus(
+ 'WebGPU requires a compatible browser on HTTPS or loopback.',
+ true,
+ );
+ return;
+ }
+
+ const runtime = await resolveRuntimeConfig();
+ elements.model.textContent = runtime.modelId;
+ let active = false;
+ let canSubmit = false;
+ let terminalStatus = false;
+
+ const adapter = new WebLlmInferenceAdapter({
+ engineConfig: runtime.engineConfig,
+ modelId: runtime.modelId,
+ onProgress: ({ progress, text }) => {
+ setStatus(
+ `${text || 'Loading the local runtime'} (${Math.round(progress * 100)}%)`,
+ );
+ },
+ });
+
+ try {
+ const selection = await createAdaptiveBrowserBroker({
+ adapter,
+ config: runtime.broker,
+ topology: {
+ lifecyclePolicy: runtime.topology.lifecyclePolicy,
+ mode: runtime.topology.mode,
+ name: runtime.workerName,
+ requiredCapabilities: runtime.topology.requiredCapabilities,
+ workerFactory: ({ name }) => new TabLoomWorker({ name }),
+ },
+ });
+ const broker = selection.broker;
+ elements.topology.textContent = selection.topology;
+
+ const refreshSubmit = () => {
+ elements.submit.disabled = active || !canSubmit;
+ };
+ const unsubscribe = broker.subscribe((snapshot) => {
+ elements.epoch.textContent = String(snapshot.epoch);
+ elements.readiness.textContent = snapshot.readiness;
+ elements.role.textContent = snapshot.role;
+ canSubmit =
+ snapshot.readiness === 'ready' &&
+ (selection.topology === 'shared-worker' || snapshot.role === 'peer');
+ if (!active && !terminalStatus) {
+ if (
+ snapshot.readiness === 'ready' &&
+ selection.topology === 'page-owner' &&
+ snapshot.role === 'leader'
+ ) {
+ setStatus('Runtime ready. Open a sibling page to submit work.');
+ } else if (snapshot.readiness === 'ready') {
+ setStatus('Runtime ready for local inference.');
+ }
+ }
+ refreshSubmit();
+ });
+
+ elements.form.addEventListener('submit', (event) => {
+ event.preventDefault();
+ if (!active && canSubmit) {
+ void runPrompt();
+ }
+ });
+
+ async function runPrompt(): Promise {
+ const prompt = elements.prompt.value.trim();
+ if (prompt.length === 0) {
+ terminalStatus = true;
+ setStatus('Enter a prompt before running inference.', true);
+ return;
+ }
+
+ active = true;
+ terminalStatus = false;
+ refreshSubmit();
+ elements.output.textContent = '';
+ setStatus('Running on the selected local owner.');
+
+ try {
+ const session = broker.request({
+ max_tokens: runtime.generation.maxTokens,
+ messages: [{ content: prompt, role: 'user' }],
+ stream_options: { include_usage: true },
+ temperature: runtime.generation.temperature,
+ });
+ const resultPromise = session.result;
+ void resultPromise.catch(() => undefined);
+ let streamedText = '';
+ for await (const chunk of session) {
+ streamedText += chunk.choices[0]?.delta.content ?? '';
+ elements.output.textContent = streamedText;
+ }
+ const result = await resultPromise;
+ elements.output.textContent = result.text;
+ terminalStatus = true;
+ setStatus('Completed locally.');
+ } catch (cause) {
+ terminalStatus = true;
+ setStatus(messageFrom(cause), true);
+ } finally {
+ active = false;
+ refreshSubmit();
+ }
+ }
+
+ window.addEventListener(
+ 'pagehide',
+ () => {
+ unsubscribe();
+ void broker.stop();
+ },
+ { once: true },
+ );
+
+ setStatus('Starting the broker and local runtime.');
+ await broker.start();
+ } catch (cause) {
+ setStatus(messageFrom(cause), true);
+ }
+}
+
+function requiredElement(field: string): T {
+ const element = document.querySelector(`[data-field="${field}"]`);
+ if (element === null) {
+ throw new Error(`Missing required element: ${field}`);
+ }
+ return element as T;
+}
+
+function setStatus(message: string, error = false): void {
+ elements.status.textContent = message;
+ elements.status.classList.toggle('error', error);
+}
+
+function messageFrom(cause: unknown): string {
+ return cause instanceof Error ? cause.message : 'Local inference failed.';
+}
diff --git a/examples/vite-webllm/src/runtime-config.ts b/examples/vite-webllm/src/runtime-config.ts
new file mode 100644
index 0000000..bc2f0d9
--- /dev/null
+++ b/examples/vite-webllm/src/runtime-config.ts
@@ -0,0 +1,107 @@
+import { createRuntimeFingerprint } from '@aantenore/tabloom/core';
+import type { AppConfig, MLCEngineConfig, ModelRecord } from '@mlc-ai/web-llm';
+
+export const runtimeManifest = {
+ adapter: 'webllm@0.2.84',
+ build: 'tabloom-vite-webllm-starter@1',
+ configuration: 'single-generation-defaults@1',
+ controlPlane: 'tabloom@0.3.0-alpha.2',
+ controlPlaneIntegrity:
+ 'sha512-E9LQMl+dovCjAksjA6jrmfFemviu6uT45IwjUK2dcsvnINiUPPiZUzXHd8pwbTHQhPeIft0SrnFcdTFNdxL3hg==',
+ modelConfigIntegrity:
+ 'sha384-q5g7Vr8NPimskWQfkKEHbD/LHSezXA8Jd/6026NLSMR2rWz/4RoTUK9HsuC3seC4',
+ modelId: 'SmolLM2-360M-Instruct-q4f16_1-MLC',
+ modelLibraryIntegrity:
+ 'sha384-orS1wyCqzySQio0yY+PZDZZAZclbgZNdjmO4FRo/7SF1jyUPqMHVwkvUo28pPsCB',
+ modelLibraryUrl:
+ 'https://raw.githubusercontent.com/mlc-ai/binary-mlc-llm-libs/025bcaf3780fa8254f5e5efd3bfea0a5397248f4/web-llm-models/v0_2_84/base/SmolLM2-360M-Instruct-q4f16_1_cs1k-webgpu.wasm',
+ modelUrl:
+ 'https://huggingface.co/mlc-ai/SmolLM2-360M-Instruct-q4f16_1-MLC/resolve/3a622fd89e0216e8bb10c410c007c786baa8a033/',
+ tokenizerIntegrity:
+ 'sha384-TYQ9bvLe/Vytaer+YyXqKp0FA6bauwU+PxOlTcHSoLqTCPKrQwSHApOPo4MCLoB0',
+} as const;
+
+const modelOptions = {
+ contextWindowSize: 4_096,
+ lowResourceRequired: true,
+ requiredFeatures: ['shader-f16'],
+ vramRequiredMb: 376.06,
+} as const;
+
+const artifactPolicy = {
+ cacheBackend: 'cache',
+ integrityMode: 'error',
+ tokenizerFiles: ['tokenizer.json'],
+} as const;
+
+const modelRecord = {
+ integrity: {
+ config: runtimeManifest.modelConfigIntegrity,
+ model_lib: runtimeManifest.modelLibraryIntegrity,
+ onFailure: artifactPolicy.integrityMode,
+ tokenizer: {
+ [artifactPolicy.tokenizerFiles[0]]: runtimeManifest.tokenizerIntegrity,
+ },
+ },
+ low_resource_required: modelOptions.lowResourceRequired,
+ model: runtimeManifest.modelUrl,
+ model_id: runtimeManifest.modelId,
+ model_lib: runtimeManifest.modelLibraryUrl,
+ overrides: { context_window_size: modelOptions.contextWindowSize },
+ required_features: [...modelOptions.requiredFeatures],
+ vram_required_MB: modelOptions.vramRequiredMb,
+} satisfies ModelRecord;
+
+const appConfig = {
+ cacheBackend: artifactPolicy.cacheBackend,
+ model_list: [modelRecord],
+} satisfies AppConfig;
+
+const engineConfig = { appConfig } satisfies MLCEngineConfig;
+
+export const starterPolicy = {
+ broker: {
+ heartbeatIntervalMs: 500,
+ leaderTimeoutMs: 3_000,
+ maxConcurrent: 1,
+ namespace: 'tabloom-vite-webllm-starter',
+ queueCapacity: 4,
+ requestTimeoutMs: 180_000,
+ },
+ generation: {
+ maxTokens: 96,
+ temperature: 0,
+ },
+ host: {
+ clientLivenessIntervalMs: 1_000,
+ clientLivenessTimeoutMs: 5_000,
+ idleTimeoutMs: 30_000,
+ },
+ topology: {
+ lifecyclePolicy: 'portable',
+ mode: 'auto',
+ requiredCapabilities: ['webgpu'],
+ },
+} as const;
+
+export async function resolveRuntimeConfig() {
+ const runtimeFingerprint = await createRuntimeFingerprint({
+ ...runtimeManifest,
+ artifactPolicy: JSON.stringify(artifactPolicy),
+ brokerPolicy: JSON.stringify(starterPolicy.broker),
+ generationPolicy: JSON.stringify(starterPolicy.generation),
+ hostPolicy: JSON.stringify(starterPolicy.host),
+ modelOptions: JSON.stringify(modelOptions),
+ topologyPolicy: JSON.stringify(starterPolicy.topology),
+ });
+
+ return {
+ broker: { ...starterPolicy.broker, runtimeFingerprint },
+ engineConfig,
+ generation: starterPolicy.generation,
+ host: starterPolicy.host,
+ modelId: runtimeManifest.modelId,
+ topology: starterPolicy.topology,
+ workerName: `${starterPolicy.broker.namespace}:${runtimeFingerprint}`,
+ } as const;
+}
diff --git a/examples/vite-webllm/src/styles.css b/examples/vite-webllm/src/styles.css
new file mode 100644
index 0000000..0fc50db
--- /dev/null
+++ b/examples/vite-webllm/src/styles.css
@@ -0,0 +1,156 @@
+:root {
+ color: #e8eef4;
+ background: #071014;
+ font-family:
+ Inter,
+ ui-sans-serif,
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ sans-serif;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ min-width: 320px;
+ min-height: 100vh;
+ margin: 0;
+ background:
+ radial-gradient(
+ circle at 80% 10%,
+ rgb(27 121 111 / 28%),
+ transparent 28rem
+ ),
+ #071014;
+}
+
+main {
+ width: min(960px, calc(100% - 2rem));
+ margin: 0 auto;
+ padding: 5rem 0 3rem;
+}
+
+header {
+ max-width: 760px;
+}
+
+.eyebrow,
+dt,
+label {
+ color: #63dec9;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+h1 {
+ margin: 0.5rem 0 1rem;
+ font-size: clamp(2.25rem, 6vw, 4.75rem);
+ line-height: 0.98;
+ letter-spacing: -0.05em;
+}
+
+.lede,
+footer {
+ color: #9fb0bb;
+ line-height: 1.65;
+}
+
+.runtime,
+.prompt-card {
+ margin-top: 2rem;
+ padding: 1.25rem;
+ border: 1px solid #263940;
+ border-radius: 1rem;
+ background: rgb(12 27 33 / 84%);
+ box-shadow: 0 1.5rem 5rem rgb(0 0 0 / 20%);
+}
+
+dl {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
+ gap: 1rem;
+ margin: 0;
+}
+
+dd {
+ overflow-wrap: anywhere;
+ margin: 0.35rem 0 0;
+}
+
+.status {
+ margin: 1.25rem 0 0;
+ color: #b9fff2;
+}
+
+.status.error {
+ color: #ff9f92;
+}
+
+form {
+ display: grid;
+ gap: 0.75rem;
+}
+
+textarea,
+pre {
+ width: 100%;
+ border: 1px solid #314b54;
+ border-radius: 0.75rem;
+ color: inherit;
+ background: #08161b;
+ font: inherit;
+}
+
+textarea {
+ resize: vertical;
+ padding: 1rem;
+ line-height: 1.5;
+}
+
+.actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+}
+
+button {
+ padding: 0.75rem 1rem;
+ border: 1px solid #63dec9;
+ border-radius: 999px;
+ color: #06110f;
+ background: #63dec9;
+ font: inherit;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+button.secondary {
+ color: #b9fff2;
+ background: transparent;
+}
+
+button:disabled {
+ cursor: not-allowed;
+ opacity: 0.45;
+}
+
+pre {
+ min-height: 7rem;
+ margin: 1rem 0 0;
+ padding: 1rem;
+ overflow: auto;
+ white-space: pre-wrap;
+}
+
+footer {
+ margin-top: 1.5rem;
+ font-size: 0.875rem;
+}
diff --git a/examples/vite-webllm/src/tabloom.worker.ts b/examples/vite-webllm/src/tabloom.worker.ts
new file mode 100644
index 0000000..35a1d41
--- /dev/null
+++ b/examples/vite-webllm/src/tabloom.worker.ts
@@ -0,0 +1,15 @@
+import { WebLlmInferenceAdapter } from '@aantenore/tabloom/adapters/webllm';
+import { createSharedWorkerBrokerHost } from '@aantenore/tabloom/shared-worker';
+import { resolveRuntimeConfig } from './runtime-config';
+
+const runtime = await resolveRuntimeConfig();
+
+createSharedWorkerBrokerHost({
+ adapter: new WebLlmInferenceAdapter({
+ engineConfig: runtime.engineConfig,
+ modelId: runtime.modelId,
+ }),
+ config: runtime.broker,
+ ...runtime.host,
+ scope: globalThis,
+});
diff --git a/examples/vite-webllm/tsconfig.json b/examples/vite-webllm/tsconfig.json
new file mode 100644
index 0000000..6d818ac
--- /dev/null
+++ b/examples/vite-webllm/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "exactOptionalPropertyTypes": true,
+ "lib": ["ES2024", "DOM", "DOM.Iterable", "WebWorker"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "noEmit": true,
+ "noUncheckedIndexedAccess": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "target": "ES2022",
+ "types": ["vite/client"],
+ "useDefineForClassFields": true,
+ "verbatimModuleSyntax": true
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/vite-webllm/vite.config.ts b/examples/vite-webllm/vite.config.ts
new file mode 100644
index 0000000..9b4d3f3
--- /dev/null
+++ b/examples/vite-webllm/vite.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ build: {
+ emptyOutDir: true,
+ outDir: 'dist',
+ },
+ preview: {
+ host: '127.0.0.1',
+ port: 4173,
+ strictPort: true,
+ },
+ server: {
+ host: '127.0.0.1',
+ port: 4173,
+ strictPort: true,
+ },
+ worker: { format: 'es' },
+});
diff --git a/package.json b/package.json
index b5f29bc..e79eeb7 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,7 @@
"test:browser:chromium": "playwright test --project=chromium",
"test:browser:preview": "playwright test --config=playwright.preview.config.ts",
"test:live:webllm": "playwright test --config=playwright.webllm.config.ts",
+ "test:live:starter": "playwright test --config=playwright.starter-webllm.config.ts",
"test:unit": "vitest run --coverage",
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.demo.json --noEmit"
},
diff --git a/playwright.starter-webllm.config.ts b/playwright.starter-webllm.config.ts
new file mode 100644
index 0000000..018ef16
--- /dev/null
+++ b/playwright.starter-webllm.config.ts
@@ -0,0 +1,33 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ expect: { timeout: 20_000 },
+ fullyParallel: false,
+ outputDir: 'test-results/starter-webllm',
+ reporter: process.env['CI'] ? [['github'], ['line']] : 'line',
+ retries: 0,
+ testDir: './tests/live-starter',
+ timeout: 900_000,
+ use: {
+ baseURL: 'http://127.0.0.1:4173',
+ screenshot: 'only-on-failure',
+ trace: 'retain-on-failure',
+ },
+ webServer: {
+ command: 'pnpm --dir examples/vite-webllm dev',
+ reuseExistingServer: false,
+ timeout: 30_000,
+ url: 'http://127.0.0.1:4173',
+ },
+ workers: 1,
+ projects: [
+ {
+ name: 'chrome-webgpu',
+ use: {
+ ...devices['Desktop Chrome'],
+ channel: 'chrome',
+ launchOptions: { args: ['--enable-unsafe-webgpu'] },
+ },
+ },
+ ],
+});
diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs
index 2ea0587..9e7fda6 100644
--- a/scripts/package-smoke.mjs
+++ b/scripts/package-smoke.mjs
@@ -1,6 +1,8 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import {
+ cpSync,
+ existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
@@ -10,12 +12,13 @@ import {
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
-import { dirname, join } from 'node:path';
+import { dirname, join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const repositoryRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const temporaryRoot = mkdtempSync(join(tmpdir(), 'tabloom-package-'));
const consumerRoot = join(temporaryRoot, 'consumer');
+const starterRoot = join(temporaryRoot, 'vite-webllm-starter');
const pnpmCli = process.env.npm_execpath;
assert.ok(pnpmCli, 'Run this smoke through the package manager script.');
@@ -266,7 +269,153 @@ try {
),
'The optional provider import must remain lazy.',
);
- console.log(`Package smoke passed: ${archive}`);
+
+ const starterSource = join(repositoryRoot, 'examples', 'vite-webllm');
+ cpSync(starterSource, starterRoot, {
+ filter: (source) => {
+ const segments = relative(starterSource, source).split(sep);
+ return !segments.some((segment) =>
+ ['dist', 'node_modules'].includes(segment),
+ );
+ },
+ recursive: true,
+ });
+ assert.equal(
+ existsSync(join(starterRoot, 'node_modules')),
+ false,
+ 'The starter smoke must begin without copied dependencies.',
+ );
+ assertPublicPackageImports(starterRoot);
+ const starterPackagePath = join(starterRoot, 'package.json');
+ const starterPackage = parsePackageManifest(
+ readFileSync(starterPackagePath, 'utf8'),
+ );
+ const repositoryPackage = parsePackageManifest(
+ readFileSync(join(repositoryRoot, 'package.json'), 'utf8'),
+ );
+ const releaseBoundary = starterPackage['tabloomRelease'];
+ assert.ok(
+ isRecord(releaseBoundary) &&
+ typeof releaseBoundary['version'] === 'string' &&
+ typeof releaseBoundary['integrity'] === 'string' &&
+ typeof releaseBoundary['webLlmVersion'] === 'string',
+ 'The starter must declare its verified published TabLoom boundary.',
+ );
+ assert.match(
+ releaseBoundary['version'],
+ /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u,
+ );
+ assert.match(releaseBoundary['integrity'], /^sha512-[A-Za-z0-9+/]+={0,2}$/u);
+ assertPinnedRuntimeArtifacts(
+ starterRoot,
+ releaseBoundary['webLlmVersion'],
+ releaseBoundary['version'],
+ releaseBoundary['integrity'],
+ );
+ const expectedArchive =
+ `https://github.com/aantenore/tabloom/releases/download/` +
+ `v${releaseBoundary['version']}/tabloom-${releaseBoundary['version']}.tgz`;
+ assert.equal(
+ starterPackage.dependencies['@aantenore/tabloom'],
+ expectedArchive,
+ 'The starter archive URL must match its verified published boundary.',
+ );
+ const starterLock = readFileSync(join(starterRoot, 'pnpm-lock.yaml'), 'utf8');
+ assert.ok(
+ starterLock.includes(expectedArchive) &&
+ starterLock.includes(`integrity: ${releaseBoundary['integrity']}`),
+ 'The starter lock must bind the declared release URL and archive integrity.',
+ );
+ assert.equal(
+ starterPackage.dependencies['@mlc-ai/web-llm'],
+ releaseBoundary['webLlmVersion'],
+ 'The starter WebLLM version must match its published TabLoom boundary.',
+ );
+ assert.ok(
+ starterLock.includes(
+ `'@mlc-ai/web-llm': ${releaseBoundary['webLlmVersion']}`,
+ ),
+ 'The starter lock must retain the published TabLoom WebLLM peer boundary.',
+ );
+ execFileSync(
+ process.execPath,
+ [pnpmCli, 'install', '--frozen-lockfile', '--ignore-scripts', '--silent'],
+ { cwd: starterRoot, stdio: 'pipe' },
+ );
+ const remoteInstalledPackage = parsePackageManifest(
+ readFileSync(
+ join(
+ starterRoot,
+ 'node_modules',
+ '@aantenore',
+ 'tabloom',
+ 'package.json',
+ ),
+ 'utf8',
+ ),
+ );
+ assert.equal(
+ remoteInstalledPackage.version,
+ releaseBoundary['version'],
+ 'The frozen starter install must resolve the declared published boundary.',
+ );
+ execFileSync(
+ process.execPath,
+ [pnpmCli, 'audit', '--prod', '--audit-level=high'],
+ { cwd: starterRoot, stdio: 'pipe' },
+ );
+ const localArchiveSpecifier = `file:${relative(
+ starterRoot,
+ join(temporaryRoot, archive),
+ )
+ .split(sep)
+ .join('/')}`;
+ starterPackage.dependencies['@aantenore/tabloom'] = localArchiveSpecifier;
+ writeFileSync(
+ starterPackagePath,
+ `${JSON.stringify(starterPackage, null, 2)}\n`,
+ );
+ execFileSync(
+ process.execPath,
+ [
+ pnpmCli,
+ 'install',
+ '--no-frozen-lockfile',
+ '--ignore-scripts',
+ '--silent',
+ ],
+ { cwd: starterRoot, stdio: 'pipe' },
+ );
+ assert.ok(
+ readFileSync(join(starterRoot, 'pnpm-lock.yaml'), 'utf8').includes(
+ localArchiveSpecifier,
+ ),
+ 'The current-source build must resolve TabLoom from the freshly packed local archive.',
+ );
+ execFileSync(process.execPath, [pnpmCli, 'build'], {
+ cwd: starterRoot,
+ stdio: 'pipe',
+ });
+ const starterAssets = listFiles(join(starterRoot, 'dist'));
+ assert.ok(
+ starterAssets.some((entry) => /tabloom\.worker-[^/]+\.js$/u.test(entry)),
+ 'The packed-artifact starter build did not emit its SharedWorker chunk.',
+ );
+ const installedPackage = parsePackageManifest(
+ readFileSync(
+ join(
+ starterRoot,
+ 'node_modules',
+ '@aantenore',
+ 'tabloom',
+ 'package.json',
+ ),
+ 'utf8',
+ ),
+ );
+ assert.equal(installedPackage.version, repositoryPackage.version);
+
+ console.log(`Package and verified starter smoke passed: ${archive}`);
} finally {
rmSync(temporaryRoot, { force: true, recursive: true });
}
@@ -283,3 +432,131 @@ function listFiles(root) {
: [entry];
});
}
+
+/**
+ * @param {string} starter
+ * @returns {void}
+ */
+function assertPublicPackageImports(starter) {
+ for (const entry of listFiles(join(starter, 'src'))) {
+ if (!entry.endsWith('.ts')) {
+ continue;
+ }
+ const source = readFileSync(join(starter, 'src', entry), 'utf8');
+ assert.doesNotMatch(
+ source,
+ /@aantenore\/tabloom\/(?:dist|src)|(?:\.\.\/)+src\//u,
+ `${entry} imports an internal TabLoom module.`,
+ );
+ }
+}
+
+/**
+ * @param {string} starter
+ * @param {string} webLlmVersion
+ * @param {string} tabloomVersion
+ * @param {string} tabloomIntegrity
+ * @returns {void}
+ */
+function assertPinnedRuntimeArtifacts(
+ starter,
+ webLlmVersion,
+ tabloomVersion,
+ tabloomIntegrity,
+) {
+ const source = readFileSync(
+ join(starter, 'src', 'runtime-config.ts'),
+ 'utf8',
+ );
+ assert.doesNotMatch(
+ source,
+ /(?:raw\.githubusercontent\.com|huggingface\.co)[^'\n]*\/main(?:\/|')/u,
+ 'The verified starter must not resolve model artifacts from a mutable main branch.',
+ );
+ assert.match(
+ source,
+ /huggingface\.co\/[^'\n]+\/resolve\/[0-9a-f]{40}\//u,
+ 'The model weights must resolve from an immutable Hugging Face revision.',
+ );
+ assert.match(
+ source,
+ /raw\.githubusercontent\.com\/[^'\n]+\/[0-9a-f]{40}\//u,
+ 'The model library must resolve from an immutable Git commit.',
+ );
+ assert.equal(
+ (source.match(/'sha384-[A-Za-z0-9+/=]+'/gu) ?? []).length,
+ 3,
+ 'The model config, tokenizer, and model library require explicit SRI.',
+ );
+ assert.match(
+ source,
+ /integrityMode: 'error'/u,
+ 'Runtime artifact integrity must fail closed.',
+ );
+ assert.match(source, /onFailure: artifactPolicy\.integrityMode/u);
+ assert.match(source, /cacheBackend: artifactPolicy\.cacheBackend/u);
+ assert.match(
+ source,
+ /createRuntimeFingerprint\(\{\s*\.\.\.runtimeManifest,/u,
+ 'The runtime fingerprint must bind the immutable artifact manifest.',
+ );
+ assert.ok(
+ source.includes(`adapter: 'webllm@${webLlmVersion}'`),
+ 'The fingerprinted adapter identity must match the published WebLLM boundary.',
+ );
+ assert.ok(
+ source.includes(`controlPlane: 'tabloom@${tabloomVersion}'`) &&
+ source.includes(`controlPlaneIntegrity:\n '${tabloomIntegrity}'`),
+ 'The runtime fingerprint must bind the verified TabLoom release boundary.',
+ );
+
+ for (const entry of ['main.ts', 'tabloom.worker.ts']) {
+ assert.match(
+ readFileSync(join(starter, 'src', entry), 'utf8'),
+ /engineConfig: runtime\.engineConfig/u,
+ `${entry} must use the integrity-pinned WebLLM AppConfig.`,
+ );
+ }
+}
+
+/**
+ * @typedef {{ dependencies: Record, version: string, [key: string]: unknown }} PackageManifest
+ */
+
+/**
+ * @param {string} source
+ * @returns {PackageManifest}
+ */
+function parsePackageManifest(source) {
+ const parsed = /** @type {unknown} */ (JSON.parse(source));
+ assert.ok(isRecord(parsed), 'The package manifest must be an object.');
+ assert.ok(
+ isStringRecord(parsed['dependencies']),
+ 'The package manifest must have string dependencies.',
+ );
+ assert.equal(
+ typeof parsed['version'],
+ 'string',
+ 'The package manifest must have a version.',
+ );
+ return /** @type {PackageManifest} */ (parsed);
+}
+
+/**
+ * @param {unknown} value
+ * @returns {value is Record}
+ */
+function isRecord(value) {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+/**
+ * @param {unknown} value
+ * @returns {value is Record}
+ */
+function isStringRecord(value) {
+ return (
+ isRecord(value) &&
+ Object.values(value).every((entry) => typeof entry === 'string')
+ );
+}
diff --git a/tests/live-starter/vite-webllm-starter.spec.ts b/tests/live-starter/vite-webllm-starter.spec.ts
new file mode 100644
index 0000000..20887d6
--- /dev/null
+++ b/tests/live-starter/vite-webllm-starter.spec.ts
@@ -0,0 +1,83 @@
+import { expect, test, type BrowserContext, type Page } from '@playwright/test';
+
+const LIVE_ENABLED = process.env['TABLOOM_WEBLLM_LIVE'] === '1';
+
+test.describe('verified Vite WebLLM starter', () => {
+ test.skip(
+ !LIVE_ENABLED,
+ 'Set TABLOOM_WEBLLM_LIVE=1 to download and run the pinned live model.',
+ );
+
+ test('loads pinned artifacts and completes through the selected owner', async ({
+ context,
+ }, testInfo) => {
+ const errors: string[] = [];
+ const first = await openStarter(context, errors);
+ await expect(first).toHaveTitle('TabLoom WebLLM starter');
+ await expect(first.locator('[data-field="topology"]')).not.toHaveText(
+ 'starting',
+ { timeout: 30_000 },
+ );
+
+ const topology = await first
+ .locator('[data-field="topology"]')
+ .textContent();
+ const peer =
+ topology === 'page-owner' ? await openStarter(context, errors) : first;
+ await expect(peer.locator('[data-field="readiness"]')).toHaveText('ready', {
+ timeout: 600_000,
+ });
+ await expect(peer.locator('[data-field="submit"]')).toBeEnabled();
+
+ await peer.locator('[data-field="prompt"]').fill('Return only: ready');
+ await peer.locator('[data-field="submit"]').click();
+ const status = peer.locator('[data-field="status"]');
+ await expect(status).toHaveText('Completed locally.', {
+ timeout: 180_000,
+ });
+ await peer.waitForTimeout(1_500);
+ await expect(status).toHaveText('Completed locally.');
+ await expect(status).not.toHaveClass(/error/u);
+ await expect
+ .poll(
+ async () =>
+ (await peer.locator('[data-field="output"]').textContent())?.trim()
+ .length ?? 0,
+ )
+ .toBeGreaterThan(0);
+ expect(errors).toEqual([]);
+
+ await testInfo.attach('starter-runtime-evidence.json', {
+ body: Buffer.from(
+ JSON.stringify(
+ {
+ browserVersion: context.browser()?.version() ?? 'unknown',
+ modelId: await peer.locator('[data-field="model"]').textContent(),
+ outputLength:
+ (
+ await peer.locator('[data-field="output"]').textContent()
+ )?.trim().length ?? 0,
+ role: await peer.locator('[data-field="role"]').textContent(),
+ topology,
+ },
+ null,
+ 2,
+ ),
+ ),
+ contentType: 'application/json',
+ });
+ });
+});
+
+async function openStarter(
+ context: BrowserContext,
+ errors: string[],
+): Promise {
+ const page = await context.newPage();
+ page.on('console', (message) => {
+ if (message.type() === 'error') errors.push(message.text());
+ });
+ page.on('pageerror', (error) => errors.push(error.message));
+ await page.goto('/');
+ return page;
+}
diff --git a/tsconfig.json b/tsconfig.json
index c146274..b19c3be 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -31,9 +31,11 @@
"scripts",
"tests/e2e",
"tests/live",
+ "tests/live-starter",
"tests/unit",
"playwright.config.ts",
"playwright.preview.config.ts",
+ "playwright.starter-webllm.config.ts",
"playwright.webllm.config.ts",
"vite.config.ts",
"vitest.config.ts",
diff --git a/tsconfig.starter-lint.json b/tsconfig.starter-lint.json
new file mode 100644
index 0000000..8bea3c5
--- /dev/null
+++ b/tsconfig.starter-lint.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./examples/vite-webllm/tsconfig.json",
+ "compilerOptions": {
+ "paths": {
+ "@aantenore/tabloom/adapters/webllm": ["./src/adapters/webllm.ts"],
+ "@aantenore/tabloom/core": ["./src/core.ts"],
+ "@aantenore/tabloom/shared-worker": ["./src/shared-worker.ts"]
+ }
+ },
+ "include": ["examples/vite-webllm/src", "examples/vite-webllm/vite.config.ts"]
+}