mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
9aae457533
* fix(errors): close call-site and consumer gaps around the central error system Audit + iOS/Android dogfood findings (see docs/adr/0010-error-system.md): - press/click/fill targets that parse as neither @ref, selector, nor point now fail with INVALID_ARGS grammar guidance (incl. unquoted multi-word selector values) instead of UNKNOWN 'Expected x to be a finite number' - daemon command-input validation throws AppError INVALID_ARGS instead of bare Error surfacing as UNKNOWN - selector-no-match and stale-ref failures carry targeted hints (selectorFailureHint / STALE_REF_HINT) - retriable/supportedOn survive wire rehydration to CLI --json and SDK (previously dropped at throwDaemonError / toDaemonHttpRpcError) - MCP tool errors carry code + hint instead of message-only text - lease busy/capacity use DEVICE_IN_USE (the retriable code) - asAppError(err, fallbackCode) replaces cause-dropping coercions in the Apple runner; new default hints for AMBIGUOUS_MATCH, DEVICE_IN_USE, UNSUPPORTED_PLATFORM, and a distinct UNKNOWN hint - ADR 0010 documents the error-system conventions * fix: format touched files and clear fallow audit gate - privatize SELECTOR_NO_MATCH_HINT / SELECTOR_NOT_UNIQUE_HINT (consumed only via selectorFailureHint in the same module) and integerSchema (only used inside command-input.ts) - dedupe the resolved-node return tail in interaction resolution into describeResolvedNode - extract stringDetail/booleanDetail readers so normalizeError stays under the complexity threshold * fix: reject unquoted trailing text after interaction selectors press/click/longpress positionals like 'press text=Gesture lab' used to silently drop the leftover tokens and act on the truncated selector (text=Gesture), potentially hitting the wrong element. Reject non-empty splitSelectorFromArgs rest with INVALID_ARGS guidance that suggests the merged quoted form (text="Gesture lab"). Fill keeps consuming rest as its text payload; wait/is/replay-heal already handle rest explicitly.
3.9 KiB
3.9 KiB
ADR 0010: Error system conventions
- Status: accepted
- Date: 2026-07-03
Context
The error system is centralized in src/kernel/errors.ts (AppError, normalizeError,
defaultHintForCode, retriableForErrorCode) but has ~1,200 construction sites across the CLI,
daemon, and platform layers. A July 2026 audit (code sweep + iOS/Android dogfooding) found the
kernel sound while call-site quality drifted: internal validation thrown as bare Error
(surfacing as UNKNOWN with a misleading hint), semantically wrong codes (COMMAND_FAILED for
busy devices, INVALID_ARGS for expired server-side resources), missing hints on the most common
agent failures (selector/ref misses), and consumers that drop fields (MCP tool errors carried only
message). Nothing documented the conventions, so each new site re-decided them.
Decision
- Every thrown error a user or agent can reach is an
AppError. Barethrow new Error(...)is reserved for provably unreachable invariants. Input validation — including daemon-side decoding of command input — throwsAppError('INVALID_ARGS', ...). Coercion of unknown caught errors usesasAppError(err, fallbackCode), which preserves the cause chain; do not hand-rollerr instanceof AppError ? err : new AppError(...). - Code selection. Use the most specific
KnownAppErrorCode;COMMAND_FAILEDis for genuine runtime failures of a well-formed request, never a catch-all for capability gaps (UNSUPPORTED_OPERATION), contention (DEVICE_IN_USE, the only retriable code), or ambiguity (AMBIGUOUS_MATCH). New codes are added to the union deliberately; machine-dispatchable sub-classification rides indetails.reason(the lease registry is the model). - Hints answer "what should the agent run next". A hint is required wherever the per-code
default from
defaultHintForCodewould mislead; it is omitted where the default suffices — mass-adding boilerplate hints is worse than the default. Shared failure modes get shared hint constants next to the code that detects them (selectorFailureHint,STALE_REF_HINTinsrc/daemon/selectors-resolve.ts;resolveIosDevicectlHint;bootFailureHint), not copy-pasted strings. Re-wraps preserve an existing hint rather than clobbering it. - Wrapping external tool failures. Prefer
exec.tserrors as-is. A hand-rolled wrap of anallowFailureresult must carry{ stdout, stderr, exitCode, processExitError: true }sonormalizeErrorcan surface the first meaningful stderr line instead of "tool exited with code N". - The wire error contract is
code,message,hint,details(redacted),diagnosticId,logPath, plus optional typed signalsretriable/supportedOn— the fields are absent unless confidently known. Every consumer surface (CLI human, CLI--json, SDK, MCP, events timeline) must carry code + message + hint at minimum; rehydration helpers (throwDaemonError,toDaemonHttpRpcError) copy all fields, andNormalizedErrorhoists the typed signals so--jsonconsumers see them. - Observability. Failed requests always flush diagnostics:
diagnosticId+ ndjsonlogPathaccompany every error (daemon-side under the session state dir, client-side under~/.agent-device/logs). Messages must not embed raw stderr dumps or secrets — structured context belongs indetails, which is redacted at normalize/write time.
Consequences
- Agents can branch on
code, retry onretriable, and followhintwithout parsing prose; wording can improve without breaking consumers (except strings pinned by help-text guarantees — update those tests in lockstep). - The known-code union stays small and meaningful; the widened
(string & {})type still lets runner/daemon codes pass through, so SDK consumers keep adefaultbranch. - New call sites have a documented bar: right code, contextual message naming the target, hint only when the default misleads, cause preserved.