mobile-debug-mcp 0.27.0 → 0.28.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/dist/interact/index.js +238 -11
- package/dist/server/common.js +134 -3
- package/dist/server-core.js +1 -1
- package/docs/CHANGELOG.md +3 -0
- package/docs/rfcs/{012.md → 012-action-trace-and-xecution-observability.md} +43 -4
- package/docs/specs/mcp-tooling-spec-v1.md +14 -0
- package/docs/tools/interact.md +44 -0
- package/package.json +1 -1
- package/src/interact/index.ts +268 -12
- package/src/server/common.ts +194 -4
- package/src/server-core.ts +1 -1
- package/src/types.ts +23 -0
- package/test/device/manual/observe/rfc012_trace.manual.ts +51 -0
- package/test/unit/interact/expect_tools.test.ts +57 -25
- package/test/unit/server/common.test.ts +24 -0
package/dist/interact/index.js
CHANGED
|
@@ -5,7 +5,26 @@ export { AndroidInteract, iOSInteract };
|
|
|
5
5
|
import { resolveTargetDevice } from '../utils/resolve-device.js';
|
|
6
6
|
import { ToolsObserve } from '../observe/index.js';
|
|
7
7
|
import { computeSnapshotSignature } from '../observe/snapshot-metadata.js';
|
|
8
|
-
import { buildActionExecutionResult } from '../server/common.js';
|
|
8
|
+
import { buildActionExecutionResult, createTraceStep, nextActionId } from '../server/common.js';
|
|
9
|
+
function buildObservationTrace({ actionType, stage, success, attempts, metadata }) {
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
const actionId = nextActionId(actionType, now);
|
|
12
|
+
const steps = [
|
|
13
|
+
createTraceStep({
|
|
14
|
+
stage,
|
|
15
|
+
timestamp: now,
|
|
16
|
+
result: success ? 'success' : 'failure',
|
|
17
|
+
attemptIndex: 0,
|
|
18
|
+
metadata
|
|
19
|
+
})
|
|
20
|
+
];
|
|
21
|
+
return {
|
|
22
|
+
action_id: actionId,
|
|
23
|
+
steps,
|
|
24
|
+
final_outcome: success ? 'success' : 'failure',
|
|
25
|
+
attempts: Math.max(1, Math.floor(attempts || 1))
|
|
26
|
+
};
|
|
27
|
+
}
|
|
9
28
|
export class ToolsInteract {
|
|
10
29
|
static _maxResolvedUiElements = 256;
|
|
11
30
|
static _uiChangeKinds = ['hierarchy_diff', 'text_change', 'state_change'];
|
|
@@ -533,7 +552,33 @@ export class ToolsInteract {
|
|
|
533
552
|
let resolvedDeviceId = deviceId;
|
|
534
553
|
const fingerprintBefore = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
535
554
|
let semanticFallbackElement = null;
|
|
555
|
+
const traceSteps = [];
|
|
556
|
+
let traceAttemptIndex = 0;
|
|
557
|
+
const recordTraceStep = (stage, result, metadata) => {
|
|
558
|
+
traceSteps.push(createTraceStep({
|
|
559
|
+
stage,
|
|
560
|
+
timestamp: Date.now(),
|
|
561
|
+
result,
|
|
562
|
+
attemptIndex: traceAttemptIndex++,
|
|
563
|
+
metadata
|
|
564
|
+
}));
|
|
565
|
+
};
|
|
536
566
|
const buildFailure = (failureCode, reason, resolved, device, actualState, attempts, adjustmentMode = 'gesture', retryable = false, uiFingerprintAfter = null) => {
|
|
567
|
+
if (!traceSteps.some((step) => step.stage === 'resolve')) {
|
|
568
|
+
recordTraceStep('resolve', 'failure', {
|
|
569
|
+
reason,
|
|
570
|
+
failure_code: failureCode
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if (!traceSteps.some((step) => step.stage === 'recover')) {
|
|
574
|
+
recordTraceStep('recover', retryable ? 'retry' : 'failure', {
|
|
575
|
+
reason,
|
|
576
|
+
failure_code: failureCode,
|
|
577
|
+
retry_allowed: retryable,
|
|
578
|
+
recovery_attempts: attempts,
|
|
579
|
+
retry_depth: attempts
|
|
580
|
+
});
|
|
581
|
+
}
|
|
537
582
|
const base = buildActionExecutionResult({
|
|
538
583
|
actionType,
|
|
539
584
|
sourceModule: 'interact',
|
|
@@ -554,7 +599,8 @@ export class ToolsInteract {
|
|
|
554
599
|
converged: false,
|
|
555
600
|
within_tolerance: false,
|
|
556
601
|
reason
|
|
557
|
-
}
|
|
602
|
+
},
|
|
603
|
+
traceSteps
|
|
558
604
|
});
|
|
559
605
|
return {
|
|
560
606
|
...base,
|
|
@@ -716,10 +762,23 @@ export class ToolsInteract {
|
|
|
716
762
|
? { property, value: currentValue, raw_value: typeof currentEl.state?.raw_value === 'number' ? currentEl.state.raw_value : undefined }
|
|
717
763
|
: null;
|
|
718
764
|
lastObservedState = actualState;
|
|
765
|
+
if (!traceSteps.some((step) => step.stage === 'resolve')) {
|
|
766
|
+
recordTraceStep('resolve', 'success', {
|
|
767
|
+
resolved_target: resolvedTarget,
|
|
768
|
+
current_value: currentValue,
|
|
769
|
+
adjustment_mode: lastAdjustmentMode
|
|
770
|
+
});
|
|
771
|
+
}
|
|
719
772
|
if (property !== 'value' && property !== 'raw_value') {
|
|
720
773
|
return buildFailure('ELEMENT_NOT_INTERACTABLE', 'adjust_control currently supports numeric value and raw_value properties only', resolvedTarget, currentDevice, actualState, attemptCount, lastAdjustmentMode, false);
|
|
721
774
|
}
|
|
722
775
|
if (currentValue !== null && Math.abs(currentValue - targetValue) <= normalizedTolerance) {
|
|
776
|
+
recordTraceStep('verify', 'success', {
|
|
777
|
+
property,
|
|
778
|
+
target_value: targetValue,
|
|
779
|
+
actual_state: actualState,
|
|
780
|
+
reason: 'control already within tolerance'
|
|
781
|
+
});
|
|
723
782
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
724
783
|
const base = buildActionExecutionResult({
|
|
725
784
|
actionType,
|
|
@@ -740,7 +799,8 @@ export class ToolsInteract {
|
|
|
740
799
|
converged: true,
|
|
741
800
|
within_tolerance: true,
|
|
742
801
|
reason: 'control already within tolerance'
|
|
743
|
-
}
|
|
802
|
+
},
|
|
803
|
+
traceSteps
|
|
744
804
|
});
|
|
745
805
|
return {
|
|
746
806
|
...base,
|
|
@@ -810,6 +870,11 @@ export class ToolsInteract {
|
|
|
810
870
|
for (let i = 0; i < probePoints.length; i++) {
|
|
811
871
|
const probePoint = probePoints[i];
|
|
812
872
|
lastAdjustmentMode = 'coordinate';
|
|
873
|
+
recordTraceStep('execute', 'retry', {
|
|
874
|
+
attempt: attemptCount + 1,
|
|
875
|
+
mode: 'coordinate',
|
|
876
|
+
point: probePoint
|
|
877
|
+
});
|
|
813
878
|
const actionResult = await ToolsInteract.tapHandler({
|
|
814
879
|
platform: resolvedPlatform,
|
|
815
880
|
x: probePoint.x,
|
|
@@ -819,11 +884,24 @@ export class ToolsInteract {
|
|
|
819
884
|
attemptCount++;
|
|
820
885
|
actionDevice = actionResult.device ?? actionDevice;
|
|
821
886
|
if (!actionResult.success) {
|
|
887
|
+
recordTraceStep('execute', 'retry', {
|
|
888
|
+
attempt: attemptCount,
|
|
889
|
+
mode: 'coordinate',
|
|
890
|
+
point: probePoint,
|
|
891
|
+
success: false
|
|
892
|
+
});
|
|
822
893
|
continue;
|
|
823
894
|
}
|
|
824
895
|
verificationResult = await runVerification();
|
|
825
896
|
observedState = verificationResult.observedState;
|
|
826
897
|
lastObservedState = observedState;
|
|
898
|
+
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
899
|
+
attempt: attemptCount,
|
|
900
|
+
property,
|
|
901
|
+
target_value: targetValue,
|
|
902
|
+
actual_state: observedState,
|
|
903
|
+
reason: verificationResult.verification?.reason ?? 'control did not converge yet'
|
|
904
|
+
});
|
|
827
905
|
if (verificationResult.withinTolerance) {
|
|
828
906
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
829
907
|
const base = buildActionExecutionResult({
|
|
@@ -845,7 +923,8 @@ export class ToolsInteract {
|
|
|
845
923
|
converged: true,
|
|
846
924
|
within_tolerance: true,
|
|
847
925
|
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
848
|
-
}
|
|
926
|
+
},
|
|
927
|
+
traceSteps
|
|
849
928
|
});
|
|
850
929
|
return {
|
|
851
930
|
...base,
|
|
@@ -864,6 +943,12 @@ export class ToolsInteract {
|
|
|
864
943
|
}
|
|
865
944
|
if (currentValue !== null) {
|
|
866
945
|
lastAdjustmentMode = 'gesture';
|
|
946
|
+
recordTraceStep('execute', 'retry', {
|
|
947
|
+
attempt: attemptCount + 1,
|
|
948
|
+
mode: 'gesture',
|
|
949
|
+
start: currentPoint,
|
|
950
|
+
end: targetPoint
|
|
951
|
+
});
|
|
867
952
|
const fallbackActionResult = await ToolsInteract.swipeHandler({
|
|
868
953
|
platform: resolvedPlatform,
|
|
869
954
|
x1: currentPoint.x,
|
|
@@ -875,12 +960,26 @@ export class ToolsInteract {
|
|
|
875
960
|
});
|
|
876
961
|
attemptCount++;
|
|
877
962
|
if (!fallbackActionResult.success) {
|
|
963
|
+
recordTraceStep('execute', 'failure', {
|
|
964
|
+
attempt: attemptCount,
|
|
965
|
+
mode: 'gesture',
|
|
966
|
+
start: currentPoint,
|
|
967
|
+
end: targetPoint,
|
|
968
|
+
success: false
|
|
969
|
+
});
|
|
878
970
|
return buildFailure('UNKNOWN', fallbackActionResult.error ?? 'adjustment gesture failed', resolvedTarget, fallbackActionResult.device ?? actionDevice, observedState ?? actualState, attemptCount, lastAdjustmentMode, false);
|
|
879
971
|
}
|
|
880
972
|
actionDevice = fallbackActionResult.device ?? actionDevice;
|
|
881
973
|
verificationResult = await runVerification();
|
|
882
974
|
observedState = verificationResult.observedState;
|
|
883
975
|
lastObservedState = observedState;
|
|
976
|
+
recordTraceStep('verify', verificationResult.withinTolerance ? 'success' : 'retry', {
|
|
977
|
+
attempt: attemptCount,
|
|
978
|
+
property,
|
|
979
|
+
target_value: targetValue,
|
|
980
|
+
actual_state: observedState,
|
|
981
|
+
reason: verificationResult.verification?.reason ?? 'gesture adjustment did not converge yet'
|
|
982
|
+
});
|
|
884
983
|
if (verificationResult.withinTolerance) {
|
|
885
984
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
886
985
|
const base = buildActionExecutionResult({
|
|
@@ -902,7 +1001,8 @@ export class ToolsInteract {
|
|
|
902
1001
|
converged: true,
|
|
903
1002
|
within_tolerance: true,
|
|
904
1003
|
reason: verificationResult.verification?.reason ?? 'control converged to target value'
|
|
905
|
-
}
|
|
1004
|
+
},
|
|
1005
|
+
traceSteps
|
|
906
1006
|
});
|
|
907
1007
|
return {
|
|
908
1008
|
...base,
|
|
@@ -922,6 +1022,13 @@ export class ToolsInteract {
|
|
|
922
1022
|
verification = verificationResult.verification;
|
|
923
1023
|
lastObservedState = observedState;
|
|
924
1024
|
if (verificationResult.withinTolerance) {
|
|
1025
|
+
recordTraceStep('verify', 'success', {
|
|
1026
|
+
attempt: attemptCount,
|
|
1027
|
+
property,
|
|
1028
|
+
target_value: targetValue,
|
|
1029
|
+
actual_state: observedState,
|
|
1030
|
+
reason: verification?.reason ?? 'control converged to target value'
|
|
1031
|
+
});
|
|
925
1032
|
const uiFingerprintAfter = await ToolsInteract._captureFingerprint(resolvedPlatform, resolvedDeviceId);
|
|
926
1033
|
const base = buildActionExecutionResult({
|
|
927
1034
|
actionType,
|
|
@@ -942,7 +1049,8 @@ export class ToolsInteract {
|
|
|
942
1049
|
converged: true,
|
|
943
1050
|
within_tolerance: true,
|
|
944
1051
|
reason: verification?.reason ?? 'control converged to target value'
|
|
945
|
-
}
|
|
1052
|
+
},
|
|
1053
|
+
traceSteps
|
|
946
1054
|
});
|
|
947
1055
|
return {
|
|
948
1056
|
...base,
|
|
@@ -1716,7 +1824,24 @@ export class ToolsInteract {
|
|
|
1716
1824
|
basis,
|
|
1717
1825
|
matched: success,
|
|
1718
1826
|
reason
|
|
1719
|
-
}
|
|
1827
|
+
},
|
|
1828
|
+
trace: buildObservationTrace({
|
|
1829
|
+
actionType: 'expect_screen',
|
|
1830
|
+
stage: 'verify',
|
|
1831
|
+
success,
|
|
1832
|
+
attempts: 1,
|
|
1833
|
+
metadata: {
|
|
1834
|
+
expected_screen: expectedScreen,
|
|
1835
|
+
observed_screen: {
|
|
1836
|
+
fingerprint: observedScreen.fingerprint,
|
|
1837
|
+
screen: observedScreenLabel
|
|
1838
|
+
},
|
|
1839
|
+
comparison: {
|
|
1840
|
+
basis,
|
|
1841
|
+
matched: success
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
})
|
|
1720
1845
|
};
|
|
1721
1846
|
}
|
|
1722
1847
|
static async expectElementVisibleHandler({ selector, element_id, timeout_ms = 5000, poll_interval_ms = 300, platform, deviceId }) {
|
|
@@ -1770,7 +1895,18 @@ export class ToolsInteract {
|
|
|
1770
1895
|
semantic: result.element.semantic ?? null
|
|
1771
1896
|
}
|
|
1772
1897
|
},
|
|
1773
|
-
reason: 'selector is visible'
|
|
1898
|
+
reason: 'selector is visible',
|
|
1899
|
+
trace: buildObservationTrace({
|
|
1900
|
+
actionType: 'expect_element_visible',
|
|
1901
|
+
stage: 'verify',
|
|
1902
|
+
success: true,
|
|
1903
|
+
attempts: 1,
|
|
1904
|
+
metadata: {
|
|
1905
|
+
selector,
|
|
1906
|
+
element_id: result.element.elementId ?? element_id ?? null,
|
|
1907
|
+
status: result.status
|
|
1908
|
+
}
|
|
1909
|
+
})
|
|
1774
1910
|
};
|
|
1775
1911
|
}
|
|
1776
1912
|
const errorCode = result?.error?.code === 'INTERNAL_ERROR' ? 'UNKNOWN' : 'TIMEOUT';
|
|
@@ -1788,7 +1924,19 @@ export class ToolsInteract {
|
|
|
1788
1924
|
},
|
|
1789
1925
|
reason: result?.error?.message ?? 'selector is not visible',
|
|
1790
1926
|
failure_code: errorCode,
|
|
1791
|
-
retryable: errorCode === 'TIMEOUT'
|
|
1927
|
+
retryable: errorCode === 'TIMEOUT',
|
|
1928
|
+
trace: buildObservationTrace({
|
|
1929
|
+
actionType: 'expect_element_visible',
|
|
1930
|
+
stage: 'verify',
|
|
1931
|
+
success: false,
|
|
1932
|
+
attempts: 1,
|
|
1933
|
+
metadata: {
|
|
1934
|
+
selector,
|
|
1935
|
+
element_id: element_id ?? null,
|
|
1936
|
+
status: result?.status ?? null,
|
|
1937
|
+
reason: result?.error?.message ?? 'selector is not visible'
|
|
1938
|
+
}
|
|
1939
|
+
})
|
|
1792
1940
|
};
|
|
1793
1941
|
}
|
|
1794
1942
|
static async expectStateHandler({ selector, element_id, property, expected, platform, deviceId, stabilization_window_ms = 1000, stable_observation_count = 2, snapshot_stale_threshold_ms = 500, poll_interval_ms = 150 }) {
|
|
@@ -1809,6 +1957,24 @@ export class ToolsInteract {
|
|
|
1809
1957
|
let lastObservedValue = null;
|
|
1810
1958
|
let lastRawValue = null;
|
|
1811
1959
|
let lastResolvedElementId = element_id ?? null;
|
|
1960
|
+
const traceSteps = [];
|
|
1961
|
+
let traceAttemptIndex = 0;
|
|
1962
|
+
let resolveRecorded = false;
|
|
1963
|
+
const recordTraceStep = (stage, result, metadata) => {
|
|
1964
|
+
traceSteps.push(createTraceStep({
|
|
1965
|
+
stage,
|
|
1966
|
+
timestamp: Date.now(),
|
|
1967
|
+
result,
|
|
1968
|
+
attemptIndex: traceAttemptIndex++,
|
|
1969
|
+
metadata
|
|
1970
|
+
}));
|
|
1971
|
+
};
|
|
1972
|
+
const buildStateTrace = (outcome) => ({
|
|
1973
|
+
action_id: nextActionId('expect_state', Date.now()),
|
|
1974
|
+
steps: traceSteps,
|
|
1975
|
+
final_outcome: outcome,
|
|
1976
|
+
attempts
|
|
1977
|
+
});
|
|
1812
1978
|
while (Date.now() <= deadline) {
|
|
1813
1979
|
attempts++;
|
|
1814
1980
|
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
@@ -1832,6 +1998,19 @@ export class ToolsInteract {
|
|
|
1832
1998
|
lastReason = 'element not found';
|
|
1833
1999
|
lastFailureCode = 'ELEMENT_NOT_FOUND';
|
|
1834
2000
|
stableCount = 0;
|
|
2001
|
+
recordTraceStep('resolve', 'retry', {
|
|
2002
|
+
selector: selector ?? null,
|
|
2003
|
+
element_id: lastResolvedElementId,
|
|
2004
|
+
matched: false,
|
|
2005
|
+
reason: lastReason,
|
|
2006
|
+
attempt: attempts
|
|
2007
|
+
});
|
|
2008
|
+
recordTraceStep('stabilize', 'retry', {
|
|
2009
|
+
stabilization_attempts: attempts,
|
|
2010
|
+
stable_observation_count: stableCount,
|
|
2011
|
+
snapshot_freshness_ms: treeAgeMs,
|
|
2012
|
+
reason: lastReason
|
|
2013
|
+
});
|
|
1835
2014
|
await sleep(pollDelay);
|
|
1836
2015
|
continue;
|
|
1837
2016
|
}
|
|
@@ -1842,9 +2021,31 @@ export class ToolsInteract {
|
|
|
1842
2021
|
lastReason = 'stale snapshot';
|
|
1843
2022
|
lastFailureCode = 'UNKNOWN';
|
|
1844
2023
|
stableCount = 0;
|
|
2024
|
+
recordTraceStep('resolve', 'retry', {
|
|
2025
|
+
selector: selector ?? null,
|
|
2026
|
+
element_id: lastResolvedElementId,
|
|
2027
|
+
matched: true,
|
|
2028
|
+
reason: lastReason,
|
|
2029
|
+
attempt: attempts
|
|
2030
|
+
});
|
|
2031
|
+
recordTraceStep('stabilize', 'retry', {
|
|
2032
|
+
stabilization_attempts: attempts,
|
|
2033
|
+
stable_observation_count: stableCount,
|
|
2034
|
+
snapshot_freshness_ms: treeAgeMs,
|
|
2035
|
+
reason: lastReason
|
|
2036
|
+
});
|
|
1845
2037
|
await sleep(pollDelay);
|
|
1846
2038
|
continue;
|
|
1847
2039
|
}
|
|
2040
|
+
if (!resolveRecorded) {
|
|
2041
|
+
recordTraceStep('resolve', 'success', {
|
|
2042
|
+
selector: selector ?? null,
|
|
2043
|
+
element_id: lastResolvedElementId,
|
|
2044
|
+
matched: true,
|
|
2045
|
+
reason: 'element resolved'
|
|
2046
|
+
});
|
|
2047
|
+
resolveRecorded = true;
|
|
2048
|
+
}
|
|
1848
2049
|
const observedState = matched.el.state ?? null;
|
|
1849
2050
|
const actual = observedState?.[property] ?? null;
|
|
1850
2051
|
let success = false;
|
|
@@ -1964,6 +2165,18 @@ export class ToolsInteract {
|
|
|
1964
2165
|
}
|
|
1965
2166
|
if (success) {
|
|
1966
2167
|
stableCount++;
|
|
2168
|
+
recordTraceStep('stabilize', 'success', {
|
|
2169
|
+
stabilization_attempts: attempts,
|
|
2170
|
+
stable_observation_count: stableCount,
|
|
2171
|
+
snapshot_freshness_ms: treeAgeMs,
|
|
2172
|
+
reason
|
|
2173
|
+
});
|
|
2174
|
+
recordTraceStep('verify', 'success', {
|
|
2175
|
+
property,
|
|
2176
|
+
expected,
|
|
2177
|
+
observed_value: observedValue,
|
|
2178
|
+
reason
|
|
2179
|
+
});
|
|
1967
2180
|
if (stableCount >= stableTarget) {
|
|
1968
2181
|
return {
|
|
1969
2182
|
success: true,
|
|
@@ -1980,7 +2193,8 @@ export class ToolsInteract {
|
|
|
1980
2193
|
stabilization_attempts: attempts,
|
|
1981
2194
|
stabilization_window_ms: Date.now() - start,
|
|
1982
2195
|
stable_observation_count: stableCount,
|
|
1983
|
-
snapshot_freshness_ms: treeAgeMs ?? undefined
|
|
2196
|
+
snapshot_freshness_ms: treeAgeMs ?? undefined,
|
|
2197
|
+
trace: buildStateTrace('success')
|
|
1984
2198
|
};
|
|
1985
2199
|
}
|
|
1986
2200
|
}
|
|
@@ -1988,6 +2202,18 @@ export class ToolsInteract {
|
|
|
1988
2202
|
stableCount = 0;
|
|
1989
2203
|
lastReason = reason || lastReason;
|
|
1990
2204
|
lastFailureCode = 'UNKNOWN';
|
|
2205
|
+
recordTraceStep('stabilize', 'retry', {
|
|
2206
|
+
stabilization_attempts: attempts,
|
|
2207
|
+
stable_observation_count: stableCount,
|
|
2208
|
+
snapshot_freshness_ms: treeAgeMs,
|
|
2209
|
+
reason: lastReason
|
|
2210
|
+
});
|
|
2211
|
+
recordTraceStep('verify', 'retry', {
|
|
2212
|
+
property,
|
|
2213
|
+
expected,
|
|
2214
|
+
observed_value: observedValue,
|
|
2215
|
+
reason: lastReason
|
|
2216
|
+
});
|
|
1991
2217
|
}
|
|
1992
2218
|
if (!success) {
|
|
1993
2219
|
lastObservedValue = observedValue;
|
|
@@ -2008,7 +2234,8 @@ export class ToolsInteract {
|
|
|
2008
2234
|
},
|
|
2009
2235
|
reason: lastReason,
|
|
2010
2236
|
failure_code: lastFailureCode,
|
|
2011
|
-
retryable: true
|
|
2237
|
+
retryable: true,
|
|
2238
|
+
trace: buildStateTrace('failure')
|
|
2012
2239
|
};
|
|
2013
2240
|
}
|
|
2014
2241
|
static async waitForUICore({ type = 'ui', query, timeoutMs = 30000, pollIntervalMs = 300, includeSnapshotOnFailure = true, match = 'present', stability_ms = 700, observationDelayMs = 0, platform, deviceId }) {
|
package/dist/server/common.js
CHANGED
|
@@ -71,6 +71,119 @@ export async function captureActionFingerprint(platform, deviceId) {
|
|
|
71
71
|
return null;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
export function createTraceStep({ stage, timestamp, result, attemptIndex, cycleId, metadata }) {
|
|
75
|
+
return {
|
|
76
|
+
stage,
|
|
77
|
+
timestamp,
|
|
78
|
+
result,
|
|
79
|
+
attempt_index: attemptIndex,
|
|
80
|
+
...(cycleId !== undefined ? { cycle_id: cycleId } : {}),
|
|
81
|
+
...(metadata ? { metadata } : {})
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function buildActionTrace({ actionId, actionType, sourceModule, selector, resolved, success, failure, details, recovery, attempts = 1, steps }) {
|
|
85
|
+
if (steps && steps.length > 0) {
|
|
86
|
+
return {
|
|
87
|
+
action_id: actionId,
|
|
88
|
+
steps,
|
|
89
|
+
final_outcome: success ? 'success' : 'failure',
|
|
90
|
+
attempts: Math.max(1, Math.floor(attempts || steps.length))
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const start = Date.now();
|
|
94
|
+
const builtSteps = [];
|
|
95
|
+
let attemptIndex = 0;
|
|
96
|
+
const totalAttempts = Math.max(1, Math.floor(attempts || 1));
|
|
97
|
+
if (selector || resolved) {
|
|
98
|
+
const stageResult = resolved ? 'success' : 'failure';
|
|
99
|
+
builtSteps.push(createTraceStep({
|
|
100
|
+
stage: 'resolve',
|
|
101
|
+
timestamp: start,
|
|
102
|
+
result: stageResult,
|
|
103
|
+
attemptIndex: attemptIndex++,
|
|
104
|
+
metadata: {
|
|
105
|
+
action_type: actionType,
|
|
106
|
+
source_module: sourceModule,
|
|
107
|
+
selector: selector ?? null,
|
|
108
|
+
resolved: resolved ? normalizeResolvedTarget(resolved) : null
|
|
109
|
+
}
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
builtSteps.push(createTraceStep({
|
|
113
|
+
stage: 'execute',
|
|
114
|
+
timestamp: Date.now(),
|
|
115
|
+
result: success ? 'success' : 'failure',
|
|
116
|
+
attemptIndex: attemptIndex++,
|
|
117
|
+
metadata: {
|
|
118
|
+
action_type: actionType,
|
|
119
|
+
source_module: sourceModule,
|
|
120
|
+
...(failure ? { failure_code: failure.failureCode, retryable: failure.retryable } : {})
|
|
121
|
+
}
|
|
122
|
+
}));
|
|
123
|
+
const hasStabilizeDetails = Boolean(details && (Object.prototype.hasOwnProperty.call(details, 'stabilization_attempts') ||
|
|
124
|
+
Object.prototype.hasOwnProperty.call(details, 'stable_observation_count') ||
|
|
125
|
+
Object.prototype.hasOwnProperty.call(details, 'snapshot_freshness_ms')));
|
|
126
|
+
const hasVerifyDetails = Boolean(details && (Object.prototype.hasOwnProperty.call(details, 'within_tolerance') ||
|
|
127
|
+
Object.prototype.hasOwnProperty.call(details, 'converged') ||
|
|
128
|
+
Object.prototype.hasOwnProperty.call(details, 'observed_state')));
|
|
129
|
+
if (hasStabilizeDetails) {
|
|
130
|
+
builtSteps.push(createTraceStep({
|
|
131
|
+
stage: 'stabilize',
|
|
132
|
+
timestamp: Date.now(),
|
|
133
|
+
result: success ? 'success' : 'failure',
|
|
134
|
+
attemptIndex: attemptIndex++,
|
|
135
|
+
metadata: {
|
|
136
|
+
stabilization_attempts: details?.stabilization_attempts ?? null,
|
|
137
|
+
stable_observation_count: details?.stable_observation_count ?? null,
|
|
138
|
+
snapshot_freshness_ms: details?.snapshot_freshness_ms ?? null
|
|
139
|
+
}
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
if (hasVerifyDetails) {
|
|
143
|
+
builtSteps.push(createTraceStep({
|
|
144
|
+
stage: 'verify',
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
result: success ? 'success' : 'failure',
|
|
147
|
+
attemptIndex: attemptIndex++,
|
|
148
|
+
metadata: {
|
|
149
|
+
within_tolerance: details?.within_tolerance ?? null,
|
|
150
|
+
converged: details?.converged ?? null,
|
|
151
|
+
actual_state: details?.actual_state ?? null,
|
|
152
|
+
reason: details?.reason ?? null
|
|
153
|
+
}
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
if (failure) {
|
|
157
|
+
builtSteps.push(createTraceStep({
|
|
158
|
+
stage: 'recover',
|
|
159
|
+
timestamp: Date.now(),
|
|
160
|
+
result: failure.retryable ? 'retry' : 'failure',
|
|
161
|
+
attemptIndex: attemptIndex++,
|
|
162
|
+
metadata: {
|
|
163
|
+
failure_class: recovery?.failure_class ?? mapFailureCodeToFailureClass(failure.failureCode),
|
|
164
|
+
runtime_code: failure.failureCode,
|
|
165
|
+
retry_allowed: failure.retryable,
|
|
166
|
+
recovery_attempts: recovery?.recovery_attempts ?? 0,
|
|
167
|
+
retry_depth: recovery?.retry_depth ?? 0
|
|
168
|
+
}
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
if (!builtSteps.length) {
|
|
172
|
+
builtSteps.push(createTraceStep({
|
|
173
|
+
stage: 'execute',
|
|
174
|
+
timestamp: start,
|
|
175
|
+
result: success ? 'success' : 'failure',
|
|
176
|
+
attemptIndex: 0,
|
|
177
|
+
metadata: { action_type: actionType, source_module: sourceModule }
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
action_id: actionId,
|
|
182
|
+
steps: builtSteps,
|
|
183
|
+
final_outcome: success ? 'success' : 'failure',
|
|
184
|
+
attempts: totalAttempts
|
|
185
|
+
};
|
|
186
|
+
}
|
|
74
187
|
export function normalizeResolvedTarget(value = null) {
|
|
75
188
|
if (!value)
|
|
76
189
|
return null;
|
|
@@ -144,11 +257,16 @@ function buildRecoveryState(failureCode, retryable) {
|
|
|
144
257
|
retry_allowed: retryable
|
|
145
258
|
};
|
|
146
259
|
}
|
|
147
|
-
export function buildActionExecutionResult({ actionType, device, selector, resolved, success, uiFingerprintBefore, uiFingerprintAfter, failure, details, sourceModule }) {
|
|
260
|
+
export function buildActionExecutionResult({ actionType, device, selector, resolved, success, uiFingerprintBefore, uiFingerprintAfter, failure, details, sourceModule, traceSteps }) {
|
|
148
261
|
const timestampMs = Date.now();
|
|
149
262
|
const timestamp = new Date(timestampMs).toISOString();
|
|
263
|
+
const actionId = nextActionId(actionType, timestampMs);
|
|
264
|
+
const recoveryState = failure ? buildRecoveryState(failure.failureCode, failure.retryable) : undefined;
|
|
265
|
+
const attempts = typeof details?.attempts === 'number' && Number.isFinite(details.attempts)
|
|
266
|
+
? Math.max(1, Math.floor(details.attempts))
|
|
267
|
+
: 1;
|
|
150
268
|
return {
|
|
151
|
-
action_id:
|
|
269
|
+
action_id: actionId,
|
|
152
270
|
timestamp,
|
|
153
271
|
action_type: actionType,
|
|
154
272
|
lifecycle_state: determineActionLifecycleState({ success, failure }),
|
|
@@ -160,7 +278,20 @@ export function buildActionExecutionResult({ actionType, device, selector, resol
|
|
|
160
278
|
},
|
|
161
279
|
success,
|
|
162
280
|
...(failure ? { failure_code: failure.failureCode, retryable: failure.retryable } : {}),
|
|
163
|
-
...(
|
|
281
|
+
...(recoveryState ? { recovery: recoveryState } : {}),
|
|
282
|
+
trace: buildActionTrace({
|
|
283
|
+
actionId,
|
|
284
|
+
actionType,
|
|
285
|
+
sourceModule,
|
|
286
|
+
selector,
|
|
287
|
+
resolved,
|
|
288
|
+
success,
|
|
289
|
+
failure,
|
|
290
|
+
details,
|
|
291
|
+
recovery: recoveryState,
|
|
292
|
+
attempts,
|
|
293
|
+
steps: traceSteps
|
|
294
|
+
}),
|
|
164
295
|
ui_fingerprint_before: uiFingerprintBefore,
|
|
165
296
|
ui_fingerprint_after: uiFingerprintAfter,
|
|
166
297
|
...(details ? { details } : {})
|
package/dist/server-core.js
CHANGED
|
@@ -6,7 +6,7 @@ import { handleToolCall } from './server/tool-handlers.js';
|
|
|
6
6
|
export { wrapResponse, toolDefinitions, handleToolCall };
|
|
7
7
|
export const serverInfo = {
|
|
8
8
|
name: 'mobile-debug-mcp',
|
|
9
|
-
version: '0.
|
|
9
|
+
version: '0.28.0'
|
|
10
10
|
};
|
|
11
11
|
export function createServer() {
|
|
12
12
|
const server = new Server(serverInfo, {
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.28.0]
|
|
6
|
+
- Added structured execution trace model for all actions within the MCP runtime. It provides visibility into resolution, execution, verification, stabilization, and recovery stages.
|
|
7
|
+
|
|
5
8
|
## [0.27.0]
|
|
6
9
|
- defines a structured recovery and replanning model for UI interaction failures, enabling the system to respond to execution uncertainty with bounded, deterministic recovery strategies.
|
|
7
10
|
|