<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Praveen Singh</title>
    <description>The latest articles on DEV Community by Praveen Singh (@praveensingh).</description>
    <link>https://dev.to/praveensingh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4172772%2F7eb3ceeb-ac00-46c4-9040-5f897425be8d.png</url>
      <title>DEV Community: Praveen Singh</title>
      <link>https://dev.to/praveensingh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/praveensingh"/>
    <language>en</language>
    <item>
      <title>Bareship: an offline-first React Native template for teams and AI agents</title>
      <dc:creator>Praveen Singh</dc:creator>
      <pubDate>Thu, 22 Oct 2026 15:39:25 +0000</pubDate>
      <link>https://dev.to/praveensingh/bareship-an-offline-first-react-native-template-for-teams-and-ai-agents-3ihi</link>
      <guid>https://dev.to/praveensingh/bareship-an-offline-first-react-native-template-for-teams-and-ai-agents-3ihi</guid>
      <description>&lt;p&gt;&lt;strong&gt;Bareship is a free, MIT-licensed template for bare React Native 0.87 apps.&lt;/strong&gt; One command, &lt;code&gt;npx create-bareship Acme&lt;/code&gt;, gives you an iOS and Android app where a like or an edit made offline is saved, survives a restart and is sent once when the phone reconnects; where the folder rules are checked by ESLint instead of code review; and where Claude Code, Cursor or Copilot get the same rules as your team. It runs with no backend, Firebase project or Apple account until you need them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffacw6kx1u4yvzddg3crq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffacw6kx1u4yvzddg3crq.gif" alt="On Android: airplane mode on, a like is saved and marked " width="480" height="853"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why another template?
&lt;/h2&gt;

&lt;p&gt;Every new React Native app I've started began the same way: a week or two rebuilding the plumbing before the first real feature. A queue for writes made without signal, somewhere safe for the auth token, folder rules everyone agrees on and nobody checks. Each time I built them a little differently, and each time the gaps showed up later, in a bug report from a user with a bad connection or in a feature folder that had quietly started importing from three others. I wanted to make those decisions once, properly, and start from there.&lt;/p&gt;

&lt;p&gt;Most starters get you to a running screen fast. The parts that cost time later are the ones a demo never touches: what happens to a write when the phone has no signal, where the auth token lives, and whether the folder structure you agreed on in week one still holds in month six, with three developers and a coding agent all adding code. Bareship is my answer to those, decided once and written down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when the phone goes offline?
&lt;/h2&gt;

&lt;p&gt;Every write in the app picks one of four patterns, so a user always knows what became of what they did:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Without a connection&lt;/th&gt;
&lt;th&gt;Use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;The change appears at once and is sent later&lt;/td&gt;
&lt;td&gt;Low-stakes toggles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;The change appears at once, marked "Waiting to sync"&lt;/td&gt;
&lt;td&gt;Likes, comments, edits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;The form is disabled, with a note saying why&lt;/td&gt;
&lt;td&gt;Sign-in, payments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;The content is replaced by an offline message&lt;/td&gt;
&lt;td&gt;Account deletion, checkout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Queued writes are saved to disk, replayed in the order they were made after a restart, and carry one idempotency key for every attempt, so a write sent twice is applied once. A banner under the header counts the changes waiting to sync. A write that picks no pattern fails at once offline, so nothing is queued by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules the build enforces
&lt;/h2&gt;

&lt;p&gt;The architecture is in the linter, not a wiki page. Layers import downward only, features can't reach into each other's files, text on screen must go through translations, and accessibility uses one set of ARIA props everywhere. ESLint and dependency-cruiser fail the check otherwise. When a rule is wrong for your app, you change it in one place and write down why: the decisions behind the template are kept as ADRs in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built for AI coding agents
&lt;/h2&gt;

&lt;p&gt;The repo ships an &lt;code&gt;AGENTS.md&lt;/code&gt;, its decision records and React Native agent skills, plus generators (&lt;code&gt;pnpm gen feature | screen | endpoint&lt;/code&gt;) that write a schema, API hooks, a mock, strings and a test the way the rest of the code does. An agent that follows them writes code that passes the same checks as your team's.&lt;/p&gt;

&lt;p&gt;I built much of Bareship with Claude Code, so these files were tested on an agent from the start: when it broke a rule, the fix went into the linter or &lt;code&gt;AGENTS.md&lt;/code&gt;, not into a comment on a pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  It runs with no accounts
&lt;/h2&gt;

&lt;p&gt;The dev build serves sample data from a mock backend inside the app, so you can sign in, scroll the feed and test offline mode on a simulator or a phone before any backend exists. Crashlytics, Analytics, Remote Config and push turn on when you add a Firebase file and fall back to logging without one. Tokens are stored in the Keychain and Keystore from day one.&lt;/p&gt;

&lt;p&gt;Also included: light and dark themes, English and Hindi (with right-to-left support you can switch on), deep links, force-update and maintenance screens, environments for dev, staging and prod, fastlane release lanes and a demo build you can install on a phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-bareship Acme
&lt;span class="nb"&gt;cd &lt;/span&gt;Acme
pnpm android   &lt;span class="c"&gt;# or: pnpm ios&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;create-bareship&lt;/code&gt; checks your tools first, asks for a bundle id and display name, installs everything and makes the first commit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvr4a4tu2u8ttmwslswl3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvr4a4tu2u8ttmwslswl3.gif" alt="npx create-bareship Acme in a terminal, then pnpm android, then the Acme app running on an Android emulator (sped up)" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sped up: the real run took 1 min 15 s to create the app. The full-quality video is &lt;a href="https://www.praveensingh.co.in/blog/introducing-bareship#try-it" rel="noopener noreferrer"&gt;on my blog&lt;/a&gt;.&lt;/em&gt; The first Android build compiles the native libraries and takes a while (about 20 minutes on my 8 GB M1, a few minutes on a faster machine); builds after that are quick.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/psingh2907" rel="noopener noreferrer"&gt;
        psingh2907
      &lt;/a&gt; / &lt;a href="https://github.com/psingh2907/bareship" rel="noopener noreferrer"&gt;
        bareship
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Production-ready, offline-first bare React Native template for teams and AI agents. Create an app: npx create-bareship MyApp
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Bareship is at 0.3.2. Next on the list: turning CI back on in the repository, and a series of posts on how the pieces work, starting with the offline queue and idempotency keys. If it saves you a week, a star on GitHub helps other people find it, and issues are the best way to tell me what's missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npx create-bareship Acme&lt;/code&gt; creates a bare React Native 0.87 app for iOS and Android in one command.&lt;/li&gt;
&lt;li&gt;Each write picks one of four offline patterns; queued writes survive a restart and are applied once.&lt;/li&gt;
&lt;li&gt;Layers, feature boundaries, translations and accessibility are enforced by ESLint and dependency-cruiser.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AGENTS.md&lt;/code&gt;, ADRs, skills and generators give AI coding agents the same rules as your team.&lt;/li&gt;
&lt;li&gt;It runs with no backend, Firebase or Apple account; you add each service when you need it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docs: &lt;a href="https://bareship.praveensingh.co.in" rel="noopener noreferrer"&gt;bareship.praveensingh.co.in&lt;/a&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>opensource</category>
      <category>mobile</category>
      <category>ai</category>
    </item>
    <item>
      <title>On-device AI in React Native, late 2026: one AI SDK call, two engines, measured</title>
      <dc:creator>Praveen Singh</dc:creator>
      <pubDate>Mon, 12 Oct 2026 04:55:34 +0000</pubDate>
      <link>https://dev.to/praveensingh/on-device-ai-in-react-native-late-2026-one-ai-sdk-call-two-engines-measured-5455</link>
      <guid>https://dev.to/praveensingh/on-device-ai-in-react-native-late-2026-one-ai-sdk-call-two-engines-measured-5455</guid>
      <description>&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Problem: Run an LLM inside a React Native app, with no server and no API key&lt;/li&gt;
&lt;li&gt;Fix: Two on-device engines behind one AI SDK call, plus a model picker that checks each engine actually works&lt;/li&gt;
&lt;li&gt;Result: 20 vs 2 (valid structured outputs out of 20: Apple's guided generation vs a 1B ExecuTorch model)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; On-device AI in React Native is practical now, but the two engines suit different jobs. Use Apple's Foundation Models when the device has it: no download, and guided generation returned valid structured output 20 times out of 20. Use ExecuTorch for everything else, but not for structured output from a 1B model: only 2 of 20 responses matched my schema. Keep a cloud fallback, and check that each engine really works rather than trusting its availability flag.&lt;/p&gt;

&lt;p&gt;React Native got a lot of AI news this year. WWDC26 expanded Apple's Foundation Models framework (image input, Private Cloud Compute, third-party models behind the same API). Software Mansion rewrote ExecuTorch for 0.10 with Core ML, MLX and Vulkan backends. Callstack's &lt;a href="https://github.com/callstackincubator/ai" rel="noopener noreferrer"&gt;&lt;code&gt;react-native-ai&lt;/code&gt;&lt;/a&gt; put Apple's model, llama.rn, MLC and Gemini Nano behind the Vercel AI SDK. An &lt;code&gt;expo-ai&lt;/code&gt; package is in review in the Expo repo.&lt;/p&gt;

&lt;p&gt;Release posts tell you what's possible. I wanted to know what it takes, so I built one small React Native 0.87 app that calls both engines through the same &lt;code&gt;streamText&lt;/code&gt; / &lt;code&gt;generateObject&lt;/code&gt; code, and measured them on the same tasks.&lt;/p&gt;

&lt;p&gt;Everything is in &lt;a href="https://github.com/psingh2907/rn-on-device-ai-lab" rel="noopener noreferrer"&gt;a public repo&lt;/a&gt;: code, raw results and the failures.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The setup, and what I couldn't measure.&lt;/strong&gt; React Native 0.87.1, AI SDK 6, &lt;code&gt;@react-native-ai/apple&lt;/code&gt; 0.12.0, &lt;code&gt;react-native-executorch&lt;/code&gt; 0.10.4 running Llama 3.2 1B. An Apple M1 with 8 GB of memory, the iPhone 17 simulator on iOS 26.3, &lt;strong&gt;no physical devices&lt;/strong&gt;. ExecuTorch ran in the app on the simulator. Apple's model wouldn't run in the simulator (more below), so I ran the same tasks against it from Swift on the Mac. These are Mac numbers, not iPhone numbers: use them to compare the two engines, not to predict latency on a phone. Gemini Nano on Android needs a supported phone, so it's covered from the docs only.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The two tasks
&lt;/h2&gt;

&lt;p&gt;Five store-audit notes, written the way an auditor types them on a phone:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Freezer 2 door seal torn, temp reading -12 instead of -18. Aisle 4 lights flickering. Fire exit at the back blocked by two pallets of water.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Summary:&lt;/strong&gt; streamed, two sentences, most urgent issue first. I measured time to first text and total time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checklist:&lt;/strong&gt; structured output, validated against a Zod schema with &lt;code&gt;generateObject&lt;/code&gt;. One item per issue, each with an area, an issue and a severity of &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt; or &lt;code&gt;high&lt;/code&gt;. 10 runs per engine, per run of the benchmark.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  One call site for both engines
&lt;/h2&gt;

&lt;p&gt;Apple's model already has an AI SDK provider. ExecuTorch doesn't, so I wrote a small adapter. Version 0.10 makes this easier: below the &lt;code&gt;useLLMChatSession&lt;/code&gt; hook it exposes the runner and the model's chat template as separate pieces.&lt;/p&gt;

&lt;p&gt;I didn't use the chat session itself. It keeps a growing conversation in the model's cache, which suits a chat screen but not a one-off &lt;code&gt;generateText&lt;/code&gt; call. The adapter resets the runner and renders the AI SDK prompt from scratch on every call:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/lab/executorchModel.ts (excerpt)&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LanguageModelV3CallOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responseFormat&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responseFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// AI SDK prompt -&amp;gt; chat messages&lt;/span&gt;
  &lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// stateless: no history from earlier calls&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;preprocessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;addGenPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;preprocessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;genConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxNewTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxOutputTokens&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;genConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stopTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// runs on a worklet thread&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;doGenerate&lt;/code&gt; and &lt;code&gt;doStream&lt;/code&gt; wrap that, and after that the calling code is the same for both engines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxOutputTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateObject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checklistSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's one difference the code can't hide. Apple's model fills the schema through &lt;strong&gt;guided generation&lt;/strong&gt;: the framework restricts what the model can output to fit the type. ExecuTorch has no equivalent here, so the adapter puts the schema in the prompt and hopes. That difference decided the structured-output results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting it to build took seven fixes
&lt;/h2&gt;

&lt;p&gt;None of these are hard, but each one stops the app from building or running, and I didn't find any of them in the docs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;iOS build fails: &lt;code&gt;unknown type name 'RCTCxxBridge'&lt;/code&gt; in &lt;code&gt;RnExecutorch.mm&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;React Native 0.87 removed it. Patch the module to install its JSI bindings through &lt;code&gt;RCTTurboModuleWithJSIBindings&lt;/code&gt; (&lt;a href="https://github.com/psingh2907/rn-on-device-ai-lab/blob/main/patches/react-native-executorch%2B0.10.4.patch" rel="noopener noreferrer"&gt;patch&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CocoaPods: "required a higher minimum deployment target"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;platform :ios, '17.0'&lt;/code&gt; in the &lt;code&gt;Podfile&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metro: "Export namespace should be first transformed"&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;@babel/plugin-transform-export-namespace-from&lt;/code&gt;, the &lt;strong&gt;v7&lt;/strong&gt; release (v8 needs Babel 8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExecuTorch native calls fail&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;react-native-worklets/plugin&lt;/code&gt;, last in the Babel plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Property 'TextDecoder' doesn't exist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Polyfills for the AI SDK on Hermes (below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ai@latest&lt;/code&gt; installs 7.x&lt;/td&gt;
&lt;td&gt;Pin &lt;code&gt;ai@6&lt;/code&gt;: the React Native AI providers implement the v6 spec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm 11 skips ExecuTorch's postinstall&lt;/td&gt;
&lt;td&gt;Approve it after reading it: it downloads the native libraries from the project's GitHub releases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The polyfills:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/polyfills.ts&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;web-streams-polyfill/polyfill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ReadableStream, TransformStream&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fast-text-encoding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// TextDecoder&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TextDecoderStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TextEncoderStream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@stardazed/streams-text-encoding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;structuredClone&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ungap/structured-clone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;globalThis&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TextEncoderStream&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="nx"&gt;TextEncoderStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TextDecoderStream&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="nx"&gt;TextDecoderStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;structuredClone&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;structuredClone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha: Use the global ReadableStream in your adapter.&lt;/strong&gt; My first streaming run died with "First parameter has member 'readable' that is not a ReadableStream". I had imported &lt;code&gt;ReadableStream&lt;/code&gt; from &lt;code&gt;web-streams-polyfill&lt;/code&gt;, which is a different class from the global one the polyfill installs, and the AI SDK checks against the global. Build streams with &lt;code&gt;globalThis.ReadableStream&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha: ExecuTorch sends download analytics by default.&lt;/strong&gt; Version 0.10 reports anonymous download events to Software Mansion unless you call &lt;code&gt;setTelemetryEnabled(false)&lt;/code&gt;. That's easy to miss, and it matters if you picked on-device AI for privacy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Two things that failed on the simulator
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Apple's model said it was available, then failed every call.&lt;/strong&gt; &lt;code&gt;apple.isAvailable()&lt;/code&gt; returned &lt;code&gt;true&lt;/code&gt; in the iOS 26.3 simulator, and all 30 calls across two runs failed with &lt;code&gt;GenerationError error -1&lt;/code&gt;. The simulator log had the real reason: the model catalog had no assets (&lt;code&gt;UnifiedAssetFramework Code=5000&lt;/code&gt;). The same model worked from Swift on the Mac, which runs macOS 26.6, so my guess is a mismatch between the simulator runtime and the Mac's model assets. Either way, the availability flag isn't proof.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The MLX build of the model downloaded, then wouldn't load.&lt;/strong&gt; I picked &lt;code&gt;LLAMA3_2_1B.MLX_INT4&lt;/code&gt; to compare against the CPU backend. It downloaded all 1.18 GB, then failed with &lt;code&gt;Error::NotFound&lt;/code&gt;. The podspec explains it: the MLX backend is only linked into device builds, because the simulator SDK lacks the Metal APIs it needs. &lt;code&gt;.DEFAULT&lt;/code&gt; would have avoided this, because it only picks backends that are actually linked into the build.&lt;/p&gt;

&lt;p&gt;Both lead to the same rule, and it's built into the model picker below: &lt;strong&gt;check that an engine works with a real call, and use the library's defaults rather than hard-coding a variant.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed: similar once warm
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ExecuTorch, Llama 3.2 1B (simulator)&lt;/th&gt;
&lt;th&gt;Apple Foundation Models (Mac)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Download before first use&lt;/td&gt;
&lt;td&gt;1.14 GB, 6 min 12 s&lt;/td&gt;
&lt;td&gt;none, part of the OS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load from disk&lt;/td&gt;
&lt;td&gt;4.4–5.3 s&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First call, time to first text&lt;/td&gt;
&lt;td&gt;0.6–0.8 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.7–19.2 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Later calls, time to first text (median)&lt;/td&gt;
&lt;td&gt;579 ms&lt;/td&gt;
&lt;td&gt;580 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Later calls, full summary (median)&lt;/td&gt;
&lt;td&gt;2.5 s&lt;/td&gt;
&lt;td&gt;2.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generation speed (median)&lt;/td&gt;
&lt;td&gt;43 tokens/s&lt;/td&gt;
&lt;td&gt;not reported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App memory while generating (median)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;907 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;not in the app's process&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once both were warm, they were close. The differences are at the edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ExecuTorch's cost is up front.&lt;/strong&gt; A 1.14 GB download on first use, then about 5 s to load the model on each launch. While generating, the app used about 900 MB of memory, which matters on phones that kill background apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple's cost is the first call.&lt;/strong&gt; That first call took 10–19 s on this Mac, and later calls took about half a second. The model runs in a system process, so it adds nothing to your app's memory, and there's nothing to download.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How I measured.&lt;/strong&gt; Two full runs per engine: 5 summaries and 10 checklists each, plus a third set of 10 checklists for the prompt fix below. The first call of each run is reported separately. Memory is the app process's resident size, sampled on the Mac at each result. The Mac was swapping during the runs (5.6 of 6 GB of swap in use), which hurts the Apple numbers most. The raw results and the summary script are in the repo's &lt;code&gt;metrics&lt;/code&gt; and &lt;code&gt;scripts&lt;/code&gt; folders.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;@react-native-ai/apple&lt;/code&gt; reports zero tokens to the AI SDK, so its token counts and tokens per second are missing. Compare time to first text and total time instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured output: 2 out of 20 vs 20 out of 20
&lt;/h2&gt;

&lt;p&gt;This is the biggest difference between the two engines:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Checklist output that passed the Zod schema&lt;/th&gt;
&lt;th&gt;Valid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ExecuTorch, schema in the prompt&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 / 20&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExecuTorch, example in the prompt, plus a repair step&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6 / 10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple, guided generation&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20 / 20&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 1B model failed in three ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It returned the schema.&lt;/strong&gt; In 5 of the 9 failures in one run, it replied with the JSON Schema itself (&lt;code&gt;{"$schema": "http://json-schema.org/draft-07/schema#", ...}&lt;/code&gt;) instead of filling it in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong case:&lt;/strong&gt; &lt;code&gt;"severity": "High"&lt;/code&gt; where the schema says &lt;code&gt;"high"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not JSON at all:&lt;/strong&gt; a bulleted list wrapped in braces.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two cheap changes raised the pass rate from 1 in 10 to 6 in 10 in the same run. Show an example instead of the schema, and lower-case the enum values before validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateObject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checklistSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// The adapter puts this example in the prompt instead of the JSON Schema.&lt;/span&gt;
  &lt;span class="na"&gt;providerOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;executorch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;jsonExample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"items":[{"area":"Aisle 2","issue":"Spilled milk","severity":"high"}]}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;experimental_repairText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/"&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;low|medium|high&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;"/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;6 out of 10 still isn't good enough to ship, and passing the schema doesn't mean the content is right. I read every valid checklist from the 1B model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two of the six copied my example into the store's checklist: "Aisle 2: Spilled milk on floor" was never in the note.&lt;/li&gt;
&lt;li&gt;Fields got mixed up: "Back fire exit: Expired yoghurt found in dairy fridge".&lt;/li&gt;
&lt;li&gt;Items were duplicated, and every item, in every valid checklist, was marked high.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apple's 20 checklists listed every issue in each note, in sensible areas, and never invented one. It rated a few minor issues too high (a frozen checkout scanner as high), but the fire, rodent and food-safety issues were high every time.&lt;/p&gt;

&lt;p&gt;Neither engine was good at the summaries. The 1B model opened every one with "Here is a two-sentence summary", then often wrote three or four. It also misread the notes: once it said the torn freezer seal was blocking the fire exit, once that a leaking roof was "not an issue". Apple's summaries were shorter (177 characters against 396), but half came back as Markdown bullet lists rather than two sentences. One left out the wet floor, and another ranked the frozen scanner above expired food.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your feature needs structured output, a 1B model in your app isn't enough.&lt;/strong&gt; Use the system model, a larger downloaded model (Llama 3.2 3B is in ExecuTorch's catalog), or the cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a model at runtime
&lt;/h2&gt;

&lt;p&gt;Here's the picker the lab uses. The order is fixed: Apple's model if it really works, then a downloaded model if the user agreed to the download, then the cloud. The result is cached for the session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/lab/router.ts&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apple.isAvailable() said true in the simulator while every call failed,&lt;/span&gt;
&lt;span class="c1"&gt;// so it's only a first filter. A one-token call is the real check.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;appleWorks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apple&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isAvailable&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;apple&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reply with OK.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxOutputTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pickModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cloud&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LanguageModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;allowDownload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;appleWorks&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;apple&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;on-device (Apple)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowDownload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// DEFAULT only picks backends linked into this build.&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;et&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createExecutorchModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;llama-3.2-1b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LLAMA3_2_1B&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEFAULT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;et&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;et&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;on-device (downloaded model)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cloud&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloud&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;})());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;where&lt;/code&gt; is for the UI. If people chose your feature because their text stays on the phone, tell them when it doesn't. That applies to the cloud fallback, and to Apple's Private Cloud Compute when iOS 27 sends a request there.&lt;/p&gt;

&lt;p&gt;This code is type-checked but not benchmarked; the lab measured each engine on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android: Gemini Nano, from the docs
&lt;/h2&gt;

&lt;p&gt;I couldn't test this one: Gemini Nano needs a supported Android 14+ phone, and an emulator won't do. On paper it fits the same pattern. &lt;a href="https://www.react-native-ai.dev/docs/adk/getting-started" rel="noopener noreferrer"&gt;&lt;code&gt;@react-native-ai/adk&lt;/code&gt;&lt;/a&gt; is another AI SDK provider, so it would be one more branch in &lt;code&gt;pickModel&lt;/code&gt;. Its availability check comes in steps: &lt;code&gt;isNanoSupported()&lt;/code&gt; (can this phone run it?), &lt;code&gt;isAvailable()&lt;/code&gt; (is it ready?) and &lt;code&gt;prepareNano()&lt;/code&gt;. Given what happened with Apple's flag, I'd still add a probe call. ExecuTorch also runs on Android, using Vulkan for the GPU, so the downloaded-model branch carries over unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to use, and when
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use Apple's Foundation Models first on iOS.&lt;/strong&gt; There's nothing to download, it doesn't use your app's memory, and guided generation makes structured output reliable. Expect a slow first call, so warm the model up before the user needs it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use ExecuTorch when there's no system model&lt;/strong&gt;, for short free-text tasks where a person reads the result: rewording, suggestions, drafts. Check its output before you act on it. Ask before the 1 GB download, show progress, and turn off telemetry. Don't depend on a 1B model for JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep a cloud fallback&lt;/strong&gt;, and label it in the UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch &lt;code&gt;expo-ai&lt;/code&gt;.&lt;/strong&gt; It's in review with an Apple provider, streaming and tool calls. If it lands, most of the setup fixes above become Expo's problem rather than yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Put both engines behind the AI SDK so the calling code stays the same; ExecuTorch needs a small adapter.&lt;/li&gt;
&lt;li&gt;Pin ai@6 for the React Native AI providers, and add stream, TextDecoder and structuredClone polyfills for Hermes.&lt;/li&gt;
&lt;li&gt;ExecuTorch 0.10.4 needs a one-file patch to build on React Native 0.87 iOS.&lt;/li&gt;
&lt;li&gt;Don't trust apple.isAvailable() alone: make a tiny probe call and cache the result.&lt;/li&gt;
&lt;li&gt;Use ExecuTorch's .DEFAULT variants; MLX variants download and then fail on the simulator.&lt;/li&gt;
&lt;li&gt;A 1B model returned schema-valid JSON 2 times in 20; an example prompt and a repair step got it to 6 in 10.&lt;/li&gt;
&lt;li&gt;Apple's guided generation returned valid output 20 times in 20.&lt;/li&gt;
&lt;li&gt;Turn off ExecuTorch telemetry, and show users when a request leaves the device.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I write about React Native in practice, measured and with public code, at &lt;a href="https://www.praveensingh.co.in/blog" rel="noopener noreferrer"&gt;praveensingh.co.in/blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ai</category>
      <category>mobile</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Giving your coding agent eyes on a React Native app: Argent vs agent-device, measured</title>
      <dc:creator>Praveen Singh</dc:creator>
      <pubDate>Fri, 09 Oct 2026 10:50:06 +0000</pubDate>
      <link>https://dev.to/praveensingh/giving-your-coding-agent-eyes-on-a-react-native-app-argent-vs-agent-device-measured-1blh</link>
      <guid>https://dev.to/praveensingh/giving-your-coding-agent-eyes-on-a-react-native-app-argent-vs-agent-device-measured-1blh</guid>
      <description>&lt;p&gt;&lt;strong&gt;At a glance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Problem: Let a coding agent run, tap through and profile a React Native app by itself&lt;/li&gt;
&lt;li&gt;Fix: Argent or agent-device, each given alone to Claude Code, on three planted bugs&lt;/li&gt;
&lt;li&gt;Result: 12 / 12 (runs fully solved, by both tools; agent-device used about a fifth of the tokens)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Both tools let a coding agent reproduce, fix, verify and profile real React Native bugs. Every one of the 12 scored runs got full marks. The difference is cost and setup. agent-device runs used about a fifth of the tokens and a third of the money, because it answers with compact text instead of a screenshot after every action. Argent's React profiler worked with no setup on React Native 0.87, while agent-device's needed a package installed and two project files patched first.&lt;/p&gt;

&lt;p&gt;Coding agents used to stop at "it compiles". In 2026 a set of tools lets them run the app, tap through it, read the screen and profile it. Two are built with React Native in mind: Software Mansion's &lt;a href="https://argent.swmansion.com/" rel="noopener noreferrer"&gt;Argent&lt;/a&gt; and Callstack's &lt;a href="https://github.com/callstack/agent-device" rel="noopener noreferrer"&gt;agent-device&lt;/a&gt;. Both claim to close the loop. I wanted to know what that looks like on a real app, and what it costs.&lt;/p&gt;

&lt;p&gt;So I planted three bugs in a small React Native 0.87 app and gave Claude Code the same bug reports twice: once with only Argent, once with only agent-device. Everything is in &lt;a href="https://github.com/psingh2907/rn-agent-sim-lab" rel="noopener noreferrer"&gt;a public repo&lt;/a&gt;: the app, the harness, the rubric, every transcript and every score.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The setup.&lt;/strong&gt; The store-audit app from my &lt;a href="https://www.praveensingh.co.in/blog/react-native-0-87-upgrade" rel="noopener noreferrer"&gt;0.87 upgrade post&lt;/a&gt;, on React Native 0.87.1 with FlashList, in the iPhone 17 simulator on an Apple M1 with 8 GB of memory. Argent 0.26.0 (its MCP server, skills and rules, as &lt;code&gt;argent init&lt;/code&gt; installs them) against agent-device 0.21.19 (its CLI, skills from &lt;code&gt;npx skills add&lt;/code&gt;, and the &lt;code&gt;CLAUDE.md&lt;/code&gt; text its docs recommend). Claude Code 2.1.42 in headless mode with &lt;code&gt;claude-sonnet-5-5&lt;/code&gt;, one session per run. Three tasks, two runs per tool per task, 12 scored runs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The three bugs
&lt;/h2&gt;

&lt;p&gt;Each run got one bug report, written the way a store auditor would send it, plus a line saying the app is installed and Metro is running. Nothing pointed at files or causes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce:&lt;/strong&gt; "I tapped yes on 'Fire exit clear of obstructions'. When I scrolled down, other questions I never touched showed yes too." Each row kept its own copy of the answer in &lt;code&gt;useState&lt;/code&gt;, and FlashList recycles rows, so a recycled row shows the previous question's answer. The agent had to reproduce it in the app and explain it, without changing code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix and verify:&lt;/strong&gt; "The yes/no buttons are missing on questions with long titles." The question text couldn't shrink, so long titles pushed the buttons off screen. The agent had to fix it and prove both long and short rows look right in the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance:&lt;/strong&gt; "Typing in the Notes box feels laggy." The note's state lived in the screen, so every keystroke re-rendered every visible row, and each row ran a slow keyword check (about 8 ms per row). The agent had to find out why with real measurements, fix it, and measure again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wrote the &lt;a href="https://github.com/psingh2907/rn-agent-sim-lab/blob/main/harness/RUBRIC.md" rel="noopener noreferrer"&gt;scoring rubric&lt;/a&gt; and committed it before the first scored run: three points per task, and points that need evidence from the running app don't count if the agent only read the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it a fair test took four fixes
&lt;/h2&gt;

&lt;p&gt;A comparison like this is only fair if each session sees exactly one tool. My machine has Argent installed globally, so that took more work than I expected:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--setting-sources project&lt;/code&gt; still loaded my global &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; and rules, including Argent's. A probe session in the agent-device setup quoted Argent's tapping rule back to me&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLAUDE_CODE_DISABLE_CLAUDE_MDS=1&lt;/code&gt; and &lt;code&gt;CLAUDE_CODE_DISABLE_AUTO_MEMORY=1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argent's global skills are relative symlinks, so copying them into a plugin folder copied broken links&lt;/td&gt;
&lt;td&gt;Copy with &lt;code&gt;cp -RL&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;My lab's commit messages described the planted bugs, and agents can read &lt;code&gt;git log&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Agents work in a separate copy of the app with a single, history-free commit, reset between runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One agent installed a package to profile, then removed it, leaving empty folders in &lt;code&gt;node_modules&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm prune&lt;/code&gt; and remove empty folders before each run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each tool's skills went in through &lt;code&gt;--plugin-dir&lt;/code&gt;, its MCP config through &lt;code&gt;--strict-mcp-config&lt;/code&gt;, and its rules through &lt;code&gt;--append-system-prompt&lt;/code&gt;. The other tool's command was shadowed on &lt;code&gt;PATH&lt;/code&gt;. A probe run before the real ones confirmed that each session listed only its own tool.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha: You can't start Claude Code from inside Claude Code.&lt;/strong&gt; Running &lt;code&gt;claude -p&lt;/code&gt; from an agent session fails with "Claude Code cannot be launched inside another Claude Code session". I started the runs from a separate terminal. The command-line tool also needs its own &lt;code&gt;claude auth login&lt;/code&gt;; being signed in to the desktop app isn't enough.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Both tools solved every task
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Argent&lt;/th&gt;
&lt;th&gt;agent-device&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reproduce&lt;/td&gt;
&lt;td&gt;6 / 6&lt;/td&gt;
&lt;td&gt;6 / 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix and verify&lt;/td&gt;
&lt;td&gt;6 / 6&lt;/td&gt;
&lt;td&gt;6 / 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;6 / 6&lt;/td&gt;
&lt;td&gt;6 / 6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every run reproduced or verified in the running app, named the right cause, and the fixes type-checked. In two runs the agent went beyond the report: one Argent run noticed the recycled-row bug while fixing the layout bug, and one agent-device run fixed the slow keyword check as well as the state problem, after checking that the new version gives identical results on all 48 questions.&lt;/p&gt;

&lt;p&gt;So on tasks like these, either tool is enough. The model does the reasoning; the tool decides how expensive it is to see the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where they differed: context and cost
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Median per run&lt;/th&gt;
&lt;th&gt;Argent&lt;/th&gt;
&lt;th&gt;agent-device&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;138 s&lt;/td&gt;
&lt;td&gt;108 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool calls&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;16.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshots in the conversation&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text returned by tools&lt;/td&gt;
&lt;td&gt;59 KB&lt;/td&gt;
&lt;td&gt;17 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens processed&lt;/td&gt;
&lt;td&gt;1.84 M&lt;/td&gt;
&lt;td&gt;0.34 M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per run (average)&lt;/td&gt;
&lt;td&gt;$1.27&lt;/td&gt;
&lt;td&gt;$0.41&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The difference comes from what each tool sends back. Every Argent interaction (tap, swipe, launch) returns a screenshot and the full accessibility tree, so the agent always sees the screen, and every image stays in the conversation. agent-device's commands return a compact text snapshot or a diff, with short references like &lt;code&gt;@e8&lt;/code&gt; to tap, and screenshots only when the agent asks for one. Over a session, that's the difference between about 1.8 million and 0.3 million tokens.&lt;/p&gt;

&lt;p&gt;The text-first approach had one concrete advantage in this app. agent-device's snapshot printed the pill's selected state as text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@e29 [button] "yes for Entrance mats clean" [selected]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Argent's tree didn't include the selected state, so Argent's agent found the stray "yes" by looking at screenshots. One of its reports says so directly. Both got there; one needed the image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where they differed: profiling setup
&lt;/h2&gt;

&lt;p&gt;Both tools can profile React renders, and both agents found and measured the slow typing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Performance runs&lt;/th&gt;
&lt;th&gt;Argent&lt;/th&gt;
&lt;th&gt;agent-device&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Before the fix&lt;/td&gt;
&lt;td&gt;155–179 ms per slow commit, 162 row re-renders&lt;/td&gt;
&lt;td&gt;145–153 ms average per keystroke, every visible row re-rendered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After the fix&lt;/td&gt;
&lt;td&gt;no commit over 16 ms&lt;/td&gt;
&lt;td&gt;about 1 ms per keystroke&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time per run&lt;/td&gt;
&lt;td&gt;195 s, 229 s&lt;/td&gt;
&lt;td&gt;296 s, 517 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Argent's profiler worked straight away on React Native 0.87. It also sampled the CPU, which pointed straight at the slow code: &lt;code&gt;RegExp&lt;/code&gt; construction took 943 ms of the 1.6 seconds of JavaScript work while typing.&lt;/p&gt;

&lt;p&gt;agent-device's React DevTools needed setup first. On 0.87 both agent-device runs had to install &lt;code&gt;agent-react-devtools&lt;/code&gt; into the project and run its &lt;code&gt;init&lt;/code&gt;, which patches &lt;code&gt;index.js&lt;/code&gt; and &lt;code&gt;metro.config.js&lt;/code&gt;. Both agents undid it afterwards, but it made these its slowest runs. In one, a &lt;code&gt;react-devtools wait --connected&lt;/code&gt; call hung and the agent had to restart the helper. Even so, agent-device's performance runs cost less: $0.80 each against $2.00.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha: Check what a profiling setup leaves behind.&lt;/strong&gt; One agent installed the DevTools package with &lt;code&gt;--save-dev&lt;/code&gt;, so it went into &lt;code&gt;package.json&lt;/code&gt; until the agent removed it again. If you let an agent set up profiling in your real repo, review the diff for &lt;code&gt;index.js&lt;/code&gt;, &lt;code&gt;metro.config.js&lt;/code&gt; and &lt;code&gt;package.json&lt;/code&gt; changes before you commit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Different habits
&lt;/h2&gt;

&lt;p&gt;One pattern held in all four fix-and-verify runs. Both Argent runs looked at the broken screen first, then changed the code and checked again. Both agent-device runs changed the code straight from reading it, then checked the app. The rubric didn't require seeing the bug first, and all four fixes were correct. But on a less obvious bug, fixing what the code suggests rather than what the app shows could miss.&lt;/p&gt;

&lt;p&gt;I can't tell from four runs whether this comes from the tools. agent-device's guidance tells the agent to "start immediately", and every Argent action shows the screen, which may encourage looking first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers don't show
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One model, one app, two runs per task.&lt;/strong&gt; I planned three runs; after two rounds every run had full marks, so I stopped. These tasks didn't separate the tools on success; harder tasks might.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One run was cut off.&lt;/strong&gt; The account running the sessions hit its usage limit during Argent's second performance run. I re-ran it once the limit reset. The cut-off attempt is in the repo, excluded from the totals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The simulator, not a phone.&lt;/strong&gt; Both tools also drive Android and physical devices; I tested neither.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I scored them with Claude.&lt;/strong&gt; The rubric was committed first, the points need evidence from tool output rather than the agent's own claims, and every transcript is public so you can check.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which should you use?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use agent-device when you run agents on the app a lot&lt;/strong&gt;: many UI checks, CI, or long sessions. It was about three times cheaper per run and quicker on the everyday tasks, and its text snapshots carry states like &lt;code&gt;[selected]&lt;/code&gt; that you'd otherwise need a screenshot for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Argent when profiling is the main job&lt;/strong&gt;, or you want the agent to see the screen by default. Its React and CPU profiler worked on React Native 0.87 with no project changes, and it pointed straight at the slow code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Either way, help the tool.&lt;/strong&gt; Both rely on the accessibility tree. The planted app had &lt;code&gt;accessibilityLabel&lt;/code&gt; and &lt;code&gt;accessibilityState&lt;/code&gt; on its buttons, and that's what let both agents find the right row and read its state. If your app doesn't, the agent falls back to screenshots and coordinates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Both Argent and agent-device let Claude Code reproduce, fix, verify and profile real React Native bugs: 12 of 12 runs got full marks.&lt;/li&gt;
&lt;li&gt;agent-device used about a fifth of the tokens and a third of the cost, because it returns text snapshots instead of a screenshot after every action.&lt;/li&gt;
&lt;li&gt;Argent's React and CPU profiler worked on React Native 0.87 with no setup; agent-device's needed agent-react-devtools installed and two files patched.&lt;/li&gt;
&lt;li&gt;Review the diff after an agent sets up profiling: index.js, metro.config.js and package.json can change.&lt;/li&gt;
&lt;li&gt;Add accessibilityLabel and accessibilityState to interactive elements; both tools read the accessibility tree first.&lt;/li&gt;
&lt;li&gt;To compare agent tools fairly, isolate each session: --setting-sources project doesn't stop global CLAUDE.md and rules loading.&lt;/li&gt;
&lt;li&gt;Keep git history out of the agent's workspace if your commits describe the bugs you're testing.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I write about React Native in practice, measured and with public code, at &lt;a href="https://www.praveensingh.co.in/blog" rel="noopener noreferrer"&gt;praveensingh.co.in/blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ai</category>
      <category>testing</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
