agentic-microformats
v0.11.0
Published
Reference implementation of the Agentic Microformats specification
Maintainers
Readme
agentic-microformats
Reference implementation of the Agentic Microformats specification (v0.2.0) — a TypeScript library for extracting data-agent-* annotations from HTML.
Works with any DOM implementation that satisfies a minimal structural interface: the browser DOM, or linkedom server-side.
Install
npm install agentic-microformatsGive your agent web-operation in ~20 lines (operate)
operate() is the episode runtime: it observes the page (graph + content +
WebMCP tools), asks your model what to do, safety-gates and executes the
action, re-observes, and loops — until the agent answers. It is
model-agnostic (you supply decide) and environment-agnostic (you
supply the transport, so auth is your session, and parse is linkedom
server-side or document in a browser).
import { operate } from 'agentic-microformats';
import { parseHTML } from 'linkedom';
const result = await operate({
task: 'Add two of the cheapest product to the cart',
start: 'https://shop.example/',
// YOUR model. Given the page state, return one action.
decide: async (state) => {
const reply = await myLLM(`Task: ${state.task}
Tools: ${JSON.stringify(state.tools)}
History: ${JSON.stringify(state.history)}
Return one JSON action: {"type":"navigate|invoke|answer", ...}`);
return JSON.parse(reply);
},
// YOUR transport — carries the user's session/cookies (this is how auth works).
fetchPage: (url) => fetch(url, { credentials: 'include' }).then(async r => ({ html: await r.text(), url: r.url })),
sendRequest: (req) => fetch(req.url, { method: req.method, headers: req.headers,
credentials: 'include', body: JSON.stringify(req.body) })
.then(async r => ({ status: r.status, body: await r.json().catch(() => null) })),
parse: (html) => parseHTML(html).document.documentElement,
// Safety is fail-closed: high-risk / human-preferred / un-hinted-mutating
// actions call this; cross-origin endpoints are refused outright.
onConfirm: ({ tool, prepared }) => askTheHuman(`Run ${tool}?`, prepared),
origin: 'https://shop.example',
});
console.log(result.answer, result.steps);The library owns the loop and the safety gates (fail-closed confirmation,
same-origin enforcement, re-observation after each mutation). You own the model
and the session. mode: 'browser' drives the real form.requestSubmit()
instead of an HTTP call.
Use it with your existing SDK (OpenAI / Anthropic / MCP)
Already using function-calling? Get the tools in your SDK's format and execute the model's calls through the same fail-closed safety gates:
import { extractAll, toOpenAITools, toAnthropicTools, toMCPTools, executeTool, AgentDOM } from 'agentic-microformats';
const dom = new AgentDOM(root);
const result = dom.extractAll();
const tools = toOpenAITools(result); // or toAnthropicTools / toMCPTools
const reply = await openai.chat.completions.create({ model, messages, tools });
for (const call of reply.choices[0].message.tool_calls ?? []) {
const out = await executeTool(dom, call.function.name, JSON.parse(call.function.arguments), {
origin: location.origin,
sendRequest: myAuthedFetch, // your session
onConfirm: ({ tool, prepared }) => askHuman(tool, prepared),
});
// out.ok / out.refused (cross-origin or unconfirmed) / out.result
}MCP keeps the safety hints as native tool annotations
(readOnlyHint/destructiveHint/idempotentHint); OpenAI/Anthropic fold them
into the description, where the model reads them.
Failures come back typed, so recovery is a rule, not a guess:
const out = await executeTool(dom, name, args, opts);
if (!out.ok && out.error) {
const e = out.error; // { kind, retryable, retryAfter?, requiresFreshState? }
if (e.kind === 'conflict') { /* re-read the page, then retry */ }
else if (e.retryable && toolIsIdempotent) { await sleep(e.retryAfter ?? 1); /* retry */ }
else if (e.kind === 'auth') { /* re-authenticate */ }
else { /* validation / forbidden / not-found → surface to the user */ }
}retryable describes the error (does the server invite another attempt);
whether the action is safe to repeat is the separate idempotentHint. Retry
only when both hold — or, for a conflict, re-read state first.
Lower-level extraction
import { AgentDOM } from 'agentic-microformats';
const agentDom = new AgentDOM(document.documentElement);
const { meta, resources, actions } = agentDom.extractAll();
const prepared = agentDom.prepareAction(actions[0], undefined, { origin: location.origin });
// prepared.confirmationRequired / prepared.blocked — the safety gates (spec §3.2, §12.5)Narrowing the graph before it hits context (0.11.0)
A catalog page's graph is mostly repetition: on the demo shop the same
add_to_cart block is 52% of every product resource, byte-identical across
all six except the SKU. Two tiers cut that down, kept separate because they
carry very different risk.
Lossless — re-encode, drop nothing. Always safe:
import { extractAll, toGraph, compactGraph, expandGraph } from 'agentic-microformats';
const result = extractAll(document.documentElement);
const compact = compactGraph(toGraph(result)); // repeated actions → one template
// expandGraph(compact) round-trips byte-identically to toGraph(result)Lossy — narrow to the task. Only the caller holds the intent, so only the caller can authorize the loss:
import { selectTools, toCompactGraphJSON } from 'agentic-microformats';
const selection = selectTools(result, 'Add 3 units of the USB-C Charger 65W to the cart');
selection.narrowed; // true
selection.reason; // 'intent matched 1/6 resources; kept 1'
const json = toCompactGraphJSON(result, selection); // 66% smaller than the full graphselectTools fails open: an intent it cannot rank confidently returns the
whole graph, and reason always says which branch ran. Notably it refuses to
narrow aggregate intents — "add the cheapest product" ranges over the
entire collection, and pruning it to the items whose text matches "cheapest"
would destroy the answer. Page-level actions are never dropped, so an agent can
always navigate off a page whose narrowed view turned out to be wrong, and a
pruned graph carries a selection block telling the model it is seeing a
filtered view.
Measured on the 13-task agent benchmark: 47% fewer graph bytes overall, and the
selector picked the correct product in 6/6 named-entity tasks. See
spec/graph-serialization.md §5 for the
wire format.
Modules
| Module | Purpose |
|---|---|
| extract | Resources, actions, properties, page meta |
| select | Intent-driven graph narrowing + lossless action-template hoisting |
| coerce | Typehint-driven value coercion (currency, dates, integers, …) |
| trust | Trust regions (data-agent-trust) and data-agent-ignore — untrusted subtrees are skipped |
| params | Parameter extraction, min/max constraints, nested dotted-path bodies |
| hints | Interaction hints: role, risk, reversibility, cost, human-preferred |
| observe | MutationObserver-based annotation change feed (browser only) |
Content observation (0.4.0) — no annotation required
extractContent(root) reads what a page says — title, authors, dates,
publisher, section, keywords, word count, language, excerpt, and a heading
outline — bridged from Schema.org JSON-LD, Microformats2, Open Graph, and
semantic HTML that the page already carries. Every field is grounded:
{ value, source, selector }. Try it on any article:
npx agentic-microformats <url> --contentThis is the "own the layer WebMCP lacks" direction: a portable, server-
renderable, grounded reading an agent uses before and after it acts. See
spec/content-observation.md.
WebMCP binding (0.5.0)
toWebMCPTools(extractAll(root)) compiles the actions into
WebMCP tool descriptors —
JSON Schema inputs, standard MCP tool annotations (readOnlyHint /
destructiveHint / idempotentHint / humanConfirmationHint), and a binding
that defaults to the real HTML control (form.requestSubmit()), not a
shadow endpoint. registerWebMCPTools(result, navigator.modelContext) registers
them live and enforces the fail-closed confirmation gate. Inspect any page:
npx agentic-microformats <url> --webmcpSee spec/webmcp-binding.md. This is the interaction half of the pivot: the
same annotations a zero-JS agent reads statically also drive live WebMCP
invocation in a capable browser — Agentic Microformats does not compete with
WebMCP's runtime, it feeds it.
API stability
The public surface is tiered — Stable (extraction core, canonical graph,
validation, safety primitives), Beta (the consumer runtime: operate, the
SDK adapters, typed errors, content, WebMCP), and Experimental. Depend on
the tier, not the version number; both breaking changes made so far landed in
Beta, never in Stable. Full contract and the path to 1.0 in
STABILITY.md.
Spec coverage
Implements core spec v0.3.x (data-agent-idempotent, endpoint origin
policy, conformance profiles, monotonic trust) plus the 0.4.0 content
observation layer. The proposed vocabulary in spec/advanced.md and the
workflow/async example attributes are not implemented — they are not part
of the released spec.
License
MIT
