mobile-debug-mcp 0.30.2 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -5
- package/dist/interact/android.js +13 -8
- package/dist/interact/index.js +33 -22
- package/dist/interact/ios.js +5 -2
- package/dist/manage/android.js +20 -10
- package/dist/manage/ios.js +20 -11
- package/dist/observe/android.js +6 -1
- package/dist/observe/index.js +22 -5
- package/dist/observe/ios.js +7 -2
- package/dist/observe/snapshot-metadata.js +94 -15
- package/dist/server/common.js +14 -7
- package/dist/server/journey-schema.js +60 -0
- package/dist/server/tool-definitions.js +59 -6
- package/dist/server/tool-handlers.js +358 -55
- package/dist/server-core.js +1 -1
- package/dist/system/index.js +485 -48
- package/dist/system/ios.js +15 -7
- package/dist/utils/android/utils.js +4 -4
- package/dist/utils/cli/idb/check-idb.js +10 -3
- package/dist/utils/cli/idb/idb-helper.js +47 -13
- package/dist/utils/exec.js +4 -2
- package/dist/utils/ios/utils.js +10 -11
- package/dist/utils/operation-budget.js +25 -0
- package/dist/utils/resolve-device.js +12 -1
- package/dist/utils/timing.js +19 -0
- package/dist/utils/ui/index.js +2 -0
- package/docs/CHANGELOG.md +6 -0
- package/docs/ROADMAP.md +4 -4
- package/docs/benchmarks/interaction-latency/README.md +26 -0
- package/docs/benchmarks/interaction-latency/final.json +1810 -0
- package/docs/benchmarks/interaction-latency/final.md +17 -0
- package/docs/benchmarks/interaction-latency/ios-validation.md +11 -0
- package/docs/specs/baseline-spec-v0.md +1 -1
- package/docs/specs-deltas/016-environment-tools.md +533 -0
- package/docs/specs-deltas/018-interaction-latency-and-batched-journeys.md +301 -0
- package/docs/tools/TOOLS.md +2 -0
- package/docs/tools/interact.md +30 -0
- package/docs/tools/manage.md +4 -2
- package/docs/tools/system.md +9 -2
- package/package.json +1 -1
- package/src/interact/android.ts +13 -8
- package/src/interact/index.ts +32 -23
- package/src/interact/ios.ts +5 -2
- package/src/manage/android.ts +19 -10
- package/src/manage/ios.ts +19 -11
- package/src/observe/android.ts +5 -1
- package/src/observe/index.ts +21 -5
- package/src/observe/ios.ts +6 -2
- package/src/observe/snapshot-metadata.ts +94 -15
- package/src/server/common.ts +13 -6
- package/src/server/journey-schema.ts +63 -0
- package/src/server/tool-definitions.ts +60 -7
- package/src/server/tool-handlers.ts +324 -53
- package/src/server-core.ts +1 -1
- package/src/system/index.ts +572 -50
- package/src/system/ios.ts +14 -7
- package/src/types.ts +31 -2
- package/src/utils/android/utils.ts +3 -3
- package/src/utils/cli/idb/check-idb.ts +11 -2
- package/src/utils/cli/idb/idb-helper.ts +49 -12
- package/src/utils/exec.ts +4 -2
- package/src/utils/ios/utils.ts +9 -10
- package/src/utils/operation-budget.ts +22 -0
- package/src/utils/resolve-device.ts +13 -1
- package/src/utils/timing.ts +15 -0
- package/src/utils/ui/index.ts +2 -0
- package/test/device/automated/latency/benchmark.ts +136 -0
- package/test/device/automated/latency/report.ts +6 -0
- package/test/fixtures/latency-journey-app/AndroidManifest.xml +6 -0
- package/test/fixtures/latency-journey-app/README.md +10 -0
- package/test/fixtures/latency-journey-app/build.ts +26 -0
- package/test/fixtures/latency-journey-app/src/MainActivity.java +48 -0
- package/test/unit/interact/tap_element.test.ts +1 -1
- package/test/unit/manage/restart_budget.test.ts +37 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +6 -0
- package/test/unit/observe/snapshot_metadata.test.ts +20 -1
- package/test/unit/observe/state_delta.test.ts +15 -0
- package/test/unit/server/benchmark_report.test.ts +4 -0
- package/test/unit/server/contract.test.ts +1 -1
- package/test/unit/server/interaction_modes.test.ts +60 -0
- package/test/unit/server/operation_budget.test.ts +32 -0
- package/test/unit/server/response_shapes.test.ts +3 -3
- package/test/unit/server/run_journey.test.ts +90 -0
- package/test/unit/server/tap_selector.test.ts +68 -0
- package/test/unit/system/get_system_status.test.ts +3 -0
- package/test/unit/system/system_status.test.ts +40 -0
- package/test/unit/utils/idb_health.test.ts +74 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# RFC 018 — Interaction Latency, Verification Modes, and Batched Journeys
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Implemented; Android and iOS device validation passed.
|
|
6
|
+
|
|
7
|
+
## 1. Summary
|
|
8
|
+
|
|
9
|
+
This RFC reduces mobile-debug MCP round-trip latency while retaining an explicit path to the current verification guarantees.
|
|
10
|
+
|
|
11
|
+
It adds optional execution controls to action tools, compact responses, selector-based tapping, state-delta observations, and a batched `run_journey` tool. Existing callers remain compatible: omitted controls retain the current full verification and detailed response behavior.
|
|
12
|
+
|
|
13
|
+
## 2. Problem
|
|
14
|
+
|
|
15
|
+
The current path performs more device work and returns more data than most interaction turns need.
|
|
16
|
+
|
|
17
|
+
- `start_app` captures a screen fingerprint before and after launch, in addition to the launch adapter's current-screen observation.
|
|
18
|
+
- Coordinate actions capture fingerprints before and after dispatch.
|
|
19
|
+
- `tap_element` captures a fingerprint, re-reads the full UI tree to revalidate a stored handle, dispatches, then captures another fingerprint.
|
|
20
|
+
- `capture_debug_snapshot` always starts a screenshot collection, even when the caller needs only hierarchy or logs.
|
|
21
|
+
- A normal agent journey requires separate resolve, tap, wait, expect, and snapshot calls, each with a protocol round trip and repeated device metadata.
|
|
22
|
+
|
|
23
|
+
These checks are valuable when diagnosing an ambiguous result. They are unnecessary for every known-safe, immediately-followed step, and they make the server appear slow even when device execution is prompt.
|
|
24
|
+
|
|
25
|
+
## 3. Goals
|
|
26
|
+
|
|
27
|
+
1. Make the latency of `start_app`, `tap_element`, `wait_for_ui`, `capture_screenshot`, and `capture_debug_snapshot` measurable and reportable.
|
|
28
|
+
2. Let callers choose `none`, `light`, or `full` verification per interaction without weakening the compatibility default.
|
|
29
|
+
3. Make normal responses small and use `debug` only when detailed traces and device metadata are needed.
|
|
30
|
+
4. Resolve and tap a selector in one server request.
|
|
31
|
+
5. Run ordered interaction journeys in one device session and stop at the first failed assertion.
|
|
32
|
+
6. Return changed accessibility nodes rather than repeated complete trees when a prior snapshot is available.
|
|
33
|
+
7. Use UI evidence as soon as it is available; do not add a fixed settling delay after a fresh expected state is observed.
|
|
34
|
+
|
|
35
|
+
## 4. Non-goals
|
|
36
|
+
|
|
37
|
+
- Changing the meaning of existing `expect_*` tools or their stabilization rules.
|
|
38
|
+
- Inferring app-specific expected outcomes when the caller has not supplied one.
|
|
39
|
+
- Removing `capture_screenshot` or the detailed debug snapshot.
|
|
40
|
+
- Bypassing target actionability checks for selector-based actions.
|
|
41
|
+
- Making `none` appropriate for actions with an asserted business outcome.
|
|
42
|
+
- Reporting benchmark figures without a controlled device run.
|
|
43
|
+
|
|
44
|
+
## 5. Compatibility and defaults
|
|
45
|
+
|
|
46
|
+
All new inputs are optional. For every existing action request:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
verificationMode = "full"
|
|
50
|
+
responseMode = "debug"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
is the effective behavior. Existing response fields, including action envelopes, fingerprints, device metadata, and traces, remain available in that mode. No existing tool is removed or renamed.
|
|
54
|
+
|
|
55
|
+
New clients SHOULD use `responseMode: "compact"` and select the least costly verification mode that satisfies their next decision. `full` remains required for a final assertion unless that assertion is separately expressed as an `expect_*` or journey assertion.
|
|
56
|
+
|
|
57
|
+
## 6. Common interaction controls
|
|
58
|
+
|
|
59
|
+
The following optional properties are added to `start_app`, `restart_app`, `tap`, `tap_element`, `swipe`, `scroll_to_element`, `type_text`, and `press_back`. A tool that cannot apply a field MUST reject it with `INVALID_ARGUMENT`, rather than silently changing its semantics.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
verificationMode?: "none" | "light" | "full" // default: "full"
|
|
63
|
+
responseMode?: "compact" | "debug" // default: "debug"
|
|
64
|
+
actionTimeoutMs?: number // adapter dispatch limit
|
|
65
|
+
verificationTimeoutMs?: number // verification budget
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Values are finite integers. `actionTimeoutMs` is clamped to 100–30,000 ms, and `verificationTimeoutMs` to 100–60,000 ms. The server reports effective values in debug responses. Platform adapters MUST receive and enforce `actionTimeoutMs`; it must not be a cosmetic field.
|
|
69
|
+
|
|
70
|
+
Unless supplied, the effective dispatch budget is 5,000 ms and the effective light-verification budget is 3,000 ms. `start_app` has a 10,000 ms dispatch default because launch includes an adapter-owned foreground check. `restart_app` has a 15,000 ms total dispatch budget: at most 5,000 ms for termination and the remaining budget, capped at 10,000 ms, for launch. `scroll_to_element` has a 10,000 ms total dispatch budget; the budget covers every scroll and resolve attempt, so no attempt may start after its deadline. Existing `full` calls retain their present adapter and stabilization behavior when the fields are omitted; a supplied budget bounds only work introduced by this RFC.
|
|
71
|
+
|
|
72
|
+
The server creates one deadline at dispatch start and passes its remaining duration to each nested operation. It cancels local polling and child processes when the deadline expires. A timeout before the command is issued is `ACTION_TIMEOUT`, `success: false`, `delivery_status: "not_delivered"`, and `retryable: true`. A timeout after dispatch begins is `ACTION_TIMEOUT`, `success: false`, `delivery_status: "unknown"`, and `retryable: false`: the device may have acted, so the client MUST observe state before retrying. The response includes `dispatch_started: boolean` and elapsed timing in both response modes.
|
|
73
|
+
|
|
74
|
+
### 6.1 Verification modes
|
|
75
|
+
|
|
76
|
+
| Mode | Required work | Result meaning |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `none` | Resolve only when required to dispatch; issue the command. No post-dispatch UI read. | `success` means the adapter accepted the action. |
|
|
79
|
+
| `light` | Dispatch, then take one fresh accessibility-tree observation. | `success` means delivery succeeded; `state_delta` is best-effort evidence, not a passed assertion. |
|
|
80
|
+
| `full` | Preserve the current tool's pre/post evidence, stale-reference checks, stabilization, and trace behavior. | Existing meaning is unchanged. |
|
|
81
|
+
|
|
82
|
+
`light` MUST not wait for an arbitrary settling interval. It MAY poll only until the first fresh observation arrives, bounded by `verificationTimeoutMs`. If the observation cannot be obtained in time, return `success: true`, `verification.status: "unavailable"`, and a retryable verification diagnostic; do not recast a delivered action as a dispatch failure. `verification.status: "unavailable"` is therefore a successful action with incomplete evidence, never a failed action.
|
|
83
|
+
|
|
84
|
+
`none` and `light` return `lifecycle_state: "pending_verification"` after delivery. `full` retains current lifecycle-state behavior.
|
|
85
|
+
|
|
86
|
+
### 6.2 Response modes
|
|
87
|
+
|
|
88
|
+
`debug` preserves the existing response shape. It includes device metadata, full action trace, fingerprints, resolved target detail, timing breakdown, and adapter diagnostics when present.
|
|
89
|
+
|
|
90
|
+
`compact` returns only:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
{
|
|
94
|
+
action_id: string,
|
|
95
|
+
action_type: string,
|
|
96
|
+
success: boolean,
|
|
97
|
+
lifecycle_state: "pending_verification" | "verified" | "failed",
|
|
98
|
+
failure_code?: string,
|
|
99
|
+
retryable?: boolean,
|
|
100
|
+
delivery_status?: "delivered" | "not_delivered" | "unknown",
|
|
101
|
+
dispatch_started: boolean,
|
|
102
|
+
verification?: { mode: "none" | "light" | "full", status: "not_requested" | "observed" | "verified" | "unavailable" | "failed" },
|
|
103
|
+
state_delta?: StateDelta,
|
|
104
|
+
timing: { total_ms: number }
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The compact form MUST retain failure and retry information. It MUST NOT emit device identifiers, full tree contents, raw adapter output, screenshots, fingerprints, or trace steps. A client can repeat an action only with a new `responseMode: "debug"`; it cannot expand a past compact result.
|
|
109
|
+
|
|
110
|
+
## 7. Selector-based tap
|
|
111
|
+
|
|
112
|
+
`tap_element` accepts exactly one of the existing `elementId` or a new `selector`; this does not change the `elementId` path.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
selector?: {
|
|
116
|
+
text?: string,
|
|
117
|
+
resource_id?: string,
|
|
118
|
+
accessibility_id?: string,
|
|
119
|
+
contains?: boolean
|
|
120
|
+
}
|
|
121
|
+
waitFor?: {
|
|
122
|
+
condition?: "exists" | "visible" | "clickable",
|
|
123
|
+
timeoutMs?: number, // default 1,500; range 100–10,000
|
|
124
|
+
pollIntervalMs?: number // default 100; range 50–1,000
|
|
125
|
+
match?: { index?: number }
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
With a selector, the server resolves a fresh UI tree, verifies that the selected node is actionable, and taps it in the same request. `waitFor` means wait for the target before resolving; it is not post-tap verification. Without `waitFor`, resolution occurs once and fails with `ELEMENT_NOT_FOUND`, `AMBIGUOUS_TARGET`, or `ELEMENT_NOT_INTERACTABLE` as applicable. A selector match must use the same matching and index semantics as `wait_for_ui`.
|
|
130
|
+
|
|
131
|
+
When `verificationMode: "full"`, selector resolution replaces the separate client resolve call but does not skip required post-action verification. The resolved element returned in debug mode includes only the selected node, never the complete tree.
|
|
132
|
+
|
|
133
|
+
## 8. State-delta snapshots and screenshot policy
|
|
134
|
+
|
|
135
|
+
The snapshot metadata layer already tracks stable node identities and aggregate change counts. It is extended to retain the prior normalized node record by device and expose changed nodes.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
type StateDelta = {
|
|
139
|
+
base_snapshot_revision: number | null,
|
|
140
|
+
snapshot_revision: number,
|
|
141
|
+
added: UIElement[],
|
|
142
|
+
removed: Array<{ stable_id?: string, resourceId?: string, contentDescription?: string, text?: string, index: number }>,
|
|
143
|
+
changed: UIElement[],
|
|
144
|
+
truncated: boolean
|
|
145
|
+
omitted_changes?: number
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For each `(platform, deviceId)`, the server retains normalized node records for the latest eight revisions or ten minutes, whichever expires first. It retains at most 500 nodes per revision. A delta contains at most 200 changed nodes; additional changes set `truncated: true` and report `omitted_changes`. Revision state is process-local and is discarded on server restart, device disconnect, or retention expiry.
|
|
150
|
+
|
|
151
|
+
`get_ui_tree` adds `responseMode` and `sinceSnapshotRevision`:
|
|
152
|
+
|
|
153
|
+
- `responseMode: "debug"` remains the complete tree response.
|
|
154
|
+
- `responseMode: "compact"` with a matching `sinceSnapshotRevision` returns `state_delta`; it does not include unchanged nodes.
|
|
155
|
+
- `responseMode: "compact"` without `sinceSnapshotRevision` returns `{ delta_available: false, snapshot_revision }` and no nodes. The caller obtains a base with a debug tree.
|
|
156
|
+
- If the requested base revision is unavailable, return `{ delta_available: false, snapshot_revision, reason: "BASE_REVISION_UNAVAILABLE" }` and no nodes. The client recovers by requesting a debug tree, then resumes from its returned revision.
|
|
157
|
+
- A truncated delta is valid only as a change signal. The client MUST request a debug tree before using it to make a target-selection decision.
|
|
158
|
+
|
|
159
|
+
For a `light` action, the server takes exactly one post-dispatch tree. It computes `state_delta` against the most recent retained revision for the same device, if one exists; otherwise it returns `state_delta: undefined` and `state_delta_available: false`. It never takes a pre-dispatch tree merely to create a delta. The returned `base_snapshot_revision` tells the client whether its local state can safely apply that delta.
|
|
160
|
+
|
|
161
|
+
Within `run_journey`, an immediately following `element_visible` assertion may supply that fresh post-dispatch tree for a `light` tap. The tap reports `verification.status: "observed"` when the assertion returns a matched count, even if the assertion fails; it reports `"unavailable"` with a retryable diagnostic if the observation cannot be confirmed.
|
|
162
|
+
|
|
163
|
+
`capture_debug_snapshot` adds `includeScreenshot?: boolean`, defaulting to `true` for compatibility. New compact callers MUST set it explicitly; `includeScreenshot: false` collects no screenshot. `capture_screenshot` remains the explicit screenshot tool. No interaction tool captures a screenshot unless its request explicitly asks for one through its observation configuration.
|
|
164
|
+
|
|
165
|
+
## 9. `run_journey`
|
|
166
|
+
|
|
167
|
+
`run_journey` runs a bounded ordered sequence against one resolved platform and device. It avoids MCP round trips; it does not hold a cross-request device lock or conceal failures.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
{
|
|
171
|
+
platform: "android" | "ios",
|
|
172
|
+
deviceId?: string,
|
|
173
|
+
responseMode?: "compact" | "debug", // default compact
|
|
174
|
+
captureOnFailure?: boolean, // default false
|
|
175
|
+
defaults?: { verificationMode?: "none" | "light" | "full", actionTimeoutMs?: number, verificationTimeoutMs?: number },
|
|
176
|
+
steps: Array<
|
|
177
|
+
{ id: string, type: "start_app", appId: string, verificationMode?: VerificationMode } |
|
|
178
|
+
{ id: string, type: "wait", selector: Selector, condition?: "exists" | "not_exists" | "visible" | "clickable", timeoutMs?: number, pollIntervalMs?: number } |
|
|
179
|
+
{ id: string, type: "tap", selector?: Selector, elementId?: string, waitFor?: WaitFor, verificationMode?: VerificationMode } |
|
|
180
|
+
{ id: string, type: "assert", assertion: ElementVisibleAssertion | ElementAbsentAssertion | ScreenFingerprintAssertion | StateEqualsAssertion, timeoutMs?: number, pollIntervalMs?: number }
|
|
181
|
+
>
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
type ElementVisibleAssertion = { kind: "element_visible", selector: Selector }
|
|
187
|
+
type ElementAbsentAssertion = { kind: "element_absent", selector: Selector }
|
|
188
|
+
type ScreenFingerprintAssertion = { kind: "screen_fingerprint", fingerprint: string }
|
|
189
|
+
type StateEqualsAssertion = {
|
|
190
|
+
kind: "state_equals",
|
|
191
|
+
property: string,
|
|
192
|
+
expected: boolean | number | string,
|
|
193
|
+
selector?: Selector,
|
|
194
|
+
elementId?: string
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Step IDs must be unique. Maximum steps: 50. A journey validates its complete schema before device work begins. `tap` requires exactly one target. `state_equals` requires exactly one of `selector` or `elementId`; every other assertion rejects irrelevant fields. `start_app` is the journey equivalent of `start_app`.
|
|
199
|
+
|
|
200
|
+
Journey assertions use a 5,000 ms timeout and 100 ms poll interval unless supplied; their ranges are 100–60,000 ms and 50–1,000 ms. `element_visible` is the existing `expect_element_visible` predicate. `element_absent` is a new journey-only predicate: zero matching nodes must be seen in two fresh trees at least 100 ms apart, within the assertion timeout. `screen_fingerprint` matches the current fingerprint on two consecutive reads 100 ms apart. `state_equals` uses the existing `expect_state` stabilization contract, with the journey's supplied or default timeout and poll interval applied as that contract's budget and observation interval; it does not add a second two-read rule. Assertions return `ASSERTION_TIMEOUT` on budget expiry and include the last observed value or matched count. A `wait` remains synchronization, not proof of a business outcome.
|
|
201
|
+
|
|
202
|
+
The journey resolves the platform and device once before the first step and passes that exact context to every step. An `elementId` is valid only when its stored platform and device ID equal the journey context. A mismatch is `STALE_REFERENCE`, no tap is dispatched, and the journey stops.
|
|
203
|
+
|
|
204
|
+
The response is:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
{
|
|
208
|
+
success: boolean,
|
|
209
|
+
stopped_at_step_id: string | null,
|
|
210
|
+
steps: Array<ExecutedJourneyStep | NotRunJourneyStep>,
|
|
211
|
+
timing: TimingBreakdown
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
type ExecutedJourneyStep = {
|
|
215
|
+
id: string, type: string, status: "passed" | "failed",
|
|
216
|
+
success: boolean, result: CompactOrDebugStepResult, timing: TimingBreakdown
|
|
217
|
+
}
|
|
218
|
+
type NotRunJourneyStep = {
|
|
219
|
+
id: string, type: string, status: "not_run", success: false,
|
|
220
|
+
reason: "PREVIOUS_STEP_FAILED", result: null, timing: { total_ms: 0 }
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
It stops immediately after a failed assertion, failed wait, target-resolution failure, or dispatch failure. Later steps have the `NotRunJourneyStep` shape and are never attempted. An action with `verification.status: "unavailable"` has `status: "passed"` because dispatch succeeded; a following assertion decides whether its intended outcome occurred. Debug mode includes the failing step's trace and a snapshot only when `captureOnFailure: true` is passed at journey level. The failure snapshot has `includeScreenshot: false` unless the caller separately requests a screenshot.
|
|
225
|
+
|
|
226
|
+
## 10. Timing measurement and reporting
|
|
227
|
+
|
|
228
|
+
Every specified tool records monotonic durations; wall clock is used only for timestamps. The timing schema is additive:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
type TimingBreakdown = {
|
|
232
|
+
total_ms: number,
|
|
233
|
+
device_resolution_ms?: number,
|
|
234
|
+
pre_observation_ms?: number,
|
|
235
|
+
target_resolution_ms?: number,
|
|
236
|
+
dispatch_ms?: number,
|
|
237
|
+
post_observation_ms?: number,
|
|
238
|
+
verification_ms?: number,
|
|
239
|
+
screenshot_ms?: number,
|
|
240
|
+
serialization_ms?: number
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`debug` returns the full applicable breakdown. `compact` returns `total_ms`; `run_journey` additionally returns each step's `total_ms`. Errors still return elapsed timing.
|
|
245
|
+
|
|
246
|
+
A checked-in benchmark script must emit JSON and Markdown to `docs/benchmarks/interaction-latency/`; reports contain commit SHA, server version, platform, device model/OS, app build identifier, sample count, warm-up policy, verification/response modes, and p50/p95/min/max for each timing component. Raw data must contain no screenshots, UI text, app package identifiers, or device serials.
|
|
247
|
+
|
|
248
|
+
## 11. Benchmark protocol and acceptance criteria
|
|
249
|
+
|
|
250
|
+
The repository owns an Android benchmark fixture at `test/fixtures/latency-journey-app/`. It is self-contained, has no network dependency, and exposes stable accessibility IDs for a repeatable navigation-and-control flow. The benchmark harness builds and installs the fixture.
|
|
251
|
+
|
|
252
|
+
Before every run, the harness must force-stop the fixture, clear its data, install the known fixture APK, and verify the reset-state `saved-session` selector. The benchmark runs against one named Android emulator image, records its API level and fixture APK SHA-256, and performs no work in parallel. It executes this exact flow:
|
|
253
|
+
|
|
254
|
+
```text
|
|
255
|
+
launch app → open detail → activate control → return Home → reopen detail → change control state → stop
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The fixture and benchmark harness MUST define and use their own stable selector map, and assert each resulting state. It performs 3 warm-up runs and 10 measured runs per mode.
|
|
259
|
+
|
|
260
|
+
The baseline invokes the current public tools over a spawned `dist/server.js` stdio MCP connection: `start_app`, `wait_for_ui`, `tap_element` with a handle returned by `wait_for_ui`, and the existing `expect_*` tools. The candidate invokes the new selector `tap_element` and `run_journey`. Both variants include the same waits/assertions and use `full`/`debug` for the compatibility comparison; the candidate additionally runs `light`/`compact`. End-to-end latency is measured by the benchmark client from immediately before its JSON-RPC request write until the corresponding response is fully read, excluding server process startup. The harness separately times explicit `capture_screenshot` and `capture_debug_snapshot` calls outside the journey.
|
|
261
|
+
|
|
262
|
+
A failed run is reported separately and excluded from percentile calculation; the report must state its failure count, first failed fixture selector, and failed tool/step. The harness emits the exact request sequence and fixture version in the report, with selectors but no UI text or serial number.
|
|
263
|
+
|
|
264
|
+
Acceptance gates:
|
|
265
|
+
|
|
266
|
+
- `full` has no regression greater than 10% at p95 for the representative journey and preserves all existing contract tests.
|
|
267
|
+
- `light` compact reduces p50 end-to-end MCP journey latency by at least 30% versus full/debug on the same benchmark environment.
|
|
268
|
+
- `none` compact has no post-dispatch accessibility read, proven by a focused unit test and its timing breakdown.
|
|
269
|
+
- A compact state delta never includes an unchanged node; a requested screenshot is the only path that captures screenshot bytes.
|
|
270
|
+
- The benchmark report is checked in for baseline and final runs. If environmental variance prevents the 30% threshold, implementation stops for diagnosis rather than relaxing verification semantics or claiming success.
|
|
271
|
+
|
|
272
|
+
## 12. Test plan
|
|
273
|
+
|
|
274
|
+
Add focused unit coverage for:
|
|
275
|
+
|
|
276
|
+
1. Every verification mode on `tap_element` and `start_app`, including the exact adapter/observation calls and lifecycle/result semantics.
|
|
277
|
+
2. Compact/debug compatibility: omitted fields produce the legacy debug envelope; compact retains actionable failure fields and omits verbose metadata.
|
|
278
|
+
3. Selector tap success, wait-until-present success, ambiguity, stale/non-actionable rejection, and timeout.
|
|
279
|
+
4. `run_journey` ordering, single platform/device resolution, first failed assertion, unexecuted remaining steps, and per-step timing.
|
|
280
|
+
5. Delta added/removed/changed identity behavior, revision miss, truncation, and no screenshot when disabled.
|
|
281
|
+
6. Action and verification timeout bounds and the absence of a fixed delay once a fresh expected state is observed.
|
|
282
|
+
7. Deterministic benchmark report aggregation from fixture timing data.
|
|
283
|
+
|
|
284
|
+
Existing unit, lint, build, and device checks remain required. Run the real benchmark only after the automated contract gates pass.
|
|
285
|
+
|
|
286
|
+
## 13. Implementation ownership
|
|
287
|
+
|
|
288
|
+
- `src/server/tool-definitions.ts`: additive schemas and documentation.
|
|
289
|
+
- `src/server/tool-handlers.ts` and `src/server/common.ts`: mode parsing, response shaping, timing envelope, and journey orchestration.
|
|
290
|
+
- `src/interact/index.ts`: selector resolve-and-tap, verification policy, deadline propagation, and step execution primitives.
|
|
291
|
+
- `src/observe/snapshot-metadata.ts` and `src/observe/index.ts`: changed-node deltas and explicit screenshot collection.
|
|
292
|
+
- `src/manage/*`: action-timeout propagation and truthful launch timing.
|
|
293
|
+
- `test/unit/{server,interact,observe}/`: contract coverage; `test/device/automated/`: benchmark journey harness.
|
|
294
|
+
|
|
295
|
+
## 14. Rollout
|
|
296
|
+
|
|
297
|
+
1. Land timing instrumentation and baseline benchmark without changing defaults.
|
|
298
|
+
2. Land verification/response modes with `full`/`debug` defaults and compatibility tests.
|
|
299
|
+
3. Land selector tap and state deltas.
|
|
300
|
+
4. Land `run_journey` and run before/after device benchmarks.
|
|
301
|
+
5. Publish the report and recommend `light`/`compact` for routine agent navigation only after the acceptance gates pass.
|
package/docs/tools/TOOLS.md
CHANGED
|
@@ -17,4 +17,6 @@ For interactive flows, the intended deterministic pattern is:
|
|
|
17
17
|
- **expect_\*** tools are for final outcome verification
|
|
18
18
|
- observation tools are supporting context, not primary success signals
|
|
19
19
|
|
|
20
|
+
For environment-sensitive work, call `get_system_status` first so the client can rely on automatic discovery when it is available and fall back to explicit tool paths only when needed.
|
|
21
|
+
|
|
20
22
|
Use the linked documents below for per-tool inputs, outputs, and usage guidance.
|
package/docs/tools/interact.md
CHANGED
|
@@ -616,3 +616,33 @@ Guidance:
|
|
|
616
616
|
- `uiChanged=true` or `expectedElementVisible=true` means the action outcome is already verified
|
|
617
617
|
- local-state actions should prefer refreshed snapshots, `expect_state`, or `expect_element_visible` over default network inspection
|
|
618
618
|
- network activity is auxiliary evidence, not mandatory proof
|
|
619
|
+
|
|
620
|
+
## Interaction execution controls (RFC 018)
|
|
621
|
+
|
|
622
|
+
`start_app`, `restart_app`, `tap`, `tap_element`, `swipe`, `scroll_to_element`,
|
|
623
|
+
`type_text`, and `press_back` accept `verificationMode` (`none`, `light`, `full`)
|
|
624
|
+
and `responseMode` (`compact`, `debug`). Omitted controls retain `full`/`debug`.
|
|
625
|
+
`none` reports dispatch without post-action accessibility reads. `light` requests
|
|
626
|
+
one fresh tree; an unavailable observation does not turn successful delivery into
|
|
627
|
+
a failed action. Use an explicit assertion to establish the intended outcome.
|
|
628
|
+
Compact responses retain delivery, retry, lifecycle, and timing fields.
|
|
629
|
+
|
|
630
|
+
`actionTimeoutMs` and `verificationTimeoutMs` are integer budgets, clamped to
|
|
631
|
+
100–30,000 and 100–60,000 ms respectively. An action timeout after dispatch starts
|
|
632
|
+
has unknown delivery and must be observed before retrying.
|
|
633
|
+
|
|
634
|
+
`tap_element` accepts exactly one `elementId` or `selector`. A selector supports
|
|
635
|
+
`text`, `resource_id`, `accessibility_id`, and `contains`. Optional `waitFor` waits
|
|
636
|
+
for the target before tapping; without it, resolution uses one observation.
|
|
637
|
+
Ambiguous targets require an explicit `waitFor.match.index`.
|
|
638
|
+
|
|
639
|
+
`run_journey` accepts 1–50 steps with unique IDs, on one platform/device. Supported
|
|
640
|
+
step types are `start_app`, `tap`, `wait`, and `assert`. Assertions support
|
|
641
|
+
`element_visible`, `element_absent`, `screen_fingerprint`, and `state_equals`.
|
|
642
|
+
The complete request is validated before device work. The first failed step stops
|
|
643
|
+
the journey; remaining steps report `not_run`. Journey responses default to compact.
|
|
644
|
+
`defaults` supplies execution controls; step verification overrides the default.
|
|
645
|
+
`captureOnFailure: true` includes a screenshot-free failure snapshot in debug mode.
|
|
646
|
+
|
|
647
|
+
See [RFC 018](../specs-deltas/018-interaction-latency-and-batched-journeys.md) for
|
|
648
|
+
contracts and the device acceptance criteria.
|
package/docs/tools/manage.md
CHANGED
|
@@ -51,6 +51,8 @@ Notes:
|
|
|
51
51
|
- Requires `platform`, `projectType`, and `projectPath`.
|
|
52
52
|
- Android builds prefer the project `gradlew` when present.
|
|
53
53
|
- iOS builds honor environment-based Xcode destination and derived-data settings where configured.
|
|
54
|
+
- Android setup typically needs `ADB_PATH` or `ANDROID_SDK_ROOT`/`ANDROID_HOME`, plus `GRADLE_JAVA_HOME` or `JAVA_HOME` for Gradle-backed builds.
|
|
55
|
+
- iOS setup typically needs `XCRUN_PATH` and `MCP_IDB_PATH` or `IDB_PATH` when `idb` is not on `PATH`.
|
|
54
56
|
|
|
55
57
|
---
|
|
56
58
|
|
|
@@ -102,8 +104,8 @@ Response:
|
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
Notes:
|
|
105
|
-
- Android: prefers `ADB_PATH` if set, otherwise falls back to `adb` on PATH.
|
|
106
|
-
- iOS: uses `xcrun simctl install` for simulators and `idb` where available for devices.
|
|
107
|
+
- Android: prefers `ADB_PATH` if set, otherwise falls back to `adb` on `PATH`, and will also use `ANDROID_SDK_ROOT`/`ANDROID_HOME` when present.
|
|
108
|
+
- iOS: uses `xcrun simctl install` for simulators and `idb` where available for devices; point `MCP_IDB_PATH` or `IDB_PATH` at a non-standard `idb` binary.
|
|
107
109
|
|
|
108
110
|
---
|
|
109
111
|
|
package/docs/tools/system.md
CHANGED
|
@@ -49,13 +49,15 @@ Response (example):
|
|
|
49
49
|
}
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
The response is additive: the existing flat fields remain available, and the nested `host`, `android`, and `ios` objects are added alongside them.
|
|
53
|
+
|
|
52
54
|
Checks performed (fast, best-effort):
|
|
53
55
|
- ADB availability and version (adb --version)
|
|
54
56
|
- Connected Android devices (adb devices -l), counts and state summary (device/unauthorized/offline)
|
|
55
57
|
- Log access probe (adb logcat -d -t 1)
|
|
56
|
-
- Android environment variables (
|
|
58
|
+
- Android environment variables and discovery order (`ADB_PATH`, then `ANDROID_SDK_ROOT`/`ANDROID_HOME`, then `PATH`)
|
|
57
59
|
- Optional: app installation check if MCP_TARGET_PACKAGE/MCP_TARGET_APP_ID is set (pm path)
|
|
58
|
-
- Basic iOS checks (
|
|
60
|
+
- Basic iOS checks (`XCRUN_PATH`, `MCP_IDB_PATH`/`IDB_PATH`, and `simctl` booted-device discovery)
|
|
59
61
|
|
|
60
62
|
Behavior notes:
|
|
61
63
|
- Always returns structured JSON and never throws; any failures are surfaced in the `issues` array.
|
|
@@ -68,3 +70,8 @@ Usage guidance:
|
|
|
68
70
|
- Call before build/install flows to avoid wasted build attempts on misconfigured systems.
|
|
69
71
|
- Call early in a session when device or toolchain availability is uncertain.
|
|
70
72
|
- If `success: false`, attempt recovery steps or report issues to the user.
|
|
73
|
+
|
|
74
|
+
Recommended setup patterns:
|
|
75
|
+
- Default: let the server discover tools automatically from standard locations and call `get_system_status` first to confirm what is available.
|
|
76
|
+
- Android: set `ADB_PATH` only if you want to override the discovered `adb` binary, or set `ANDROID_SDK_ROOT`/`ANDROID_HOME` when discovery needs help finding `platform-tools/adb`.
|
|
77
|
+
- iOS: set `XCRUN_PATH` only if Xcode tools are not on `PATH`, and set `MCP_IDB_PATH` or `IDB_PATH` only when `idb` lives outside the default search paths.
|
package/package.json
CHANGED
package/src/interact/android.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { markDispatchStarted } from '../utils/operation-budget.js'
|
|
1
2
|
import { TapResponse, SwipeResponse, TypeTextResponse, PressBackResponse } from "../types.js"
|
|
2
3
|
import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "../utils/android/utils.js"
|
|
3
4
|
import { AndroidObserve } from "../observe/index.js"
|
|
@@ -7,31 +8,33 @@ import { scrollToElementShared } from "../utils/ui/index.js"
|
|
|
7
8
|
export class AndroidInteract {
|
|
8
9
|
private observe = new AndroidObserve();
|
|
9
10
|
|
|
10
|
-
async tap(x: number, y: number, deviceId?: string): Promise<TapResponse> {
|
|
11
|
+
async tap(x: number, y: number, deviceId?: string, timeoutMs?: number): Promise<TapResponse> {
|
|
11
12
|
const metadata = await getAndroidDeviceMetadata("", deviceId)
|
|
12
13
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
13
14
|
|
|
14
15
|
try {
|
|
15
|
-
|
|
16
|
+
markDispatchStarted()
|
|
17
|
+
await execAdb(['shell', 'input', 'tap', x.toString(), y.toString()], deviceId, { timeout: timeoutMs })
|
|
16
18
|
return { device: deviceInfo, success: true, x, y }
|
|
17
19
|
} catch (e) {
|
|
18
20
|
return { device: deviceInfo, success: false, x, y, error: e instanceof Error ? e.message : String(e) }
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
async swipe(x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string): Promise<SwipeResponse> {
|
|
24
|
+
async swipe(x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string, timeoutMs?: number): Promise<SwipeResponse> {
|
|
23
25
|
const metadata = await getAndroidDeviceMetadata("", deviceId)
|
|
24
26
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
25
27
|
|
|
26
28
|
try {
|
|
27
|
-
|
|
29
|
+
markDispatchStarted()
|
|
30
|
+
await execAdb(['shell', 'input', 'swipe', x1.toString(), y1.toString(), x2.toString(), y2.toString(), duration.toString()], deviceId, { timeout: timeoutMs })
|
|
28
31
|
return { device: deviceInfo, success: true, start: [x1, y1], end: [x2, y2], duration }
|
|
29
32
|
} catch (e) {
|
|
30
33
|
return { device: deviceInfo, success: false, start: [x1, y1], end: [x2, y2], duration, error: e instanceof Error ? e.message : String(e) }
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
async typeText(text: string, deviceId?: string): Promise<TypeTextResponse> {
|
|
37
|
+
async typeText(text: string, deviceId?: string, timeoutMs?: number): Promise<TypeTextResponse> {
|
|
35
38
|
const metadata = await getAndroidDeviceMetadata("", deviceId)
|
|
36
39
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
37
40
|
|
|
@@ -39,19 +42,21 @@ export class AndroidInteract {
|
|
|
39
42
|
// Encode spaces as %s to ensure proper input handling by adb shell input text
|
|
40
43
|
const encodedText = text.replace(/\s/g, '%s')
|
|
41
44
|
// Note: 'input text' might fail with some characters or if keyboard isn't ready, but it's the standard ADB way.
|
|
42
|
-
|
|
45
|
+
markDispatchStarted()
|
|
46
|
+
await execAdb(['shell', 'input', 'text', encodedText], deviceId, { timeout: timeoutMs })
|
|
43
47
|
return { device: deviceInfo, success: true, text }
|
|
44
48
|
} catch (e) {
|
|
45
49
|
return { device: deviceInfo, success: false, text, error: e instanceof Error ? e.message : String(e) }
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
async pressBack(deviceId?: string): Promise<PressBackResponse> {
|
|
53
|
+
async pressBack(deviceId?: string, timeoutMs?: number): Promise<PressBackResponse> {
|
|
50
54
|
const metadata = await getAndroidDeviceMetadata("", deviceId)
|
|
51
55
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
52
56
|
|
|
53
57
|
try {
|
|
54
|
-
|
|
58
|
+
markDispatchStarted()
|
|
59
|
+
await execAdb(['shell', 'input', 'keyevent', '4'], deviceId, { timeout: timeoutMs })
|
|
55
60
|
return { device: deviceInfo, success: true }
|
|
56
61
|
} catch (e) {
|
|
57
62
|
return { device: deviceInfo, success: false, error: e instanceof Error ? e.message : String(e) }
|