Skip to main content

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 apiKey is enough.
  • The engine loads its files from your bundle (or a pinned CDN fallback). Serve them yourself with assetBase for production — see Installation.
  • You must serve over HTTPS or localhost (WebAssembly needs a secure context) and allow wasm-unsafe-eval in 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 set autoDownload: true to 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:

CapabilityBrowser (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 exec option enables python and javascript tools 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 reports terminals as 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 assistantBrowser (WebAssembly), user token as apiKey.
A server endpoint or workerNode; install the harness for the full tool set.
A CLI-like local toolNode 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