A Session Is Not a Stateless Function

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.

A remote model API often encourages a simple mental model:

const answer = await complete(prompt);

Input goes in. Output comes back. Any conversation state is reconstructed by the application and sent again.

A browser-managed Prompt API session is a different kind of object. It owns resources, may retain conversational state, reports a finite input budget, supports branching in some implementations, and should be destroyed when its work is finished.

The correct abstraction is not a stateless function.

It is a resource with a lifecycle.


1. Availability is not readiness

The first distinction appears before prompting:

API exposed
    โ†“
configuration accepted
    โ†“
capability available
    โ†“
session created
    โ†“
session ready
    โ†“
prompt completed

Each transition can fail independently. A browser may expose LanguageModel while reporting that a requested configuration is unavailable. It may report availability and then reject session creation because an option is incompatible with the active runtime.

Our first real Observatory trace made this distinction concrete. LanguageModel was exposed, availability returned available, and session creation completed in about 8.2 milliseconds. The resulting session reported an input quota of 9,216 units, zero initial usage, and support for both cloning and destruction.

That observation describes one run on one machine. It does not establish a universal quota or startup time.

It does establish that capability state and session state are separately observable.


2. Creation establishes a contract

A session is configured when it is created:

const session = await LanguageModel.create({
  samplingMode: "most-predictable",
  expectedInputs: [{ type: "text", languages: ["en"] }],
  expectedOutputs: [{ type: "text", languages: ["en"] }]
});

Creation options are not incidental metadata. They define the contract under which later prompts run.

They may include language expectations, sampling behavior, initial instructions, response constraints or multimodal declarations. Two sessions backed by the same browser capability can therefore have different behavior and different context consumption.

This creates an observability problem. Reproducible runs need the options, but options may contain an initial system prompt or private examples.

Browser AI Observatory consequently applies its capture policy to creation options. Metrics-only mode records their keys and serialized size. Content mode records the values explicitly.

That closes a subtle privacy gap: redacting prompt() while exporting initialPrompts from create() would not be meaningful redaction.


3. Context is session state

Where the runtime exposes them, two properties make the state visible:

console.log(session.inputUsage);
console.log(session.inputQuota);

The important quantity is the remaining budget:

$$ R_t = Q_t - U_t $$
where $Q_t$ is the reported quota and $U_t$ is usage at time $t$.

The Observatory records a snapshot before and after each prompt:

{
  "type": "session.snapshot",
  "data": {
    "stage": "after-prompt",
    "session": {
      "inputUsage": 486,
      "inputQuota": 9216,
      "remaining": 8730
    }
  }
}

The example shape shows the measurement. The value 486 is illustrative until a real prompt trace produces it.

We should not assume that usage equals the character count, byte count or a tokenizer estimate calculated elsewhere. The browser’s counter is the operational authority for that session. A separate estimate can still be useful for admission control, but disagreement between estimate and runtime is itself evidence.


4. A conversation changes the resource

Suppose a session receives two prompts:

await session.prompt("My project is called Observatory.");
await session.prompt("What is my project called?");

If the second response uses information from the first exchange, state exists somewhere in the session lifecycle. The application did not pass the first prompt again.

This changes how we test a browser feature. A prompt cannot be interpreted only from its own input. We need:

  • the session identifier;
  • creation configuration;
  • prior prompts and outputs, subject to capture policy;
  • usage before and after;
  • whether the session was original or cloned;
  • browser and runtime configuration.

Without session identity, two calls that look independent in a trace may actually share history.


5. Cloning creates an experimental branch

Cloning is more than a convenience for chat interfaces. It creates a controlled branch from shared prior state.

const branch = await session.clone();

Conceptually:

shared history
     โ”œโ”€โ”€ original โ†’ intervention A
     โ””โ”€โ”€ clone    โ†’ intervention B

This is valuable for evaluation. We can establish the same context once, clone it, and compare two next prompts without replaying a possibly nondeterministic history.

The Observatory assigns the clone a new session identifier while recording its parent:

{
  "type": "session.cloned",
  "data": {
    "parentSessionId": "session-a",
    "durationMs": 3.4
  },
  "correlation": {
    "sessionId": "session-b"
  }
}

We still need to test what the active implementation preserves. A clone operation being exposed does not prove that every internal cache, constraint or hidden state is duplicated in the way we imagine.


6. Destruction is part of correctness

A session should have an explicit end:

session.destroy();

Destruction communicates that the application no longer needs the resource. It also creates a boundary after which further prompts should not be treated as valid continuation.

Relying only on garbage collection obscures this boundary. The object may become unreachable, but the trace cannot tell whether the application intentionally ended the session, leaked it, or lost its reference during navigation.

The debugger therefore records:

  • why destruction occurred;
  • final usage and quota;
  • the destroyed session identifier;
  • whether the session was replaced, the API changed, or the operator ended it.

Lifecycle discipline is especially important in extensions, where a DevTools panel may remain open across multiple experiments.


7. Session tests need invariants

A useful session test does more than check for text output. It can assert:

  1. creation produced a distinct session identifier;
  2. usage did not decrease within one session;
  3. a prompt completion refers to the session that started it;
  4. a clone has a new identity and a recorded parent;
  5. destroying a session makes the Run control unavailable;
  6. metrics-only export contains no prompt or initial-prompt text.

The model’s wording may vary while all lifecycle invariants remain stable. Conversely, a plausible answer can hide a broken lifecycle.

Operational correctness and behavioral quality belong in the same trace, but they are not the same score.


8. Our first compatibility failure belongs to the session layer

After the first successful acquisition trace, the runtime later rejected session creation with this message:

The sampling options are incompatible with speculative decoding (MTP). Prompt API sessions must specify compatible sampling options, i.e. samplingMode:'most-predictable' or topK:1 or temperature:0.

The error proves that the active runtime imposed a sampling constraint and named compatible options. It does not prove every detail of the speculative-decoding implementation.

The Observatory now supplies:

samplingMode: "most-predictable"

The larger lesson is architectural: availability did not validate the complete session configuration. Only session creation crossed that boundary.


9. Run the session-lineage laboratory

Select Run with Browser AI from Chapter 09 to open:

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

This experiment keeps two live session objects after cloning. It does not replace the original reference with the clone. That allows the page to apply different interventions to two descendants of the same measured history.

The sequence is deliberately constrained:

  1. inspect LanguageModel with the exact creation options;
  2. create an original session and record its initial snapshot;
  3. establish one shared fact in that session;
  4. clone only after the shared-history prompt completes;
  5. run intervention A on the original and intervention B on the clone;
  6. compare identity, lineage, usage and narrow output assertions;
  7. destroy both resources explicitly.

The shared prompt asks the session to remember the project codename Observatory Nine. After cloning, the original receives a strict review mode and the clone receives an exploratory review mode. Each branch must report both the inherited codename and its branch-specific intervention.

This is not a benchmark of whether strict produces better prose than exploratory. The fixture tests a structural claim: the two sessions began from common history, acquired distinct identities and could then evolve independently.

Identity and parentage

The clone receives a new Observatory session identifier. Its event records the original as its parent:

{
  "type": "session.cloned",
  "data": {
    "apiId": "prompt",
    "parentSessionId": "original-session-id",
    "session": {
      "inputUsage": 84,
      "inputQuota": 9216
    }
  },
  "correlation": {
    "sessionId": "clone-session-id"
  }
}

The usage values above illustrate the event shape. The live laboratory displays only values reported by the browser.

Usage as a within-session invariant

Each branch takes a snapshot immediately before and after its intervention. If both values are present, the automatic invariant requires:

$$ U_{after} \ge U_{before} $$
A missing browser counter remains **Not reported** and the check remains **Not observed**. The laboratory never substitutes prompt characters, bytes or an external tokenizer for runtime usage.

The comparison is intentionally within each session. A clone may report the same, greater or otherwise implementation-dependent usage at the branching point. We record that fact before deciding what invariants are stable across browser versions.

One action, two independent prompts

The Run A/B branches control starts one prompt on each live session. They may execute concurrently because the instances are independent resources. Both calls receive one abort signal so the operator can stop the experiment, while each trace retains its own session and prompt correlation identifiers.

Operational completion and behavioral checks remain separate. A branch can return successfully while failing to recover the inherited codename. Conversely, a wording variation can preserve state even if a brittle lexical assertion misses it. The result panel exposes those narrow checks rather than turning them into a universal quality score.

Explicit destruction

Destroy both records each resource’s final observable snapshot, calls destroy(), marks the lane as destroyed and disables further branch execution. The UI therefore distinguishes:

reference never created
resource ready
resource running
resource failed
resource explicitly destroyed

Garbage collection cannot provide that product-level distinction.

What the replay proves

The reviewed September 2 trace contains one successful session creation. That session reported zero initial usage, a quota of 9,216, clone support and destroy support. It contains no completed prompt, session.cloned event or session.destroyed event.

The replay panel presents those missing transitions as Not observed. A method being exposed is evidence that an experiment can be attempted; it is not evidence that the operation already succeeded.

The chapter-specific module emits its events into the same validated Observatory export used by the earlier capability and task laboratories. The session identifiers now make the accumulated trace a lineage rather than a flat list of prompts.


Conclusion

A Prompt API session is an owned, finite and evolving resource.

It begins with a configuration contract. It accumulates context. It can expose usage and quota. It may be cloned into an experimental branch. It should end through explicit destruction.

Treating it as complete(prompt) hides every one of those properties. Chapter 09 now makes them executable: one measured resource becomes two addressable descendants, each evolves under a different intervention, and both end through an observable destruction boundary.

The next chapter follows the finite budget to its boundary. What should an application do when the context window approaches full, and which parts of history must survive compaction?


Sources and further reading

  1. Chrome for Developers, The Prompt API.
  2. Chrome for Developers, Get started with built-in AI.
  3. Chrome for Developers, Understand built-in model management in Chrome.