Browser vs Node
The SDK runs the same agent in two environments. You write the same code; the package picks the right build automatically through its exports map. What differs is how the agent executes and which capabilities the surrounding platform can give it.
Browser (WebAssembly)
In the browser, the entire agentic loop runs client-side from a small WebAssembly engine bundled with the package. Planning, tool selection, and retries all happen in the page. The only network request is to the Pathvela backend for model responses.
What that means for you:
- No server of your own is required to run an agent in a web app. A valid
apiKeyis enough. - The engine loads its files from your bundle (or a pinned CDN fallback). Serve them yourself with
assetBasefor production — see Installation. - You must serve over HTTPS or
localhost(WebAssembly needs a secure context) and allowwasm-unsafe-evalin your CSP. - Keep the SDK out of server-side rendering — import it on the client only:
const { createLilyAgent } = await import('@poolot/lily-web')
const agent = await createLilyAgent({ apiKey: userToken })
For browser apps, prefer a signed-in user token as the apiKey (or proxy through your own server) rather than embedding a long-lived programmatic key in the page.
The browser engine reaches every capability below through host bridges — small pieces of JS the SDK installs on your behalf (__lilyFs, __lilyStore, __lilyLlm). You don't wire them by hand; they're set up when createLilyAgent builds the browser agent.
Node
On Node, the SDK prefers a native harness when one is installed, and otherwise falls back to the same WebAssembly engine.
- Harness backend — a native subprocess with the full tool set, a real shell, disk-backed sessions, and process isolation. It's large (~90 MB) and not bundled; install it with
npx lily-web-install-harness, or setautoDownload: trueto fetch it on demand. - WebAssembly backend — the same engine the browser uses, as a fallback when no harness is present.
Force a backend with the backend option, or let the SDK decide:
// Prefer the harness, download it if missing
const agent = await createLilyAgent({ apiKey, autoDownload: true })
// Or pin the engine explicitly
const wasmAgent = await createLilyAgent({ apiKey, backend: 'wasm' })
What works in the browser
The browser engine does far more than proxy model calls. Files, memory, research, audit, and reasoning streaming all work in-page — persisted in the browser's own storage. A few things a browser genuinely cannot do stay on the harness. Here is the honest matrix:
| Capability | Browser (WebAssembly) | Node harness |
|---|---|---|
| Agentic loop (planning, tools, retries) | ✅ In-page | ✅ Native |
| Filesystem | ✅ OPFS-backed, persists across reloads | ✅ Real disk |
root confinement | ✅ Named OPFS subfolder | ✅ Disk path |
| Memory / research / audit | ✅ IndexedDB, scoped by project | ✅ Disk under dataDir |
| Reasoning-token stream | ✅ onReasoning / result.reasoning (reasoning-capable routes only) | ✅ |
Sandboxed code exec (python, javascript) | ✅ Browser sandbox via exec | ✅ |
Real OS shell (bash, git, arbitrary processes) | ⚠️ Only via a paired lily bridge | ✅ Native |
| BYOK (bring your own provider key) | ❌ Use a same-origin proxy | ✅ |
Filesystem
The browser agent gets a real, persistent filesystem backed by the Origin Private File System (OPFS), with a File System Access fallback and an in-memory fallback for browsers without either. Files an agent writes survive a page reload. Pass root to confine every path to a named subfolder:
const agent = await createLilyAgent({ apiKey, root: 'my-workspace' })
// The agent's files live under an OPFS "my-workspace" folder; paths cannot escape it.
If a user grants a real disk folder through the browser's folder picker, that granted folder becomes the boundary and root cannot narrow it further without another user gesture.
Memory, research, and audit
The engine's remember / recall, research save/list, and per-tool audit log all work in the browser, stored in IndexedDB. Records are isolated per agent by the project option:
const agent = await createLilyAgent({ apiKey, project: 'acme-support-bot' })
dataDir (a filesystem path) is inherently a no-op in the browser — IndexedDB scoped by project is the browser equivalent, and it too persists across reloads.
Reasoning stream
For reasoning-capable model routes, the browser engine streams reasoning tokens the same way the harness does:
const result = await agent.runDetailed(messages, {
onReasoning: (delta) => renderThinking(delta),
})
console.log(result.reasoning) // the full reasoning trace for this run
Reasoning is kept out of the returned assistant message — it's transient UI, not transcript. Two things to know: the default route is a chat model that emits no reasoning (only reasoning-capable routes produce any), and reasoning surfaces only when the backend forwards it in the response stream.
Shell and code execution
A browser tab cannot spawn an operating-system process — that limit never changes. So the real shell (bash, git, arbitrary binaries) is unavailable in a plain browser. Two paths give the agent execution anyway:
- Sandboxed code — the
execoption enablespythonandjavascripttools that run inside the browser's own sandbox (no OS access). This works in any browser, no pairing needed. - A real shell over a bridge — if the page pairs with the user's own machine through a running
lily bridge, the engine borrows a real shell over loopback. Until that pairing exists, the terminal tool is withheld and the engine reportsterminalsas unsupported.
BYOK
Bring-your-own-key stays server-side. A browser page cannot hold a provider credential safely — direct provider calls are blocked by CORS, and the key would be exposed in page JS. The safe pattern is your own same-origin proxy that holds the key server-side; point the SDK at it with baseUrl (default /api/agent-server). BYOK on the harness backend has no such constraint.
Which backend am I on?
The handle exposes its environment, and the run result reports the session when a harness produced it:
console.log(agent.environment)
const result = await agent.runDetailed(messages)
console.log(result.sessionId) // set by the harness backend
Choosing
| You're building... | Use |
|---|---|
| A web app / in-page assistant | Browser (WebAssembly), user token as apiKey. |
| A server endpoint or worker | Node; install the harness for the full tool set. |
| A CLI-like local tool | Node with the harness — or just use the Pathvela CLI. |
Try it in your browser
The chat below is the browser (WebAssembly) backend described above, running live in this page — the whole agentic loop client-side, calling only the Pathvela backend for model responses. Paste your own Pathvela API token to talk to it.
See also
- Installation — assets, CSP, and harness install.
- createLilyAgent reference — the environment-specific options.
- Try Lily — the full-page version of this demo.