Skip to content

Commit a8bd552

Browse files
README: professional rewrite; tighter structure, precise, complete
1 parent af511e1 commit a8bd552

1 file changed

Lines changed: 40 additions & 36 deletions

File tree

README.md

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# Agentic Task Manager
22

3-
An Android task manager that exposes its operations as [AppFunctions](https://developer.android.com/ai/appfunctions) so an on-device agent can call them from natural language. Gemini is the reference agent, but the functions are designed and measured to route reliably across function-calling models, not just one. The UI is intentionally minimal.
3+
An Android task manager whose operations are exposed as [AppFunctions](https://developer.android.com/ai/appfunctions), so an on-device agent can carry them out from natural language. Gemini is the reference agent, but the functions are written and measured to route reliably across function-calling models rather than a single one. The user interface is deliberately minimal; the subject of the project is the agent-facing layer and how reliably it can be driven.
44

55
## Overview
66

7-
AppFunctions is the Jetpack layer that lets an app publish selected operations as tools an agent can invoke, roughly the Android counterpart of MCP. This project publishes five of them over a domain that has enough structure to be worth reasoning about: tasks form a dependency graph rather than a flat list.
7+
AppFunctions is the Jetpack layer that lets an app publish selected operations as tools an agent can call. It is roughly the Android counterpart of MCP. This project publishes five such operations over a domain with enough structure to be worth reasoning about: tasks form a dependency graph rather than a flat list.
88

9-
Everything turns on one rule, actionability. A task can depend on other tasks, and it is actionable only once every task it depends on is completed. Actionability is computed by walking the graph rather than stored, so completing one task immediately re-derives what its dependents can do next. The dependency edges have to stay acyclic, and an edge that would close a cycle is rejected.
9+
The rule that holds the domain together is actionability. A task can depend on other tasks and becomes actionable only once every task it depends on is complete. Actionability is computed by walking the graph rather than stored, so completing one task immediately re-derives what its dependents can do next. Dependency edges must stay acyclic, and an edge that would close a cycle is rejected before it is saved.
1010

11-
## Project layout
11+
## Architecture
1212

13-
Two Gradle modules, with a strict boundary between them.
13+
Two Gradle modules with a strict boundary between them.
1414

1515
```
1616
:domain pure Kotlin/JVM, no Android
@@ -26,47 +26,47 @@ Two Gradle modules, with a strict boundary between them.
2626
di/ Hilt
2727
```
2828

29-
`:domain` does not apply the Android Gradle plugin, so it cannot reference Android even by accident; that is enforced by the compiler, not by convention. Dependencies run inward toward it. The UI depends on use cases, the data layer implements a domain interface, and Hilt is the only place that binds an implementation to an interface. The use cases carry no DI annotations and are constructed in a Hilt module, which keeps the domain free of any framework.
29+
`:domain` does not apply the Android Gradle plugin, so it cannot reference the Android framework even by accident; the boundary is enforced by the compiler rather than by convention. Dependencies point inward toward it: the UI depends on use cases, the data layer implements a domain interface, and Hilt is the only place that binds an implementation to that interface. The use cases carry no dependency-injection annotations and are constructed in a Hilt module, which keeps the domain free of any framework. Because the domain has no Android dependency, its tests run on a plain JVM without an emulator.
3030

31-
The classes under `agent/` are deliberately thin: they parse the agent's input, delegate to a use case, and map the result to a serializable type. No domain logic lives there.
31+
The classes under `agent/` are intentionally thin. They parse the agent's input, delegate to a use case, and map the result to a serializable type. No domain logic lives there.
3232

3333
## The dependency graph
3434

35-
`Task` does not store its dependencies. They are edges, `DependencyEdge(dependent, prerequisite)`, owned by the graph, which keeps both cycle detection and actionability as pure graph operations.
35+
`Task` does not store its own dependencies. They are edges, `DependencyEdge(dependent, prerequisite)`, owned by the graph, which keeps both cycle detection and actionability as pure graph operations.
3636

37-
Before an edge is persisted, `DependencyGraph` checks whether the prerequisite can already reach the dependent. If it can, the edge would create a cycle and is refused. The reachability search is iterative, so deep chains do not overflow the stack and a graph that is already cyclic still terminates. Actionability treats an unknown prerequisite as not completed, so a dangling edge keeps a task blocked instead of silently releasing it.
37+
Before an edge is saved, `DependencyGraph` checks whether the prerequisite can already reach the dependent. If it can, the edge would close a cycle and is refused. The reachability search is iterative, so deep chains do not overflow the stack and a graph that is already cyclic still terminates. Actionability treats an unknown prerequisite as not complete, so a dangling edge keeps a task blocked instead of releasing it silently.
3838

39-
Time comes from an injected `Clock` and ids from an injected generator, so overdue checks, recurrence, and id assignment are deterministic under test. The graph and use cases have 41 unit tests that run without a device.
39+
Time comes from an injected `Clock` and identifiers from an injected generator, so overdue checks, recurrence, and id assignment are deterministic under test. The graph and use cases are covered by 41 unit tests that run without a device.
4040

4141
## Exposed functions
4242

43-
The five functions live under `agent/` and cover the cases an app runs into when it becomes agent-callable:
43+
The five functions live under `agent/` and cover the cases an app meets when it becomes agent-callable.
4444

45-
- `getActionableTasks` open tasks whose prerequisites are all completed.
46-
- `getBlockingOverdueTasks` overdue tasks that are blocking at least one other open task.
47-
- `addTask` creates a task, optionally linking prerequisites by id or by natural-language title.
48-
- `completeTask` completes a task and reports which dependents it unblocked.
49-
- `deleteTask` destructive, so it confirms first; the initial call only describes what would be removed.
45+
- `getActionableTasks` returns open tasks whose prerequisites are all complete.
46+
- `getBlockingOverdueTasks` returns overdue tasks that block at least one other open task.
47+
- `addTask` creates a task, optionally linking prerequisites by id or by natural-language title.
48+
- `completeTask` completes a task and reports which dependents it unblocked.
49+
- `deleteTask` is destructive, so it confirms first; the initial call only describes what would be removed.
5050

51-
Each function's KDoc is written as the description the agent reads. The annotation processor encodes it into the function metadata, which makes it the prompt that decides whether the agent picks the right tool, not documentation for a human.
51+
Each function's KDoc is written as the description the agent reads. The annotation processor encodes it into the function metadata, which makes it the prompt that decides whether the agent picks the right tool, not documentation meant for a person.
5252

53-
## Building
53+
## Build and run
5454

55-
Requires JDK 17, the Android SDK, and an API 36+ emulator or device with Google Play (AppFunctions needs Android 16 and Play services).
55+
Requirements: JDK 17, the Android SDK, and an API 36 or newer emulator or device with Google Play (AppFunctions needs Android 16 and Play services).
5656

5757
```
5858
./gradlew :app:assembleDebug # build the app
5959
./gradlew :domain:test # domain unit tests
6060
./gradlew test # all unit tests
6161
```
6262

63-
In Android Studio, open the project, start an API 36+ Play emulator, and run the `app` configuration (committed under `.run/`).
63+
In Android Studio, open the project, start an API 36+ Play emulator, and run the `app` configuration committed under `.run/`.
6464

65-
The current AndroidX libraries require AGP 9.1+ and compileSdk 37, so the project uses AGP 9.2.1, Gradle 9.4.1, Kotlin 2.3.21 and KSP 2.3.9, with compileSdk 37 and minSdk/targetSdk 36. AGP 9 ships built-in Kotlin and a new DSL that KSP does not yet support, so `gradle.properties` sets `android.builtInKotlin=false` and `android.newDsl=false` to keep the Kotlin-plus-KSP path. Both flags can be dropped once KSP supports built-in Kotlin.
65+
Current AndroidX libraries require AGP 9.1 or newer and compileSdk 37, so the project uses AGP 9.2.1, Gradle 9.4.1, Kotlin 2.3.21, and KSP 2.3.9, with compileSdk 37 and minSdk/targetSdk 36. AGP 9 ships built-in Kotlin and a new DSL that KSP does not yet support, so `gradle.properties` sets `android.builtInKotlin=false` and `android.newDsl=false` to keep the Kotlin-with-KSP path. Both flags can be removed once KSP supports built-in Kotlin.
6666

6767
## Invoking the functions
6868

69-
The on-device Gemini integration is currently in private preview, so the functions are driven through paths that work without it.
69+
The on-device Gemini integration is in private preview, so the functions are exercised through paths that do not depend on it.
7070

7171
List what the system has indexed for the app:
7272

@@ -88,11 +88,11 @@ The official [Testing Agent](https://github.com/android/appfunctions) discovers
8888

8989
## Testing
9090

91-
The domain has unit tests for the graph (cycle detection and reachability), actionability, and each use case, including the cascade unlock and the recurrence date math. The data layer has tests for the entity-to-domain mappers. End-to-end execution is verified through `adb` and the Testing Agent (and measured across models — see Reliability below).
91+
The domain has unit tests for the graph (cycle detection and reachability), actionability, and each use case, including the cascade unlock and the recurrence date arithmetic. The data layer has tests for the entity-to-domain mappers. End-to-end execution is verified through `adb` and the Testing Agent, and measured across models as described below.
9292

9393
## Reliability
9494

95-
A harness under `tools/reliability-harness` measures how reliably a model turns a natural-language request into the right function call. Gemini is the reference, but the same dataset runs against GitHub Models (OpenAI, DeepSeek, Mistral, Meta), because the goal is a function surface that any capable model can drive. The dataset is 68 intents covering the five functions plus deliberately out-of-scope requests; it is run twice — with the real descriptions ("rich") and with one-line stand-ins ("terse") to see how much the wording moves accuracy.
95+
A harness under `tools/reliability-harness` measures how often, when a request is phrased in natural language, the agent picks the right function with the right arguments, and whether that holds across models rather than a single one. It sends each request to a model together with the five functions as tool declarations, records the function and arguments the model chose, and scores three things: function accuracy, restraint on out-of-scope requests, and parameter accuracy. The dataset is 68 intents covering the five functions plus deliberately out-of-scope requests. Each run is repeated with the real descriptions ("rich") and with one-line stand-ins ("terse") to measure how much the wording moves accuracy.
9696

9797
Early results on a balanced 24-intent subset (free tier, single run):
9898

@@ -101,37 +101,41 @@ Early results on a balanced 24-intent subset (free tier, single run):
101101
| OpenAI gpt-4.1 | 100% / 100% | 1.00 | 1.00 |
102102
| Ministral 3B | 83% / 79% | 0.25 | 1.00 |
103103

104-
The useful signal is that a capable model routes the surface perfectly: gpt-4.1 picks the right function every time, with correct parameters, and correctly declines the out-of-scope requests — so the functions and their descriptions are unambiguous. The 3B model routes most intents but lacks restraint, calling a function on requests it should refuse; that is a model-size limit, not an ambiguity in the surface. These are small-subset, single-run numbers and should be read as directional; full 68-intent runs and more models are still to come.
104+
A capable model routes the surface perfectly: gpt-4.1 picks the right function every time, extracts the right parameters, and declines the out-of-scope requests. That is the useful signal, because it says the functions and their descriptions are unambiguous. The 3B model routes most intents but lacks restraint, calling a function on requests it should refuse, which is a limit of model size rather than of the surface. These are small-subset, single-run figures and should be read as directional; fuller runs and more models are planned.
105105

106-
`tools/reliability-harness/agent_demo.py` closes the loop end to end: it sends a natural-language instruction to a model, takes the function the model chooses, and executes it on the running app via `adb`, so the result shows up in the UI. It is the same loop the on-device assistant would run, with `adb` standing in for the preview-gated system integration.
106+
One result was not obvious in advance: verbose safety wording can lower accuracy. A description that leads with warnings ("destructive and irreversible, confirm first") can make a model abstain from a call it should make. The descriptions are therefore written action-first: they state plainly when to call the function and express the safety semantics as instructions to act safely, for example that deletion is always safe to call because the first call only previews what would be removed. The harness is how that choice is checked and re-checked across models.
107107

108-
### A run, end to end
108+
Scoring is exact for the function choice and lenient where exactness would be unfair: a title matches on a normalized substring, a dependency matches whether it was passed as a title or an id, and priority and recurrence match case-insensitively. Calls run at temperature 0, but hosted models are not perfectly deterministic and per-goal counts are small, so a single small difference is treated as noise and the signal is read from consistent patterns.
109109

110-
Starting state — four actionable tasks:
110+
### A run end to end
111111

112-
<img src="assets/demo-before.png" alt="Starting state: four actionable tasks" width="260">
112+
`tools/reliability-harness/agent_demo.py` closes the loop. It sends a natural-language instruction to a model, takes the function the model chooses, and runs it on the live app through `adb`, so the result appears in the UI. It is the same loop the on-device assistant would run, with `adb` in place of the preview-gated system integration.
113113

114-
The instruction *"add a high priority task to call the plumber this afternoon"* is sent to a model, which picks one function and its arguments:
114+
Starting state, four actionable tasks:
115+
116+
<img src="assets/demo-before.png" alt="Starting state with four actionable tasks" width="260">
117+
118+
The instruction "add a high priority task to call the plumber this afternoon" is sent to a model, which picks one function and its arguments:
115119

116120
```
117121
addTask({ "title": "Call the plumber this afternoon", "priority": "HIGH" })
118122
```
119123

120-
That call runs on the device, and the UI updates on its ownfive actionable tasks, the new one in place:
124+
The call runs on the device and the UI updates on its own: five actionable tasks, with the new one in place.
121125

122-
<img src="assets/demo-after.png" alt="Result: five actionable tasks, the new one added" width="260">
126+
<img src="assets/demo-after.png" alt="Result with five actionable tasks, the new one added" width="260">
123127

124-
## Notes on some decisions
128+
## Design notes
125129

126-
Enum-typed fields cross the agent boundary as documented strings (`"HIGH"`, `"WEEKLY"`), parsed case-insensitively with a default. This is more tolerant of imperfect agent input; whether a closed enum extracts more reliably is one of the things the harness measures.
130+
Enum-typed fields cross the agent boundary as documented strings (`"HIGH"`, `"WEEKLY"`) and are parsed case-insensitively with a default. This tolerates imperfect agent input; whether a closed enum would extract more reliably is one of the things the harness measures.
127131

128-
The write use cases read, check, then write (load the graph, test for a cycle, persist). Two of them interleaving could each observe an acyclic graph and both commit, closing the cycle the check exists to prevent, so all writers share a single mutex. It matters once an agent and the UI issue calls at the same time.
132+
The write use cases read, check, then write: they load the graph, test for a cycle, and persist. Two of them interleaving could each observe an acyclic graph and both commit, closing the very cycle the check exists to prevent, so all writers share a single mutex. It matters once an agent and the UI issue calls at the same time.
129133

130134
`@AppFunction` and `AppFunctionConfiguration` live in `androidx.appfunctions.service`, while `AppFunctionContext` and the serializable annotations live in `androidx.appfunctions`. The documentation omits the sub-package, and the build error is how you find out.
131135

132136
## Status
133137

134-
The domain, Room persistence, the Compose UI, and the five AppFunctions are implemented and unit-tested. The functions are invocable over `adb` and through the official Testing Agent. The reliability harness is in place; collecting and publishing its numbers is the remaining work.
138+
The domain, Room persistence, the Compose UI, and the five AppFunctions are implemented and unit-tested. The functions are invocable over `adb` and through the official Testing Agent. The reliability harness is in place; broadening its results across more models and the full dataset is the remaining work.
135139

136140
## License
137141

0 commit comments

Comments
 (0)