19 KiB
Desktop application tasks
A desktop application export can carry one task: a prompt for a computer-use agent and checks that grade what the agent left in the application's files. An agent, not REA, writes the prompt and the checks from the change the capture recorded. REA validates the task against the capture and keeps it only when its checks fail on the seed and all pass on the captured end state. REA runs no model and no code the agent wrote: the checks are declarative, and the exported verifier evaluates them the same way REA does.
rea describe-desktop-application-task ./mousepad-capture --format json > draft.json
# edit the draft's task into task.json: a name, the user's request, the checks
rea export-desktop-application ./mousepad-capture ./mousepad-bundle --task task.json
agent-env run ./mousepad-bundle --task add-milk --agent claude-code
Describe the change
rea describe-desktop-application-task <capture-folder> reads the capture and
its state view and returns Evidence with
operation describe_desktop_application_task and predicate type
rea.desktop-application-task-draft:
application: the environment name the export would use, the title and the window class.owned_paths: the paths the export owns, the only ones whose files the app'sdata/getserves and checks can read.checkpoints: the two checkpoints compared,baselinefor the seed andfinalfor the end. They are the capture's first and last unless--seed-checkpointand--end-checkpointname others, as for the export, which takes the same options; a name the capture does not hold, or an end before the seed, is refused.files: each file under them whose state differs between those checkpoints, with its home path, role, its versions at both (baselineandfinal, the last recorded at or before each) and the net change between them, from the state view's own diff (lines,values,keysorrowswith what was added, removed and changed, orcreated,deleted,format,content,unreadable;contentalso when the views hold only part of a file and match while the file changed). A database is compared by its rows, since its hash leaves out the-walfile. Text changes list each line once and addline_counts, the lines found at both ends whose number of occurrences changed. Views appear only for the files the suggestions cover, with arrays past 200 items and strings past 2,000 characters cut (view_cut).task: a draft with the namechange, a placeholder prompt the export refuses, and suggested checks derived from the net change of the files with roledocument, or of the app's own state files when no document changed. A text file gainslines_presentfor its new lines andlines_absentfor lines it lost (when either is at most ten), andline_countfor each line holding a letter whose count changed; JSON and key files gainvalue_equalsandvalue_absentby pointer; SQLite tables gain rows added, changed and removed, by primary key, and their new row count. An XML file, and the JSON, key file and XML views of a zip archive's entries, gain checks by selectors and never through an element by position: in XML, firsttext_equalsfor each element whose text changed, then an element added whole that it exists, one removed whole that it is absent, then values changed, added and removed, an entry or a JSON value added whole through its members, and an empty array or object as it is. The text check names the paragraph rather than a run inside it: it goes up from an element sitting in its parent's text, and from one named by its tag or position alone to the nearest ancestor named by an attribute, as AbiWord'spby itsxid; the text nodes and runs inside it get no other check, but an element holding no text that is added or removed there, such as an image or a line break, keeps its own, since the text check never reads it. Whitespace an app lays out between elements is not checked, and values that look like a time the app stamps, in ISO 8601 or as C'sasctimewrites it (Thu Nov 5 09:20:31 2026), are left out.
The suggestions are a starting point. Keep the ones that say what the user
did; drop the ones that only follow from how the app saves its file. A LibreCAD
capture that added four holes to a drawing rewrote it from DXF R12 to DXF 2007,
so most of its lines changed; the draft suggests counts of SECTION, ENDSEC
and CIRCLE, and only CIRCLE going from 5 to 9 is the user's change.
The task file
{
"name": "add-milk",
"prompt": "Add buy milk to my to-do list in Mousepad and save it.",
"checks": [
{
"id": "milk",
"kind": "lines_present",
"file": "Documents/todo.txt",
"lines": ["buy milk"]
}
]
}
name is a portable bundle name other than smoke (lowercase letters, digits
and hyphens); prompt is what the user asks for; checks holds 1 to 32
checks. Each check has a kind, a file given as a path under the home such as
Documents/todo.txt, and an optional id (check-1, check-2 and so on by
position otherwise). A check reads the typed view data/get returns for that
file, in the shapes of the state view.
| Kind | Fields | Passes when |
|---|---|---|
file_present |
The file exists | |
file_absent |
The file does not exist | |
lines_present |
lines |
Every listed line is a whole line of the text view |
lines_absent |
lines |
None of the listed lines is |
line_count |
exact or contains; equals, at_least or at_most |
The number of lines equal to exact, or holding contains, compares so |
value_equals |
pointer, value; optional tolerance |
The pointer names a value equal to value, or within tolerance of it |
value_absent |
pointer |
The pointer names no value |
text_equals |
pointer, value; optional normalize_whitespace |
The XML element's whole text equals value |
text_contains |
pointer, value; optional normalize_whitespace |
The XML element's whole text holds value |
row_present |
table, where |
A row of the SQLite table has every column in where equal |
row_absent |
table, where |
No row does |
row_count |
table; optional where; equals, at_least or at_most |
The number of rows, or of rows matching where, compares so |
The rules both evaluators follow:
- An absent file reads as empty: no lines, values or rows. So
lines_absent,value_absentandrow_absentpass on it and a count is 0. - A file that has no view (binary, over the read limit, or unreadable) fails
every check but
file_presentandfile_absent. Line checks need a text view, row checks a SQLite view and text checks an XML view, in a file or an archive entry; another format fails. - Equality is JSON equality: numbers by value, never a boolean for a number, objects by their keys whatever their order, arrays in order.
- Pointers follow RFC 6901 (
~1for/,~0for~) and must start with/; on an array a segment may be a selector, below. Key files read as{"groups": {"GROUP": {"KEY": "VALUE"}}}, so a setting is/groups/GROUP/KEY; XML as{"root": {"name", "attributes", "children"}}; SQLite as{"tables": {"TABLE": {"columns", "rows"}}}; a zip archive as{"entries": [{"name", "format", "view", …}]}. - With
tolerance, a non-negative number,valuemust be a number, and the value found passes when it is a number, or a string that is a decimal numeral such as"5.0000000000000000", at mosttolerancefromvalue.tolerance: 0compares numerically, so"1.2500"equals1.25. Anything else fails as not a number. containsmatches a line that holds it anywhere, case and all. Checks take no regular expressions: a pattern run on what the agent wrote could stall the verifier, and JavaScript and Python read some patterns differently.- A check decided by what a view leaves out (
lines_absent,value_absent,row_absent, the counts,value_equalsof a whole object or array, and the text checks of an element) fails when that view is truncated; a check satisfied by what the view holds is decided on it.
The export refuses, naming the check, a file outside the owned paths, a file
spelled with a leading slash, . or .., a pointer into a file that is text
at the seed, as the bundle ships it, or at the end checkpoint, the empty
pointer (the whole file), a value that names the captured home, or the
capture host's name or user@host anywhere (which the
export removes from the seed), a
tolerance with a value that is not a number, an empty text_contains
value, or one of only whitespace with normalize_whitespace, and a value that
looks like a date and time, in ISO 8601 or asctime's form. Other volatile
content, such as a
counter the app bumps on every save, is for the author to leave out.
Pointers and selectors
An app that rewrites a document moves things: FreeCAD 1.0 records more properties than 0.21, so a hole's radius sits at another position among its siblings, and a plate's entry moves once a hole is added before it. A pointer segment applied to an array may therefore name a member by what it holds rather than by its position:
[KEY=VALUE]names the member whose string fieldKEYisVALUE, such as a zip entry by[name=Document.xml]or an XML element by its tag,[name=Object].[@KEY=VALUE]names the member whoseattributesholdKEY="VALUE", as the XML view keeps an element's attributes.- Conditions can follow one another,
[name=Object][@name=Hole], and all must hold. They compare strings only, the selector is escaped like any pointer segment (~1for/), and a value cannot hold][.
A selector must name exactly one member. When several match, or none does
before the last segment, the check fails and says which selector matched what.
When the last segment's selector matches none, the pointer names no value:
value_absent passes, which is how a check says an element was removed, and
value_equals fails. On an object, a bracketed segment is an ordinary key.
A FreeCAD task that checks the user cut a 10 mm hole through the plate, in FreeCAD 0.21 and 1.0 alike:
{
"name": "cut-hole",
"prompt": "Cut a 10 mm hole through the middle of the plate and save it.",
"checks": [
{
"id": "cut",
"kind": "value_equals",
"file": "Documents/plate.FCStd",
"pointer": "/entries/[name=Document.xml]/view/root/children/[name=Objects]/children/[name=Object][@type=Part::Cut]/attributes/type",
"value": "Part::Cut"
},
{
"id": "radius",
"kind": "value_equals",
"file": "Documents/plate.FCStd",
"pointer": "/entries/[name=Document.xml]/view/root/children/[name=ObjectData]/children/[name=Object][@name=Hole]/children/[name=Properties]/children/[name=Property][@name=Radius]/children/[name=Float]/attributes/value",
"value": 5,
"tolerance": 0.01
}
]
}
Before the edit the archive has no Part::Cut and no Hole, so both checks
fail on the seed; after it, Radius is "5.0000000000000000" and both pass.
A check decided by what a view leaves out depends, in an archive, on the entry
its pointer goes into: it fails when that entry's view is missing or
truncated, or when the entry is not listed and the listing is truncated. A
selector must see every member to know it names one, so a check whose pointer
searches an array the view may hold only part of fails as undecided: any array
of a truncated XML view, of a truncated entry's view, or the listing when it
is cut. A position is decided on what the view holds. Like a file, an entry
that has no view (binary, not read, or a database or archive inside the
archive) fails every check into its view; its null there is no value, while
an entry holding the JSON document null reads as null. Checks on its name,
size, sha256 and format still read.
XML that uses namespaces is named as the document spells it, in both
evaluators: an element or attribute by its prefixed name, such as text:p
or inkscape:label, an element in a default namespace by its bare name, such
as svg, and namespace declarations as the attributes xmlns and
xmlns:PREFIX. Text is a string among an element's children. These checks
hold a poster's headline in an Inkscape SVG and a date in a LibreOffice Writer
document:
[
{
"id": "headline",
"kind": "value_equals",
"file": "Pictures/poster.svg",
"pointer": "/root/children/[name=g][@inkscape:label=Layer 1]/children/[name=text][@id=headline]/children/[name=tspan]/children/0",
"value": "Summer Bake Sale"
},
{
"id": "date",
"kind": "value_equals",
"file": "Documents/flyer.odt",
"pointer": "/entries/[name=content.xml]/view/root/children/[name=office:body]/children/[name=office:text]/children/[name=text:p][@text:style-name=Subtitle]/children/0",
"value": "Monday 24 November"
}
]
Inkscape writes a line of text as a tspan inside its text element, and
Writer keeps a paragraph's style in text:style-name. A prefix is the one the
document uses, so a check names the prefixes the app writes.
Whole text
A text node is only the stretch of text between two pieces of markup, and
apps split a paragraph into runs as they save it. AbiWord wraps text typed
into a reopened document in c elements, so changing a time to 3:30 saves
<p>The team meeting starts at <c>3</c>:<c>30</c> in room 4.</p>; Writer,
which records revision IDs by default, writes text typed in one session as
text:span runs. A value_equals on the paragraph's children/0 then reads
"The team meeting starts at " and fails. text_equals and text_contains
read the element's whole text instead:
{
"id": "time",
"kind": "text_equals",
"file": "Documents/meeting.abw",
"pointer": "/root/children/[name=section]/children/[name=p][@xid=3]",
"value": "The team meeting starts at 3:30 in room 4."
}
- The pointer names an element of an XML view, in a file or in an archive
entry's
view, or a text node or attribute value in one, whose text is its value. Another format, a pointer into an archive outside an XML entry's view, or any other value fails, an element'snameand thedoctype's fields included: they are strings of the view, not text of the document. - An element's text is XPath's
string()of what the view keeps: every text node in it and in its descendants, CDATA included, in document order, with nothing between them. The view drops whitespace-only text between child elements only in an element that holds no other text, outsidexml:space="preserve", so the space between two runs of a paragraph is part of its text. Elements that stand for characters, such as Writer'stext:s,text:tabandtext:line-break, add none. text_equalspasses when the text equalsvalueexactly, case and all;text_containswhen it holdsvalue, as a time within a sentence, so the check holds whatever the rest of the sentence says.normalize_whitespace: truecollapses each run of XML whitespace (space, tab, carriage return, line feed) in both the text andvalueto one space and trims both ends before comparing, as XPath'snormalize-space()does, for an app that reflows text or a value written across lines. A no-break space is not whitespace there.
Validation and export
rea export-desktop-application <capture> <directory> --task <task.json>
evaluates the checks on the capture's state view: on each file as it was at
the seed checkpoint, which is the seed, as the bundle ships it with the
capture host's name and user removed, and at the end checkpoint, the
captured end state. Those are the capture's first and last checkpoints unless
--seed-checkpoint and --end-checkpoint name others.
It refuses a task whose every check passes on the seed, and a check that fails
on the end state, with that check's reason. Only a failure on what the seed's
view shows counts: a task whose checks fail on the seed only because a view
could not be read or may have left out what they look for, such as a line past
the 5,000 a text view keeps, is refused, naming each failure. A capture yields
one task, so the option is taken once. The result's task names the task, its
number of checks and those that fail on what the seed shows. A refused task
writes no bundle.
The bundle then holds, besides the app and tasks/smoke.json:
| Path | Contents |
|---|---|
tasks/<name>.json |
Deploy the app, deploy the agent and its skills, record the screen, prompt the agent, run the verifier, stop recording |
artifacts/<name>-check/verify.py |
Reads the app's data/get through the computer and evaluates the checks, one result per check |
evals/<env>.toml |
The task, so a run of the bundle runs it; the smoke task runs with --task smoke |
The verifier finds the app among the computer's child environments, calls its
data/get under the run's grant of the computer's control token, and reads
the state data part. It raises when the app cannot be reached, so the run
fails rather than scoring the agent. The README lists the task, its prompt, its
checks and the recipe below.
Solve rate
REA does not run agents. To measure how often an agent solves the task, run it
k times, three by default, and count the runs whose summary line says
passed:
for run in 1 2 3; do
agent-env run ./mousepad-bundle --task add-milk --agent claude-code --model MODEL
done
The agent defaults to Claude Code and the model is the caller's choice (Claude
Sonnet 5.5 is a reasonable reference). A run reported as scored below 1
failed the checks and counts as a miss; one reported as failed did not finish,
as when the computer or the app could not be reached, and is run again rather
than counted. Each run's instance keeps the verifier's results per check under
verifications.