You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+40-36Lines changed: 40 additions & 36 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,16 +1,16 @@
1
1
# Agentic Task Manager
2
2
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.
4
4
5
5
## Overview
6
6
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.
8
8
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.
10
10
11
-
## Project layout
11
+
## Architecture
12
12
13
-
Two Gradle modules, with a strict boundary between them.
13
+
Two Gradle modules with a strict boundary between them.
14
14
15
15
```
16
16
:domain pure Kotlin/JVM, no Android
@@ -26,47 +26,47 @@ Two Gradle modules, with a strict boundary between them.
26
26
di/ Hilt
27
27
```
28
28
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.
30
30
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.
32
32
33
33
## The dependency graph
34
34
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.
36
36
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.
38
38
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.
40
40
41
41
## Exposed functions
42
42
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.
44
44
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.
50
50
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.
52
52
53
-
## Building
53
+
## Build and run
54
54
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).
56
56
57
57
```
58
58
./gradlew :app:assembleDebug # build the app
59
59
./gradlew :domain:test # domain unit tests
60
60
./gradlew test # all unit tests
61
61
```
62
62
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/`.
64
64
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.
66
66
67
67
## Invoking the functions
68
68
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.
70
70
71
71
List what the system has indexed for the app:
72
72
@@ -88,11 +88,11 @@ The official [Testing Agent](https://github.com/android/appfunctions) discovers
88
88
89
89
## Testing
90
90
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.
92
92
93
93
## Reliability
94
94
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 naturallanguage, 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.
96
96
97
97
Early results on a balanced 24-intent subset (free tier, single run):
98
98
@@ -101,37 +101,41 @@ Early results on a balanced 24-intent subset (free tier, single run):
101
101
| OpenAI gpt-4.1 | 100% / 100% | 1.00 | 1.00 |
102
102
| Ministral 3B | 83% / 79% | 0.25 | 1.00 |
103
103
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.
105
105
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.
107
107
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.
109
109
110
-
Starting state — four actionable tasks:
110
+
### A run end to end
111
111
112
-
<imgsrc="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.
113
113
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
+
<imgsrc="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:
115
119
116
120
```
117
121
addTask({ "title": "Call the plumber this afternoon", "priority": "HIGH" })
118
122
```
119
123
120
-
That call runs on the device, and the UI updates on its own — five 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.
121
125
122
-
<imgsrc="assets/demo-after.png"alt="Result: five actionable tasks, the new one added"width="260">
126
+
<imgsrc="assets/demo-after.png"alt="Result with five actionable tasks, the new one added"width="260">
123
127
124
-
## Notes on some decisions
128
+
## Design notes
125
129
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.
127
131
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.
129
133
130
134
`@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.
131
135
132
136
## Status
133
137
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.
0 commit comments