Edgent
Edgent is a browser-only, headless TypeScript SDK for building lightweight agents that can inspect and edit a CodeMirror editor, call schema-first JavaScript tools, and stream every step to your own UI. It is built for apps that want an agent loop in the browser without adopting a full IDE, backend runtime, Git integration, terminal emulator, or React UI kit.
For the announcement essay, see /edgent. Source lives at github.com/mv37-org/edgent.
Overview
Edgent 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 ESM-only, runtime-dep free, and intentionally small. v1 is browser-only, CodeMirror-first, headless, and host-controlled.
What Edgent provides
What is included
- A headless agent loop with
run(),cancel(), async event iteration, and event listeners. - A provider-neutral
ModelAdapterinterface. Use OpenAI, Anthropic, Gemini, OpenRouter, your own backend, or a mock model. - Schema-first JavaScript tools with lightweight argument validation.
- Mutating lifecycle hooks for model calls, tool calls, and runtime errors.
- Model-driven context compaction for long browser-local runs.
- A CodeMirror 6 workspace adapter for snapshots, versioned edits, stale-edit checks, and diff approval.
- A Pyodide helper that wraps a host-provided Python runtime as a tool.
- A small Vite example app that demonstrates model streaming, tool calls, edit approvals, and event rendering.
What is not included
- No bundled model provider clients.
- No hidden API keys or hosted service.
- No Git, terminal, project tree, or filesystem abstraction.
- No React components.
- No bundled Pyodide runtime. The host app brings and initializes Pyodide.
This keeps the package small and lets the host app remain the authority for credentials, storage, permissions, UI, and side effects.
Install
npm install @mv37/edgent @codemirror/state @codemirror/view @codemirror/mergeThe package is ESM-only and intended for browser builds.
Quick start
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 });
const model: ModelAdapter = async function* (request) {
// Call a model provider directly, or send this normalized request to your backend.
// Then yield normalized stream events back to Edgent.
yield { type: "message.delta", content: "I can help edit this buffer." };
yield { type: "done" };
};
const pyodideRuntime: PythonRuntime = {
async runPython(code, input, signal) {
signal?.throwIfAborted();
pyodide.globals.set("input", input);
return pyodide.runPythonAsync(code);
}
};
const agent = createBrowserAgent({
model,
system: "You are a precise browser-local coding assistant.",
tools: [
createCodeMirrorEditTool(workspace),
createPyodideTool(pyodideRuntime),
defineTool({
name: "read_selection",
description: "Read the current CodeMirror selection.",
parameters: { type: "object", additionalProperties: false },
execute() {
return workspace.snapshot().selection;
}
})
]
});
for await (const event of agent.run("Refactor this function.")) {
renderTimeline(event);
if (event.type === "approval.requested") {
showDiff(event.approval.data);
// Resolve this later from your own UI.
agent.resolveApproval(event.approval.id, { approved: true });
}
}Core API
createBrowserAgent
const agent = createBrowserAgent({
model,
system,
tools,
maxTurns,
responseFormat,
settings,
hooks,
contextCompaction
});Run, cancel, approvals
agent.run(input) starts a run and returns an AsyncIterable<AgentEvent>.
for await (const event of agent.run("Update the code.")) {
console.log(event.type, event);
}You can also subscribe to every event:
const unsubscribe = agent.on("event", (event) => {
renderTimeline(event);
});Cancel the active run:
agent.cancel("User cancelled");Resolve approvals from your UI:
agent.resolveApproval(approvalId, { approved: true });
agent.resolveApproval(approvalId, { approved: false, reason: "Not this change." });Model adapter
Edgent does not own API keys or vendor protocols. Your app passes a model adapter:
type ModelAdapter = (request: {
system: string;
messages: AgentMessage[];
tools: ToolSpec[];
responseFormat?: unknown;
settings?: Record<string, unknown>;
signal: AbortSignal;
}) => AsyncIterable<ModelStreamEvent>;Supported normalized stream events:
type ModelStreamEvent =
| { type: "message.delta"; content: string }
| { type: "message.completed"; content?: string }
| { type: "tool.call"; toolCall: { id?: string; name: string; arguments: unknown } }
| { type: "error"; error: unknown }
| { type: "done" };This makes both deployment styles possible:
- Browser BYOK: users paste their own provider key and the host app calls the provider directly.
- Backend proxy: the host app sends the normalized request to a server and streams normalized events back.
For public browser apps, never embed a secret provider key in client code. User-submitted keys are visible to that user’s browser runtime by design.
Tools
Tools are schema-first JavaScript functions.
const readDocument = defineTool({
name: "read_document",
description: "Read the current document text.",
parameters: {
type: "object",
additionalProperties: false
},
execute(_args, context) {
context.emit({ type: "message.delta", content: "Reading editor state..." });
return workspace.snapshot();
}
});Tool arguments are validated against a small JSON Schema subset before execute() runs. Failed validation emits tool.error and returns the error to the model as a tool result.
Tools receive:
runIdturnsignalemit(event)requestApproval(request)
Use tools as the permission boundary. If the host does not register a tool, the agent cannot do that action.
Lifecycle hooks
Hooks let the host inspect or mutate agent lifecycle steps without replacing the whole run loop.
const agent = createBrowserAgent({
model,
hooks: [
{
event: "before_model_call",
name: "add_context",
handler(input) {
if (input.event !== "before_model_call") return;
return {
request: {
messages: [...input.request.messages, { role: "user", content: "Remember the current task." }]
}
};
}
}
]
});Supported hook events:
before_model_callafter_model_callbefore_tool_callafter_tool_callon_error
Hooks may replace model request fields, model content, tool calls, or tool results. A thrown hook error emits hook.error and fails the run.
Context compaction
Context compaction checks the estimated request size before each model turn. When the configured threshold is reached, Edgent asks the host-provided compaction model to summarize the oldest message prefix, keeps the recent tail verbatim, and continues with:
- System prompt
- Compacted summary message
- Recent messages
const agent = createBrowserAgent({
model,
contextCompaction: {
enabled: true,
thresholdPercent: 80,
contextWindowTokens: 128000,
prompt: "Summarize the conversation so far, preserving goals, decisions, tool results, and next steps.",
model: compactionModel,
preserveRecentMessages: 6,
estimateTokens(input) {
return Math.ceil(JSON.stringify(input).length / 4);
}
}
});Edgent does not bundle tokenizers or provider clients. The host supplies the compaction model adapter and can override token estimation.
CodeMirror adapter
Workspace API
import { createCodeMirrorEditTool, createCodeMirrorWorkspace } from "@mv37/edgent/codemirror";
const workspace = createCodeMirrorWorkspace({
view,
documentId: "main.ts"
});
const editTool = createCodeMirrorEditTool(workspace);The workspace provides:
snapshot()previewEdit(edit)proposeEdit(edit)applyEdit(edit)applyProposal(proposalId)rejectProposal(proposalId, reason)getProposal(proposalId)onProposal(handler)
Edit semantics
- Edits target a document version derived from the current text.
- Empty target content applies immediately.
- Non-empty target content creates an edit proposal, emits approval events, and waits for the host to approve or reject.
- Stale edits fail before changing the editor.
Diff rendering
Render a proposed diff with CodeMirror merge primitives:
import { createProposalDiffState } from "@mv37/edgent/codemirror";
const diffState = createProposalDiffState(proposal);
const diffView = new EditorView({ state: diffState, parent });Pyodide
Edgent requires the host to bring and initialize Pyodide. The SDK only wraps that runtime as a tool.
import { createPyodideTool } from "@mv37/edgent/pyodide";
const runPython = createPyodideTool({
async runPython(code, input, signal) {
signal?.throwIfAborted();
pyodide.globals.set("input", input);
return pyodide.runPythonAsync(code);
}
});The helper registers a run_python tool by default. You can override the name, description, and input schema.
Event stream
agent.run(input) returns an async iterable and also publishes through agent.on("event", handler).
Core events include:
run.startedmessage.deltamessage.completedtool.startedtool.resulttool.errorhook.startedhook.completedhook.errorcontext.compaction.startedcontext.compaction.completededit.proposededit.appliedapproval.requestedapproval.resolvedrun.completedrun.cancelledrun.error
These events are designed to be rendered directly by custom chat panes, timeline views, inspectors, or debug consoles.
Package exports
import { createBrowserAgent, defineTool } from "@mv37/edgent";
import { createCodeMirrorWorkspace, createCodeMirrorEditTool } from "@mv37/edgent/codemirror";
import { createPyodideTool } from "@mv37/edgent/pyodide";Example app
git clone https://github.com/mv37-org/edgent.git
cd edgent
npm install
npm run build
npm install --prefix examples/codemirror-basic
npm run dev --prefix examples/codemirror-basicThe example uses a fake streaming model so you can inspect the event stream, approval flow, and CodeMirror edit path without configuring a provider.
Local development
npm install
npm run typecheck
npm run lint
npm run test
npm run format
npm run build
npm pack --dry-runThe package publishes only dist, README.md, CHANGELOG.md, LICENSE, and package.json.
Status
Edgent is early. The v1 surface is intentionally small: browser-only ESM, CodeMirror-first, headless, and host-controlled.
The repository is open at github.com/mv37-org/edgent. Licensed MIT. For use cases, feedback, or rough edges, email v@mv37.org.