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 and Xcode. See the Status Glossary for chip definitions.
Runtime configuration system for experimental features, performance tuning, and debugging AutoMobile. At present these flags can only be set on MCP startup as CLI args. The plan is to have them configurable via IDE integrations for Android Studio & XCode
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.
Observation Diagnostics¶
--safe-area-warnings / --edge-to-edge-warnings - Equivalent aliases that enable report-only layout inspection in observe. Results include layoutWarnings only when enabled, flagging text or interactive elements that may overlap safe areas, system bars, cutouts, or Android gesture regions. This is a daemon-start option rather than a stored feature flag: it applies for that daemon process only. A client requesting it starts, or adds it to, the shared daemon; restarting the daemon without the flag disables the inspection again.
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-drop-elements |
AUTOMOBILE_OBSERVE_RESULT_DROP_ELEMENTS |
Omit the flattened elements array from observe results. |
--observe-result-compact |
AUTOMOBILE_OBSERVE_RESULT_COMPACT |
Emit observe results in a compact form: every bounds object in the result is flattened from {left, top, right, bottom} to the positional tuple [left, top, right, bottom] — view-hierarchy nodes, the elements arrays, window/root/region bounds, and the focused/awaited element fields. The order is fixed and documented, so the tuple round-trips losslessly; the four repeated key strings are dropped from every occurrence. Only fields literally named bounds are flattened, so insets (systemInsets, a {top,bottom,left,right} object) keep their shape. rawViewHierarchy (returned with raw: true) is left unshaped. Applied output-only (the in-memory lastHierarchy cache is unaffected) and composes with --observe-result-drop-elements, --tool-results-no-structured-content, --actions-diff-observe, and --actions-no-observe (the latter removes the post-action observation, so there is nothing to flatten). The tuple shape is advertised machine-readably only when this flag is on: every tool outputSchema that carries a typed bounds field routes through elementBoundsSchema (src/server/toolOutputSchemas.ts), a bounds-object \| [left, top, right, bottom] union whose description names the tuple order and the enabling flag. advertiseBoundsForCompact (src/server/compactBoundsAdvertisement.ts) collapses that union to its object arm in tools/list when the flag is off, so the advertised shape always matches the emitted shape (mirroring the --tool-results-no-structured-content outputSchema suppression). The observe tool declares an ObserveResult outputSchema (observeResultSchema in src/server/toolOutputSchemas.ts, issue #3025) whose hierarchy-node, window/root/region, and elements bounds all route through the same union, so those bounds — the bulk of every compacted result — are advertised machine-readably too (and suppressed together with the schema under --tool-results-no-structured-content). |
--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 composes with --observe-result-compact (the diff operates on the already-compacted observation; 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. |
--tool-results-compact-json |
AUTOMOBILE_TOOL_RESULTS_COMPACT_JSON |
Serialize tool results as compact (non-pretty-printed) JSON — drops the 2-space indentation in stringifyToolResponse. Same data (parses back identically, no effect on tapOn/text matching); ~35% fewer characters on element-heavy payloads. Composes with the other flags. |
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) and--safe-area-warnings(which changes the returned observation contract) are the exceptions: a request to add either 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,--observe-result-compact) 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). Known limitation: in the default proxy topologyDaemonMcpProxydoes not yet forward this notification or invalidate its cached tool list, so proxy-mode clients still need a daemon restart to observe the change — a pre-existing, cross-cutting proxy limitation shared withresources/list_changed, tracked as a follow-up.