From Prompt Demo to AI Debugger

Explain this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Apply this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Our first DevTools panel can run a prompt and display its lifecycle.

That makes it a useful experiment console.

It does not yet make it a debugger.

A debugger must help explain a failure that occurred in the application under inspection. It needs to connect model activity to application context, preserve events long enough to compare them, and distinguish facts it observed from inferences it derived. It must do this without quietly capturing every prompt, response and page the user visits.

Those requirements pull in opposite directions.

The more data we capture, the easier diagnosis becomes.

The more data we capture, the greater the privacy and security risk becomes.

The extension therefore needs an architecture before it needs another button.

This chapter designs that architecture and implements its smallest end-to-end path: an application emits a sanitized browser-AI event, the inspected page bridge validates it, a content script transports it across the extension boundary, and the DevTools panel displays it as an external trace.

The central rule is:

Observability is a contract between the application and the debugger, not permission to collect everything.


1. Define what the debugger can know

Chrome DevTools extensions have useful APIs. They can create panels, identify the inspected tab, evaluate code in the inspected window, retrieve resources, and observe the requests displayed in the Network panel.

None of those facts implies that Chrome publishes a universal event whenever any page calls LanguageModel.prompt().

There are three observation modes we can legitimately support.

Mode Source of events Strength Limit
Extension-owned Calls made by the observatory Complete lifecycle for our own calls Does not describe the application
Application-instrumented App emits events through our small SDK Semantically rich and explicit Requires developer integration
Page-probed Developer explicitly runs a probe in inspected page context Useful for diagnosis Powerful, fragile and unsuitable for silent continuous capture

We will build the second mode as the default route for application debugging.

An application under development can import a tiny instrumentation helper and wrap the Prompt API operations it already owns. This produces reliable events at meaningful boundaries without monkey-patching a platform global or guessing from DOM changes.

Later we may offer an explicit development-only injection mode. It must remain visibly separate because code evaluated in the inspected page has access to page state and inherits a larger risk surface.


2. Separate four planes

The observatory needs four distinct planes.

    flowchart TD
    A[Application SDK] --> B[Page bridge]
    B --> C[Extension transport]
    C --> D[Trace store]
    D --> P[DevTools panel]
  

Instrumentation plane

Application code decides which semantic events exist. It knows that a particular session belongs to “summarize selected review” or that a response fed a visible feature.

Transport plane

The page and extension live in different JavaScript worlds. A narrow message bridge moves validated event envelopes across that boundary.

Storage plane

Serializable events are retained according to explicit scope and retention settings. Runtime objects, DOM nodes and raw exception instances do not enter the store.

Presentation plane

The DevTools panel projects events into timelines, metrics, errors and comparisons. It must not rewrite the underlying event to fit the current visualization.

This separation lets us change the UI without changing the trace, and change transport without requiring applications to understand extension internals.


3. Design a versioned event envelope

The Chapter 3 trace stored { id, type, at, detail }. Cross-context events need a stronger contract.

const event = {
  schema: "browser-ai-observatory.event/1",
  id: "019...",
  type: "prompt.finished",
  at: "2026-09-02T11:42:18.219Z",
  source: {
    kind: "application",
    origin: "https://example.test",
    sdkVersion: "0.1.0"
  },
  correlation: {
    pageId: "...",
    sessionId: "...",
    traceId: "..."
  },
  capture: {
    mode: "metrics-only",
    redacted: true
  },
  data: {
    outcome: "completed",
    timeToFirstChunkMs: 214,
    totalMs: 961,
    inputChars: 483,
    outputChars: 892
  }
};

Each field answers a different question.

  • schema tells consumers how to parse the event.
  • id distinguishes this record from retries or duplicates.
  • type describes the fact.
  • at provides wall-clock ordering across contexts.
  • source identifies who claimed the fact.
  • correlation joins page, session and prompt events.
  • capture states the privacy mode instead of leaving it implicit.
  • data contains event-specific values.

The source is part of epistemology. An application-emitted timing is not the same as a browser-internal measurement. A value derived later by the panel is not a directly observed fact. We will use source.kind values such as application, observatory, browser-surface and derived rather than mixing them in one unlabelled timeline.


4. Use two clocks for two jobs

Within one JavaScript context, performance.now() is useful for durations because it is monotonic. Across contexts, separate performance time origins make raw values difficult to compare.

For transported events we store:

{
  at: new Date().toISOString(),
  monotonicMs: performance.now()
}

The wall-clock timestamp supports approximate ordering across the page, content script and extension panel. The monotonic value supports accurate duration calculations performed by the same producer.

We do not subtract a page’s performance.now() from the panel’s performance.now().

If we later need tighter cross-context alignment, we can implement a clock synchronization exchange and preserve its uncertainty. Version one does not need to pretend distributed clocks are exact.


5. Make capture modes explicit

The observatory will support three capture modes:

Mode Stored Default use
metrics-only Timing, sizes, states, outcomes, option names Normal development
metadata Metrics plus developer-provided task labels and safe tags Feature diagnosis
content Prompt and response fields selected by the developer Short, explicit debugging session

metrics-only is the default.

Content mode must require an action in the DevTools panel and show a persistent visible indicator while active. It should expire when DevTools closes, the inspected tab changes, or a short retention window elapses. Export must be a separate action.

Redaction happens before transport whenever possible:

function summarizeText(text) {
  return {
    chars: [...text].length,
    bytes: new TextEncoder().encode(text).byteLength,
  };
}

function captureText(text, mode) {
  if (mode === "content") {
    return { ...summarizeText(text), text, redacted: false };
  }

  return { ...summarizeText(text), redacted: true };
}

The transport should not receive a sensitive string only to discard it later. The safest prompt is the prompt the extension never sees.


6. Build the application SDK

The SDK is a small event producer. It does not need access to Chrome extension APIs.

const CHANNEL = "browser-ai-observatory";
const SCHEMA = "browser-ai-observatory.event/1";

export function createObservatory({ captureMode = "metrics-only", tags = {} } = {}) {
  const pageId = crypto.randomUUID();

  function emit(type, { sessionId = null, traceId = null, data = {} } = {}) {
    const envelope = {
      schema: SCHEMA,
      id: crypto.randomUUID(),
      type,
      at: new Date().toISOString(),
      monotonicMs: performance.now(),
      source: {
        kind: "application",
        origin: location.origin,
        sdkVersion: "0.2.0",
      },
      correlation: { pageId, sessionId, traceId },
      capture: { mode: captureMode },
      data: { ...data, tags },
    };

    window.postMessage({ channel: CHANNEL, envelope }, location.origin);
    return envelope;
  }

  return { emit, pageId };
}

The message target is location.origin, not "*". That does not authenticate the extension, but it avoids broadcasting the event outside the current origin.

The application wraps operations at the same boundaries introduced in Chapter 2:

const observatory = createObservatory({
  tags: { feature: "explain-selection" },
});

const sessionId = crypto.randomUUID();
const createStarted = performance.now();

observatory.emit("session.create.started", { sessionId });

const session = await LanguageModel.create({
  ...sessionOptions,
  monitor(monitor) {
    monitor.addEventListener("downloadprogress", (event) => {
      observatory.emit("model.download.progress", {
        sessionId,
        data: { loaded: event.loaded },
      });
    });
  },
});

observatory.emit("session.create.finished", {
  sessionId,
  data: { durationMs: performance.now() - createStarted },
});

We can later package this as a wrapper around LanguageModel. Starting with explicit calls helps us validate the event vocabulary before hiding it behind convenience functions.


7. Run the instrumentation contract in this chapter

Select Run with Browser AI from Chapter 04 to open:

/tools/ai/browser-ai-from-first-principles/04-chapter/

This experience makes the transport contract executable without pretending that the website and extension are the same context.

The page constructs a metrics-only browser-ai-observatory.event/1 envelope and sends it through the same-origin channel used by the SDK:

application envelope
        ↓ window.postMessage
local contract validator
        ↓ when the extension is enabled
injected extension bridge
        ↓ chrome.runtime.sendMessage
tab-scoped service worker
attached DevTools panel

Click Emit valid application event. The page posts a feature.validation.finished event containing source identity, correlation identifiers, capture mode and sanitized data. The local mirror applies the bridge checks and reports the serialized byte size.

Local acceptance proves two things: the page created an envelope, and that envelope satisfies the contract implemented by the mirror. It does not prove extension receipt. The experience therefore leaves the final field at Check attached DevTools rather than changing it to a reassuring but unsupported “Delivered.”

To test the entire path:

  1. load the unpacked Browser AI Observatory extension;
  2. open DevTools for the Chapter 04 experience;
  3. select the Observatory panel;
  4. click Enable for this site;
  5. return to the page and emit the valid event;
  6. confirm the application-sourced event in the DevTools trace.

The page cannot grant the extension host access or confirm what appears in its panel. Those operations remain on the extension side of the boundary.

Now click Try malformed envelope. The page deliberately uses the wrong event-schema version. The local validator rejects it. An enabled extension bridge silently rejects the same message rather than forwarding untrusted diagnostics about it into the application.

The replay button demonstrates another negative fact. Our reviewed September 2 trace contains extension-owned capability and session events but no application-sourced event. Chapter 04 labels that absence instead of rewriting the earlier run into evidence for a bridge we had not yet captured.

This is the first chapter experience that can participate in the actual extension architecture. It remains useful without the extension because it explains and tests the envelope contract locally. With the extension installed and explicitly enabled, it becomes an instrumented test page.


8. Cross the page boundary narrowly

Extension content scripts normally run in an isolated world. They can interact with the DOM but do not share JavaScript variables with the page. A window.postMessage bridge is a common way to transport deliberately published page events.

The extension does not inject the bridge into every page by default. The current manifest requests scripting and session-storage capability while keeping host access optional:

{
  "manifest_version": 3,
  "name": "Browser AI Observatory",
  "version": "0.3.2",
  "description": "Inspect, measure, and debug browser-managed AI from Chrome DevTools.",
  "devtools_page": "devtools.html",
  "background": {
    "service_worker": "service-worker.js",
    "type": "module"
  },
  "permissions": ["scripting", "storage"],
  "optional_host_permissions": ["http://*/*", "https://*/*"]
}

When the user chooses Enable for this site, the panel requests authority for the inspected origin and the service worker injects bridge.js into that tab. This converts a broad installation-time claim into an explicit, origin-scoped debugging action. The extension can still inspect its own Prompt API session without host access.

The injected bridge accepts only our versioned envelope:

const CHANNEL = "browser-ai-observatory";
const SCHEMA = "browser-ai-observatory.event/1";
const ALLOWED_TYPES = new Set([
  "capability.inspect.finished",
  "model.download.progress",
  "session.create.started",
  "session.create.finished",
  "session.create.failed",
  "prompt.started",
  "prompt.chunk",
  "prompt.finished",
]);

window.addEventListener("message", (event) => {
  if (event.source !== window) return;
  if (event.origin !== location.origin) return;

  const message = event.data;
  if (!message || message.channel !== CHANNEL) return;

  const envelope = message.envelope;
  if (!isValidEnvelope(envelope)) return;

  chrome.runtime.sendMessage({
    kind: "browser-ai-observatory.event",
    envelope,
  });
});

function isValidEnvelope(value) {
  if (!value || value.schema !== SCHEMA) return false;
  if (typeof value.id !== "string" || value.id.length > 128) return false;
  if (!ALLOWED_TYPES.has(value.type)) return false;
  if (typeof value.at !== "string") return false;
  if (!value.source || value.source.origin !== location.origin) return false;
  if (!value.correlation || typeof value.correlation.pageId !== "string") return false;
  if (!value.data || typeof value.data !== "object") return false;

  const serializedSize = new TextEncoder().encode(JSON.stringify(value)).byteLength;
  return serializedSize <= 256_000;
}

This is validation, not proof that the page is honest. Any script running in the page can imitate the message format. The event source remains application; the observatory must never label it as browser-attested.

That distinction becomes essential once traces can influence automated diagnosis.


9. Route events to the inspected panel

The content script cannot assume the DevTools panel is open. The panel also cannot listen directly to page globals. A service worker can act as the extension transport hub.

Add it to the manifest:

"background": {
  "service_worker": "service-worker.js",
  "type": "module"
}

The worker maintains ports for open DevTools panels and forwards events by tab:

const panelsByTab = new Map();

chrome.runtime.onConnect.addListener((port) => {
  if (port.name !== "observatory-panel") return;

  let tabId = null;

  port.onMessage.addListener((message) => {
    if (message.kind !== "panel.attach") return;
    tabId = message.tabId;
    const ports = panelsByTab.get(tabId) ?? new Set();
    ports.add(port);
    panelsByTab.set(tabId, ports);
  });

  port.onDisconnect.addListener(() => {
    if (tabId === null) return;
    const ports = panelsByTab.get(tabId);
    ports?.delete(port);
    if (ports?.size === 0) panelsByTab.delete(tabId);
  });
});

chrome.runtime.onMessage.addListener((message, sender) => {
  if (message.kind !== "browser-ai-observatory.event") return;
  if (sender.tab?.id === undefined) return;

  for (const port of panelsByTab.get(sender.tab.id) ?? []) {
    port.postMessage(message);
  }
});

The panel attaches using the tab ID supplied by the DevTools API:

const port = chrome.runtime.connect({ name: "observatory-panel" });

port.postMessage({
  kind: "panel.attach",
  tabId: chrome.devtools.inspectedWindow.tabId,
});

port.onMessage.addListener((message) => {
  if (message.kind !== "browser-ai-observatory.event") return;
  appendExternalEvent(message.envelope);
});

The service worker is a router, not the permanent owner of in-memory state. Manifest V3 workers can stop when idle. Anything that must survive belongs in extension storage, IndexedDB, or an explicit export.


10. Store less, and store it deliberately

Persistence is useful for comparison. It is also where a temporary debugging observation becomes a durable data asset.

Version 0.2.0 can use chrome.storage.session for trace events while the browser session remains active. Later, an explicit “Keep this trace” action can copy a selected run to chrome.storage.local or export JSON.

async function appendSessionEvent(tabId, envelope) {
  const key = `trace:${tabId}`;
  const current = await chrome.storage.session.get(key);
  const events = current[key] ?? [];
  events.push(envelope);

  const bounded = events.slice(-2000);
  await chrome.storage.session.set({ [key]: bounded });
}

The bound prevents an accidental endless stream from consuming unbounded storage. A production version should also batch writes rather than persisting every chunk independently.

Retention policy becomes part of the trace metadata:

{
  "retention": {
    "scope": "browser-session",
    "maxEvents": 2000,
    "contentExpiresAt": null
  }
}

If content capture is enabled, contentExpiresAt must have a value.


11. Correlate model behavior with application behavior

Timing alone rarely explains whether an AI feature worked.

Suppose a local model responds in 420 milliseconds. That is operationally healthy. If the response fails validation and the application discards it, the product outcome failed.

The application should be able to add outcome events using the same trace ID:

observatory.emit("feature.validation.finished", {
  traceId,
  sessionId,
  data: {
    validator: "summary-shape/1",
    accepted: false,
    reasonCodes: ["missing_required_section"],
  },
});

Now one run contains two different truths:

operational outcome: completed
behavioral outcome: rejected

This is where the tool begins to connect with the larger work on reliable AI systems. A successful model call is a transport fact. Acceptance is an application decision.

The observatory should show them beside one another, never collapse them into one green check.


12. Build a useful trace view

The next panel projection should group events by prompt trace rather than render an undifferentiated event list.

Trace Feature Availability Session First output Total Runtime Validation
7a1… explain-selection available warm 214 ms 961 ms completed accepted
9f3… summarize-page available reused 188 ms 744 ms completed rejected
c82… explain-error downloading cold 18.4 s aborted not run

Every cell should be traceable to an event or visibly labelled as derived.

For example:

function derivePromptSummary(events) {
  const started = events.find((event) => event.type === "prompt.started");
  const finished = events.find((event) => event.type === "prompt.finished");
  const validation = events.find(
    (event) => event.type === "feature.validation.finished"
  );

  return {
    source: "derived",
    traceId: started?.correlation.traceId,
    runtimeOutcome: finished?.data.outcome ?? "running",
    totalMs: finished?.data.totalMs ?? null,
    accepted: validation?.data.accepted ?? null,
  };
}

The projection can be recomputed. The source events remain unchanged.


13. Define the product boundary now

Browser AI Observatory is becoming two related tools:

Monitor

The monitor answers what happened across capability, acquisition, session, prompt and application validation events.

Debugger

The debugger helps explain why a run failed by filtering related events, comparing successful and unsuccessful traces, exposing input/output capture when explicitly enabled, and linking operational state to behavioral validation.

Later, a third role will appear.

Agent inspector

When we add WebMCP, the tool will display discovered tools, schemas, selected calls, arguments, permission decisions, results and validation. It will keep untrusted tool descriptions and outputs visibly separate from trusted control instructions.

This is one continuous architecture:

browser AI lifecycle
prompt trace
application outcome
tool discovery and action
agent run

Starting with event identity and source labels means we will not need to rebuild the substrate when the system becomes agentic.


14. Definition of done for the opening group

The first build is complete when we can demonstrate all of the following:

  • the DevTools panel loads without host access;
  • the extension can inspect and exercise its own Prompt API capability;
  • model download and session creation appear as separate phases;
  • streaming requests can be stopped;
  • metrics-only capture is the default;
  • an instrumented test page can emit a versioned event;
  • the content script rejects malformed or oversized envelopes;
  • the service worker routes events only to a panel attached to the matching tab;
  • operational completion and application acceptance remain separate fields;
  • closing the panel does not imply that an in-memory service worker will preserve state;
  • the interface states exactly when content capture is active;
  • no claim is made that application events are browser-attested.

These are testable properties. They give us a foundation stronger than a mockup and narrower than an imaginary universal debugger.


Conclusion

A real AI debugger begins by being honest about evidence.

The extension directly observes calls it owns. It receives application events through an explicit SDK. It transports them across a narrow, validated bridge. It labels the source, correlation identity and capture mode. It persists only according to a declared retention rule. It separates operational completion from behavioral acceptance.

That architecture gives us the monitor/debugger application that will grow with the book.

The chapter now participates in that architecture. Without the extension it validates the public envelope contract. With the extension explicitly enabled, it becomes a real instrumented application whose event can be confirmed in DevTools. At no point does the page promote local validation into browser-attested delivery.

The opening group has now moved through four layers:

browser as model host
capability lifecycle
working DevTools experiment console
instrumented AI monitor and debugger

The next group can begin testing the model itself. We will design repeatable prompt fixtures, measure cold and warm performance, inspect session context, compare task APIs with general prompting, and make behavioral changes visible when the browser-managed implementation evolves.


Sources and further reading

  1. Chrome for Developers, Extend DevTools.
  2. Chrome for Developers, chrome.devtools.inspectedWindow.
  3. Chrome for Developers, chrome.devtools.network.
  4. Chrome for Developers, Message passing.
  5. Chrome for Developers, Extension service workers.
  6. Chrome for Developers, Declare permissions.
  7. Chrome for Developers, WebMCP tool security.