Feature Flags¶
✅ Implemented
Current state: Feature flags are implemented as CLI args (e.g.,
--debug,--accessibility-audit,--mem-perf-audit). IDE integration for runtime flag toggling is described in linked docs but is<kbd>🚧 Design Only</kbd>for Android Studio. See the Status Glossary for chip definitions.
Runtime configuration system for experimental features, performance tuning, and debugging AutoMobile. At
present these flags are set on MCP startup as CLI args — and, for the output-size flags below, equivalently
via their AUTOMOBILE_* environment variables. The plan is to have them configurable via IDE integrations for
Android Studio
Debug Flags¶
--debug - Enable debug logging
--debug-perf / --ui-perf-debug - Enable performance debug output, including response performance audits
Performance Flags¶
--no-ui-perf-mode - Disable UI performance monitoring. UI perf mode is on by default (uiPerfMode = !args.includes("--no-ui-perf-mode") in src/index.ts); there is no --ui-perf-mode enable flag — passing it has no effect since the mode is already on. Use --no-ui-perf-mode to turn off selection-state visual capture on taps and UI perf auditing.
--mem-perf-audit - Memory performance auditing
Behavior Flags¶
--accessibility-audit - Enable accessibility checks
--predictive-ui - AI-powered UI prediction
Recording Flags¶
--mcp-recording - Enable the recordSteps tool for capturing MCP tool calls as replayable YAML test plans. Off by default.
Output-Size Reduction Flags¶
Part of the MCP output-context reduction effort. Each flag can be set either as a CLI arg or via its AUTOMOBILE_* environment variable (set the env var to 1 to enable). The CLI flag wins when both are present. All are off by default.
| CLI flag | Environment variable | Effect |
|---|---|---|
--observe-result-include-elements |
AUTOMOBILE_OBSERVE_RESULT_INCLUDE_ELEMENTS |
Opt back in to the flattened elements array on observe results. It is dropped by default to reduce output size; this flag restores it. (Note the default observe projection is the skeleton, which omits elements regardless — request project: "full" to get the full hierarchy, and combine with this flag to include elements there.) |
--tool-results-no-structured-content |
AUTOMOBILE_TOOL_RESULTS_NO_STRUCTURED_CONTENT |
Omit the structuredContent field from tool results (the serialized content[0].text still carries the full payload, so no data is lost). Also drops the advertised outputSchema from tools/list so the server does not declare structured output it will not return. |
--actions-diff-observe |
AUTOMOBILE_ACTIONS_DIFF_OBSERVE |
Non-observe tools (tapOn, swipeOn, …) emit a diff of their post-action observation against the last one shown to the agent, instead of the full embedded observation. The baseline is the “last observation output to the agent” — the sanitized ObserveResult cached per session (SessionCacheData.lastRenderedObservation). observe always emits the full sanitized observation and resets the baseline; each non-observe action emits its diff then updates the baseline to its own observation, so the next diff is against current state. Nodes carry no stable id, so diffing uses a synthetic key: resource-id + bounds + text + sibling index. The diff is { isDiff: true, added, removed, changed, fields } — added/removed/changed are keyed nodes (a node whose key fields change reads as remove+add; a same-key node whose other attributes change, e.g. checked, reads as changed), and fields are changed top-level fields — scalars (rotation, wakefulness, awaitDuration, …; updatedAt is excluded as pure churn) and the Element mirror fields (focusedElement, accessibilityFocusedElement, awaitedElement), each as {from, to}. The mirror fields are emitted node-subtree-stripped (the subtree is redundant with the node diff) and compared bounds-tolerantly; a focus gain reads as {from: undefined, to: element}, a loss as {from: element, to: undefined}. Falls back to the full observation (no diff) when the active window/package changed (cross-screen diff is meaningless), the baseline is missing (first action), or there is no sessionUuid (legacy single-agent path). Content-hash node identity (#3053): the positional key cascades on scroll / mid-list insert — a shift changes every following node’s bounds and sibling index, so whole rows would surface as remove+add. After positional matching, a leftover removed and added node are re-paired into one changed delta when they share a stable content key (resource-id/view-id/content-desc/text — no bounds, no sibling index) that is unique among the leftovers on both sides; uniqueness guarantees exactly one candidate each side, so distinct content never false-merges, and a keyless node (no stable identity) never re-pairs. This collapses a scroll into “N nodes moved” instead of “N removed + N added”. Duplicate/interchangeable cells stay remove+add (ambiguous key). Applied output-only in finalizeToolResponse after sanitizeObserveResult, so it operates on the already-compacted observation (bounds compaction is a permanent default; the synthetic key normalizes both object- and tuple-shaped bounds). Internal tool-to-tool calls are never diffed: PlanExecutor marks its step calls internal (__internalNoDiff), so a plan step’s finalized envelope always carries the full observation — a current or future internal .observation.viewHierarchy reader is never handed a diff. Combining it with --actions-no-observe is moot — that flag removes the observation, so there is nothing to diff. |
--actions-no-observe |
AUTOMOBILE_ACTIONS_NO_OBSERVE |
Strip the embedded observation from non-observe tool results entirely (deleted from both structuredContent and the serialized content[0].text). Output-only — BaseVisualChange still computes the observation internally for its own success detection, so visual-change behavior is unchanged; the observe tool’s own observation is never stripped. Applied at finalizeToolResponse. Precedence: when both --actions-no-observe and --actions-diff-observe are set, no-observe wins — the observation is removed, so there is nothing to diff. |
Now permanent defaults (formerly flags)¶
The following reductions were once opt-in flags and are now unconditional defaults. Their old flags and AUTOMOBILE_* env vars are silently ignored — passing one is a harmless no-op, with no error or migration warning:
- Compact bounds tuples (was
--observe-result-compact): everyboundsin anobserveresult is emitted as the positional tuple[left, top, right, bottom]instead of{left, top, right, bottom}— view-hierarchy nodes,elements, window/root/region, and the focused/awaited element fields. The order is fixed and round-trips losslessly; only fields literally namedboundsare flattened, so insets keep their shape, andrawViewHierarchy(returned withraw: true) is left unshaped. The tuple arm is now advertised intools/listunconditionally:elementBoundsSchema(src/server/toolOutputSchemas.ts) is abounds-object \| [left, top, right, bottom]union, andadvertiseBoundsForCompact(src/server/compactBoundsAdvertisement.ts) passes it through as-is (it is still suppressed together with the schema under--tool-results-no-structured-content). - Skeleton projection (was
--observe-result-project-skeleton): the headlineobservepayload projects to the flat, actionable-onlyskeleton(id/label/bounds/affordances) in place ofviewHierarchy+elements. Opt out per call withproject: "full"(orraw: true). - Compact (non-pretty) JSON (was
--tool-results-compact-json): tool results are serialized without the 2-space indentation instringifyToolResponse. Same data (parses back identically, no effect ontapOn/text matching); ~35% fewer characters on element-heavy payloads. - Observe-scope gates (were
--observe-focus-scope/--observe-overview/--observe-region): the FOCUS / OVERVIEW / REGION dimensions of theobservescopeinput (see below) are always honored when a call supplies the matchingscope.*field. A call with noscopeis unaffected.
Observe scope input (issue #4344)¶
Applies the multimodal “crop tool” concept to the view hierarchy (the artifact
agents consume) rather than screenshots: three progressive-disclosure transforms
that return a scoped view of the tree. This is the “spatial axis” complementing
--actions-diff-observe’s “temporal axis”. Implemented output-only in
finalizeToolResponse (src/features/observe/output/ObserveScopeExperiments.ts),
so it never mutates the in-memory result, and applied to the agent-facing
observe payload only — internal tool-to-tool calls and the diff baseline keep
the full sanitized tree.
The parameters ride in the tool call: the agent picks where to zoom on each
screen via an optional scope field on observe. Scoping is always available —
buildObserveScopeConfig applies a dimension whenever the call requests it (the
per-dimension server gates that once dark-launched this are now always on).
scope field |
Shape | Behavior |
|---|---|---|
scope.focus |
true | { resourceId?, text? } |
An anchor scopes to the first matching node’s subtree; true scopes to the foreground app, dropping identifiable foreign-package chrome. FOCUS classifies chrome by the resource-id dotted-package prefix (com.android.systemui:id/...), which survives cleanNodeProperties; the undotted android: framework namespace (android:id/content, dialog/ActionBar ids) and iOS colon identifiers (row:0) are neutral and kept, so app content is never dropped. |
scope.region |
true | { x1, y1, x2, y2 } |
Crop to a normalized (0..1) box (x1<x2, y1<y2, enforced at runtime), or the inset content rectangle when true. Nodes with no readable bounds are kept. |
scope.overview |
true |
Collapse to a structural/addressable skeleton (kept: children, scrollable, clickable, resource-id, content-desc). |
Every scoped payload carries an observeScope field (applied transforms,
nodesBefore/nodesAfter, regionPx, focus resolution) so the reduction is
measurable on the wire. The scope input and observeScope output are advertised
in the observe tool schema.
Tool Output Artifact Mode¶
--tool-outputs-dir <path> (alias --tool-output-dir; env
AUTOMOBILE_TOOL_OUTPUTS_DIR or legacy AUTO_MOBILE_TOOL_OUTPUTS_DIR) enables
artifact mode for large MCP tool-output subtrees. The directory is resolved to a
host-local path at startup and validated again at write time. Artifact write
failures are tool failures; AutoMobile does not fall back to inlining the large
payload.
Every artifact replacement uses the shared metadata shape:
{
"artifact": {
"path": "/absolute/host/path/to/file.json",
"format": "json",
"payload": "NetworkGraph",
"bytes": 123456,
"tool": "getNetworkGraph"
}
}
Artifact mode currently applies to:
observe: the final agent-facingObserveResultorObserveDiffpayload is written to an artifact after existing observe output transforms.- Action tools with embedded
observation: the embedded final agent-facing observation or diff is replaced with artifact metadata; internal tool-to-tool calls still receive full in-memory observations. executePlan: largeviewHierarchyandrawViewHierarchysubtrees insidefailedStep.failureObservationand captured debug observations are artifacted. Small fields such as success/counts/errors, device metadata, video paths, and observation samples remain inline.bugReport:viewHierarchy.rawXml,logcat, and longwindowState.windowslists are artifacted. Report identity, device/screen state, hierarchy counts, clickable-element summary, saved report path, errors, and log counts remain inline.getNetworkGraph: the aggregategraphtree is artifacted, withgraphSummary.hostCountleft inline.
Evaluated but not selected for the first non-observation pass: debugSearch
returns bounded summaries rather than raw hierarchies; exportPlan and
recordSteps can return long YAML but that content is the primary result agents
usually need immediately. Revisit them if size tests or real-world traces show
they dominate context.
--actions-diff-observe chooses the full-vs-diff policy by action class before
calling the shared diff implementation. Navigation-prone actions (tapOn,
tapAny, homeScreen, recentApps, openLink, and pressButton for
back/home/recent/power, plus submit-style IME actions such as done/go/search/send)
use strict screen identity and prefer a full observation when identity is
uncertain, because a tap, navigation button, or submit action can move to a new
screen or modal. In-place mutations (inputText without a submit IME action,
clearText, selectAllText, focus-traversal imeAction values, keyboard and
clipboard operations, and non-navigation pressButtons such as volume) may diff
when the app/activity/package surface is stable and any present screen-identity
key still matches, even if that identity is low confidence. Scroll-like actions
(swipeOn, dragAndDrop) use the same stable-surface policy so scroll deltas can
stay compact while still falling back to full output across screens. The diff
shape remains { isDiff: true, added, removed, changed, fields } for every
class; only the gate that decides whether a diff is safe changes.
Note on persistence: like the other feature flags above, enabling one of these (via either the CLI flag or the env var) writes
enabled=trueto the daemon’s feature-flag store, so it stays on for subsequent daemon runs until it is turned off through another surface — there is no--no-*CLI counterpart today. To clear a flag, toggle it off through the IDE feature-flag integration or reset the store.Note on the shared daemon: in the default proxy mode these flags are applied to the daemon at startup. If a daemon is already running, a new MCP client that requests a different output-reduction flag value does not restart it.
--embedded-sdk(which changes the exposed tool surface) is the exception: a request to add it restarts the daemon with the existing options preserved. To pick up a changed output-reduction flag on an already-running daemon, restart the daemon (--daemon restart).Note on
tools/list_changed(issue #2963): toggling a flag that changes tool definitions — the advertisedoutputSchema(--tool-results-no-structured-content) or tool availability (--debug) — emits anotifications/tools/list_changedat runtime so a directly-connected MCP client re-fetchestools/listand sees the updated advertisement. The set of tool-definition-affecting flags lives inTOOL_DEFINITION_AFFECTING_FLAGS(src/features/featureFlags/FeatureFlagDefinitions.ts) and is guarded against drift by a source-scan test. The emit is best-effort and fires only on an actual value change (no storm on redundant or unrelated toggles). In the default proxy topologyDaemonMcpProxydoes forward this notification: it invalidates its cached tool list and re-emits the event to the connected client viasendToolListChanged()(issue #3223, covered bytest/server/proxyServerListChanged.test.ts), so a proxy-mode client re-fetchestools/listand sees the change without a daemon restart. The same path forwardsresources/list_changed.