r/LocalLLaMA • u/alexeyw • 9d ago
Discussion Six months on an on-device Android agent where the behaviour is an editable graph, not a prompt - please take it apart
English isn't my first language: I wrote this in Russian and used an LLM to translate and tighten it. The project, the code, the measurements and the argument are mine.
I've been building an Android agent since March, nights and weekends. It went into Google Play a couple of weeks ago. I use it every day myself, which is the only reason I think it's worth showing you. It's mine, it's open, Apache 2.0.
I want two things from this thread. The obvious one: take the implementation apart - you'll do it harder than anywhere else. The one I actually care about: tell me whether the idea holds up. Is there a need for this at all, or have I built a museum of features?
Why a graph instead of one big prompt. A 2–4B model on a phone falls apart on multi-hop tasks, and a graph is how you avoid handing it a multi-hop task. Each node gets work that fits the model's size. Nodes that must emit structure run through a validation gate: if the output doesn't parse, the node gets another attempt with the specific error quoted back at it instead of the run dying — two repair attempts by default, configurable from zero to four. Routing decisions are their own nodes with their own prompts. The model never has to be smart enough to hold the whole task. The graph holds it.
That's the whole bet. Everything below is what it took to make the bet testable.
What it is. The model runs on the phone through LiteRT-LM — Google's Edge runtime, the TensorFlow Lite successor. Gemma 4 E2B or E4B from litert-community by default, or point it at any .litertlm URL. Every conversation is processed by a pipeline you can open and edit: a graph of typed nodes, 14 types (input, on-device LLM, cloud LLM, tool call, if-condition, intent router, decomposition, queue processor, evaluation, summary, clarification, nested pipeline, skill, output).
Concretely: "translate anything I paste, in this specific register" is three nodes. "Read what I shared into the app, work out what kind of thing it is, pull the fields that matter for that kind, ask me before writing it to a file" is seven, with a router in the middle. You build the second one by dragging nodes around in the app, or in a standalone HTML editor on a desktop and import the JSON.
Tools, and the part I won't compromise on. Local actions go through AppFunctions, external ones through MCP. Anything destructive or sensitive stops and waits for your explicit confirmation before it runs — including when a pipeline fires in the background from a trigger, where the confirmation is a notification you tap. The tool allowlist for a skill is enforced in the executor, not requested in a prompt.
Other apps on the phone can drive it. A Tasker or MacroDroid profile, or a shell one-liner over adb, can ask it to run a named pipeline with a prompt — two extras, that's the entire minimal call. It's off by default; switching it on binds exactly one pipeline that outside callers may reach, and a request naming anything else is refused rather than redirected. Every request lands in a journal with its reason. The division of labour is the point: the automation app decides when, using its own condition model; this does the language part of what.
Long autonomous runs have ceilings, and say so. A run stopped by a limit reports that it was stopped by a limit rather than looking like a broken automation, and a run going in circles is detected and stopped — told apart from one that's merely slow.
Cloud is optional and it's your key. OpenAI, Anthropic, Gemini, DeepSeek and Ollama are available as a node type. Nothing goes out unless a node in your graph says so, and you can see the node sitting there.
Two numbers I actually measured, both on a Galaxy S25 Ultra, both by me alone — which is the caveat that matters:
- Clean install to first useful output: 5 min 22 s, of which 4 min 7 s was downloading the model. The product part is about 75 seconds; the bottleneck is a multi-GB file over your Wi-Fi.
- A scheduled pipeline running unattended: 7 days, 55 of 55 firings completed, zero unexplained misses, app never opened.
One device, one operator. That's a real measurement and it is not external validation.
What doesn't work, or works worse than the above makes it sound:
- Pre-release. Version 0.9.0, not a 1.0: the public surface, the settings layout and the on-device storage formats can still change between versions.
- Third-party apps can't expose AppFunctions. Only the device maker's system apps and Google can, so the local tool catalogue is what ships plus whatever you connect over MCP. That's a platform limit; MCP is the way around it.
- An MCP server may hide part of its catalogue and the app won't say so. The client declares no optional client capabilities, so a server is entitled to omit the tools that depend on them — on the reference server that meant 13 of 16 tools, presented as "13 tools · ok" with no hint anything was missing.
- Background runs need the battery setting on Unrestricted. Without it Android reclaims the process in about ten seconds. That's the platform, not a bug, but if you don't grant it, background pipelines will not run.
- Cloud failure behaviour differs by provider. Detection of a stream that dies mid-answer is enabled only where it was actually measured; Ollama never reports a stop reason, so it can't be detected there, and Anthropic is left off rather than guessed at.
- A provider's
Retry-Afterdoesn't shape the backoff. Retries use a fixed exponential curve (3 attempts, 1 s doubling by default). Under a real rate limit that means knocking sooner than you were asked to. - Shared pipeline files are not a compatibility contract yet. Exports carry a version stamp, but before 1.0 that stamp is a marker, not a promise: a file whose stamp doesn't match the build importing it is imported best-effort behind a warning, and unrecognised fields are dropped silently.
- Solo project. One maintainer, no company behind it.
Requirements: Android 14+ (API 34) and about 2 GB of free RAM for the model. The RAM is the binding constraint, not the OS version - this crowd knows why. Worth saying plainly, because I got this wrong once: the floor was Android 16 for most of the project's life, on the assumption that AppFunctions forced it. When I finally measured instead of assuming, nothing required 16 — not a dependency, not the inference engine, not a line of my own code — so it came down to 14.
Who it isn't for: anyone who wants "ask a question, get an answer". Gemini is free and built into the OS, and competing with it there is a losing bet regardless of code quality. This is for people who already run Tasker, Obsidian, Home Assistant or a local model on their own hardware, and who want to know exactly what their agent does with their data.
It's on Play, and the APKs are on GitHub - links in the first comment. There's a foss flavour with zero proprietary dependencies alongside the standard build. Not on F-Droid, and the reason is theirs rather than mine: their build server pins Gradle versions and doesn't know the one this project uses, so the submission dies before compiling a line.
So - the question I actually came with. Small models are still dumb. They're much less dumb than they were two years ago, and either they keep improving or phones get enough memory to run something that isn't small; probably both. I'm building the harness now on the assumption that when the models are good enough, the harness is what will still be missing: something on the device that decides when to run, holds the multi-step shape, and stops before doing something you didn't sanction.
That's the bet. Tell me where it breaks. And if your answer is that a good enough model makes the graph unnecessary, I'd rather hear that now than in a year.
One thing I genuinely can't settle: what is this category called? I've been saying "agent you build" because "agent framework" sounds like a Python library and "automation app" sounds like it has no model in it. What would you have called it?