File evals (io.eval.js and o.eval.js)

A file eval runs JavaScript by writing a file. An agent writes code to a request file, io.eval.js or o.eval.js. The playground reads it, deletes it, evaluates it and writes the result to io.eval.json or o.eval.json. This is how an agent inspects the live playground, operates it and verifies its own changes. File evals need a folder shared with Agent πŸ“‚ Shareβ„’.

Two channels

There are two request and result pairs. Both need an active share, and the request must be a top-level file in the work folder.

Propertyio.eval.js β†’ io.eval.jsono.eval.js β†’ o.eval.json
Runs inThe playground window, the editor page, with Code, edge, sel, o, s and the output settings.The output document.
document isThe playground page.The output page.
JavaScript modeStrict.Sloppy.
Console callsNever reach πŸ“œ.json.Are written to πŸ“œ.json.
Reply timeoutNone.15 seconds.
Returned valuesAnything that JSON can encode.Only values that postMessage can clone.

Both channels are independent of each other. See Agent files for where the four files sit in the folder.

The request and result protocol

Writing a request

Any change that the folder observer reports for the request file starts a run, except a deletion: the file was created, modified, or moved or renamed into place. There is no settling delay, no verification read, no commit marker and no rename requirement. The playground reads the file at once, so it can evaluate a request that is only partly written. Write the whole request in one write, or write a temporary file and rename it.

The request is read as UTF-8 text, and a byte-order mark is stripped. An empty or whitespace-only request is a valid eval, and its result is empty.

Consumption and acknowledgement

Consumption is serialized per request name, and takes four steps:

  1. The playground reads the request.
  2. It deletes the previous result file.
  3. It deletes the request. This deletion is the acknowledgement.
  4. It releases the claim and evaluates the request.

The request file vanishing therefore means accepted, and a missing result file means still running.

Reading the result

  • The result is written as JSON.stringify(value, null, ' '): two-space indentation and no trailing newline. undefined gives an empty file of 0 bytes.
  • An agent that writes a request and immediately reads *.eval.json can see the previous result, because the old result is deleted only when the new request is consumed. Wait until the request file has disappeared, and then for the result. Alternatively, delete the old result before you write the request.
  • In Chromium, a result file can be created empty first and filled when its writer closes, with the bytes staged in *.eval.json.crswap. A fresh result can therefore be empty for a moment. Before you treat an empty result as undefined, read it again after a short delay, or check that no *.eval.json.crswap exists.
  • On an error, the result file holds plain text, not JSON: error.stack when there is one, otherwise String(error). The playground also shows a danger toast with the kind of change, the file name and the message.
  • After each successful run, a new πŸ“Έ.png is requested when a capture is active.

Example: write a request atomically and wait for its result

In a shell, in the work folder:

plain
printf '%s' 'Code.D' > io.eval.tmp && mv io.eval.tmp io.eval.js
while [ -e io.eval.js ]; do sleep 0.05; done
until [ -e io.eval.json ] && [ ! -e io.eval.json.crswap ]; do sleep 0.05; done
cat io.eval.json

With File Sync on, *.tmp files raise no toast. The .crswap check is Chromium-specific. It narrows, but does not close, the moment in which the new result exists empty before its writer opens: if io.eval.json is empty, check again after a short delay before you treat it as undefined. An error result is plain text, not JSON.

What a request may contain

RTCode compiles the request, without running it, to pick a form:

For statements, RTCode inserts return before the last non-empty top-level statement when that statement is an expression. The parser that does this is loaded when the share starts. A declaration, a block, an if, a loop or any other statement in last place returns nothing. An explicit top-level return works anywhere, and automatic semicolon insertion applies: return\n42 returns undefined. If the parser cannot load, the console warns agent eval final expression unavailable once. Single expressions still return, but statement requests return only through an explicit return.

What a request returns

These results are verified in both contexts. A result of undefined is an empty file. Objects appear with two-space indentation.

RequestResult
42, 42;, 42;;;, ;;42;;;42
await Promise.resolve(42);42
const value = 41; value + 1;42
42 // comment42
/* c */ 42; // c42
{ answer: 42 }{"answer": 42}
({ answer: 42 });{"answer": 42}
{}{}
{ answer: 42 }; (a block)undefined
{};undefined
{ const value = 42; value; }undefined
const value = 42;undefined
An empty request, and ;;;undefined
'' (a string literal)""
return 42;42
if (true) return 42;\n0;42
if (false) return 0;\n42;42
return\n42undefined
/a;b/.source;"a;b"
`a;b`;"a;b"
[1, 2, 3];[1, 2, 3]
"use strict"; const value = 40; value + 2;42
undefined;undefined
function named() {}A function value: io.eval.json is empty, and o.eval.js times out.
function named() {};undefined

Example: several statements with a final value

Semicolons matter. This request returns { before, after }. Without the semicolons, the line that starts with ( would continue the await line and throw TypeError: (intermediate value) is not a function.

js
const before = sel;
sel = [[1, 1, 1, 1, 0]];
await new Promise(r => setTimeout(r, 50));
({ before, after: sel })

io.eval.js: the playground

io.eval.js is evaluated as the body of a function in strict mode, in the playground's global scope. The globals and accessors of the Playground API are reachable: sel, Code, codeTitle, edge, o, s, i, ovp, oz and so on.

Reading the playground's state is a single expression. The routes, the title and the selections:

js
({ routes: Code.D, title: codeTitle, selections: sel })

io.eval.json then holds two-space JSON without a final newline, for example:

plain
{
  "routes": [
    "demo.rt.ht"
  ],
  "title": "Demo",
  "selections": [
    [
      3,
      1,
      3,
      1,
      0
    ]
  ]
}

Because each request runs in its own scope and in strict mode, keep state across requests on window. A bare count2 = 1 throws ReferenceError: count2 is not defined, while this form returns 1, then 2, and so on:

js
window.count = (window.count ?? 0) + 1

o.eval.js: the output

Before an o.eval.js runs, RTCode waits for the editor's latest input-to-output sync, and for the output to initialize and become ready. If an earlier initialization had failed, it initializes the output again, once per request. If the output is not ready in time, the result is an error text, for example a stack that starts with Error: Output document readiness timed out. The request has been consumed by then and does not run.

Scope and names

  • The output runs the code with a direct eval in sloppy mode, inside with ({ sync, x }). An undeclared assignment creates a global on the output window.
  • Some names are shadowed: x is the request message, with the keys agentSource, agentEval, n and eval; sync is the output runtime's sync object; e is the message event and n the message id; t, y, moWasActive and moNextActive are locals of the handler; src, err, run, pack and body come from the wrapper. Other names of the runtime may resolve to RTCode internals too.
  • Reach page globals, and elements named by their id, through window: use window.x for <b id=x>.
  • sync.input is installed afresh for each request, against the current editor source. See o and sync.

With this element in the output, x is the request message, so read the element through window:

html
<button id=x>Save</button>
js
window.x.textContent

Returned values

Values travel by postMessage, so they are structured-cloned. A value that cannot be cloned, which is a function, a DOM node, a Window, a symbol or an object that contains one, makes the output log a warning and send no reply. The result then becomes the timeout text, message[<n>] timeout after: 15000. Return plain data instead: strings, numbers, arrays, objects, or DOMRect.toJSON().

js
[...document.querySelectorAll('h1, h2')].map(h => ({ tag: h.localName, text: h.textContent.trim(), box: h.getBoundingClientRect().toJSON() }))

Returned values are JSON-encoded:

ValueResult
Map, Set{}
DateAn ISO string.
undefined or NaN in arraysnull
"abc""abc", with quotes.
A promiseIts resolved value.
A BigInt, a cyclic objectA TypeError text, such as TypeError: Do not know how to serialize a BigInt.
A functionIn io.eval.js: an empty file. In o.eval.js: the timeout.

Thrown values that are not errors behave differently in the two channels. throw 'oops' writes oops from io.eval.js, but only Error from o.eval.js. A thrown Error keeps its stack in both: throw new TypeError('bad') gives TypeError: bad plus the stack.

Timeout and long work

An o.eval.js reply must arrive within 15 seconds. Otherwise the result is the plain text message[<n>] timeout after: 15000, and the code keeps running in the output. To work for longer, start the job in one request, return at once, and poll in later requests. Do not return or await a long-running promise: the reply waits for it and hits the timeout.

js
window.job ??= { done: false, started: Date.now() };
setTimeout(() => job.done = true, 30_000);
'started'

A later request reads job and gets plain data.

More about o.eval.js

  • The request goes to every connected output window: the embedded output, and any output tabs or popups. The result file gets the first reply, but the code runs in each window.
  • The output's console calls from o.eval.js are written to πŸ“œ.json.
  • The output's mutation observer may still be active while the code runs, so DOM changes that the code makes can be treated like changes by a user. With Input ← Output sync on (Toggle Input ← Output Sync, Alt+O), such changes can be written back into the input and into πŸ“„.html. With it off, they stay in the live output.

Failures, retries and overlapping evals

Tip: Send one request at a time, wait for its new result, and only then send the next. Put a unique token in the returned value, as the diagnostic below does, to prove which request a result belongs to. After you edit files, check the expected state with an eval that reads the editor or the output before you rely on it: File Sync gives no guarantee that it has imported an edit before a following eval runs.

Timeouts and retries at a glance

WhatValue
o.eval.js reply15 seconds, then message[<n>] timeout after: 15000.
io.eval.js runNo timeout.
Retries after a failed readAfter 1, 2, 4, 8 and 16 seconds: at most five re-runs.
Transient file system errors7 attempts, with waits of 25, 75, 150, 300, 600 and 1000 ms, about 2.15 seconds in all. Permission errors are not retried.
Diagnostics command1 second per context.

See Limits and quotas.

Diagnostics: Test Playground and Output File Evals

When evals do not seem to run, use Diagnostics: Test Playground and Output File Evals in the command palette. It is available while a folder is shared, and has no default shortcut. It runs a playground round trip (io.eval.js to io.eval.json) and then an output round trip (o.eval.js to o.eval.json) through the real files, each with a timeout of 1 second.

Each round trip works like this:

  1. It deletes the existing result file, so your last result is lost.
  2. It writes the request return { token: "<uuid>", context: window === top ? 'playground' : 'output', value: 6 * 7 };.
  3. It polls for the result every 100 ms.
  4. It requires JSON with the same token, the expected context and the value 42.

The probe's files are quiet: no transfer toasts and no screenshot refresh. It removes its own request if it is still there. Calling the command again while it runs joins the running check. Each context shows one toast: Playground eval: or Output eval: followed by Success, Failure or Busy, then <input> β†’ <output>, then the message and the time in milliseconds.

OutcomeMessage
SuccessFile round trip passed.
FailureTimed out after 1 second., Folder share changed., the first line of a result that is not JSON, such as an error stack (Invalid eval response. when that line is empty), Unexpected eval response: token, context or value did not match., or the message of a failed request or result write.
BusyAnother eval request is pending or running. (a request file exists, a run is active or a probe is working; your pending request is left untouched), Another eval request is running. (a run started during the check), or Another eval request arrived before the check could start. (a request arrived after the old result was deleted)

Note: The output check expects its reply from the embedded output. If your output runs in a separate tab or popup, which reports the context playground, the check may fail with Unexpected eval response… even though evals work.

Cautions

Warning: Anything that can write to the work folder can run JavaScript in the playground through io.eval.js, including deploys with edge.save.limit(). Share the folder only with agents and tools that you trust.

Deploying from an eval, with await edge.save.limit({ message }), is part of what the agent guide teaches agents. See Agent files.

Availability

FeaturePlaygroundRTEdge.netNotes
io.eval.js and o.eval.jsYesNoNeed an active Agent Folder Share: Chrome or Edge on a desktop.
Diagnostics commandYesNoAvailable while sharing.

See also