Status: implemented. Recorded run: the WhatsApp example
(crates/tinycomputer-examples/tasks/whatsapp/).
A flow saves what it finds with read, extract, and pick. Two things
went wrong with that on a task that walks a list (reading five WhatsApp
chats one by one):
- The run forgot what it had saved. Jev's state held the screen and the recent actions ("read … into chat_1"), never the values. A step such as "open the next chat not yet read" could not be judged, and it looped over the same chats. A rescue started a new run, which forgot even the names.
- The caller got raw screen text.
done.recordsholds whatever the screen showed: duplicates, chrome, text a screen reader adds ("message, …, Received from …"), in screen order, with no fixed shape. A caller that wants "five chats, ten messages each, as JSON" had to parse it.
- Every state Jev is shown, narrow or wide, carries
already_collectedonce anything is saved: each saved variable's value (anextract's as its row count and first row), wrapped asuntrusted_accessibility_data, the last 12 variables at 120 characters each (MAX_COLLECTED,COLLECTED_CHARS). It is masked like the rest of the state, so a secret's value never shows. RunFlowRequest.collectedcarries what earlier runs of the same task saved. A run starts with those as variables (a caller'svarswin a clash) and recalls them as already collected. The task controller fills it on every run, so a rescued, approved, or resumed task remembers.- The rescuer's briefing lists what the task has saved, fact values redacted, 200 characters each, and its steps may use those names.
- On a screen where nothing repeats by ordinal (a desktop tree), a run of
three or more same-role leaf siblings is a list (
MIN_FLAT_ITEMS), soextractandpickwork on native applications. - When several lists show,
extract, and apickwhose criterion is judged, ask Jev which list theirwhat/fromnames (thelistquestion), and fall back to the longest when it is unsure. - A
readof an element whose name and value differ (a chat's button named for the chat, holding its last message) offers the name and the value as separate sources.
StartTask.outputis{instructions, schema?}. When every step has finished, one reasoning-model pass (output_model, defaultopenai/gpt-6-luna) gets the goal, the instructions, the schema, and the saved values (fact values redacted, wrapped as untrusted data, 60,000 characters at most) and answers with one JSON object.- The object is checked against the schema. A failing one goes back with
every violation, by path, up to twice. What passes is
done.result; the rawrecordsstay beside it. - The schema is a subset of JSON Schema:
type,properties,required,additionalProperties(a boolean),items,enum,minItems,maxItems,description,title, with anobjectat the top. Anything else is refused when the task starts (INVALID_OUTPUT), so every rule given is enforced rather than half-checked. - The model is told to select, clean, reorder, and restructure only, never to invent a value.
- A result that never fits fails the task, not recoverable, with its
records in
TaskReport;doneis never returned without a result that was asked for. outputneeds the planner configured (OUTPUT_UNAVAILABLEotherwise);Describereportsoutput_configured.- A plain
RunFlowhas no shaping pass: it belongs to the task controller. It does takecollected. - Contract 2.5 adds
StartTask.output,done.result,Capabilities.output_configured, andRunFlowRequest.collected, all optional.
- A
pressstep for an application's own shortcuts (WhatsApp's ⌘1–⌘9 open the Nth chat) would make "open the Nth item" deterministic; list picks and the memory made it unnecessary for the WhatsApp run, so it was left out. extractreads what is on screen; it does not scroll for more.