mv37.org

Experiment · 2026.05

Edgent: a headless browser agent SDK

Edgent is a small TypeScript SDK for putting an agent inside a browser app that already has an editor. It gives you the agent loop, tool calls, approvals, and a clean event stream. You keep the UI, model provider, API keys, storage, and permissions.

The package is built for the moment when you do not want to ship an entire IDE, but you also do not want to rebuild all the boring agent plumbing yourself.

Source on GitHub


Why Edgent exists

A lot of useful software already has a code editor in it: notebooks, teaching tools, prompt playgrounds, config editors, data apps, internal dashboards, and small coding surfaces. Adding an agent to those products should not mean adopting somebody else's file tree, chat UI, terminal, hosted service, and model stack.

Edgent takes the opposite shape. It is headless and browser-only. You pass in a model adapter and a list of tools. Edgent turns the model stream into typed events, runs tools when the model asks, pauses for approvals when needed, and lets your app render every step however it wants.

What you get

  • A browser-local agent loop with run(), cancel(), event listeners, and async iteration.
  • A provider-neutral ModelAdapter. Use a backend route, browser BYOK, OpenAI, Anthropic, Gemini, OpenRouter, a gateway, or a fake model in tests.
  • Schema-first JavaScript tools. The registered tool list is the permission boundary.
  • A CodeMirror 6 workspace adapter for snapshots, versioned edits, stale-edit checks, proposals, and diff approval.
  • A Pyodide helper that wraps a host-provided Python runtime as a tool without bundling Pyodide into the SDK.

Install it

If you already have a browser app with CodeMirror, try Edgent by installing the package and the CodeMirror peer dependencies:

npm i @mv37/edgent @codemirror/state @codemirror/view @codemirror/merge

The package is ESM-only and meant for browser builds. It does not include provider SDKs, React components, Pyodide, a server, a filesystem, or a terminal.

Quick start docs

There are three pieces to wire together: a model adapter, a set of tools, and the event stream your app renders.

The core loop

A model adapter is just an async generator. It receives normalized messages and tool specs, then yields normalized model events back to Edgent.

import { createBrowserAgent, defineTool, type ModelAdapter } from "@mv37/edgent";

const model: ModelAdapter = async function* (request) {
  // Call your provider here, or stream through your backend.
  // Then yield Edgent's normalized events.
  yield { type: "message.delta", content: "I can help with that." };
  yield { type: "done" };
};

const readDocument = defineTool({
  name: "read_document",
  description: "Read the current document.",
  parameters: { type: "object", additionalProperties: false },
  execute(_args, context) {
    context.emit({ type: "message.delta", content: "Reading the document..." });
    return getCurrentDocumentText();
  },
});

const agent = createBrowserAgent({
  model,
  system: "You are a careful assistant inside a browser editor.",
  tools: [readDocument],
});

for await (const event of agent.run("What should I change first?")) {
  renderEvent(event);
}

A CodeMirror and Pyodide coding agent

This is the shape of a small coding assistant inside a page. The agent can read the selected code, propose CodeMirror edits, and run Python through a Pyodide runtime that your app already loaded.

import { createBrowserAgent, defineTool, type ModelAdapter } from "@mv37/edgent";
import {
  createCodeMirrorEditTool,
  createCodeMirrorWorkspace,
} from "@mv37/edgent/codemirror";
import { createPyodideTool, type PythonRuntime } from "@mv37/edgent/pyodide";

const workspace = createCodeMirrorWorkspace({
  view,
  documentId: "notebook-cell.py",
});

const pythonRuntime: PythonRuntime = {
  async runPython(code, input, signal) {
    signal?.throwIfAborted();
    pyodide.globals.set("input", input);
    return pyodide.runPythonAsync(code);
  },
};

const model: ModelAdapter = async function* (request) {
  yield* streamModelFromYourApp(request);
};

const agent = createBrowserAgent({
  model,
  system: [
    "You are a coding assistant in a browser notebook.",
    "Prefer small edits. Explain what you are about to change.",
    "Use Python when it helps check an answer.",
  ].join("\n"),
  tools: [
    createCodeMirrorEditTool(workspace),
    createPyodideTool(pythonRuntime),
    defineTool({
      name: "read_selection",
      description: "Read the selected code and document metadata.",
      parameters: { type: "object", additionalProperties: false },
      execute() {
        return workspace.snapshot().selection;
      },
    }),
  ],
});

The edit tool proposes changes when it would replace existing content. Your UI can show the diff, then approve or reject it. Inserts into empty ranges can apply immediately.

Using the event stream

Edgent emits the same events whether you read them with for await or subscribe with agent.on("event", ...). That makes it easy to build a timeline, status bar, inspector, or approval UI without coupling your interface to the agent internals.

const unsubscribe = agent.on("event", (event) => {
  timeline.append(event);

  if (event.type === "message.delta") {
    assistantText.append(event.content);
  }

  if (event.type === "tool.started") {
    status.set(`Running ${event.toolCall.name}...`);
  }

  if (event.type === "edit.proposed") {
    diffPanel.show(event.proposal);
  }

  if (event.type === "approval.requested") {
    approvals.show(event.approval, {
      approve() {
        agent.resolveApproval(event.approval.id, { approved: true });
      },
      reject(reason) {
        agent.resolveApproval(event.approval.id, { approved: false, reason });
      },
    });
  }

  if (event.type === "run.completed") {
    status.set("Done");
  }
});

for await (const event of agent.run("Clean up this notebook cell.")) {
  console.debug(event.type, event);
}

unsubscribe();

Use cases

Edgent is useful when the app already knows what the agent should be allowed to touch. The SDK does not need broad powers; it only needs the tools you register.

  • Notebook and playground UIs. Let an agent edit a CodeMirror cell, run small checks, and show proposed changes before they land.
  • Learning environments. Expose run_python, read_tests, and a constrained edit tool. Students see what changed and why.
  • Prompt and config editors. Let the agent read a JSON, YAML, or prompt document, propose a focused patch, and hand final approval to the user.
  • Code review surfaces.Add an “ask about this block” feature to snippets, examples, diffs, or docs without embedding a whole IDE.
  • Internal tools with BYOK. Keep credentials and model calls in the host app. Edgent only sees normalized model events and tool results.
  • Debug and eval harnesses. Record the typed event stream from each run, inspect tool calls, and compare how different prompts or tools behave.

Status

Edgent is early, and the surface area is intentionally small: browser ESM, CodeMirror-first, headless, and host-controlled. The next useful pieces are more examples, more workspace adapters where they make sense, and reference model adapters that are easy to copy into real apps.

If this sounds like the missing piece for a browser app you already have, install it with npm i @mv37/edgent and try it on one editor first.

The repository is open at github.com/mv37-org/edgent. For use cases, feedback, or rough edges, email v@mv37.org.


← Back to mv37.org