Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

All notable changes are documented here.

## [0.2.0-alpha.1] - 2026-07-19

### Added

- Optional WebLLM 0.2.84 adapter with lazy provider loading, host-selected model configuration, OpenAI-shaped streaming, usage aggregation, and provider-runtime evidence.
- Abort-safe owner initialization, engine-wide cancellation with stream drain, ordered disposal, and single-generation enforcement.
- Opt-in two-tab Chrome/WebGPU live lab and Playwright gate using a real SmolLM2 model.
- Dedicated `@aantenore/tabloom/adapters/webllm` package export and bundle-isolation smoke assertion.

### Changed

- Runtime integration guidance now distinguishes TabLoom's fenced elected-page topology from WebLLM's worker topologies.
- Delivery, compatibility, operations, threat-model, and visual evidence now cover the real provider seam without widening browser or model claims.

## [0.1.0-alpha.1] - 2026-07-17

### Added
Expand Down
61 changes: 56 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ TabLoom is a same-origin browser inference broker. It coordinates sibling pages

It supplies the coordination layer, not a model runtime: exclusive ownership, monotonic fencing epochs, protocol validation, bounded admission, streaming sessions, cancellation, timeouts, takeover, and privacy-safe telemetry.

> **Alpha:** the release-gated adapter is deterministic simulation. WebLLM and Transformers.js are documented integration seams, not verified GPU or model evidence.
The core stays provider-neutral. A deterministic adapter exercises lifecycle behavior in every CI job, while a dedicated optional adapter composes [WebLLM 0.2.84](https://github.com/mlc-ai/web-llm) without bundling it into the core package.

> **Alpha:** the WebLLM path is verified separately on Chrome/WebGPU with an actual model. It is not a compatibility claim for every browser, GPU, model, or future WebLLM version. Transformers.js remains an unverified integration seam.

## Why

Expand All @@ -34,11 +36,11 @@ flowchart LR

## Install the prerelease archive

The first alpha is distributed as a GitHub release archive rather than an npm registry publication.
The alpha is distributed as a GitHub release archive rather than an npm registry publication.

```bash
curl -LO https://github.com/aantenore/tabloom/releases/download/v0.1.0-alpha.1/tabloom-0.1.0-alpha.1.tgz
pnpm add ./tabloom-0.1.0-alpha.1.tgz
curl -LO https://github.com/aantenore/tabloom/releases/download/v0.2.0-alpha.1/tabloom-0.2.0-alpha.1.tgz
pnpm add ./tabloom-0.2.0-alpha.1.tgz
```

Verify the adjacent `.sha256` asset before installing in a controlled delivery pipeline.
Expand Down Expand Up @@ -83,6 +85,47 @@ try {

The deterministic adapter makes lifecycle behavior reproducible. Replace it with an application adapter for a real runtime; see [adapter integrations](docs/integrations.md).

## Optional WebLLM adapter

Install the tested peer explicitly, then import only the dedicated subpath:

```bash
pnpm add @mlc-ai/web-llm@0.2.84
```

```ts
import { createBrowserBroker } from '@aantenore/tabloom';
import { WebLlmInferenceAdapter } from '@aantenore/tabloom/adapters/webllm';

const broker = createBrowserBroker({
adapter: new WebLlmInferenceAdapter({
modelId: 'SmolLM2-360M-Instruct-q4f16_1-MLC',
onProgress: ({ progress, text }) => {
console.log(Math.round(progress * 100), text);
},
}),
config: {
maxConcurrent: 1,
namespace: 'my-app-webllm',
queueCapacity: 4,
requestTimeoutMs: 180_000,
},
});

await broker.start();
const session = broker.request({
messages: [{ role: 'user', content: 'Explain fenced ownership.' }],
stream_options: { include_usage: true },
});

for await (const chunk of session) {
console.log(chunk.choices[0]?.delta.content ?? '');
}
console.log(await session.result);
```

The host chooses the model, model source, runtime config, cache policy, and prompt history. The request cannot switch the configured model. Keep `maxConcurrent: 1`: WebLLM interruption is engine-wide and the adapter rejects a competing generation.

## Session semantics

| Concern | Alpha contract |
Expand All @@ -100,7 +143,7 @@ The deterministic adapter makes lifecycle behavior reproducible. Replace it with

Serve from HTTPS, or loopback for development. The alpha requires Web Locks, BroadcastChannel, local storage, and cryptographic UUID support in the same storage partition.

The multi-page suite is locally verified with Playwright 1.61.1 against Chromium 149.0.7827.55, Firefox 151.0, and WebKit 26.5. Each engine exercises one-owner convergence, peer streaming, cancellation, backpressure, and owner takeover.
The deterministic multi-page suite is locally verified with Playwright 1.61.1 against Chromium 149.0.7827.55, Firefox 151.0, and WebKit 26.5. Each engine exercises one-owner convergence, peer streaming, cancellation, backpressure, and owner takeover. The separate live lab targets installed Chrome with WebGPU; see the [compatibility matrix](docs/compatibility.md).

## Development

Expand All @@ -122,6 +165,14 @@ corepack pnpm test:browser
corepack pnpm run audit
```

Run the opt-in real-model gate only when downloading the configured model is acceptable:

```bash
TABLOOM_WEBLLM_LIVE=1 \
TABLOOM_WEBLLM_MODEL=SmolLM2-360M-Instruct-q4f16_1-MLC \
corepack pnpm test:live:webllm
```

## Evidence and boundaries

- [Delivery contract](docs/delivery-contract.md)
Expand Down
263 changes: 263 additions & 0 deletions demo/webllm.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Opt-in TabLoom live lab for one WebLLM runtime shared across two same-origin tabs."
/>
<meta name="theme-color" content="#07121f" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22/>"
/>
<title>TabLoom WebLLM live lab</title>
<style>
:root {
color: #e8f0f7;
background: #07121f;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
}

* {
box-sizing: border-box;
}

body {
margin: 0;
min-width: 320px;
}

main {
width: min(960px, calc(100% - 32px));
margin: 0 auto;
padding: 40px 0 56px;
}

header,
.card,
.metric {
border: 1px solid #24384c;
background: #0c1a29;
border-radius: 14px;
}

header,
.card {
padding: 24px;
}

h1,
h2,
p {
margin-top: 0;
}

h1 {
margin-bottom: 8px;
font-size: clamp(1.7rem, 4vw, 2.5rem);
}

h2 {
font-size: 1rem;
}

.lede,
.hint,
dt {
color: #9eb0c2;
}

.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 12px;
margin: 16px 0;
}

.metric {
padding: 14px;
}

dl,
dd {
margin: 0;
}

dt {
margin-bottom: 5px;
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}

dd {
overflow-wrap: anywhere;
font-weight: 650;
}

.card + .card {
margin-top: 16px;
}

label {
display: block;
margin-bottom: 8px;
font-weight: 650;
}

textarea {
width: 100%;
min-height: 108px;
resize: vertical;
border: 1px solid #35516b;
border-radius: 10px;
padding: 12px;
color: inherit;
background: #07121f;
font: inherit;
}

.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}

button {
border: 1px solid #58c8b3;
border-radius: 999px;
padding: 10px 16px;
color: #04110f;
background: #58c8b3;
font: inherit;
font-weight: 700;
cursor: pointer;
}

button.secondary {
color: #dce9f4;
background: transparent;
border-color: #58728b;
}

button:disabled {
cursor: not-allowed;
opacity: 0.45;
}

output {
display: block;
min-height: 72px;
white-space: pre-wrap;
overflow-wrap: anywhere;
border-radius: 10px;
padding: 14px;
color: #dffbf5;
background: #061018;
}

.status-ready,
.role-peer,
.role-leader {
color: #65dbc5;
}

.status-error {
color: #ff9d9d;
}

.test-evidence {
display: none;
}
</style>
</head>
<body>
<main>
<header>
<h1>WebLLM live lab</h1>
<p class="lede">
Open one peer tab, then submit from that peer. TabLoom keeps a single
fenced runtime owner while the request and stream cross the
same-origin broker.
</p>
<button class="secondary" id="open-peer" data-testid="open-peer">
Open peer tab
</button>
</header>

<section class="metrics" aria-label="Runtime state">
<dl class="metric">
<dt>Role</dt>
<dd id="role" data-testid="role">stopped</dd>
</dl>
<dl class="metric">
<dt>Readiness</dt>
<dd id="readiness" data-testid="readiness">idle</dd>
</dl>
<dl class="metric">
<dt>Runtime evidence</dt>
<dd id="evidence" data-testid="evidence">provider-runtime</dd>
</dl>
<dl class="metric">
<dt>WebGPU</dt>
<dd id="webgpu" data-testid="webgpu">checking</dd>
</dl>
<dl class="metric">
<dt>Model</dt>
<dd id="model-id" data-testid="model-id"></dd>
</dl>
<dl class="metric">
<dt>Peers seen</dt>
<dd id="peer-count" data-testid="peer-count">0</dd>
</dl>
</section>

<section class="card" aria-labelledby="progress-heading">
<h2 id="progress-heading">Owner initialization</h2>
<p id="progress" data-testid="progress">Waiting to start.</p>
<p class="hint">
Model initialization runs only in the elected owner tab. A peer
observes broker readiness without loading its own runtime.
</p>
</section>

<section class="card" aria-labelledby="request-heading">
<h2 id="request-heading">Peer request</h2>
<label for="prompt">Prompt</label>
<textarea id="prompt" data-testid="prompt">
Respond with a short confirmation that the shared runtime is ready.</textarea>
<div class="actions">
<button id="send" data-testid="send" disabled>Send from peer</button>
</div>
<p>
Status:
<strong id="request-status" data-testid="request-status">idle</strong>
</p>
<output id="output" data-testid="output" aria-live="polite"></output>
</section>

<div class="test-evidence" aria-hidden="true">
<span id="owner-id" data-testid="owner-id"></span>
<span id="tab-id" data-testid="tab-id"></span>
<span id="epoch" data-testid="epoch">0</span>
<span id="progress-event-count" data-testid="progress-event-count"
>0</span
>
<span id="result-text" data-testid="result-text"></span>
<span id="terminal-count" data-testid="terminal-count">0</span>
<span id="usage-tokens" data-testid="usage-tokens">0</span>
</div>
</main>
<script type="module" src="/webllm.ts"></script>
</body>
</html>
Loading
Loading