# OPM Flow Playground — agent API

`window.opmflow` (version `1.0`) lets an agent or a script drive the playground: write deck files, check them, run them, and read the results. It is plain JSON in and out, so it works through Playwright's `page.evaluate`. Every call is `async`. A bad call throws an `Error` with a readable message; `page.evaluate` re-throws it.

What the API does also shows on the page: written files appear in the explorer, runs fill the 3D view and the plots. A person can watch the agent work.

```js
const out = await page.evaluate(async () => {
  await opmflow.ready();
  return opmflow.examples.list();
});
```

A run takes from seconds to minutes. `page.evaluate` itself waits as long as the page needs, but an agent's tool call may have a time limit. In that case, start the run with `{ wait: false }` and then wait on `<body data-run-state>` or poll `status()`.

## Names

- **Paths** are workspace paths: `"SPE1/SPE1.DATA"`, `"SPE1/include/PVT.INC"`. They are relative and `/`-separated, with no `.` or `..`.
- **Projects:** each top-level folder is a project. Its **main deck** is the `.DATA` file **Run** runs: the one set with `setMain`, else the `.DATA` file nearest the folder's top. INCLUDE paths are relative to the main deck's folder, as in flow.
- **Runs** are named by their folder, `"SPE1/runs/SPE1_20261106-141441-207"`: the deck, the local date and time, and milliseconds. A folder that already exists gets `-2`, `-3`, …. In the result calls, `null` means the run on screen: the one that just ran, or the saved run last opened.
- **Decks** in results (`status().deck`, `runs.list()[].deck`, `summary().deck`, `run().deck`) are always workspace paths, `"SPE1/SPE1.DATA"`.

## Calls

| call | returns |
|---|---|
| `ready()` | resolves when the engine, the workspace and the deck checks are loaded |
| `status()` | `{ engine, state, text, running, flow, deck, step, total, days, shownRun, storage }`. `engine`: `loading`/`ready`/`failed`. `state`: `idle`/`running`/`finished`/`failed`/`stopped` |
| `files.list()` | `[{ path, project, binary, main }]` |
| `files.read(path)` | the text, with unsaved typing included; `{ binary: true, bytes }` for a binary file |
| `files.write(path, text, { open })` | `{ path, bytes }`. Creates or replaces the file; folders are made as needed |
| `files.remove(path)` | `{ removed }`. Removes a file, or a folder with everything in it, runs included |
| `files.rename(from, to)` | `{ from, to }`. Renames or moves a file or a folder. INCLUDE paths that name it are not changed |
| `files.mkdir(path)` | `{ folder }` |
| `mains()` | `{ project: mainDeckPath }` |
| `setMain(path)` | `{ project, main }` |
| `open(path, { line })` | shows the file in the editor, optionally at a line |
| `check(path)` | the deck checks of the project that `path` (a file or a project folder) is in, as flow reads its main deck: `[{ file, line, column, endColumn, severity, message }]`, 1-based. `severity` is `error` (flow stops) or `warning` |
| `run(deck?, { wait, timeout })` | runs `deck`, or the open project's main deck when omitted. **Throws** when no run can start: a run is already in progress, no such `.DATA` file, or no deck. Once it has started, the outcome is resolved, not thrown. With `wait` (the default): `{ deck, ok, run, steps, seconds, errors }` when the run ends; `run` is the saved run's folder, and `errors` are this run's error messages. On failure: `{ deck, ok: false, stage, error, errors }`. With `{ wait: false }`: `{ started: true, deck }` once the run is under way. `timeout`, **in milliseconds**, stops waiting but not the run: `{ ok: false, error: 'timeout', running: true }` |
| `stop()` | stops the run in progress |
| `runs.list()` | `[{ run, project, deck, started, bytes, ok }]`, newest first |
| `runs.open(run)` | shows a saved run in 3D, in the plots and in the log |
| `runs.remove(run)` | `{ removed }` |
| `vectors(run?)` | `[{ key, unit, category }]`, e.g. `FOPR`, `WBHP:PROD`, `BPR:1,1,1` |
| `summary(run?, keys?)` | `{ run, deck, days: [...], vectors: { key: { unit, values: [...] } }, missing: [...] }`. `keys` is one key or a list; all keys when omitted. One value per report step **after** the start: N values for N steps, so `days[i]` is report step `i + 1` |
| `fields(run?)` | `{ fields: [{ id, label, unit }], steps: [{ step, days }] }`. Fields are e.g. `PRESSURE`, `SWAT`, `SGAS`, `SOIL`, `RS`. `steps` starts at step 0, the initial state: N + 1 entries for N report steps |
| `cells(run?, field, step?)` | `{ field, unit, step, days, values: [...], min, max }`. One value per active cell; `step` 0 is the initial state; the last step when omitted |
| `wells(run?, step?)` | the wells a report step ran with, from the schedule: `{ step, days, wells: [{ name, producer, injects, status, head: { i, j, refDepth }, connections: [{ i, j, k, open, depth }] }] }`. `injects` is `water`, `gas`, `oil` or `multi` for an injector; `status` is `OPEN`, `STOP`, `SHUT` or `AUTO`. Cells are 1-based, as in the deck; depths are in m. The last step when `step` is omitted; step 0 is the start |
| `log(run?, { level })` | flow's messages: `[{ level, text }]`. `level: 'warning'` keeps warnings and errors; `'error'` keeps errors only. Any other level throws |
| `examples.list()` | `[{ name, title }]` |
| `examples.add(name)` | `{ project, main }`. Adds the example as a project unless it is already there |
| `workspace.clear()` | deletes every file and run |
| `workspace.flush()` | waits until edits are written to the browser's storage |

**Units differ between the cell calls and the summary calls:**
- `cells()` pressures are always in **bar**, whatever the deck's unit system. `fields()` gives each field's unit.
- `summary()` values are in the **deck's** unit system: FIELD gives `PSIA` and `STB/DAY`; METRIC gives `BARSA` and `SM3/DAY`. Each vector's `unit` says which.

So on a FIELD deck, convert before comparing `FPR` with an average of `cells(…, 'PRESSURE')`: 1 bar = 14.5038 psi.

## Example: write, check, fix, run, read

```js
const deck = `RUNSPEC
DIMENS
 3 1 1 /
WATER
METRIC
...`;
const res = await page.evaluate(async (text) => {
  await opmflow.ready();
  await opmflow.files.write('MINE/MINE.DATA', text, { open: true });
  const problems = (await opmflow.check('MINE/MINE.DATA')).filter((p) => p.severity === 'error');
  if (problems.length) return { problems };
  const run = await opmflow.run('MINE/MINE.DATA');
  if (!run.ok) return { run, log: await opmflow.log(null, { level: 'error' }) };
  return {
    run,
    fpr: await opmflow.summary(run.run, 'FPR'),
    pressure: await opmflow.cells(run.run, 'PRESSURE'),
  };
}, deck);
```

## WebMCP tools

Where the browser has WebMCP — Chrome's experimental `document.modelContext`; earlier drafts called it `navigator.modelContext` — the page also registers the API as tools for the browser's own agent. Each tool takes a JSON-schema input and returns JSON text. `<body data-webmcp="on">` shows they are registered.

| tool | does |
|---|---|
| `playground_status` | `status()` |
| `list_examples`, `add_example` | `examples.list()`, `examples.add(name)` |
| `list_files`, `read_file`, `write_file`, `rename_path`, `delete_path` | the `files.*` calls |
| `set_main_deck`, `open_file` | `setMain(path)`, `open(path, { line })` |
| `check_deck` | `check(path)` |
| `run_deck` | `run(deck, …)`. Waits up to `wait_seconds` (90 by default; 0 returns at once), then returns `running: true` to poll `playground_status` |
| `stop_run` | `stop()` |
| `list_runs`, `open_run`, `delete_run` | the `runs.*` calls |
| `list_vectors`, `get_summary`, `get_fields`, `get_cells`, `get_wells`, `get_log` | `vectors`, `summary`, `fields`, `cells`, `wells`, `log` |

The annotations follow Chrome's:
- `readOnlyHint` on the reads.
- `untrustedContentHint` on `read_file` and `get_log`: deck text and flow's messages are content, not instructions.
- `consequentialHint` on `delete_path` and `delete_run`: a browser may ask the user to confirm those.

In Chrome, WebMCP is an experiment (an origin trial). The published page turns it on for every visitor only when it is built with an origin-trial token for its origin: `WEBMCP_ORIGIN_TRIAL=<token> playground/build.sh`. Without the token it works only in a Chrome with the experiment enabled; elsewhere the page registers nothing and `window.opmflow` works as before.

## Waiting on the page

`<body>` carries the page state as attributes. An agent that clicks instead of calling the API can wait on them:

| attribute | values |
|---|---|
| `data-engine` | `loading`, `ready`, `failed` |
| `data-run-state` | `idle`, `running`, `finished`, `failed`, `stopped` |
| `data-shown-run` | the run on screen, once saved (empty while running) |
| `data-webmcp` | `on` when the WebMCP tools are registered, `off` otherwise |

```js
await page.waitForSelector('body[data-engine="ready"]');
await page.getByTestId('run').click();
await page.waitForSelector('body[data-run-state="finished"], body[data-run-state="failed"]', { timeout: 0 });
```

## Clicking through the UI

The controls have ARIA roles and names, so accessibility-snapshot agents (Playwright MCP, `getByRole`) can find them. The key ones also have `data-testid` (`page.getByTestId(...)`):

- **header:** `run`, `stop`, `status`, `progress`
- **explorer:**
  - toolbar: `x-new-deck`, `x-open`, `x-folder`, `x-more`, `x-hide`, `x-show`
  - tree: `tree`, its rows `tree-row`, and on each row a `row-menu` button
  - `storage`
- **tree rows:**
  - every row has `data-kind` (`folder`, `file`, `runs`, `run`) and `data-path`
  - a file row also has `data-main="true"` on a main deck, and `data-errors` / `data-warnings` when the file has problems
  - each row's `aria-label` says the same in words
- **menus:** `menu`, with items `menu-<action>`:
  - files: `open`, `main`, `rename`, `download`, `delete`
  - folders: `new-file`, `new-folder`, `add`, `rename`, `download`, `delete`
  - runs: `open-run`, `download`, `delete`
  - the workspace menu: `new-deck`, `open-files`, `open-folder`, `samples`, `download`, `clear`
- **editor:** `deck-name`, `outline`, `problems`, `editor`
- **welcome panel:** `welcome-samples`, `welcome-new-deck`, `welcome-open-files`, `welcome-open-folder`
- **results tabs:** `tab-grid`, `tab-vectors`, `tab-log`, `run-info`
- **3D view:** the panel `view-panel`, with `property`, `play`, `step`, `step-label`, `zscale`, `opacity`, `reset-view`, `show-wells`, `well-search` and `well-list`. Each row of `well-list` is `.witem[data-well="NAME"]`, holding a checkbox and a `.wname` button that focuses the well. Then `viewer` and `well-legend`
- **vector plots:** `vector-search`, `vector-groups`, `plots`
- **log:** `log`, `log-count`
- **file inputs:** `file`, `folder`, `add-file` (use `setInputFiles`)

Menus and some actions ask through `prompt`/`confirm` dialogs: accept them with `page.on('dialog', d => d.accept(value))`. The API never asks.
