Built-in client tools & the local model#

Two things that come with every Janux app: a set of browser tools the copilot always has, and the option to run the whole loop on the user's machine.

import { CLIENT_TOOL_SPECS, CLIENT_TOOL_NAMES } from 'janux';
import { DEFAULT_LOCAL_MODEL, probeLocalLlm, supportsLocalLlm } from '@janux/agent/local';

CLIENT_TOOL_SPECS#

The tools that exist in every app, independent of your components — the agent's equivalent of hands and eyes in the browser:

Tool What it does
ui_navigate Same-origin SPA navigation, query params allowed. Picks the destination from the routes list and the current page's links
ui_get_view_context Reads the current view: path, title, same-origin links, mounted components
ui_read_page Reads the visible page content
ui_click Clicks an element
ui_fill Fills a field
ui_wait_settled Waits for in-flight work to finish before the next step

Each spec is { name, description, parameters } with the parameters as JSON Schema, so the list drops straight into a provider's tool array. Descriptions are written for the modelui_navigate's tells it to choose from real links, which is what makes a wrong guess self-correcting.

ui_wait_settled is the one people forget: it's how an agent avoids acting on a half-updated page, the same settled() guarantee your tests use.

CLIENT_TOOL_NAMES#

A Set of those names, for the check you'll actually write — telling a built-in tool from one of your component intents when routing a tool call:

if (CLIENT_TOOL_NAMES.has(call.name)) {
  // a browser tool: run it in the page
} else {
  // an app tool: component.intent or an api()
}

Prefer this over string matching on the ui_ prefix: the set is generated from the specs, so it can't drift when a tool is added.

Local model: DEFAULT_LOCAL_MODEL#

DEFAULT_LOCAL_MODEL is 'onnx-community/Qwen3-0.6B-ONNX' — the Hugging Face ONNX model localLlm() loads when you don't name one. Small on purpose: it downloads in seconds and runs on WebGPU in the page, so a copilot works with no server and no API key — including on a fully static export. Override it with localLlm({ modelId, device: 'webgpu' | 'wasm', dtype, worker, provider }); pass a worker to keep inference off the main thread, or a provider (the interface of @browser-ai/transformers-js) to swap the model factory — a scripted stub makes a whole local turn testable without a GPU.

supportsLocalLlm() and probeLocalLlm()#

supportsLocalLlm(): boolean — the sync fast-path: whether the browser exposes WebGPU at all. Headless browsers expose navigator.gpu without a usable adapter, so before defaulting to the local brain ask for one:

probeLocalLlm({ timeoutMs }?): Promise<boolean> — requests a real adapter (navigator.gpu.requestAdapter()) and resolves false on a null adapter, a thrown probe or one that hangs past timeoutMs (1s by default). The verdict is cached per page, so gating every mount on it costs one probe:

const copilot = (await probeLocalLlm()) ? localLlm() : serverLlm();

Related: A copilot with a local, in-browser model · The agent and your copilot · Client runtime internals