Skip to content

feat(agent): add experimental local document assistant - #249

Merged
chaxus merged 203 commits into
mainfrom
feat/local-multilingual-assistant
Oct 10, 2026
Merged

chaxus merged 203 commits into
mainfrom
feat/local-multilingual-assistant

Conversation

@chaxus

@chaxus chaxus commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Add an experimental document assistant behind the explicit ?agent=1 opt-in. Support browser-local WebGPU inference with CPU fallback, configurable writing endpoints, model loading and cancellation, and bounded Word, spreadsheet and presentation operations with native Undo and readback verification.

Harden delayed spreadsheet writes and SUM cancellation, endpoint connection lifecycle, provider abort propagation, truncated responses, and normalized numeric readback. Retain cached assets needed by live pages and align silent-update tests with the production service-worker build protocol. Remove unused code and transient artifacts while preserving hash-bound evaluation evidence.

Validation

  • Local: 151 unit-test files / 4,666 tests passed with coverage; build, formatting, lint, TypeScript and Docker Compose validation passed.
  • Local browser regression: 199 passed / 16 skipped; two independent serial regression groups each passed all three repetitions.
  • Current head e81be1da: all required GitHub checks passed, including ordinary, Cloudflare Pages and Docker E2E, preview smoke and Cloudflare Pages deployment.

Scope and limits

The assistant remains experimental and opt-in. Enabling it starts local model loading after the editor is ready, visible and idle; first downloads can compete for bandwidth, and initialization/inference consume CPU/GPU and memory. Ordinary editor visits do not initialize the assistant.

Archived model screens do not establish reliable seven-language factual-writing quality. Physical mobile devices and the full device/offline matrix remain incomplete. Authenticated custom model URL persistence remains a known limitation. Real cloud API calls were not exercised by this repair pass; provider behavior is covered by deterministic tests.

Integration

GitHub rejected rebase merging for this branch. Squash merging preserves the protected main branch's linear history and retains the tested branch tree as one experimental feature release.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Deploying document with  Cloudflare Pages  Cloudflare Pages

Latest commit: e81be1d
Status: ✅  Deploy successful!
Preview URL: https://96ae9707.document-7hm.pages.dev
Branch Preview URL: https://feat-local-multilingual-assi.document-7hm.pages.dev

View logs

chaxus added 28 commits October 7, 2026 11:50
chaxus added 23 commits October 7, 2026 22:06
Writing into a document was silently routed to the in-browser engines
(WebLLM / wllama). Sixteen candidates were measured across seven languages
and none kept dates, names, actors, negation or modality straight, so a
rewrite could quietly turn "not approved" into "approved" -- unacceptable
for a zero-tolerance operation.

- New pure policy `resolveWritingRoute`: a connected loopback service wins;
  browser-local engines are used only with an explicit opt-in and are marked
  experimental; otherwise the request is refused with a localized reason.
- Finish the opt-in loopback service: `ProviderId` gains `loopback` (an
  explicit model is required), exported from `llm/index.ts`, with connection,
  timeout, cancellation, tool-refusal and incomplete-response cases.
- Panel: a settings block for the local service (origin, model, connect,
  disconnect, status) plus the browser-local writing consent. Only a
  validated loopback origin and a model name are ever persisted.
- Eleven new strings across the seven shell locales.

Chat, structured tool plans and offline use keep running on the browser-local
model; no cloud or LAN fallback is introduced, and the CSP needed no change
because `connect-src` already allows `http:`.

Reverse verification: disabling the consent gate turns the policy case red
and, after rebuilding the package, the panel case red.

Checks: 148 files / 4598 tests, lint:ts, format:check and the production
build pass. Real-device Ollama connectivity and a real-browser writing run
stay unverified (no local service was available) and are recorded as such in
docs/evaluations/2026-10-10-local-model-writing-scope-decision.md.
The writing route only accepted a loopback service, which asks every user to
install one. The providers for the alternative were already in the library
(OpenAIProvider has baseURL/apiKey, OllamaProvider has ollamaBaseURL, and the
Claude/OpenAI/Gemini labels were still in the panel), so this is wiring plus a
policy, not new plumbing.

Doing it honestly meant changing what the site claims. The core editor and the
converters keep the document on the device; a cloud endpoint with the user's own
key does not. The help center (all seven locales) and llms.txt now state the two
cases instead of one, and the panel always shows where a writing request goes.

- `llm/endpoint.ts`: WritingEndpoint (loopback | openai-compatible | anthropic |
  gemini). A remote endpoint must be HTTPS -- plain HTTP would put the key and
  the selected text on the wire -- and may not carry credentials, a query or a
  fragment. A loopback endpoint stays origin-only over http.
- `keys.ts`: per-origin key slots. The provider-keyed slots cannot hold two
  OpenAI-compatible services; the second would overwrite the first. The existing
  functions are untouched.
- `writing-route.ts`: endpoint model with a user-chosen preference
  (device-first / remote-first). The result carries `dataPath`, so the host can
  say whether the text stayed or left.
- Panel: destination kind, address, model, key, connect/disconnect, status, the
  preference, and a "writing goes to ..." line. A cloud endpoint reports
  "configured (reachability not verified)" -- readiness is not reachability.
- 23 strings across the seven locales; the vendor names that are legitimately
  identical in de/es/pt are registered in the locales test rather than given
  invented translations.

Reverse verification: removing the HTTPS requirement turns
`agent-endpoint.test.ts` and the route case red.

Checks: 149 files / 4610 tests, lint:ts, format:check and the production build
pass. Still unverified, and recorded as such: a real cloud request with a real
key, real loopback connectivity, and a real-browser writing run.
Offline, a cloud endpoint cannot be reached at all, so a user who configured one
got an unexplained network failure on rewrite -- while the two destinations that
do work offline (a loopback service, and the in-page engines) were behind a
consent gate. The mode the site advertises most loudly was the one with the worst
answer.

- `writing-route.ts` takes `offline`. A remote endpoint is dropped from the
  candidates rather than attempted; a malformed endpoint is still rejected, since
  being offline must not hide a bad configuration.
- Its own blocked reason, `offline-needs-device-destination`, so the host explains
  the case instead of reporting "nothing configured".
- The panel follows `online`/`offline`, adds the note next to the resolved
  destination, and maps the reason to a localized message.
- It does not fall back to the browser-local engines on its own: that is exactly
  the silent substitution the previous commit removed. Consent still decides, and
  the reported destination keeps saying where the text would go.
- Help center (seven locales) now states that a cloud destination is unreachable
  offline.

Reverse verification: removing the offline drop turns the three policy cases and
the panel case red.

Checks: 149 files / 4615 tests, lint:ts, format:check and the production build
pass. `navigator.onLine` is simulated in jsdom, so real-device online/offline
switching remains unverified and is recorded as such.
Review of the previous three commits. The load-a-model gate in `submit()` runs
before the writing branch and assumes the in-browser engine is the only writing
backend. With a cloud endpoint connected and no in-browser model, a rewrite
therefore did nothing at all and only asked the user to download a model -- which
is precisely the case the endpoint feature exists for (no WebGPU, or no wish to
download two gigabytes).

Why the tests missed it: the panel cases asserted the destination line but never
actually sent a writing request, and the older cases set `ready = true`, which
steps around the gate.

- The gate is skipped when this request would go to a connected endpoint. Reverse
  verification: removing that condition turns the new regression case red.
- Address, model and key now define the destination, so changing one disconnects
  the endpoint. Previously the panel reported the new destination while requests
  still went to the old one, with the old key; changing the address also clears
  the key field, since a different address is a different service.
- `clearEndpointKey` finally has a caller: emptying the key field removes the
  stored secret instead of leaving it with no way out.
- Offline with a configured cloud endpoint says "the configured cloud endpoint
  (unavailable offline)" rather than "not configured yet".
- A cloud endpoint without a key asks for the key instead of reporting a generic
  failure.
- Filling the key field is separated from rendering status, so a key being typed
  is not overwritten from storage before it is stored.
- Drops the 8 superseded loopback-named strings (56 entries across the locales)
  and the near-dead message added for the case the gate now owns.

Checks: 149 files / 4621 tests, lint:ts, format:check and the production build
pass.
@chaxus
chaxus marked this pull request as ready for review October 10, 2026 05:52
The agent's write tools had unit tests and no end-to-end coverage at all, so the
two mechanisms that actually touch a document were only ever exercised against
mocks. Three cases now drive the real panel against the real v9 editor:

- Excel writes the spreadsheet model directly (`writeExcelCellText`), reached
  through the closed "read <range>, then set <cell> to <literal>" phrase.
- Word goes through the native HTML paste wrapper (`pasteWordHtml`), reached
  through the write-the-last-answer intent.
- Presentations go through `slide_action`.

Each asserts the real change and a native Undo round trip, and each was chosen
because it is reachable without a model -- which is what makes it usable in CI.
The fixture pins the provider to a GGUF path with no model, so the panel's idle
auto-load has nothing to download.

Reaching the Word case needed one fixture note: conversation history lives in the
panel's own store (memory plus opt-in IndexedDB), so there is no record to seed;
the case supplies exactly the node `getLastAnswer()` reads. Everything downstream
is the real path. Model-driven tool choice therefore stays uncovered -- recorded
as a gap rather than implied.

That work also exposed a third instance of the model gate over-reaching:
`generateDocumentToolSequence` applies the fixed cell phrase *without* the
provider, but the gate assumed every "operate document" request needs the model,
so that model-free path was unreachable -- and the endpoint exemption added
earlier was too broad in the other direction, letting open-ended operations past
with no provider to run them. The predicate that decides this is now a named
function, `isModelFreeToolRequest`, and the gate asks it instead of guessing.
Reverse verification: making the tools branch exempt unconditionally turns the
new case red.

Checks: 149 files / 4628 tests, lint:ts, format:check and the production build
pass; the new spec passes 3/3 in about nine seconds.
… updates

Guard native spreadsheet writes and SUM against delayed callbacks after cancellation, preserve undo ownership, and verify normalized numeric values.

Cancel stale endpoint connections, preserve provider credentials, propagate abort reasons, and reject truncated Gemini responses.

Retain assets needed by live pages, align silent-update regression tests with production builds, and remove redundant code and temporary artifacts.
@chaxus
chaxus enabled auto-merge (rebase) October 10, 2026 09:35
auto-merge was automatically disabled October 10, 2026 09:46

Rebase failed

@chaxus
chaxus merged commit 5b13118 into main Oct 10, 2026
19 checks passed
@chaxus
chaxus deleted the feat/local-multilingual-assistant branch October 10, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant