Run tools in the user's browser tab

Use clientBrowser() for tools that read or change the user's current page. Create sessions with aex.sessions.create(); the SDK opens the command stream and returns results. Account and model-provider keys stay on your backend.

Share the composition

npm install @aexhq/sdk@0.88.0 @aexhq/agentloop-pi@9.0.0 zod@4.4.3
// composition.ts
import { clientBrowser, tool } from "@aexhq/sdk";
import { pi } from "@aexhq/agentloop-pi";
import { z } from "zod";
const readSelection = tool({
  name: "read_selection",
  description: "Read the user's current text selection",
  input: z.object({}),
  run: (_, ctx) => ctx.finish(window.getSelection()?.toString() ?? ""),
});
export const composition = {
  agentloop: pi(),
  tools: [readSelection({ env: clientBrowser({ name: "editor" }) })],
};
export const model = { provider: "openai", name: "gpt-4.1-mini" };

The backend prepares this declaration without running the browser function. Serve the Agentloop package's runtime.component.wasm and loop.program.jsassets with your frontend; bundlers that support new URL(..., import.meta.url) can include both.

Authorize the application user

Your backend checks the user's permission to run this composition before issuing access. The authorization helper below belongs to your application.

import { Aex } from "@aexhq/sdk";
import { composition, model } from "./composition.js";
// Inside your authenticated application's bootstrap route:
await requireAuthorizedApplicationUser();
const aex = new Aex({ apiKey: process.env.AEX_API_KEY! });
try {
  const access = await aex.clients.grant({
    origin: "https://your-app.example",
    session: {
      ...composition,
      model: { ...model, apiKey: process.env.OPENAI_API_KEY! },
    },
  });
  return Response.json(access, { headers: { "cache-control": "no-store" } });
} finally {
  await aex.close();
}

Create the session in the browser

import { Aex } from "@aexhq/sdk";
import { composition, model } from "./composition.js";
const access = await fetch("/api/agent-access", { method: "POST" }).then(r => r.json());
const aex = new Aex({ clientAccess: access });
const session = await aex.sessions.create({ ...composition, model });
await session.send("Summarize my selected text");

The browser connects directly to Aex. The bootstrap route can finish immediately, including when it runs in a serverless function.

Access and reconnecting

Access fixes one origin, composition, host and resulting session. Repeating sessions.create() with that composition and access reattaches to the same session, including after an Aex restart. It cannot list other sessions, change model selection, admit unrelated components or manage the account.

Create within five minutes. Pending provider credentials stay in server memory until creation; expiry or a restart before creation requires fresh backend authorization. An uncertain creation is never silently repeated.

Access expires after one hour by default. expiresAt accepts Unix seconds up to 24 hours; revoke access earlier with aex.clients.revoke(access.id) on your backend. Parent-key revocation and account suspension also apply.

Use your application's HTTPS origin. HTTP localhost is supported for development.

The command connection suspends after five idle seconds and reconnects before the same live client starts more work. History reads leave it asleep. Use connectionIdleTimeoutMs: 0when other callers or future autonomous events must reach the tab; a suspended tab has no remote wake-up channel. Access is checked again on reconnect.

Closing the tab removes its tools and may interrupt their work; the durable session remains. Call aex.close() when the page no longer needs its client. Use an Application environment for backend tools that must outlive the tab.

hostEnv remains available for connected processes. The browserextension controls an automation browser. A frontend that only sends messages needs no browser tool environment.