diff --git a/libraries/llm/client.js b/libraries/llm/client.js index 6914109c..9cebe166 100644 --- a/libraries/llm/client.js +++ b/libraries/llm/client.js @@ -743,9 +743,12 @@ export async function generateWithTools(messages, opts = {}) { finalText = res.text // Native structured tool_calls if the provider returned them; otherwise - // parse the model's text (XML / fenced / bare JSON). + // parse the model's text (XML / fenced / bare JSON), keeping + // only calls to real tools — prose can contain JSON that looks like one. const nativeCalls = res.toolCalls && res.toolCalls.length > 0 - const calls = /** @type {any[]} */ (nativeCalls ? res.toolCalls : parseToolCalls(res.text)) + const calls = /** @type {any[]} */ ( + nativeCalls ? res.toolCalls : parseToolCalls(res.text).filter((call) => findTool(call.name)) + ) if (!calls.length) break if (nativeCalls) { diff --git a/libraries/llm/picker.js b/libraries/llm/picker.js index 9960a555..9df94e0e 100644 --- a/libraries/llm/picker.js +++ b/libraries/llm/picker.js @@ -239,7 +239,7 @@ const CSS = ` * @property {string[]} [locked] * @property {{name?: string, text?: string}} [toolPrompt] * @property {any[]} [tools] - * @property {Array<{name: string, description?: string}>} [toolTools] host-tool-provided built-in tools + * @property {Array<{name: string, description?: string, defaultOff?: boolean}>} [toolTools] host-tool-provided built-in tools * @property {string} [toolName] label for the host tool's built-in tools * @property {() => void} [onRequestClose] * @property {{toolId: string, docId?: string, toolName?: string, docName?: string}} [scope] @@ -1837,16 +1837,17 @@ function buildPickerInto(host, opts) { text: "Runs each tool call's JS in an isolated Worker with no access to this page, your repo, or your account — safer for tool calls added by URL. Off = handlers run on the page with full access (needed for tool calls that read/write documents or the DOM).", }) - // Tools the host tool itself provides (opts.toolTools: [{name, description}]). - // Read-only — their handlers live in the tool — but each can be toggled off; - // the tool reads cfg.toolToggles[name] to decide what to offer the model. + // Tools the host tool itself provides (opts.toolTools: [{name, description, + // defaultOff?}]). Read-only — their handlers live in the tool — but each can + // be toggled; cfg.toolToggles[name] decides what's offered to the model. A + // `defaultOff` tool is off until toggled on. let providedCard = null const provided = (opts && opts.toolTools) || [] if (provided.length) { if (!cfg.toolToggles) cfg.toolToggles = {} - const rows = provided.map((/** @type {{name: string, description?: string}} */ t) => { + const rows = provided.map((/** @type {{name: string, description?: string, defaultOff?: boolean}} */ t) => { const cb = el("input", {type: "checkbox"}) - cb.checked = cfg.toolToggles[t.name] !== false + cb.checked = t.defaultOff ? cfg.toolToggles[t.name] === true : cfg.toolToggles[t.name] !== false cb.addEventListener("change", () => { cfg.toolToggles = {...(cfg.toolToggles || {}), [t.name]: cb.checked} }) diff --git a/llm-host/.gitignore b/llm-host/.gitignore new file mode 100644 index 00000000..96f32a31 --- /dev/null +++ b/llm-host/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.pushwork diff --git a/llm-host/README.md b/llm-host/README.md new file mode 100644 index 00000000..e81c445e --- /dev/null +++ b/llm-host/README.md @@ -0,0 +1,124 @@ +# @grjte/llm-host + +The Patchwork side of [`@chee/patchwork-llm`](../libraries/llm): everything that +has to run in the host realm because it touches the LLM settings doc, which holds +the API key. A consumer tool like chat only holds a stream pair, and runs the +same code whether or not it's inside an isolation sandbox. + +The transport underneath (`connectWorker`, `serveWorkerSpec`, discovery, the +stream handoff) is generic and documented in +[`@grjte/patchwork-worker`](../libraries/patchwork-worker/README.md). This +package adds the LLM protocol on top of it, and the config tray. + +## Plugins + +| id | type | what | +| ----------------- | ------------------------- | -------------------------------------------------------- | +| `llm-config-tray` | `patchwork:component` | system-tray icon + popover for the model/config picker | +| `llm` | `patchwork:worker` | the serve half: `makeLLMWorkerSpec` (`src/worker-spec.js`) | +| `llm` | `patchwork:worker-client` | the consume half: `makeLLMClient` (`src/client.js`) | + +All three are behind `load()`, so importing the package registers descriptors and +nothing else. Both protocol halves ship together, so they can't drift. + +## Using it from a tool + +A tool never imports this package. It asks the registry for the client: + +```js +import {connectWorkerClient} from "@grjte/patchwork-worker/client.js" + +const llm = await connectWorkerClient("llm", {sessionOpts: {element}}) + +const {text, toolCalls} = await llm.generate(messages, { + scope: {toolId: "my-tool", docId: handle.url}, + system: "…", // or {default, local, openrouter, …} + tools: [{name, description, parameters, defaultOff}], + onToken: (delta, full) => {}, + signal, +}) +``` + +`scope` is all a tool says about config. The host resolves the provider, model, +prompts and toggles for that scope from the settings doc. + +To change config, a tool dispatches +`new CustomEvent("patchwork:open-tool", {detail: {component: "llm-config-tray", scope, toolPrompt, toolTools}, bubbles: true, composed: true})`. +The tray opens the picker for that scope, listing `toolTools` with their toggles +(`defaultOff` ones start unchecked). From inside a sandbox the isolation +open-tool bridge relays the event to the host. + +### Tools + +`generate` returns `toolCalls` (or `null`) for every provider. Calls are either +structured, from native function calling (OpenRouter, Ollama, WebLLM), or parsed +on the host from `` text (local, Chrome built-in). Only calls to +offered tools are returned, so JSON in a normal answer isn't mistaken for a call. +Names are the ones you declared, even when a provider needed them sanitized. +`toolMode` (`"native"`, `"template"` or `"text"`) says how results should be fed +back. + +Before offering tools, the host drops the ones the user toggled off in the picker +and keeps `defaultOff` ones out until toggled on. If a model rejects native tools, +it retries once with the tools described in the system prompt, and remembers that +for the model. + +`generateWithTools` runs the whole loop: generate, run each call, thread the +results back in the right shape, and repeat until the model stops calling tools +or `maxRounds` (6) runs out. + +```js +const {text, messages} = await llm.generateWithTools(messages, { + scope, + tools: [{name: "add", description, parameters, handler: ({a, b}) => a + b}], + onToolCall: ({name, args, result, error}) => {}, +}) +``` + +Handlers run in the tool's own realm; only descriptors cross to the host. + +The user's own tools (the `llm:tool` docs added in the picker) are offered too. +Only the host can read them, so their calls come back tagged `host: true` and run +on the host via `op:"run-tool"`, always in a sandboxed Worker. A tool running its +own loop over `generate` can opt in with `userTools: true` and run those calls +with `llm.runTool(call, {scope})`. + +## Protocol + +The transport only reads `id` and the reserved `op:"abort"`. Everything else is +defined here, in `src/worker-spec.js` and `src/client.js`, which must stay in step: + +``` +generate {op, scope, system, tools, userTools, messages|text, sessionKey, temperature, topP, topk, maxNewTokens} + -> model, token, status, stats, prediction ... result {text, toolCalls, toolMode} | error +run-tool {op, scope, name, args} -> tool-result {result} | error +predict, score-tokens, compute-importance, compute-attention-weights, +extract-features, extract-cut-features, decode-tokens, probe-attention, preload +``` + +## Trust boundary + +- Config comes from the frame's `scope` only. A `frame.config` is ignored, and + only sampling knobs are passed through as overrides. `callConfig` also honours + provider, apiKey, model and url, so passing more would let a tool point + generation at its own endpoint with its own key. +- The API key is applied host-side in `callConfig` and never appears in any frame. +- The settings doc is denylisted from the sandbox, so the tray and the worker spec + only run on the host. +- `src/client.js` imports nothing. It's the only code of this package a + sandboxed tool loads; `@chee/patchwork-llm` and `worker-spec.js` stay host-side. +- The user's own tools always run sandboxed when a consumer asks for them, so a + sandboxed tool can't get page access through them. + +Under isolation the host answers the worker subscription and the stream pair is +transferred across the boundary. That needs `patchwork:worker-channel` in the +isolation element's `shared-providers`, and opening the picker needs +`llm-config-tray` in `shared-tools`. See ISOLATION.md for the bridges. + +## Build + +```sh +pnpm build # vite; rewrites the automerge: @chee/patchwork-llm dep to a cross-origin URL +pnpm test +pnpm push # build + pushwork sync +``` diff --git a/llm-host/package.json b/llm-host/package.json new file mode 100644 index 00000000..ffc8b1e3 --- /dev/null +++ b/llm-host/package.json @@ -0,0 +1,34 @@ +{ + "name": "@grjte/llm-host", + "version": "0.2.0", + "description": "The host-realm LLM package: owns the settings doc + config picker (llm-config-tray) and registers the LLM worker as a patchwork:worker plugin. The library itself is @chee/patchwork-llm.", + "type": "module", + "main": "./dist/index.js", + "exports": { + ".": "./dist/index.js" + }, + "scripts": { + "build": "vite build", + "dev": "vite build --watch", + "push": "pnpm build && pushwork sync", + "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"", + "test": "vitest run", + "test:watch": "vitest" + }, + "author": "grjte", + "license": "MIT", + "dependencies": { + "@chee/patchwork-llm": "automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey", + "solid-js": "^1.9.11" + }, + "devDependencies": { + "@chee/patchwork-bundles": "^0.2.0", + "@inkandswitch/patchwork-bootloader": "^0.0.3", + "@inkandswitch/patchwork-plugins": "^0.0.5", + "happy-dom": "^15.11.7", + "typescript": "^5.8.3", + "vite": "^5.4.19", + "vite-plugin-solid": "^2.10.0", + "vitest": "^3.2.4" + } +} diff --git a/llm-host/pnpm-lock.yaml b/llm-host/pnpm-lock.yaml new file mode 100644 index 00000000..cd54ddfe --- /dev/null +++ b/llm-host/pnpm-lock.yaml @@ -0,0 +1,2072 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: + pnpm-plugin-patchwork: + specifier: 0.4.1 + version: 0.4.1 + +packages: + + pnpm-plugin-patchwork@0.4.1: + resolution: {integrity: sha512-IzQJkoeQaSaYSa1bRko3KUJ+6nBmYg8Qxuyy0llnhPfAGSDP/+gM/Z7R0HGLtTpCE3K57CXS9DnAItqb/pNIZQ==} + +snapshots: + + pnpm-plugin-patchwork@0.4.1: {} + +--- +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@chee/patchwork-llm': + specifier: automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey + version: automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey@kHSoEYZn3QrxfLpLC7Lbm2j979WAsxGgNMH8wZd6jbasFTkx9(@automerge/automerge-repo@2.6.0-subduction.40) + solid-js: + specifier: ^1.9.11 + version: 1.9.15 + devDependencies: + '@chee/patchwork-bundles': + specifier: ^0.2.0 + version: 0.2.0(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40) + '@inkandswitch/patchwork-bootloader': + specifier: ^0.0.3 + version: 0.0.3(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40) + '@inkandswitch/patchwork-plugins': + specifier: ^0.0.5 + version: 0.0.5 + happy-dom: + specifier: ^15.11.7 + version: 15.11.7 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vite: + specifier: ^5.4.19 + version: 5.4.21(@types/node@20.19.43) + vite-plugin-solid: + specifier: ^2.10.0 + version: 2.11.14(solid-js@1.9.15)(vite@5.4.21(@types/node@20.19.43)) + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(happy-dom@15.11.7) + +packages: + + '@automerge/automerge-repo-keyhive@0.0.0-alpha.71': + resolution: {integrity: sha512-bDP6QPNTG9eCYVnLW/PuODLFmGrOe4rwaOSAWLgW9VrHg/5aBlP0PmrzeJ58loYvGlqYeN6PWrV+GFDqCPUcug==} + + '@automerge/automerge-repo-network-broadcastchannel@2.5.0': + resolution: {integrity: sha512-57rUTYf0W6kL9DXwjRtyGLqeyz65tDIpiNQdc1nXHnN4VkNMvNo0xzb37nyFKWHEqPWA0DGCcBA8HRlbVHHyHw==} + + '@automerge/automerge-repo-network-messagechannel@2.5.0': + resolution: {integrity: sha512-G7m161tId4djPMzDPlP7nIEsDY7ddbKfKvpJP7kkVqv3OA/Ml8fzyKaU7C1++s/g0FSNCTiN6hOhFCbSkb6/Dw==} + + '@automerge/automerge-repo-network-websocket@2.5.0': + resolution: {integrity: sha512-zrwbkW8hk3fWkXdeaH+YDfGG63WaB9WLfWYyinZx2AQv4mr6D6UhIhviIjweMmRxMLen6NlIcdu3toHV33ZuKg==} + + '@automerge/automerge-repo-network-websocket@2.5.0-alpha.2': + resolution: {integrity: sha512-EtdsSA9PhuiWWbvT36DGylBeGCL7Aop8nSe4FrBYchBCWpqqZOBLdbxD0crwbCUwTBTJy76pXhSHjObQi0y4zQ==} + + '@automerge/automerge-repo-storage-indexeddb@2.5.0': + resolution: {integrity: sha512-7MJYJ5S6K7dHlbvs5/u/v9iexqOeprU/qQonup28r2IoVqwzjuN5ezaoVk6JRBMDI/ZxWfU4rNrqVrVlB49yXA==} + + '@automerge/automerge-repo@2.5.0': + resolution: {integrity: sha512-bdxuMuKmxw0ZjwQXecrIX1VrHXf445bYCftNJJ5vqgGWVvINB5ZKFYAbtgPIyu1Y0TXQKvc6eqESaDeL+g8MmA==} + + '@automerge/automerge-repo@2.5.0-alpha.2': + resolution: {integrity: sha512-Sjl1CaD4IpmboNIG9/EL9Run+AKrL8/1h3Om1NZp9syBnJN8OWjeG4ttpQce6k8HdPyl8sllAHYy8/Q4Ukh39w==} + + '@automerge/automerge-repo@2.6.0-subduction.40': + resolution: {integrity: sha512-9r3GS8FtZKoIJOtJqkQR0ukF7HSu+IyV5Iqf7MP3ELDz/TEaUbZV8ysyl5Ju4fVZjmagI/n3oH2h4ySk4FdEFQ==} + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.0': + resolution: {integrity: sha512-BOOn/4p8cF4LhjDUQdgM3qyZFya+M7ZuZywuC99x/kbhk4UK7dw/QcsifcHG5E1TAW4R/K17ZGFUC5nCVUSSWg==} + + '@automerge/automerge@3.2.1': + resolution: {integrity: sha512-xsEZlZe8lWyVCbD7Z7roH4X6ZKlRxMmmumb0M5CuxJWptUEfYsf+1z9lFEJh4aDBbWTLAgxsXd+TBi6jKTj0+g==} + + '@automerge/automerge@3.3.0-fragments.2': + resolution: {integrity: sha512-F5KL+YXD0GMFH/as0/kJ3+lotpuJsH1SSrhhKdYSGtR+S9ytBNTw+s3pAjF2Vr0+ZdTMHxsGjqj5SRkJiSNokQ==} + + '@automerge/vanillajs@2.5.0': + resolution: {integrity: sha512-18Z179fBkRYhfJOvzcz5NU+TSJOfOBez1ZQqjNnRlmHB0xwaC9P/MFuuc9ujWtJSiRAh6jzB3HxUZpCIjEfXFw==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.18.6': + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@chee/patchwork-bundles@0.2.0': + resolution: {integrity: sha512-Hc8YhdYM1s/6ij08wMFFBa0+LUyPyW7EBh8yrIsWtBzurWcsHnCtdF7ObZtWVqslkLjlgLqcvpDHYpWhfg6I0g==} + + '@chee/patchwork-llm@automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey@kHSoEYZn3QrxfLpLC7Lbm2j979WAsxGgNMH8wZd6jbasFTkx9': + resolution: {automergeUrl: automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey, type: custom:automerge} + version: 0.3.0 + peerDependencies: + '@automerge/automerge-repo': ^2.5.6 + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@inkandswitch/patchwork-bootloader@0.0.3': + resolution: {integrity: sha512-FPVsH+ncVw4gPWUvOweotcTdL421Dg+/KcYBOUVyqZ3PxpWoLwLPw1jevZVMuGrIURR2MNkLCQqVPd5tlZ2qsA==} + peerDependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.5.0 + '@automerge/automerge-repo-keyhive': 0.0.0-alpha.71 + '@automerge/vanillajs': 2.5.0 + '@keyhive/keyhive': 0.0.0-alpha.40 + + '@inkandswitch/patchwork-filesystem@0.0.3': + resolution: {integrity: sha512-x6yqX1Xyr05ug4YfRyfKJ7jSm/zjhMxU7g/e70nz4QbxfLmcG98LHB9qAkFI2zd+VvKLnTvSpPGu2UvJzXGciw==} + + '@inkandswitch/patchwork-filesystem@0.2.9': + resolution: {integrity: sha512-To2zBNQoAC4BT+PwpZrc0aKZhjLOTd0poJJs/wYEP1wX3r9xr97Og9CdLEvsqsVnEIUXEdZvizH0fbmY0VA14w==} + peerDependencies: + '@automerge/automerge': '*' + '@automerge/automerge-repo': '*' + + '@inkandswitch/patchwork-plugins@0.0.5': + resolution: {integrity: sha512-pQv5M7asDdN8J2moTmLoSnqA8FC/vcOuNJnKveL3MzjEhH0GQdIl6vV3k5jxs83ZpB8stwceE85Zsve/QRnVSA==} + + '@inkandswitch/patchwork-providers@0.3.0': + resolution: {integrity: sha512-CUxWbONfOiz5SCzOpX/7zDTMePWgq7e4778eBJpbjC5Et4lnyL2/UrOZ6YJ4HiCG7r7MbfM86GUi63bqjTeutg==} + peerDependencies: + '@automerge/automerge-repo': '*' + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyhive/keyhive@0.0.0-alpha.40': + resolution: {integrity: sha512-FlvtX6ZK2q47OqqJZ/8G+SAevKJxjSdldhwJPlhUT9QTIv0LNmJO17pkEtLJ0VVrmPryP0tKQDIaQtmcE1f7Tg==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@rollup/rollup-android-arm-eabi@4.63.4': + resolution: {integrity: sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.4': + resolution: {integrity: sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.4': + resolution: {integrity: sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.4': + resolution: {integrity: sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.4': + resolution: {integrity: sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.4': + resolution: {integrity: sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + resolution: {integrity: sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + resolution: {integrity: sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + resolution: {integrity: sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.4': + resolution: {integrity: sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + resolution: {integrity: sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.4': + resolution: {integrity: sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + resolution: {integrity: sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + resolution: {integrity: sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + resolution: {integrity: sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + resolution: {integrity: sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + resolution: {integrity: sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.4': + resolution: {integrity: sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.4': + resolution: {integrity: sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.4': + resolution: {integrity: sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.4': + resolution: {integrity: sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + resolution: {integrity: sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + resolution: {integrity: sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.4': + resolution: {integrity: sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.4': + resolution: {integrity: sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==} + cpu: [x64] + os: [win32] + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + + '@types/serviceworker@0.0.153': + resolution: {integrity: sha512-/cg6dFEkNchJLyRCGo4Gb8mF200qr3xskM5dCPgbtK0OzXxcFcXa6BEBdyG7JksRsTrvCR+V6aFPncoOYAwYhQ==} + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + babel-plugin-jsx-dom-expressions@0.40.10: + resolution: {integrity: sha512-lxve6Y02YiZTldB7efKpnbf1BH00XCFZNYYW235jSGsYaJNFtHrYlKV6/O+miHbjqpIr9FTe5+0no4hofAMbfA==} + peerDependencies: + '@babel/core': ^7.20.12 + + babel-preset-solid@1.9.15: + resolution: {integrity: sha512-GBmg1OiPb+OwcH51XbDAKPtvrPfQW7rCJTJxcp8+yhtWwN+kqnbEJk2SgVybd+uhTxTKAvjaFyiQSr/eUZBwzg==} + peerDependencies: + '@babel/core': ^7.0.0 + solid-js: ^1.9.15 + peerDependenciesMeta: + solid-js: + optional: true + + base-x@4.0.1: + resolution: {integrity: sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + baseline-browser-mapping@2.11.25: + resolution: {integrity: sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.29.0: + resolution: {integrity: sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs58@5.0.0: + resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@3.0.1: + resolution: {integrity: sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.433: + resolution: {integrity: sha512-5lCAbyZBjtmUt/RAGHRqrL2q0oEFRThDAsZHHDn9XHa89Qw7gMYOeSicBTy+AHfvo0r6vwsZvqNJTQIQy1BLzA==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + 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] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + happy-dom@15.11.7: + resolution: {integrity: sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==} + engines: {node: '>=18.0.0'} + + html-entities@2.3.3: + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + merge-anything@5.1.7: + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} + engines: {node: '>=12.13'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + node-releases@2.0.56: + resolution: {integrity: sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==} + engines: {node: '>=18'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + rollup@4.63.4: + resolution: {integrity: sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + seroval-plugins@1.5.6: + resolution: {integrity: sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.6: + resolution: {integrity: sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==} + engines: {node: '>=10'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + solid-js@1.9.15: + resolution: {integrity: sha512-EeiY2xfpZJqPLjXspVEKjAII4yv8NyG//NxZ3IpOFHdUNnnTyL0uJOeS9LWGvA7cFCz5y94cjFwYlmw5Luncsg==} + + solid-refresh@0.6.3: + resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} + peerDependencies: + solid-js: ^1.3 + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + tinyargs@0.1.4: + resolution: {integrity: sha512-5OpdhMRRE70j0zT7mrBpx12FJI+y0EGx8zMe8Vl2/aiwGgpW3X70OVHlsHmHeeG0ncnnWdq1o+k/S5d9ONgzGA==} + engines: {node: '>=14'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} + engines: {node: '>=14.0.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-plugin-solid@2.11.14: + resolution: {integrity: sha512-7ZVBt8rpoyqmlwin2kRIUveaHoF6/kulY7gsnD+qFh4nS29V4OPAnw+ojoAspXIjObiL9o1xh9a/nTuYHm02Rw==} + peerDependencies: + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.0.0 || ^7.0.0 + solid-js: ^1.7.2 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + peerDependenciesMeta: + '@testing-library/jest-dom': + optional: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.33.2: + resolution: {integrity: sha512-8tC7yXgeCvpT8gKEeEje6ikJJG1wpnoLiXe+HfECW8m10ubMd3QxKOwWA8KxPJVBcLwfdRoMKYxIBGYKmo37/A==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@automerge/automerge-repo-keyhive@0.0.0-alpha.71': + dependencies: + '@automerge/automerge-repo': 2.5.0-alpha.2 + '@automerge/automerge-repo-network-websocket': 2.5.0-alpha.2 + '@keyhive/keyhive': 0.0.0-alpha.40 + eventemitter3: 5.0.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo-network-broadcastchannel@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo-network-messagechannel@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + debug: 4.4.3 + eventemitter3: 5.0.4 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo-network-websocket@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + isomorphic-ws: 5.0.0(ws@8.21.3) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo-network-websocket@2.5.0-alpha.2': + dependencies: + '@automerge/automerge-repo': 2.5.0-alpha.2 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + isomorphic-ws: 5.0.0(ws@8.21.3) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo-storage-indexeddb@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo@2.5.0': + dependencies: + '@automerge/automerge': 3.2.1 + bs58check: 3.0.1 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + uuid: 9.0.1 + xstate: 5.33.2 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo@2.5.0-alpha.2': + dependencies: + '@automerge/automerge': 3.2.1 + bs58check: 3.0.1 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + uuid: 9.0.1 + xstate: 5.33.2 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo@2.6.0-subduction.40': + dependencies: + '@automerge/automerge': 3.3.0-fragments.2 + '@automerge/automerge-subduction': 0.16.0 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.33.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.0': {} + + '@automerge/automerge@3.2.1': {} + + '@automerge/automerge@3.3.0-fragments.2': {} + + '@automerge/vanillajs@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + '@automerge/automerge-repo-network-broadcastchannel': 2.5.0 + '@automerge/automerge-repo-network-messagechannel': 2.5.0 + '@automerge/automerge-repo-network-websocket': 2.5.0 + '@automerge/automerge-repo-storage-indexeddb': 2.5.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.18.6': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@chee/patchwork-bundles@0.2.0(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40)': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.40 + '@inkandswitch/patchwork-bootloader': 0.0.3(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40) + '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1) + es-module-lexer: 2.3.2 + resolve.exports: 2.0.3 + unplugin: 2.3.11 + transitivePeerDependencies: + - '@automerge/automerge' + - '@automerge/automerge-repo-keyhive' + - '@automerge/vanillajs' + - '@keyhive/keyhive' + - bufferutil + - supports-color + - utf-8-validate + + '@chee/patchwork-llm@automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey@kHSoEYZn3QrxfLpLC7Lbm2j979WAsxGgNMH8wZd6jbasFTkx9(@automerge/automerge-repo@2.6.0-subduction.40)': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.40 + '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.6.0-subduction.40) + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@inkandswitch/patchwork-bootloader@0.0.3(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40)': + dependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.6.0-subduction.40 + '@automerge/automerge-repo-keyhive': 0.0.0-alpha.71 + '@automerge/vanillajs': 2.5.0 + '@keyhive/keyhive': 0.0.0-alpha.40 + '@types/debug': 4.1.13 + debug: 4.4.3 + resolve.exports: 2.0.3 + service-worker-types: '@types/serviceworker@0.0.153' + tinyargs: 0.1.4 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-filesystem@0.0.3': + dependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.5.0 + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + debug: 4.4.3 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1)': + dependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.6.0-subduction.40 + debug: 4.4.3 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-plugins@0.0.5': + dependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.5.0 + '@inkandswitch/patchwork-filesystem': 0.0.3 + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + debug: 4.4.3 + eventemitter3: 5.0.4 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.40)': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.40 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@keyhive/keyhive@0.0.0-alpha.40': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@noble/hashes@1.8.0': {} + + '@rollup/rollup-android-arm-eabi@4.63.4': + optional: true + + '@rollup/rollup-android-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-x64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.4': + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/ms@2.1.0': {} + + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + + '@types/serviceworker@0.0.153': {} + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@5.4.21(@types/node@20.19.43))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@20.19.43) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.6 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn@8.18.0: {} + + assertion-error@2.0.1: {} + + babel-plugin-jsx-dom-expressions@0.40.10(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.8 + html-entities: 2.3.3 + parse5: 7.3.0 + + babel-preset-solid@1.9.15(@babel/core@7.29.7)(solid-js@1.9.15): + dependencies: + '@babel/core': 7.29.7 + babel-plugin-jsx-dom-expressions: 0.40.10(@babel/core@7.29.7) + optionalDependencies: + solid-js: 1.9.15 + + base-x@4.0.1: {} + + base-x@5.0.1: {} + + baseline-browser-mapping@2.11.25: {} + + browserslist@4.29.0: + dependencies: + baseline-browser-mapping: 2.11.25 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.433 + node-releases: 2.0.56 + update-browserslist-db: 1.3.3(browserslist@4.29.0) + + bs58@5.0.0: + dependencies: + base-x: 4.0.1 + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@3.0.1: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 5.0.0 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cac@6.7.14: {} + + caniuse-lite@1.0.30001810: {} + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + convert-source-map@2.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + detect-libc@2.1.2: + optional: true + + electron-to-chromium@1.5.433: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + es-module-lexer@1.7.0: {} + + es-module-lexer@2.3.2: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + eventemitter3@5.0.4: {} + + expect-type@1.4.0: {} + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + happy-dom@15.11.7: + dependencies: + entities: 4.5.0 + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + + html-entities@2.3.3: {} + + is-what@4.1.16: {} + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + loupe@3.2.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + merge-anything@5.1.7: + dependencies: + is-what: 4.1.16 + + ms@2.1.3: {} + + nanoid@3.3.19: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-releases@2.0.56: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + resolve.exports@2.0.3: {} + + rollup@4.63.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.4 + '@rollup/rollup-android-arm64': 4.63.4 + '@rollup/rollup-darwin-arm64': 4.63.4 + '@rollup/rollup-darwin-x64': 4.63.4 + '@rollup/rollup-freebsd-arm64': 4.63.4 + '@rollup/rollup-freebsd-x64': 4.63.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.4 + '@rollup/rollup-linux-arm-musleabihf': 4.63.4 + '@rollup/rollup-linux-arm64-gnu': 4.63.4 + '@rollup/rollup-linux-arm64-musl': 4.63.4 + '@rollup/rollup-linux-loong64-gnu': 4.63.4 + '@rollup/rollup-linux-loong64-musl': 4.63.4 + '@rollup/rollup-linux-ppc64-gnu': 4.63.4 + '@rollup/rollup-linux-ppc64-musl': 4.63.4 + '@rollup/rollup-linux-riscv64-gnu': 4.63.4 + '@rollup/rollup-linux-riscv64-musl': 4.63.4 + '@rollup/rollup-linux-s390x-gnu': 4.63.4 + '@rollup/rollup-linux-x64-gnu': 4.63.4 + '@rollup/rollup-linux-x64-musl': 4.63.4 + '@rollup/rollup-openbsd-x64': 4.63.4 + '@rollup/rollup-openharmony-arm64': 4.63.4 + '@rollup/rollup-win32-arm64-msvc': 4.63.4 + '@rollup/rollup-win32-ia32-msvc': 4.63.4 + '@rollup/rollup-win32-x64-gnu': 4.63.4 + '@rollup/rollup-win32-x64-msvc': 4.63.4 + fsevents: 2.3.3 + + semver@6.3.1: {} + + seroval-plugins@1.5.6(seroval@1.5.6): + dependencies: + seroval: 1.5.6 + + seroval@1.5.6: {} + + siginfo@2.0.0: {} + + solid-js@1.9.15: + dependencies: + csstype: 3.2.3 + seroval: 1.5.6 + seroval-plugins: 1.5.6(seroval@1.5.6) + + solid-refresh@0.6.3(solid-js@1.9.15): + dependencies: + '@babel/generator': 7.29.8 + '@babel/helper-module-imports': 7.29.7 + '@babel/types': 7.29.8 + solid-js: 1.9.15 + transitivePeerDependencies: + - supports-color + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + tinyargs@0.1.4: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.6: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.18.0 + picomatch: 4.0.7 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.3.3(browserslist@4.29.0): + dependencies: + browserslist: 4.29.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + uuid@14.0.2: {} + + uuid@9.0.1: {} + + vite-node@3.2.4(@types/node@20.19.43): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 5.4.21(@types/node@20.19.43) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-plugin-solid@2.11.14(solid-js@1.9.15)(vite@5.4.21(@types/node@20.19.43)): + dependencies: + '@babel/core': 7.29.7 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.9.15(@babel/core@7.29.7)(solid-js@1.9.15) + merge-anything: 5.1.7 + solid-js: 1.9.15 + solid-refresh: 0.6.3(solid-js@1.9.15) + vite: 5.4.21(@types/node@20.19.43) + vitefu: 1.1.3(vite@5.4.21(@types/node@20.19.43)) + transitivePeerDependencies: + - supports-color + + vite@5.4.21(@types/node@20.19.43): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.28 + rollup: 4.63.4 + optionalDependencies: + '@types/node': 20.19.43 + fsevents: 2.3.3 + + vitefu@1.1.3(vite@5.4.21(@types/node@20.19.43)): + optionalDependencies: + vite: 5.4.21(@types/node@20.19.43) + + vitest@3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(happy-dom@15.11.7): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@5.4.21(@types/node@20.19.43)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 5.4.21(@types/node@20.19.43) + vite-node: 3.2.4(@types/node@20.19.43) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + happy-dom: 15.11.7 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + webidl-conversions@7.0.0: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-mimetype@3.0.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.3: {} + + xstate@5.33.2: {} + + yallist@3.1.1: {} diff --git a/llm-host/pnpm-workspace.yaml b/llm-host/pnpm-workspace.yaml new file mode 100644 index 00000000..b6a60e31 --- /dev/null +++ b/llm-host/pnpm-workspace.yaml @@ -0,0 +1,11 @@ +# pnpm 11 reads its settings from this file. There is no root workspace, so +# every package carries the settings it needs rather than inheriting them. +minimumReleaseAge: 0 +verifyDepsBeforeRun: false +allowBuilds: + "@swc/core": true + cbor-extract: true + core-js: true + esbuild: true +configDependencies: + pnpm-plugin-patchwork: "0.4.1+sha512-IzQJkoeQaSaYSa1bRko3KUJ+6nBmYg8Qxuyy0llnhPfAGSDP/+gM/Z7R0HGLtTpCE3K57CXS9DnAItqb/pNIZQ==" diff --git a/llm-host/src/LlmConfigTray.tsx b/llm-host/src/LlmConfigTray.tsx new file mode 100644 index 00000000..1f11fe3e --- /dev/null +++ b/llm-host/src/LlmConfigTray.tsx @@ -0,0 +1,206 @@ +import { createSignal, Show } from "solid-js"; +import { Portal, render } from "solid-js/web"; +// @ts-ignore — plain-JS library, ships no type declarations +import { + dom as pickerDom, + ensureConfig, + readScopedConfig, + readConfig, + writeScopeOverride, + writeConfig, +} from "@chee/patchwork-llm"; + +/** + * The system-tray icon and popover for the LLM model/config picker. Host realm + * only: it reads and writes the settings doc, which holds the API key. + * + * Opened by clicking the icon, or by a `patchwork:open-tool` event with + * {detail: {component: "llm-config-tray", scope, toolPrompt, toolTools}}, which + * isolation relays out of a sandbox. Scoped edits go through writeScopeOverride, + * the global default through writeConfig. + */ + +// A per-tool (and optionally per-doc) config scope. No scope = the global default. +type Scope = { toolId: string; docId?: string; toolName?: string; docName?: string }; + +export function LlmConfigTray(element: HTMLElement) { + const style = document.createElement("style"); + style.textContent = ` + .llm-config-tray { position: relative; display: inline-flex; align-items: center; } + .llm-config-tray-button { + display: inline-flex; align-items: center; gap: var(--studio-space-2xs, 0.25rem); + height: 1.75rem; padding: 0 var(--studio-space-xs, 0.375rem); + border: 1px solid var(--studio-chrome-offset-20, var(--studio-fill-offset-20, #d4d4d4)); + border-radius: var(--studio-radius-sm, 4px); + background: var(--studio-chrome, var(--studio-fill, white)); + color: var(--studio-chrome-line, var(--studio-line, black)); + font: 500 0.75rem/1 var(--studio-family-sans, system-ui, sans-serif); cursor: pointer; + } + .llm-config-tray-button:hover { + background: var(--studio-chrome-offset-10, var(--studio-fill-offset-10, #f2f2f2)); + } + .llm-config-tray-popover { + position: fixed; z-index: 2147483000; + max-width: min(92vw, 520px); max-height: 80vh; overflow: auto; + background: var(--studio-fill, white); color: var(--studio-line, black); + border: 1px solid var(--studio-fill-offset-20, #d4d4d4); + border-radius: var(--studio-radius-md, 8px); + box-shadow: var(--studio-shadow-lg, 0 8px 32px rgba(0,0,0,0.24)); + padding: var(--studio-space-sm, 0.5rem); + } + `; + element.append(style); + + // Current scope the picker edits (set when opened via event; icon click uses + // the global default scope). + const [open, setOpen] = createSignal(false); + const [scope, setScope] = createSignal(); + const [payload, setPayload] = createSignal({}); + const [pos, setPos] = createSignal<{ left: number; top: number } | null>( + null + ); + + let buttonEl!: HTMLButtonElement; + let popoverEl: HTMLDivElement | undefined; + let pickerHost: HTMLDivElement | undefined; + let currentPicker: any = null; + + function sourceFor(sc: Scope | undefined) { + if (sc) { + return { + read: () => { + try { + return readScopedConfig(sc); + } catch { + return readConfig(); + } + }, + write: (next: any) => writeScopeOverride(sc, next), + }; + } + return { + read: () => readConfig(), + write: (next: any) => writeConfig(next), + }; + } + + async function mountPicker() { + const sc = scope(); + const p = payload() || {}; + await ensureConfig(sc, element); + if (!pickerHost) return; + pickerHost.replaceChildren(); + currentPicker = pickerDom({ + source: sourceFor(sc), + scope: sc, + toolName: sc?.toolName || p.toolName, + toolPrompt: p.toolPrompt, + toolTools: p.toolTools, + }); + pickerHost.append(currentPicker); + } + + function positionPopover() { + const r = buttonEl.getBoundingClientRect(); + // Prefer opening above the tray (tray sits at the bottom), aligned right. + setPos({ left: Math.max(8, r.right - 520), top: Math.max(8, r.top - 8) }); + } + + function openPicker(sc: Partial | undefined, p: any) { + setScope(sc?.toolId ? (sc as Scope) : undefined); + setPayload(p || {}); + positionPopover(); + setOpen(true); + queueMicrotask(mountPicker); + } + + function closePicker() { + try { + currentPicker?.destroy?.(); + } catch {} + currentPicker = null; + setOpen(false); + } + + // Open via the `patchwork:open-tool` event (from chat / other tools, incl. + // relayed across isolation). Match our component id. + const onOpenTool = (e: Event) => { + const detail = (e as CustomEvent).detail; + if (!detail || detail.component !== "llm-config-tray") return; + openPicker(detail.scope, detail); + }; + document.addEventListener("patchwork:open-tool", onOpenTool as EventListener); + + // Close on outside click. Use composedPath() (captured at event time, before + // the picker re-renders its own DOM on a selection) rather than + // popoverEl.contains(target): the picker rebuilds its option list when config + // changes, so the clicked node is often already detached by the time the click + // bubbles to document — contains() would then wrongly report "outside" and + // close the popover on every model click. + const onDocClick = (e: MouseEvent) => { + if (!open()) return; + const path = e.composedPath(); + if ( + (popoverEl && path.includes(popoverEl)) || + (buttonEl && path.includes(buttonEl)) + ) { + return; // click was inside the popover or on the tray button + } + closePicker(); + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape" && open()) closePicker(); + }; + document.addEventListener("click", onDocClick); + document.addEventListener("keydown", onKey); + + const dispose = render( + () => ( +
+ + + +
event.stopPropagation()} + > +
+
+ + +
+ ), + element + ); + + return () => { + closePicker(); + dispose(); + style.remove(); + document.removeEventListener( + "patchwork:open-tool", + onOpenTool as EventListener + ); + document.removeEventListener("click", onDocClick); + document.removeEventListener("keydown", onKey); + }; +} diff --git a/llm-host/src/client.js b/llm-host/src/client.js new file mode 100644 index 00000000..9ebc673a --- /dev/null +++ b/llm-host/src/client.js @@ -0,0 +1,211 @@ +/** + * The consume half of the LLM protocol, the mirror of worker-spec.js. + * + * Tools don't import this. index.ts registers `makeLLMClient` as the + * `patchwork:worker-client` plugin "llm", and a tool calls + * `connectWorkerClient("llm", …)` from @grjte/patchwork-worker to get one bound + * to a session. + * + * This is the chunk a sandboxed tool loads, so it must import nothing: never + * @chee/patchwork-llm, never ./worker-spec.js. + * + * Frames: + * generate {op, scope, system, tools, userTools, messages|text, sessionKey} + * -> token | status | model | stats | prediction ... result | error + * run-tool {op, scope, name, args} -> tool-result | error + */ + +/** + * @typedef {Object} Tool + * @property {string} name + * @property {string} [description] + * @property {any} [parameters] JSON Schema for the arguments + * @property {boolean} [defaultOff] only offered once the user turns it on + * @property {(args: any) => any} [handler] generateWithTools runs this; never sent + * + * @typedef {Object} ToolCall + * @property {string} name as declared in `tools` + * @property {any} args + * @property {string} [id] + * @property {boolean} [host] one of the user's own tools: run it with `runTool` + * + * @typedef {Object} GenOpts + * @property {any} [scope] {toolId, docId}; the host resolves config for it + * @property {string|Record} [system] a string, or {default, [provider]} + * @property {Tool[]} [tools] + * @property {boolean} [userTools] also offer the user's own tools (host-run) + * @property {string} [sessionKey] + * @property {HTMLElement} [element] a node in the mounted , for discovery + * @property {AbortSignal} [signal] + * @property {(delta: string, full: string, round?: number) => void} [onToken] + * @property {(message: string) => void} [onStatus] + * @property {(label: string) => void} [onModel] + * + * @typedef {Object} GenResult + * @property {string} text + * @property {ToolCall[]|null} toolCalls + * @property {"native"|"template"|"text"} [toolMode] how to thread results back + * + * @typedef {GenOpts & { + * maxRounds?: number, + * onToolCall?: (info: {name: string, args: any, result?: any, error?: string}) => void, + * }} ToolLoopOpts + */ + +/** @param {{request: (frame: any, opts: any) => {promise: Promise}}} session */ +export function makeLLMClient(session) { + /** + * Generate a completion. Streams tokens via `onToken` and resolves with the + * text and any tool calls. Running the calls is up to the caller; see + * generateWithTools. + * @param {any[]|string} messages + * @param {GenOpts} [opts] + * @returns {Promise} + */ + function generate(messages, opts = {}) { + const {promise} = session.request( + { + op: "generate", + sessionKey: opts.sessionKey, + scope: opts.scope, + system: opts.system, + // Handlers can't be structured-cloned across the isolation boundary. + tools: opts.tools?.map(({name, description, parameters, defaultOff}) => ({ + name, + description, + parameters, + defaultOff, + })), + userTools: opts.userTools, + ...(typeof messages === "string" ? {text: messages} : {messages}), + }, + { + element: opts.element, + signal: opts.signal, + terminal: { + result: (/** @type {any} */ f) => ({text: f.text, toolCalls: f.toolCalls || null, toolMode: f.toolMode}), + error: (/** @type {any} */ f) => { + throw new Error(f.message) + }, + }, + onFrame: (/** @type {any} */ f) => { + if (f.type === "token") opts.onToken?.(f.delta, f.text) + else if (f.type === "status") opts.onStatus?.(f.message) + else if (f.type === "model") opts.onModel?.(f.label) + }, + } + ) + return promise + } + + /** + * Run one of the user's own tools (a call tagged `host`). It runs on the + * host, sandboxed. + * @param {ToolCall} call + * @param {Pick} [opts] + */ + function runTool(call, opts = {}) { + return session.request( + {op: "run-tool", scope: opts.scope, name: call.name, args: call.args}, + { + element: opts.element, + signal: opts.signal, + terminal: { + "tool-result": (/** @type {any} */ f) => f.result, + error: (/** @type {any} */ f) => { + throw new Error(f.message) + }, + }, + } + ).promise + } + + /** + * Generate, run the tool calls, feed the results back and generate again, + * until the model stops calling tools or `maxRounds` (default 6) runs out. + * Calls to `tools` run their `handler` here; the user's own tools are offered + * too and run on the host. + * @param {any[]|string} messages + * @param {ToolLoopOpts} [opts] + * @returns {Promise<{text: string, messages: any[]}>} + */ + async function generateWithTools(messages, opts = {}) { + const {maxRounds = 6, onToolCall, onToken, ...genOpts} = opts + const tools = opts.tools || [] + const convo = typeof messages === "string" ? [{role: "user", content: messages}] : [...messages] + + /** @param {ToolCall} call @returns {Promise} */ + async function run(call) { + const {name, args} = call + try { + let result + if (call.host) result = await runTool(call, opts) + else { + const handler = tools.find((t) => t.name === name)?.handler + if (!handler) throw new Error(`no tool named "${name}"`) + result = await handler(args || {}) + } + onToolCall?.({name, args, result}) + return typeof result === "string" ? result : (JSON.stringify(result) ?? "") + } catch (e) { + const error = /** @type {any} */ (e)?.message || String(e) + onToolCall?.({name, args, error}) + return "Error: " + error + } + } + + let text = "" + for (let round = 0; round < maxRounds; round++) { + const res = await generate(convo, { + ...genOpts, + userTools: genOpts.userTools ?? true, + onToken: onToken && ((delta, full) => onToken(delta, full, round)), + }) + text = res.text + if (!res.toolCalls) break + const calls = res.toolCalls.map((call, i) => ({...call, id: call.id || `call_${round}_${i}`})) + const results = [] + for (const call of calls) results.push(await run(call)) + convo.push(...threadResults(res, calls, results)) + } + return {text, messages: convo} + } + + return {generate, generateWithTools, runTool} +} + +/** + * The messages that carry one round's tool calls and results back to the + * model, in the shape its `toolMode` expects. + * @param {GenResult} res + * @param {(ToolCall & {id: string})[]} calls + * @param {string[]} results + */ +function threadResults(res, calls, results) { + if (res.toolMode === "native") + return [ + { + role: "assistant", + content: res.text || null, + tool_calls: calls.map((c) => ({ + id: c.id, + type: "function", + function: {name: c.name, arguments: JSON.stringify(c.args || {})}, + })), + }, + ...calls.map((c, i) => ({role: "tool", tool_call_id: c.id, content: results[i]})), + ] + if (res.toolMode === "template") + return [ + { + role: "assistant", + content: "", + tool_calls: calls.map((c) => ({type: "function", function: {name: c.name, arguments: c.args || {}}})), + }, + ...results.map((content) => ({role: "tool", content})), + ] + return [ + {role: "assistant", content: res.text}, + ...calls.map((c, i) => ({role: "user", content: `Tool "${c.name}" returned:\n${results[i]}`})), + ] +} diff --git a/llm-host/src/client.test.js b/llm-host/src/client.test.js new file mode 100644 index 00000000..839bd31f --- /dev/null +++ b/llm-host/src/client.test.js @@ -0,0 +1,220 @@ +import {describe, it, expect, vi} from "vitest" +import {makeLLMClient} from "./client.js" + +/** + * A fake transport session. `reply(frame, opts)` decides how each request + * settles: return a terminal frame, or call `opts.onFrame` first for progress. + * Every request is recorded in `requests`. + */ +function fakeSession(reply = () => ({type: "result", text: ""})) { + /** @type {{frame: any, opts: any}[]} */ + const requests = [] + const session = { + request(frame, opts) { + requests.push({frame, opts}) + const promise = Promise.resolve().then(() => { + const f = reply(frame, opts) + return opts.terminal[f.type](f) + }) + return {promise, abort: vi.fn()} + }, + } + return {session, requests} +} + +describe("makeLLMClient · generate", () => { + it("sends a generate frame with messages, scope, system, tools and sessionKey", async () => { + const {session, requests} = fakeSession(() => ({type: "result", text: "hello", toolCalls: null})) + const el = {} + const signal = new AbortController().signal + const res = await makeLLMClient(session).generate([{role: "user", content: "hi"}], { + sessionKey: "doc-1", + scope: {toolId: "chat"}, + system: {default: "be brief"}, + tools: [{name: "x"}], + element: /** @type {any} */ (el), + signal, + }) + expect(requests[0].frame).toEqual({ + op: "generate", + sessionKey: "doc-1", + scope: {toolId: "chat"}, + system: {default: "be brief"}, + tools: [{name: "x"}], + messages: [{role: "user", content: "hi"}], + }) + expect(requests[0].opts.element).toBe(el) + expect(requests[0].opts.signal).toBe(signal) + expect(res).toEqual({text: "hello", toolCalls: null, toolMode: undefined}) + }) + + it("sends only tool descriptors, never handlers", async () => { + const {session, requests} = fakeSession() + await makeLLMClient(session).generate("x", { + tools: [{name: "x", description: "d", parameters: {}, defaultOff: true, handler: () => 1}], + }) + expect(requests[0].frame.tools).toEqual([{name: "x", description: "d", parameters: {}, defaultOff: true}]) + expect(() => structuredClone(requests[0].frame)).not.toThrow() + }) + + it("sends a string prompt as text", async () => { + const {session, requests} = fakeSession() + await makeLLMClient(session).generate("continue this") + expect(requests[0].frame.text).toBe("continue this") + expect(requests[0].frame.messages).toBeUndefined() + }) + + it("passes toolCalls and toolMode through from the result frame", async () => { + const {session} = fakeSession(() => ({type: "result", text: "t", toolCalls: [{name: "x", args: {}}], toolMode: "template"})) + await expect(makeLLMClient(session).generate("x")).resolves.toEqual({ + text: "t", + toolCalls: [{name: "x", args: {}}], + toolMode: "template", + }) + }) + + it("rejects with the error frame's message", async () => { + const {session} = fakeSession(() => ({type: "error", message: "boom"})) + await expect(makeLLMClient(session).generate("x")).rejects.toThrow("boom") + }) + + it("routes token / status / model frames to the callbacks", async () => { + const {session} = fakeSession((_frame, opts) => { + opts.onFrame({type: "token", delta: "h", text: "h"}) + opts.onFrame({type: "status", message: "loading"}) + opts.onFrame({type: "model", label: "Local 1B"}) + opts.onFrame({type: "stats", tokPerSec: 1}) + return {type: "result", text: "h"} + }) + const onToken = vi.fn() + const onStatus = vi.fn() + const onModel = vi.fn() + await makeLLMClient(session).generate("x", {onToken, onStatus, onModel}) + expect(onToken).toHaveBeenCalledWith("h", "h") + expect(onStatus).toHaveBeenCalledWith("loading") + expect(onModel).toHaveBeenCalledWith("Local 1B") + }) +}) + +describe("makeLLMClient · runTool", () => { + it("asks the host to run the call and resolves with its result", async () => { + const {session, requests} = fakeSession(() => ({type: "tool-result", result: {temp: 12}})) + const result = await makeLLMClient(session).runTool({name: "weather", args: {city: "x"}}, {scope: {toolId: "t"}}) + expect(requests[0].frame).toEqual({op: "run-tool", scope: {toolId: "t"}, name: "weather", args: {city: "x"}}) + expect(result).toEqual({temp: 12}) + }) +}) + +describe("makeLLMClient · generateWithTools", () => { + /** Replies to generate frames from `results` in order. */ + function scripted(results, runTool = () => ({type: "tool-result", result: "host result"})) { + let i = 0 + return fakeSession((frame) => (frame.op === "run-tool" ? runTool(frame) : {type: "result", ...results[i++]})) + } + + it("threads native calls back as tool_calls + role:tool messages", async () => { + const {session, requests} = scripted([ + {text: "", toolCalls: [{id: "c1", name: "add", args: {a: 1, b: 2}}], toolMode: "native"}, + {text: "3", toolCalls: null, toolMode: "native"}, + ]) + const add = vi.fn(({a, b}) => a + b) + const res = await makeLLMClient(session).generateWithTools("1+2?", {tools: [{name: "add", handler: add}]}) + expect(add).toHaveBeenCalledWith({a: 1, b: 2}) + expect(res.text).toBe("3") + expect(requests[1].frame.messages).toEqual([ + {role: "user", content: "1+2?"}, + { + role: "assistant", + content: null, + tool_calls: [{id: "c1", type: "function", function: {name: "add", arguments: '{"a":1,"b":2}'}}], + }, + {role: "tool", tool_call_id: "c1", content: "3"}, + ]) + expect(res.messages).toEqual(requests[1].frame.messages) + }) + + it("threads template calls as role:tool messages without ids", async () => { + const {session, requests} = scripted([ + {text: "…", toolCalls: [{name: "add", args: {a: 1}}], toolMode: "template"}, + {text: "done", toolCalls: null}, + ]) + await makeLLMClient(session).generateWithTools([{role: "user", content: "q"}], {tools: [{name: "add", handler: () => "ok"}]}) + expect(requests[1].frame.messages.slice(1)).toEqual([ + {role: "assistant", content: "", tool_calls: [{type: "function", function: {name: "add", arguments: {a: 1}}}]}, + {role: "tool", content: "ok"}, + ]) + }) + + it("threads text-mode calls as plain messages", async () => { + const {session, requests} = scripted([ + {text: "calling add", toolCalls: [{name: "add", args: {}}], toolMode: "text"}, + {text: "done", toolCalls: null}, + ]) + await makeLLMClient(session).generateWithTools("q", {tools: [{name: "add", handler: () => ({n: 1})}]}) + expect(requests[1].frame.messages.slice(1)).toEqual([ + {role: "assistant", content: "calling add"}, + {role: "user", content: 'Tool "add" returned:\n{"n":1}'}, + ]) + }) + + it("runs host-tagged calls on the host and offers the user's tools by default", async () => { + const {session, requests} = scripted([ + {text: "", toolCalls: [{id: "c1", name: "Weather now", args: {city: "x"}, host: true}], toolMode: "native"}, + {text: "sunny", toolCalls: null}, + ]) + const onToolCall = vi.fn() + await makeLLMClient(session).generateWithTools("weather?", {scope: {toolId: "t"}, onToolCall}) + expect(requests[0].frame.userTools).toBe(true) + expect(requests[1].frame).toEqual({op: "run-tool", scope: {toolId: "t"}, name: "Weather now", args: {city: "x"}}) + expect(onToolCall).toHaveBeenCalledWith({name: "Weather now", args: {city: "x"}, result: "host result"}) + expect(requests[2].frame.messages.at(-1)).toEqual({role: "tool", tool_call_id: "c1", content: "host result"}) + }) + + it("reports handler errors and unknown tools to the model instead of throwing", async () => { + const {session, requests} = scripted([ + { + text: "", + toolCalls: [ + {id: "c1", name: "boom", args: {}}, + {id: "c2", name: "nope", args: {}}, + ], + toolMode: "native", + }, + {text: "sorry", toolCalls: null}, + ]) + const onToolCall = vi.fn() + const res = await makeLLMClient(session).generateWithTools("q", { + tools: [ + { + name: "boom", + handler: () => { + throw new Error("kaboom") + }, + }, + ], + onToolCall, + }) + expect(res.text).toBe("sorry") + expect(requests[1].frame.messages.slice(-2).map((m) => m.content)).toEqual(["Error: kaboom", 'Error: no tool named "nope"']) + expect(onToolCall).toHaveBeenCalledWith({name: "boom", args: {}, error: "kaboom"}) + }) + + it("stops after maxRounds and tags tokens with the round", async () => { + const {session, requests} = fakeSession((frame, opts) => { + opts.onFrame({type: "token", delta: "t", text: "t"}) + return {type: "result", text: "again", toolCalls: [{name: "loop", args: {}}], toolMode: "text"} + }) + const onToken = vi.fn() + const res = await makeLLMClient(session).generateWithTools("q", { + tools: [{name: "loop", handler: () => "ok"}], + maxRounds: 2, + onToken, + }) + expect(requests).toHaveLength(2) + expect(res.text).toBe("again") + expect(onToken.mock.calls).toEqual([ + ["t", "t", 0], + ["t", "t", 1], + ]) + }) +}) diff --git a/llm-host/src/index.ts b/llm-host/src/index.ts new file mode 100644 index 00000000..02336b0e --- /dev/null +++ b/llm-host/src/index.ts @@ -0,0 +1,49 @@ +import type { Plugin } from "@inkandswitch/patchwork-plugins"; + +/** + * The Patchwork side of @chee/patchwork-llm: everything that has to run in the + * host realm because it touches the settings doc or the API key. + * + * Every plugin is behind `load()`, so importing this registers descriptors only. + * Descriptors must stay plain data: the isolation bridge structured-clones them. + */ +export const plugins: Plugin[] = [ + { + // System-tray icon that opens the model/config picker. Tools open it with a + // `patchwork:open-tool` event, which isolation relays to the host. + type: "patchwork:component", + id: "llm-config-tray", + name: "LLM Model & Config", + icon: "Bot", + tags: ["system-tray"], + async load() { + const { LlmConfigTray } = await import("./LlmConfigTray.js"); + return LlmConfigTray; + }, + }, + { + // The serve half, run by the host's worker provider when a tool connects. + type: "patchwork:worker", + id: "llm", + name: "LLM", + async load() { + // @ts-ignore — plain-JS library, ships no bundled types + const lib = await import("@chee/patchwork-llm"); + // @ts-ignore — plain JS + const { makeLLMWorkerSpec } = await import("./worker-spec.js"); + return makeLLMWorkerSpec(lib); + }, + }, + { + // The consume half, resolved by `connectWorkerClient("llm", …)`. Imports + // nothing, so it's safe to load into a sandbox. + type: "patchwork:worker-client", + id: "llm", + name: "LLM client", + async load() { + // @ts-ignore — plain JS + const { makeLLMClient } = await import("./client.js"); + return makeLLMClient; + }, + }, +]; diff --git a/llm-host/src/worker-spec.js b/llm-host/src/worker-spec.js new file mode 100644 index 00000000..04414e60 --- /dev/null +++ b/llm-host/src/worker-spec.js @@ -0,0 +1,348 @@ +/** + * The serve half of the LLM protocol: a WorkerSpec that @grjte/patchwork-worker + * serves in the host realm, over @chee/patchwork-llm's compute worker. + * + * Config and the API key are resolved here from the frame's `scope` and never + * leave the host. Request preparation is the library's own `prepareGenerate`, so + * this path can't drift from the library's same-realm `generate()`. What's added + * here is host policy: which overrides a consumer may set, provider-conditional + * system prompts, `toolToggles`, the user's own tools, and turning model output + * into `toolCalls` so consumers never parse it. + * + * Consumers only ever see the tool names they declared. Providers need names + * matching [a-zA-Z0-9_-]{1,64}, so names are sanitized on the way in (including + * in `tool_calls` the consumer threads back) and mapped back on the way out. + * + * `lib` is passed in because llm-host loads the library from its automerge doc + * at runtime. + */ + +/** + * `system` is a string, or a map like `{default, local, openrouter}` where the + * provider's entry wins. + * @param {any} system + * @param {string} provider + */ +function systemFor(system, provider) { + if (system == null || typeof system === "string") return system ?? undefined + return system[provider] ?? system.default ?? undefined +} + +/** + * A tool is offered unless toggled off; a `defaultOff` tool only if toggled on. + * @param {any[]|undefined} tools + * @param {Record} [toggles] + */ +function enabledTools(tools, toggles = {}) { + if (!Array.isArray(tools)) return [] + return tools.filter((tool) => (tool.defaultOff ? toggles[tool.name] === true : toggles[tool.name] !== false)) +} + +/** @param {any} lib */ +export function makeLLMWorkerSpec(lib) { + const {sanitizeToolName} = lib + + // "provider:model" pairs that rejected native tool calling. Their later + // requests describe the tools in the system prompt instead. + const noNativeTools = new Set() + + /** @type {Promise|null} */ + let openrouterModels = null + /** Human-readable model label; never includes secrets. @param {any} cfg */ + async function modelLabel(cfg) { + try { + if (cfg.provider === "openrouter") openrouterModels ??= lib.fetchOpenRouterModels().catch(() => []) + return lib.describeConfig(cfg, {openrouterModels: (await openrouterModels) || []}) + } catch { + return "the configured model" + } + } + + /** + * Config comes from `frame.scope` only. A consumer-supplied `frame.config` + * would let a tool choose the provider and its own key. + * @param {any} frame + * @param {Promise|null} settingsWarm + */ + async function resolveCfg0(frame, settingsWarm) { + if (settingsWarm) await settingsWarm + // ensureConfig quietly falls back to defaults (and an empty API key) when + // the settings doc isn't loaded yet, e.g. when the warm timed out. + if (!lib.settingsDocHandle()) await lib.ensureSettingsDoc() + return lib.ensureConfig(frame.scope) + } + + /** + * callConfig also honours provider / apiKey / model / url overrides, so only + * the sampling knobs a consumer owns are passed through. + * @param {any} frame + */ + function samplingOverrides(frame) { + return { + temperature: frame.temperature, + topP: frame.topP, + topk: frame.topk, + maxNewTokens: frame.maxNewTokens, + } + } + + /** @param {any} frame @param {Promise|null} settingsWarm */ + async function resolveForFrame(frame, settingsWarm) { + const cfg = await lib.resolveCfgPrompts(await resolveCfg0(frame, settingsWarm)) + return {cfg, config: lib.callConfig(cfg, samplingOverrides(frame))} + } + + /** + * The consumer's enabled tools, then (when `frame.userTools`) the user's own + * `llm:tool` docs, tagged `host`. A consumer tool wins a name clash. + * @param {any} frame + * @param {any} cfg0 + */ + async function toolsFor(frame, cfg0) { + const tools = enabledTools(frame.tools, cfg0.toolToggles).map((t) => ({ + name: t.name, + description: t.description, + parameters: t.parameters, + })) + if (!frame.userTools) return tools + const taken = new Set(tools.map((t) => sanitizeToolName(t.name))) + for (const t of await lib.resolveTools(cfg0)) { + if (taken.has(sanitizeToolName(t.name))) continue + taken.add(sanitizeToolName(t.name)) + tools.push({name: t.name, description: t.description, parameters: t.parameters, host: true}) + } + return tools + } + + /** @param {any[]} tools @param {string} name */ + function findTool(tools, name) { + return tools.find((t) => t.name === name) ?? tools.find((t) => sanitizeToolName(t.name) === name) + } + + /** + * Sanitize tool names in `tool_calls` the consumer threads back. + * @param {any} messages + */ + function wireNames(messages) { + if (!Array.isArray(messages)) return messages + return messages.map((m) => + Array.isArray(m?.tool_calls) + ? { + ...m, + tool_calls: m.tool_calls.map((/** @type {any} */ c) => + c?.function ? {...c, function: {...c.function, name: sanitizeToolName(c.function.name)}} : c + ), + } + : m + ) + } + + /** + * `toolCalls` and `toolMode` for a result. Structured calls come from native + * function calling. Otherwise the text is parsed, keeping only calls to + * offered tools, since prose can contain JSON that merely looks like a call. + * `toolMode` says how to thread the results back: "native", "template" or + * "text". + * @param {any} msg {text, toolCalls, toolMode} from the worker + * @param {any[]} tools + */ + function toolCallsFor(msg, tools) { + if (!tools.length) return {toolCalls: null, toolMode: msg.toolMode} + const structured = Array.isArray(msg.toolCalls) && msg.toolCalls.length > 0 + const raw = structured ? msg.toolCalls : lib.parseToolCalls(msg.text || "") + const calls = [] + for (const call of raw) { + const tool = findTool(tools, call.name) + if (!tool && !structured) continue + calls.push({...call, name: tool?.name ?? call.name, ...(tool?.host ? {host: true} : {})}) + } + return { + toolCalls: calls.length ? calls : null, + toolMode: structured ? "native" : msg.toolMode === "template" ? "template" : "text", + } + } + + return { + createWorker: lib.createWorker, + + /** + * Warm the settings doc. `ctx.element` reaches the tool-storage provider; + * the transport bounds this with a timeout. + * @param {{element?: HTMLElement}} ctx + */ + open(ctx) { + return ctx.element ? Promise.resolve(lib.ensureSettingsDoc(ctx.element)).catch(() => null) : null + }, + + /** + * @param {any} frame + * @param {any} io @grjte/patchwork-worker's IO: {post, emit, on, workerId, state} + */ + async handle(frame, io) { + const {post, emit, on, workerId, state: settingsWarm} = io + const op = frame.op + const sessionKey = frame.sessionKey || workerId + + if (op === "generate") { + const cfg0 = await resolveCfg0(frame, settingsWarm) + const tools = await toolsFor(frame, cfg0) + const system = systemFor(frame.system, cfg0.provider) + const base = { + messages: frame.text != null ? frame.text : wireNames(frame.messages), + // A continuation is a raw string prompt with no chat messages. + continuation: !!frame.continuation && frame.messages === undefined, + overrides: samplingOverrides(frame), + } + const withTools = () => lib.prepareGenerate(cfg0, {...base, system, tools}) + const toolsInPrompt = () => + lib.prepareGenerate(cfg0, { + ...base, + system: [lib.buildToolsSystem(tools), system].filter(Boolean).join("\n\n"), + }) + + let prepared = await withTools() + const {cfg, config} = prepared + const modelKey = `${config.provider}:${config.model}` + let native = tools.length > 0 && lib.NATIVE_TOOL_PROVIDERS.has(config.provider) + if (native && noNativeTools.has(modelKey)) { + prepared = await toolsInPrompt() + native = false + } + // If the model rejects native tools, retry once with them in the prompt. + const fallback = native ? await toolsInPrompt() : null + + void modelLabel(cfg).then((label) => emit({type: "model", label})) + + // builtin (Chrome Prompt API) runs in this realm, not in the worker. It + // can't be aborted: the transport only routes op:"abort" to requests + // tracked with `on`, which needs a worker message to finish. + if (prepared.builtin) { + try { + const text = await lib.builtinGenerate(prepared.input, { + temperature: config.temperature, + topK: config.topK, + system: lib.effectiveSystem(cfg, prepared.extraSystem), + onToken: (/** @type {string} */ _delta, /** @type {string} */ full) => + emit({type: "token", delta: "", text: full}), + onStatus: (/** @type {string} */ message) => emit({type: "status", message}), + }) + emit({type: "result", text, ...toolCallsFor({text}, tools)}) + } catch (e) { + emit({type: "error", message: /** @type {any} */ (e)?.message || String(e)}) + } + return {sessionKey} + } + + const send = (/** @type {any} */ p) => + post(lib.buildGeneratePayload(p.config, p.input, {id: workerId, sessionKey})) + let streamed = false + let fellBack = false + on((msg) => { + switch (msg.type) { + case "token": + streamed = true + emit(msg) + return false + case "prediction": + case "stats": + emit(msg) + return false + case "result": + if (fellBack) noNativeTools.add(modelKey) + emit({...msg, ...toolCallsFor(msg, tools)}) + return true + case "error": + if (fallback && !streamed && !fellBack) { + fellBack = true + send(fallback) + return false + } + emit(msg) + return true + } + return false + }) + send(prepared) + return {sessionKey} + } + + // Runs one of the user's own tools (a call the result tagged `host`). + // Always sandboxed: the caller may itself be a sandboxed tool, and must + // not get page access through us. + if (op === "run-tool") { + const cfg0 = await resolveCfg0(frame, settingsWarm) + const tool = findTool(await lib.resolveTools(cfg0), frame.name) + if (!tool) throw new Error(`no tool named "${frame.name}"`) + const result = await lib.runTool(tool, frame.args, {sandbox: true}) + emit({type: "tool-result", result}) + return {sessionKey} + } + + if (op === "predict") { + const {cfg, config} = await resolveForFrame(frame, settingsWarm) + config.continuation = !!frame.continuation + if (config.provider === "builtin") { + emit({type: "predictions", candidates: []}) + return {sessionKey} + } + on((msg) => { + if (msg.type !== "predictions" && msg.type !== "error") return false + emit(msg) + return true + }) + post({ + type: "predict", + id: workerId, + sessionKey, + provider: config.provider, + text: lib.applyPrompts(frame.text, cfg, systemFor(frame.system, config.provider)), + config, + }) + return {sessionKey} + } + + // Local-only analysis ops: op -> [terminal type, optional progress type]. + /** @type {Record} */ + const analytical = { + "score-tokens": ["token-scores", "score-progress"], + "compute-importance": ["importance-scores"], + "compute-attention-weights": ["attention-weights"], + "extract-features": ["features"], + "extract-cut-features": ["cut-features"], + "decode-tokens": ["decoded-tokens"], + "probe-attention": ["probe-attention-result"], + } + if (analytical[op]) { + const {config} = await resolveForFrame(frame, settingsWarm) + const [terminal, progress] = analytical[op] + on((msg) => { + if (msg.type === progress) emit(msg) + if (msg.type !== terminal && msg.type !== "error") return false + emit(msg) + return true + }) + /** @type {any} */ + const payload = {type: op, id: workerId, sessionKey, provider: config.provider, config} + if (frame.text != null) payload.text = frame.text + if (frame.ids != null) payload.ids = frame.ids + post(payload) + return {sessionKey} + } + + if (op === "preload") { + const {config} = await resolveForFrame(frame, settingsWarm) + post({type: "preload", provider: config.provider, config}) + } + return {sessionKey} + }, + + /** + * The worker keys in-flight generations by sessionKey. + * @param {{sessionKey: string}} token + * @param {(m: any) => void} post + */ + abort(token, post) { + post({type: "abort", sessionKey: token.sessionKey}) + }, + } +} diff --git a/llm-host/src/worker-spec.test.js b/llm-host/src/worker-spec.test.js new file mode 100644 index 00000000..9ecf1844 --- /dev/null +++ b/llm-host/src/worker-spec.test.js @@ -0,0 +1,272 @@ +import {describe, it, expect, vi, beforeEach} from "vitest" +import {makeLLMWorkerSpec} from "./worker-spec.js" + +/** + * Just enough of @chee/patchwork-llm for the spec's host policy to be + * observable. `prepareGenerate` echoes what it was given. + */ +function fakeLib(cfg = {provider: "local", toolToggles: {}}) { + return { + createWorker: () => ({}), + ensureConfig: vi.fn(async () => cfg), + ensureSettingsDoc: vi.fn(async () => ({})), + settingsDocHandle: () => ({}), + callConfig: (c, o) => ({provider: c.provider, ...o}), + applyPrompts: (input) => input, + effectiveSystem: () => "", + describeConfig: () => "Model", + fetchOpenRouterModels: async () => [], + builtinGenerate: vi.fn(async () => "built-in text"), + resolveCfgPrompts: async (c) => c, + prepareGenerate: vi.fn(async (cfg0, opts) => ({ + cfg: cfg0, + config: {provider: cfg0.provider, model: "m", ...opts.overrides, ...(opts.tools ? {tools: opts.tools} : {})}, + extraSystem: opts.system, + builtin: cfg0.provider === "builtin", + input: opts.messages, + })), + buildGeneratePayload: (config, input, ids) => ({type: "generate", ...ids, provider: config.provider, config, messages: input}), + parseToolCalls: vi.fn((text) => { + const m = /(.*?)<\/tool_call>/.exec(text) + return m ? [JSON.parse(m[1])] : [] + }), + buildToolsSystem: (tools) => "TOOLS: " + tools.map((t) => t.name).join(","), + sanitizeToolName: (name) => name.replace(/[^a-zA-Z0-9_-]/g, "_"), + NATIVE_TOOL_PROVIDERS: new Set(["openrouter", "ollama", "webllm"]), + resolveTools: vi.fn(async () => []), + runTool: vi.fn(async () => "ran"), + } +} + +function fakeIO() { + /** @type {any} */ + const io = { + post: vi.fn(), + emit: vi.fn(), + handler: null, + on: (fn) => { + io.handler = fn + }, + workerId: "w1", + state: null, + } + return io +} + +const emitted = (io, type) => io.emit.mock.calls.map((c) => c[0]).filter((f) => f.type === type) +const flush = () => new Promise((r) => setTimeout(r, 0)) + +describe("makeLLMWorkerSpec · generate", () => { + let lib, spec, io + beforeEach(() => { + lib = fakeLib() + spec = makeLLMWorkerSpec(lib) + io = fakeIO() + }) + + it("parses text host-side when the request offered tools", async () => { + await spec.handle({op: "generate", messages: [{role: "user", content: "hi"}], tools: [{name: "x"}]}, io) + io.handler({type: "result", text: '{"name":"x","args":{}}', toolCalls: null, toolMode: "template"}) + const [result] = emitted(io, "result") + expect(result.toolCalls).toEqual([{name: "x", args: {}}]) + expect(result.toolMode).toBe("template") + }) + + it("drops parsed calls to tools that weren't offered", async () => { + await spec.handle({op: "generate", messages: [], tools: [{name: "x"}]}, io) + io.handler({type: "result", text: '{"name":"my-package","version":"1.0.0"}', toolCalls: null}) + const [result] = emitted(io, "result") + expect(result.toolCalls).toBeNull() + expect(result.toolMode).toBe("text") + }) + + it("marks structured calls native and maps sanitized names back to the declared ones", async () => { + await spec.handle({op: "generate", messages: [], tools: [{name: "read doc"}]}, io) + io.handler({type: "result", text: "", toolCalls: [{id: "c1", name: "read_doc", args: {a: 1}}, {id: "c2", name: "nope", args: {}}]}) + const [result] = emitted(io, "result") + expect(result.toolMode).toBe("native") + // An unknown structured call is still a real call; the caller reports the error to the model. + expect(result.toolCalls).toEqual([ + {id: "c1", name: "read doc", args: {a: 1}}, + {id: "c2", name: "nope", args: {}}, + ]) + expect(lib.parseToolCalls).not.toHaveBeenCalled() + }) + + it("sanitizes tool names in tool_calls the consumer threads back", async () => { + const messages = [{role: "assistant", content: null, tool_calls: [{id: "c1", type: "function", function: {name: "read doc", arguments: "{}"}}]}] + await spec.handle({op: "generate", messages, tools: [{name: "read doc"}]}, io) + const [, opts] = lib.prepareGenerate.mock.calls[0] + expect(opts.messages[0].tool_calls[0].function.name).toBe("read_doc") + expect(messages[0].tool_calls[0].function.name).toBe("read doc") + }) + + it("does not parse prose when no tools were offered", async () => { + await spec.handle({op: "generate", messages: []}, io) + io.handler({type: "result", text: '{"name":"x"}', toolCalls: null}) + const [result] = emitted(io, "result") + expect(result.toolCalls).toBeNull() + expect(lib.parseToolCalls).not.toHaveBeenCalled() + }) + + it("forwards only sampling overrides, never provider / apiKey / model / url", async () => { + await spec.handle( + { + op: "generate", + messages: [], + temperature: 0.2, + topP: 0.9, + provider: "openrouter", + apiKey: "sk-evil", + model: "gpt", + url: "https://attacker", + config: {apiKey: "sk-evil"}, + }, + io + ) + const [, opts] = lib.prepareGenerate.mock.calls[0] + expect(opts.overrides).toEqual({temperature: 0.2, topP: 0.9, topk: undefined, maxNewTokens: undefined}) + }) + + it("applies the provider-conditional system map and toolToggles, and sends only descriptors", async () => { + lib = fakeLib({provider: "local", toolToggles: {off: false, optin: true}}) + spec = makeLLMWorkerSpec(lib) + await spec.handle( + { + op: "generate", + messages: [], + system: {default: "d", local: "L"}, + tools: [ + {name: "on", description: "d", parameters: {}, extra: 1}, + {name: "off"}, + {name: "optin", defaultOff: true}, + {name: "hidden", defaultOff: true}, + ], + }, + io + ) + const [, opts] = lib.prepareGenerate.mock.calls[0] + expect(opts.system).toBe("L") + expect(opts.tools.map((t) => t.name)).toEqual(["on", "optin"]) + expect(opts.tools[0]).toEqual({name: "on", description: "d", parameters: {}}) + }) + + it("posts the library-built payload tagged with the transport's workerId", async () => { + const token = await spec.handle({op: "generate", messages: [{role: "user", content: "hi"}], sessionKey: "s1"}, io) + expect(token).toEqual({sessionKey: "s1"}) + expect(io.post).toHaveBeenCalledWith( + expect.objectContaining({type: "generate", id: "w1", sessionKey: "s1", messages: [{role: "user", content: "hi"}]}) + ) + }) + + it("runs builtin in-realm and still fills toolCalls", async () => { + lib = fakeLib({provider: "builtin", toolToggles: {}}) + lib.builtinGenerate = vi.fn(async () => '{"name":"x","args":{}}') + spec = makeLLMWorkerSpec(lib) + await spec.handle({op: "generate", messages: [], tools: [{name: "x"}]}, io) + await flush() + expect(io.post).not.toHaveBeenCalled() + const [result] = emitted(io, "result") + expect(result.toolCalls).toEqual([{name: "x", args: {}}]) + expect(result.toolMode).toBe("text") + }) +}) + +describe("makeLLMWorkerSpec · the user's own tools", () => { + let lib, spec, io + beforeEach(() => { + lib = fakeLib() + lib.resolveTools = vi.fn(async () => [ + {url: "u1", name: "Weather now", description: "w", parameters: {type: "object"}, handlerUrl: "h1"}, + {url: "u2", name: "x", description: "clash", handlerUrl: "h2"}, + ]) + spec = makeLLMWorkerSpec(lib) + io = fakeIO() + }) + + it("are offered only when the request asks for them", async () => { + await spec.handle({op: "generate", messages: [], tools: [{name: "x"}]}, io) + expect(lib.prepareGenerate.mock.calls[0][1].tools.map((t) => t.name)).toEqual(["x"]) + }) + + it("are appended after the consumer's tools, which win a name clash", async () => { + await spec.handle({op: "generate", messages: [], tools: [{name: "x"}], userTools: true}, io) + const [, opts] = lib.prepareGenerate.mock.calls[0] + expect(opts.tools).toEqual([ + {name: "x", description: undefined, parameters: undefined}, + {name: "Weather now", description: "w", parameters: {type: "object"}, host: true}, + ]) + }) + + it("come back tagged `host`", async () => { + await spec.handle({op: "generate", messages: [], tools: [{name: "x"}], userTools: true}, io) + io.handler({type: "result", text: "", toolCalls: [{id: "c1", name: "Weather_now", args: {}}, {id: "c2", name: "x", args: {}}]}) + const [result] = emitted(io, "result") + expect(result.toolCalls).toEqual([ + {id: "c1", name: "Weather now", args: {}, host: true}, + {id: "c2", name: "x", args: {}}, + ]) + }) + + it("run on the host, always sandboxed", async () => { + await spec.handle({op: "run-tool", name: "Weather_now", args: {city: "x"}}, io) + expect(lib.runTool).toHaveBeenCalledWith(expect.objectContaining({handlerUrl: "h1"}), {city: "x"}, {sandbox: true}) + expect(emitted(io, "tool-result")).toEqual([{type: "tool-result", result: "ran"}]) + }) + + it("reject a run-tool for a tool that doesn't exist", async () => { + await expect(spec.handle({op: "run-tool", name: "nope", args: {}}, io)).rejects.toThrow('no tool named "nope"') + expect(lib.runTool).not.toHaveBeenCalled() + }) +}) + +describe("makeLLMWorkerSpec · native tool fallback", () => { + let lib, spec, io + beforeEach(() => { + lib = fakeLib({provider: "openrouter", toolToggles: {}}) + spec = makeLLMWorkerSpec(lib) + io = fakeIO() + }) + + const generate = (io) => spec.handle({op: "generate", messages: [], tools: [{name: "x"}], system: "s"}, io) + + it("retries once with the tools in the system prompt when native tools are rejected", async () => { + await generate(io) + expect(io.post.mock.calls[0][0].config.tools).toBeDefined() + expect(io.handler({type: "error", message: "No endpoints found that support tool use"})).toBe(false) + expect(io.post).toHaveBeenCalledTimes(2) + const retry = io.post.mock.calls[1][0] + expect(retry.config.tools).toBeUndefined() + expect(lib.prepareGenerate.mock.calls.at(-1)[1].system).toBe("TOOLS: x\n\ns") + expect(emitted(io, "error")).toEqual([]) + + io.handler({type: "result", text: '{"name":"x","args":{}}', toolCalls: null}) + const [result] = emitted(io, "result") + expect(result.toolCalls).toEqual([{name: "x", args: {}}]) + expect(result.toolMode).toBe("text") + + // Remembered for the model: the next request goes straight to the prompt. + const io2 = fakeIO() + await generate(io2) + expect(io2.post.mock.calls[0][0].config.tools).toBeUndefined() + }) + + it("doesn't retry once tokens have streamed", async () => { + await generate(io) + io.handler({type: "token", delta: "a", text: "a"}) + expect(io.handler({type: "error", message: "boom"})).toBe(true) + expect(io.post).toHaveBeenCalledTimes(1) + expect(emitted(io, "error")).toEqual([{type: "error", message: "boom"}]) + }) + + it("doesn't remember the model when the retry fails too", async () => { + await generate(io) + io.handler({type: "error", message: "401"}) + expect(io.handler({type: "error", message: "401"})).toBe(true) + expect(emitted(io, "error")).toEqual([{type: "error", message: "401"}]) + + const io2 = fakeIO() + await generate(io2) + expect(io2.post.mock.calls[0][0].config.tools).toBeDefined() + }) +}) diff --git a/llm-host/tsconfig.json b/llm-host/tsconfig.json new file mode 100644 index 00000000..4696f12f --- /dev/null +++ b/llm-host/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNEXT", + "module": "ESNEXT", + "moduleResolution": "bundler", + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/llm-host/vite.config.ts b/llm-host/vite.config.ts new file mode 100644 index 00000000..886e68f2 --- /dev/null +++ b/llm-host/vite.config.ts @@ -0,0 +1,26 @@ +import {defineConfig} from "vite" +import solidPlugin from "vite-plugin-solid" +import patchworkBundles from "@chee/patchwork-bundles/vite" +import external from "@inkandswitch/patchwork-bootloader/externals" + +export default defineConfig({ + base: "./", + // Rewrite the automerge:-versioned @chee/patchwork-llm dep to a shared + // service-worker URL resolved at runtime (cross-origin), so the LLM lib loads + // as one canonical copy. This tool runs HOST-only, so loading patchwork-llm + // here is fine. (The tray injects its own