Why is sensor-42 missing data? A phone separates evidence from a hypothesis, beside a sensor with 16% battery, a gap from 08:12 to 09:42 and a hypothesis card: low battery likely causing intermittent connectivity
AI
• 11 min read

GenUI for IoT Part 2: A Detective, Not a Judge

The agent gathers the evidence. The verdict stays open.

Mario Corzo

Mario Corzo

Senior Mobile Developer

Share:

Introduction

GenUI for IoT is a series about generative UI: apps where the language model does not just answer in text, but composes the screen itself from components the app already knows how to draw. We build it into a Flutter app that watches a fleet of connected sensors, one design question per part.

In Part 1, the agent composed a fleet’s home screen from facts and never guessed at causes. That kept it honest, and it stopped right where users start asking: why is this sensor missing data?

Part 2 is the agent that answers it without making things up: it investigates with tools, shows what it found, and labels what it infers as a hypothesis.

Not every tap is a model call

Before writing any prompt, sort every interaction into one of three kinds. An agent app that sends every tap to the model is slow, expensive, and unpredictable.

InteractionGoes toExample
Agent actionThe model, as a new turnThe “Why is this sensor missing data?” button, a tapped follow-up chip.
Local actionApp state, no modelChanging a chart’s time range, filtering the device list
Device operationA local queue, behind an explicit buttonPing a device, request a reading

For device operations, the user is the one who runs them. The model can suggest pinging a device by composing a card with a button on it, but it cannot ping the device itself: a person reads the card and taps the button. That keeps every action with side effects behind a human, and keeps the model out of anything that does not need judgment.

Inside each answer there is a second split: code supplies the facts, the model supplies the judgment.

  • Code decides the facts: every count, timestamp and gap, whether devices share a gap, each device’s status, which device is being asked about, and whether a tool call is valid.
  • The model decides the reasoning: which tools to call and in what order, when it has enough, which facts deserve a card, what they suggest, and which follow-ups to offer.

The model never writes a measurement. Components that show readings or events get only a device id and load the data themselves, so the numbers on screen never pass through the model.

Device Copilot answer for sensor-42: fact cards for its gaps and for the gateway status, then a dashed hypothesis card, Sensor issue is likely isolated, listing the facts it is based on and a Check sync status button to confirm it

Tools return evidence, never a cause

The tempting tool is diagnoseSensor. It would return a ready-made answer, and it would move the whole investigation into code, leaving the model to read the answer out loud. We built the opposite: five small tools, each answering one question with a deterministic result, none of them allowed to definitively name a cause.

ToolQuestion it answersReturnsNever returns
getRecordGapsHow much is missing, and where?Expected and received counts, each gap with its span, the device’s gateway, neighbours and battery statusWhy; the readings themselves
compareRecordGapsIs this isolated or shared?Each device’s gaps, plus the stretches several devices shareThe word “cause”
getGatewayEventsDid the gateway or the sensor lose its connection?Every connect and disconnect recorded, for the gateway and for each deviceWhether an event explains a gap
getSyncHistoryDid the missing readings arrive later?Readings that arrived late, grouped by when they arrivedWhether readings still missing will arrive
getBatteryHistoryWas the battery running down?Its level at the start, now and at its lowest, when it dropped below the low mark, and one reading per hourWhether the battery explains a gap

Two rules shaped these tools:

  • Return enough to decide the next call, not everything. getRecordGaps returns the ids of the neighbouring devices, so the model can choose to compare. It does not return their gaps: that is compareRecordGaps’ job, and many investigations never need it.
  • Count model calls, not characters. Each tool result is a few hundred characters, tiny next to the 14k-token system prompt sent with every call. What costs time is the number of model calls, so the prompt carries one rule: do not call a tool whose result cannot change what you say.

The logic behind the hypotheses

The investigation has a written plan. The prompt tells the model to start with getRecordGaps, then gives one rule per tool, saying when its result can change the answer:

1. Call getRecordGaps for the device.
2. Decide what else you need:
   other devices on the same gateway     → compareRecordGaps
   a shared gap, or the device offline   → getGatewayEvents
   a delivered batch, or a reconnect     → getSyncHistory
   low battery, or short gaps that keep
   coming back while neighbours are fine → getBatteryHistory
3. Do not call a tool whose result cannot change what you say.

What the prompt does not fix is the order after the first call, or when to stop. The model reads each result and decides. The tree below is what it did with these rules in our runs, taken from each run’s trace, with the branch each of the five cases took.

The five cases are five data sets where sensor-42 is missing data, each with a different story behind it:

  • A: one sensor alone
  • B: a gateway outage
  • C: a delayed sync
  • D: a low battery
  • E: a battery that ran out.

One question, five answers below shows what the agent said in each.

Decision tree, Each answer follows the evidence down one branch: starting from getRecordGaps, questions answered by compareRecordGaps, getSyncHistory, getBatteryHistory and the getRecordGaps reply lead to dashed hypothesis boxes for each case: gateway outage likely (B), arrived late or readings may still arrive (C), battery ran out (E), low battery likely (D) and isolated device issue (A)

Every question in the tree is answered by a tool, with exact data. Only the dashed boxes come from the model, and each reaches the screen as a hypothesis, with the facts behind it and the check that would rule it out.

A guess has to look like a guess

Part 1’s rule still holds: the agent shows facts and never states a cause as one. Part 2 builds on it. When the user asks why data is missing, the agent can now add its interpretation on top of those facts, as long as the screen never lets it pass for one.

The model shows every finding as an EvidenceCard, and it must tell the card which kind it is:

  • A fact is something a tool returned, quoted back:
    “Gap 08:12 to 09:42, 3 readings”, “Gateway disconnected 07:58”.
  • A hypothesis is the model’s reading of those facts. The card draws it differently: italic, its own border, a HYPOTHESIS label.
    It can offer one follow-up, the check that would confirm or rule it out, and tapping it becomes the user’s next question.

The schema only allows fact or hypothesis, but the model writes the surface as text, so nothing forces it. If it ever sends a label the app does not recognise, the card falls back to a hypothesis, never to a fact.

What the fallback cannot catch is a guess labelled fact: a valid value with the wrong label. Today the model that writes the card also labels it.

A typed classifier such as Jev, which picks one option from a fixed set and says how confident it is, could check that label on its own: read the tool results and the card, and answer “fact” or “hypothesis”. We have not tried it yet.

The prompt adds three rules for the hypothesis card:

  • Only one per answer. The model has to pick its best reading, not list every possibility.
  • It lists the facts it rests on, so the user can judge it.
  • No words that sound certain: “confirmed”, “root cause”, “failure”.

In short: the model does not mark a guess with words like “probably”. It sets a field, and the app draws facts and guesses differently, so the user can tell them apart at a glance.

One question, five answers

We built five data sets where sensor-42 is missing data and asked the same question each time.
The prompt never names the case, so any difference in the answers comes from the tools.

CaseScenarioWhat the data holdsTools calledIts hypothesis
AOne sensorTwo gaps on sensor-42 only, no connection dropsgaps, compare, events, sync“Isolated connectivity or device issue”
BGatewayOne gap shared by three devices, during a gateway outagegaps, compare, events“Gateway outage likely caused the missing data”
CDelayed syncAn open gap; the sensor lost its connection but stores its readingsgaps, compare, events“Local communication interruption”, plus an upload action
DLow batteryShort, growing gaps; battery down from 30% to 6%gaps, compare, battery“Low battery likely causing intermittent connectivity”
EBattery ran outLike delayed sync, but the battery fell to 1% and nothing arrivesgaps, compare, events, battery“Battery exhaustion likely caused disconnect”, no upload offered
Case A: One sensor
Case B: Gateway
Case C: Delayed sync
Case D: Low battery
Case E: Battery ran out

In the delayed sync case the agent cannot yet tell whether the readings are lost or late, and says so. It offers an action to upload the stored readings over Bluetooth, for someone near the sensor, and a follow-up chip to check later. Either way, getSyncHistory then shows the readings arrived, and the agent recommends no action. When the battery ran out it offers no upload: there is nothing to connect to.

No component knows any of these diagnoses: all five answers use the same components, and the conclusion always sits in a card labelled as a hypothesis.

What we would keep

Part 1 asked four design questions. Part 2 is our answer to them, in code:

  • What should become a tool? An investigative capability with a deterministic answer. getRecordGaps, not diagnoseSensor.
  • How much context should a tool return? Enough to decide the next call. Progressive, not exhaustive.
  • Which decisions belong in code? Every count, timestamp, correlation and status. The model keeps which evidence to fetch, what it suggests, and how to show it.
  • How specific should a component be? Specific in what it loads, neutral in what it concludes. The diagnosis lives in a card the model fills, labelled as its own.

None of this needed a smarter model. It needed clearer boundaries: tools that return evidence, components that paint a guess as a guess, and a human in front of every action. Next in the series: streaming under the hood, and what it is like to build all of this on a library that is still pre-1.0.

About the Author

Mario Corzo

Mario Corzo

Senior Mobile Developer

Senior Mobile Developer with 10+ years of experience building cross-platform applications across mobile, web, and gaming. Specialized in Flutter, BLE-connected mobile apps, and over-the-air firmware update flows for IoT systems. At Croxel, Mario bridges firmware and user experience to deliver intuitive interfaces for connected devices.

Mario Corzo has written 3 articles for Croxel Insights.