@mobileai/react-native 0.9.18 → 0.9.19

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.
Files changed (80) hide show
  1. package/LICENSE +28 -20
  2. package/MobileAIFloatingOverlay.podspec +25 -0
  3. package/android/build.gradle +61 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +151 -0
  6. package/android/src/main/java/com/mobileai/overlay/MobileAIOverlayPackage.kt +23 -0
  7. package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +45 -0
  8. package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +29 -0
  9. package/ios/MobileAIFloatingOverlayComponentView.mm +73 -0
  10. package/lib/module/components/AIAgent.js +902 -136
  11. package/lib/module/components/AIConsentDialog.js +439 -0
  12. package/lib/module/components/AgentChatBar.js +828 -134
  13. package/lib/module/components/AgentOverlay.js +2 -1
  14. package/lib/module/components/DiscoveryTooltip.js +21 -9
  15. package/lib/module/components/FloatingOverlayWrapper.js +108 -0
  16. package/lib/module/components/Icons.js +123 -0
  17. package/lib/module/config/endpoints.js +12 -2
  18. package/lib/module/core/AgentRuntime.js +373 -27
  19. package/lib/module/core/FiberAdapter.js +56 -0
  20. package/lib/module/core/FiberTreeWalker.js +186 -80
  21. package/lib/module/core/IdleDetector.js +19 -0
  22. package/lib/module/core/NativeAlertInterceptor.js +191 -0
  23. package/lib/module/core/systemPrompt.js +203 -45
  24. package/lib/module/index.js +3 -0
  25. package/lib/module/providers/GeminiProvider.js +72 -56
  26. package/lib/module/providers/ProviderFactory.js +6 -2
  27. package/lib/module/services/AudioInputService.js +3 -12
  28. package/lib/module/services/AudioOutputService.js +1 -13
  29. package/lib/module/services/ConversationService.js +166 -0
  30. package/lib/module/services/MobileAIKnowledgeRetriever.js +41 -0
  31. package/lib/module/services/VoiceService.js +29 -8
  32. package/lib/module/services/telemetry/MobileAI.js +44 -0
  33. package/lib/module/services/telemetry/TelemetryService.js +13 -1
  34. package/lib/module/services/telemetry/TouchAutoCapture.js +44 -18
  35. package/lib/module/specs/FloatingOverlayNativeComponent.ts +19 -0
  36. package/lib/module/support/CSATSurvey.js +95 -12
  37. package/lib/module/support/EscalationSocket.js +70 -1
  38. package/lib/module/support/ReportedIssueEventSource.js +148 -0
  39. package/lib/module/support/escalateTool.js +4 -2
  40. package/lib/module/support/index.js +1 -0
  41. package/lib/module/support/reportIssueTool.js +127 -0
  42. package/lib/module/support/supportPrompt.js +77 -9
  43. package/lib/module/tools/guideTool.js +2 -1
  44. package/lib/module/tools/longPressTool.js +4 -3
  45. package/lib/module/tools/pickerTool.js +6 -4
  46. package/lib/module/tools/tapTool.js +12 -3
  47. package/lib/module/tools/typeTool.js +19 -10
  48. package/lib/module/utils/logger.js +175 -6
  49. package/lib/typescript/react-native.config.d.ts +11 -0
  50. package/lib/typescript/src/components/AIAgent.d.ts +28 -2
  51. package/lib/typescript/src/components/AIConsentDialog.d.ts +153 -0
  52. package/lib/typescript/src/components/AgentChatBar.d.ts +15 -2
  53. package/lib/typescript/src/components/DiscoveryTooltip.d.ts +3 -1
  54. package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +51 -0
  55. package/lib/typescript/src/components/Icons.d.ts +8 -0
  56. package/lib/typescript/src/config/endpoints.d.ts +5 -3
  57. package/lib/typescript/src/core/AgentRuntime.d.ts +4 -0
  58. package/lib/typescript/src/core/FiberAdapter.d.ts +25 -0
  59. package/lib/typescript/src/core/FiberTreeWalker.d.ts +2 -0
  60. package/lib/typescript/src/core/IdleDetector.d.ts +11 -0
  61. package/lib/typescript/src/core/NativeAlertInterceptor.d.ts +55 -0
  62. package/lib/typescript/src/core/types.d.ts +106 -1
  63. package/lib/typescript/src/index.d.ts +9 -4
  64. package/lib/typescript/src/providers/GeminiProvider.d.ts +6 -5
  65. package/lib/typescript/src/services/ConversationService.d.ts +55 -0
  66. package/lib/typescript/src/services/MobileAIKnowledgeRetriever.d.ts +9 -0
  67. package/lib/typescript/src/services/telemetry/MobileAI.d.ts +7 -0
  68. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +1 -1
  69. package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts +9 -6
  70. package/lib/typescript/src/services/telemetry/types.d.ts +3 -1
  71. package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +17 -0
  72. package/lib/typescript/src/support/EscalationSocket.d.ts +17 -0
  73. package/lib/typescript/src/support/ReportedIssueEventSource.d.ts +24 -0
  74. package/lib/typescript/src/support/escalateTool.d.ts +5 -0
  75. package/lib/typescript/src/support/index.d.ts +2 -1
  76. package/lib/typescript/src/support/reportIssueTool.d.ts +20 -0
  77. package/lib/typescript/src/support/types.d.ts +56 -1
  78. package/lib/typescript/src/utils/logger.d.ts +15 -0
  79. package/package.json +19 -5
  80. package/react-native.config.js +12 -0
@@ -16,9 +16,18 @@ import { walkFiberTree } from "./FiberTreeWalker.js";
16
16
  import { dehydrateScreen } from "./ScreenDehydrator.js";
17
17
  import { buildSystemPrompt, buildKnowledgeOnlyPrompt } from "./systemPrompt.js";
18
18
  import { KnowledgeBaseService } from "../services/KnowledgeBaseService.js";
19
+ import { installAlertInterceptor, uninstallAlertInterceptor } from "./NativeAlertInterceptor.js";
19
20
  import { createTapTool, createLongPressTool, createTypeTool, createScrollTool, createSliderTool, createPickerTool, createDatePickerTool, createKeyboardTool, createGuideTool, createSimplifyTool, createRestoreTool } from "../tools/index.js";
20
21
  import { actionRegistry } from "./ActionRegistry.js";
21
22
  const DEFAULT_MAX_STEPS = 25;
23
+ function generateTraceId() {
24
+ return `trace_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
25
+ }
26
+ const APPROVAL_GRANTED_TOKEN = '__APPROVAL_GRANTED__';
27
+ const APPROVAL_REJECTED_TOKEN = '__APPROVAL_REJECTED__';
28
+ const APPROVAL_ALREADY_DONE_TOKEN = '__APPROVAL_ALREADY_DONE__';
29
+ const USER_ALREADY_COMPLETED_MESSAGE = '✅ It looks like you already completed that step yourself. Great — let me know if you want help with anything else.';
30
+ const ACTION_NOT_APPROVED_MESSAGE = "Okay — I won't do that. If you'd like, I can help with something else instead.";
22
31
 
23
32
  // ─── Agent Runtime ─────────────────────────────────────────────
24
33
 
@@ -30,6 +39,7 @@ export class AgentRuntime {
30
39
  lastAskUserQuestion = null;
31
40
  knowledgeService = null;
32
41
  lastDehydratedRoot = null;
42
+ currentTraceId = null;
33
43
 
34
44
  // ─── Task-scoped error suppression ──────────────────────────
35
45
  // Installed once at execute() start, removed after grace period.
@@ -39,6 +49,14 @@ export class AgentRuntime {
39
49
  lastSuppressedError = null;
40
50
  graceTimer = null;
41
51
  originalReportErrorsAsExceptions = undefined;
52
+
53
+ // ─── App-action approval gate ────────────────────────────────
54
+ // Tracks whether the support consent flow (ask_user + request_app_action=true)
55
+ // has been issued and whether the user has explicitly approved it via button tap.
56
+ // Only UI-altering tools are gated; informational tools (done, query_knowledge) are not.
57
+ appActionApproved = false; // true only after __APPROVAL_GRANTED__ received
58
+ // Tools that physically alter the app — must be gated by appAction approval
59
+ static APP_ACTION_TOOLS = new Set(['tap', 'type', 'scroll', 'navigate', 'long_press', 'slider', 'picker', 'date_picker', 'keyboard']);
42
60
  getConfig() {
43
61
  return this.config;
44
62
  }
@@ -182,6 +200,11 @@ export class AgentRuntime {
182
200
  description: 'Response message to the user',
183
201
  required: true
184
202
  },
203
+ message: {
204
+ type: 'string',
205
+ description: 'Alternative to text parameter',
206
+ required: false
207
+ },
185
208
  success: {
186
209
  type: 'boolean',
187
210
  description: 'Whether the task was completed successfully',
@@ -189,7 +212,12 @@ export class AgentRuntime {
189
212
  }
190
213
  },
191
214
  execute: async args => {
192
- return args.text;
215
+ let cleanText = args.text || args.message || '';
216
+ if (typeof cleanText === 'string') {
217
+ // Strip bracketed indices safely avoiding regex stack overflows on large strings
218
+ cleanText = cleanText.replace(/\[\d+\]/g, '').replace(/ +/g, ' ').trim();
219
+ }
220
+ return cleanText;
193
221
  }
194
222
  });
195
223
 
@@ -214,23 +242,58 @@ export class AgentRuntime {
214
242
  // ask_user — ask for clarification
215
243
  this.tools.set('ask_user', {
216
244
  name: 'ask_user',
217
- description: 'Ask the user a question and wait for their answer. Use this if you need more information or clarification.',
245
+ description: 'Communicate with the user. Use this to ask questions, request permission for app actions, OR answer a question the user asked.',
218
246
  parameters: {
219
247
  question: {
220
248
  type: 'string',
221
- description: 'Question to ask the user',
249
+ description: 'The message or question to say to the user',
250
+ required: true
251
+ },
252
+ request_app_action: {
253
+ type: 'boolean',
254
+ description: 'Set to true when requesting permission to take an action in the app (navigate, tap, investigate). Shows explicit approval buttons to the user.',
222
255
  required: true
223
256
  }
224
257
  },
225
258
  execute: async args => {
259
+ // Strip any leaked bracketed indices like [41] safely
260
+ let cleanQuestion = args.question || '';
261
+ if (typeof cleanQuestion === 'string') {
262
+ cleanQuestion = cleanQuestion.replace(/\[\d+\]/g, '').replace(/ +/g, ' ').trim();
263
+ }
264
+ const kind = args.request_app_action ? 'approval' : 'freeform';
265
+
266
+ // Mark that the support approval flow has been initiated
267
+ if (args.request_app_action) {
268
+ this.appActionApproved = false; // reset until user taps Allow
269
+ logger.info('AgentRuntime', '🔒 App action gate: approval requested, UI tools now BLOCKED until granted');
270
+ }
271
+ logger.info('AgentRuntime', `❓ ask_user emitted (kind=${kind}): "${cleanQuestion}"`);
226
272
  if (this.config.onAskUser) {
227
273
  // Block until user responds, then continue the loop
228
274
  this.config.onStatusUpdate?.('Waiting for your answer...');
229
- const answer = await this.config.onAskUser(args.question);
275
+ logger.info('AgentRuntime', `â¸ī¸ Waiting for user response via onAskUser callback (kind=${kind})`);
276
+ const answer = await this.config.onAskUser({
277
+ question: cleanQuestion,
278
+ kind
279
+ });
280
+ logger.info('AgentRuntime', `✅ ask_user resolved with: "${String(answer)}"`);
281
+
282
+ // Resolve approval gate based on button response
283
+ if (answer === '__APPROVAL_GRANTED__') {
284
+ this.appActionApproved = true;
285
+ logger.info('AgentRuntime', '✅ App action gate: APPROVED — UI tools unblocked');
286
+ } else if (answer === '__APPROVAL_REJECTED__') {
287
+ this.appActionApproved = false;
288
+ logger.info('AgentRuntime', 'đŸšĢ App action gate: REJECTED — UI tools remain blocked');
289
+ }
290
+ // Any other text answer (conversational interruption) leaves appActionApproved as-is
291
+
230
292
  return `User answered: ${answer}`;
231
293
  }
232
294
  // Legacy fallback: break the loop (context will be lost)
233
- return `❓ ${args.question}`;
295
+ logger.warn('AgentRuntime', 'âš ī¸ ask_user has no onAskUser callback; returning legacy fallback');
296
+ return `❓ ${cleanQuestion}`;
234
297
  }
235
298
  });
236
299
 
@@ -712,7 +775,7 @@ ${screen.elementsText}
712
775
  * The global ErrorUtils handler is task-scoped (installed in execute()),
713
776
  * so this method only needs to CHECK for errors, not install/remove.
714
777
  */
715
- async executeToolSafely(tool, args, toolName) {
778
+ async executeToolSafely(tool, args, toolName, stepIndex) {
716
779
  // Clear any previous suppressed error before this tool
717
780
  this.lastSuppressedError = null;
718
781
 
@@ -720,10 +783,20 @@ ${screen.elementsText}
720
783
  // This prevents AI-driven taps from being tracked as user_interaction events.
721
784
  this.config.onToolExecute?.(true);
722
785
  try {
786
+ this.emitTrace('tool_execution_started', {
787
+ tool: toolName,
788
+ args
789
+ }, stepIndex);
790
+
723
791
  // ── Argument Validation (Pattern from Detox/Appium: typeof checks before native dispatch) ──
724
792
  const validationError = this.validateToolArgs(args, toolName);
725
793
  if (validationError) {
726
794
  logger.warn('AgentRuntime', `đŸ›Ąī¸ Arg validation rejected "${toolName}": ${validationError}`);
795
+ this.emitTrace('tool_validation_rejected', {
796
+ tool: toolName,
797
+ args,
798
+ validationError
799
+ }, stepIndex);
727
800
  return validationError;
728
801
  }
729
802
 
@@ -732,8 +805,29 @@ ${screen.elementsText}
732
805
  // user confirmation before execution. This is the code-level safety net
733
806
  // complementing the prompt-level copilot instructions.
734
807
  if (this.config.interactionMode !== 'autopilot') {
735
- const confirmResult = await this.checkCopilotConfirmation(toolName, args);
808
+ logger.info('AgentRuntime', `đŸ›Ąī¸ Checking copilot confirmation for ${toolName}(${JSON.stringify(args)})`);
809
+ const confirmResult = await this.checkCopilotConfirmation(toolName, args, stepIndex);
736
810
  if (confirmResult) return confirmResult;
811
+ } else {
812
+ logger.info('AgentRuntime', `🚀 interactionMode=autopilot, skipping copilot confirmation for "${toolName}"`);
813
+ this.emitTrace('confirmation_skipped_autopilot', {
814
+ tool: toolName,
815
+ args
816
+ }, stepIndex);
817
+ }
818
+
819
+ // ── App-action approval gate ────────────────────────────────────────
820
+ // Mandate explicit ask_user approval for all UI-altering tools ONLY if we are in
821
+ // copilot mode AND the host app has provided an onAskUser callback.
822
+ // If the model tries to use a UI tool without explicitly getting approval, we block it.
823
+ if (this.config.interactionMode !== 'autopilot' && this.config.onAskUser && AgentRuntime.APP_ACTION_TOOLS.has(toolName) && !this.appActionApproved) {
824
+ const blockedMsg = `đŸšĢ APP ACTION BLOCKED: You are attempting to use "${toolName}" but have not yet received explicit user approval. You MUST first call ask_user(request_app_action=true) and wait for the user to explicitly tap 'Allow' before executing ANY UI actions (including navigate, tap, scroll, etc).`;
825
+ logger.warn('AgentRuntime', blockedMsg);
826
+ this.emitTrace('app_action_gate_blocked', {
827
+ tool: toolName,
828
+ args
829
+ }, stepIndex);
830
+ return blockedMsg;
737
831
  }
738
832
  const result = await tool.execute(args);
739
833
 
@@ -744,11 +838,27 @@ ${screen.elementsText}
744
838
  if (suppressedError) {
745
839
  logger.warn('AgentRuntime', `đŸ›Ąī¸ Tool "${toolName}" caused async error (suppressed): ${suppressedError.message}`);
746
840
  this.lastSuppressedError = null;
841
+ this.emitTrace('tool_async_error_suppressed', {
842
+ tool: toolName,
843
+ args,
844
+ result,
845
+ error: suppressedError.message
846
+ }, stepIndex);
747
847
  return `${result} (âš ī¸ a background error was safely caught: ${suppressedError.message})`;
748
848
  }
849
+ this.emitTrace('tool_execution_finished', {
850
+ tool: toolName,
851
+ args,
852
+ result
853
+ }, stepIndex);
749
854
  return result;
750
855
  } catch (error) {
751
856
  logger.error('AgentRuntime', `Tool "${toolName}" threw: ${error.message}`);
857
+ this.emitTrace('tool_execution_failed', {
858
+ tool: toolName,
859
+ args,
860
+ error: error?.message ?? String(error)
861
+ }, stepIndex);
752
862
  return `❌ Tool "${toolName}" failed: ${error.message}`;
753
863
  } finally {
754
864
  // Always restore the flag — even on error or validation rejection
@@ -784,6 +894,18 @@ ${screen.elementsText}
784
894
  }
785
895
  return null;
786
896
  }
897
+ emitTrace(stage, data = {}, stepIndex) {
898
+ if (!this.currentTraceId || !this.config.onTrace) return;
899
+ const event = {
900
+ traceId: this.currentTraceId,
901
+ stage,
902
+ timestamp: new Date().toISOString(),
903
+ stepIndex,
904
+ screenName: this.getCurrentScreenName(),
905
+ data
906
+ };
907
+ this.config.onTrace(event);
908
+ }
787
909
 
788
910
  // ─── Copilot Confirmation ─────────────────────────────────────
789
911
 
@@ -794,39 +916,145 @@ ${screen.elementsText}
794
916
  * Check if a tool call targets an aiConfirm element and request user confirmation.
795
917
  * Returns null if the action should proceed, or an error string if rejected.
796
918
  */
797
- async checkCopilotConfirmation(toolName, args) {
919
+ async checkCopilotConfirmation(toolName, args, stepIndex) {
798
920
  // Only gate write tools
799
- if (!AgentRuntime.WRITE_TOOLS.has(toolName)) return null;
921
+ if (!AgentRuntime.WRITE_TOOLS.has(toolName)) {
922
+ logger.info('AgentRuntime', `đŸ›Ąī¸ No confirmation needed for "${toolName}" because it is not a write tool`);
923
+ this.emitTrace('confirmation_not_needed', {
924
+ tool: toolName,
925
+ reason: 'not_write_tool',
926
+ args
927
+ }, stepIndex);
928
+ return null;
929
+ }
800
930
 
801
931
  // Look up the target element by index
802
932
  const index = args.index;
803
- if (typeof index !== 'number') return null;
933
+ if (typeof index !== 'number') {
934
+ logger.info('AgentRuntime', `đŸ›Ąī¸ No confirmation needed for "${toolName}" because no element index was provided`);
935
+ this.emitTrace('confirmation_not_needed', {
936
+ tool: toolName,
937
+ reason: 'missing_index',
938
+ args
939
+ }, stepIndex);
940
+ return null;
941
+ }
804
942
  const screen = this.lastDehydratedRoot;
805
- if (!screen?.elements) return null;
943
+ if (!screen?.elements) {
944
+ logger.warn('AgentRuntime', `đŸ›Ąī¸ Could not evaluate confirmation for "${toolName}" because no dehydrated screen was available`);
945
+ this.emitTrace('confirmation_not_evaluated', {
946
+ tool: toolName,
947
+ reason: 'missing_dehydrated_screen',
948
+ args
949
+ }, stepIndex);
950
+ return null;
951
+ }
806
952
  const element = screen.elements.find(e => e.index === index);
807
- if (!element?.requiresConfirmation) return null;
953
+ if (!element) {
954
+ logger.warn('AgentRuntime', `đŸ›Ąī¸ Could not find element index ${index} for "${toolName}" on screen "${screen.screenName}"`);
955
+ this.emitTrace('confirmation_not_evaluated', {
956
+ tool: toolName,
957
+ reason: 'element_not_found',
958
+ args,
959
+ index
960
+ }, stepIndex);
961
+ return null;
962
+ }
963
+ logger.info('AgentRuntime', `đŸ›Ąī¸ Copilot gate inspect: tool="${toolName}" index=${index} label="${element.label}" type="${element.type}" aiConfirm=${element.requiresConfirmation === true}`);
964
+ if (!element.requiresConfirmation) {
965
+ logger.info('AgentRuntime', `đŸ›Ąī¸ No confirmation needed for "${toolName}" on "${element.label}" because aiConfirm is not set`);
966
+ this.emitTrace('confirmation_not_needed', {
967
+ tool: toolName,
968
+ reason: 'aiConfirm_not_set',
969
+ args,
970
+ elementLabel: element.label,
971
+ elementType: element.type,
972
+ index
973
+ }, stepIndex);
974
+ return null;
975
+ }
808
976
 
809
977
  // Element has aiConfirm — request user confirmation
810
978
  const label = element.label || `[${element.type}]`;
811
979
  const description = this.getToolStatusLabel(toolName, args);
812
- const question = `I'm about to ${description} on "${label}". Should I proceed?`;
980
+ const question = `I can do this in the app for you by tapping and typing where needed, and ${description} on "${label}". If you'd rather do it yourself, I can guide you step by step instead.`;
813
981
  logger.info('AgentRuntime', `đŸ›Ąī¸ Copilot: aiConfirm gate triggered for "${toolName}" on "${label}"`);
982
+ this.emitTrace('confirmation_required', {
983
+ tool: toolName,
984
+ args,
985
+ elementLabel: label,
986
+ elementType: element.type,
987
+ index,
988
+ question
989
+ }, stepIndex);
814
990
 
815
991
  // Use onAskUser if available (integrated into chat UI), otherwise Alert.alert
816
992
  if (this.config.onAskUser) {
817
- const response = await this.config.onAskUser(question);
818
- const approved = /^(yes|ok|sure|go|proceed|confirm|y)/i.test(response.trim());
819
- if (!approved) {
993
+ logger.info('AgentRuntime', `đŸ›Ąī¸ Requesting explicit confirmation via ask_user for "${label}"`);
994
+ this.emitTrace('confirmation_prompted', {
995
+ tool: toolName,
996
+ elementLabel: label,
997
+ elementType: element.type,
998
+ question,
999
+ channel: 'ask_user'
1000
+ }, stepIndex);
1001
+ const response = await this.config.onAskUser({
1002
+ question,
1003
+ kind: 'approval'
1004
+ });
1005
+ logger.info('AgentRuntime', `đŸ›Ąī¸ Confirmation response for "${label}": "${String(response)}"`);
1006
+ this.emitTrace('confirmation_response_received', {
1007
+ tool: toolName,
1008
+ elementLabel: label,
1009
+ response: String(response)
1010
+ }, stepIndex);
1011
+ if (response === APPROVAL_ALREADY_DONE_TOKEN) {
1012
+ this.emitTrace('confirmation_already_done', {
1013
+ tool: toolName,
1014
+ elementLabel: label
1015
+ }, stepIndex);
1016
+ return APPROVAL_ALREADY_DONE_TOKEN;
1017
+ }
1018
+ if (response === APPROVAL_GRANTED_TOKEN) {
1019
+ logger.info('AgentRuntime', `✅ User approved "${toolName}" on "${label}"`);
1020
+ this.emitTrace('confirmation_approved', {
1021
+ tool: toolName,
1022
+ elementLabel: label,
1023
+ response: 'explicit_button'
1024
+ }, stepIndex);
1025
+ return null;
1026
+ }
1027
+ if (response === APPROVAL_REJECTED_TOKEN) {
820
1028
  logger.info('AgentRuntime', `🛑 User rejected "${toolName}" on "${label}"`);
821
- return `❌ User rejected "${toolName}" action on "${label}". Ask what they would like to do instead.`;
1029
+ this.emitTrace('confirmation_rejected', {
1030
+ tool: toolName,
1031
+ elementLabel: label,
1032
+ response: 'explicit_button'
1033
+ }, stepIndex);
1034
+ return ACTION_NOT_APPROVED_MESSAGE;
822
1035
  }
823
- return null;
1036
+
1037
+ // If it's conversational input (e.g. "Why?"), pause the action and pass the user's question back to the LLM so it can answer it!
1038
+ this.emitTrace('confirmation_interrupted', {
1039
+ tool: toolName,
1040
+ elementLabel: label,
1041
+ response: String(response)
1042
+ }, stepIndex);
1043
+ return `Action paused because the user interrupted with this message: "${response}". Please answer the user by fully explaining your logic.`;
824
1044
  }
825
1045
 
826
1046
  // Fallback: React Native Alert
827
1047
  const {
828
1048
  Alert
829
1049
  } = require('react-native');
1050
+ logger.info('AgentRuntime', `đŸ›Ąī¸ Requesting explicit confirmation via native Alert for "${label}"`);
1051
+ this.emitTrace('confirmation_prompted', {
1052
+ tool: toolName,
1053
+ elementLabel: label,
1054
+ elementType: element.type,
1055
+ question,
1056
+ channel: 'native_alert'
1057
+ }, stepIndex);
830
1058
  const approved = await new Promise(resolve => {
831
1059
  Alert.alert('Confirm Action', question, [{
832
1060
  text: 'Cancel',
@@ -841,8 +1069,19 @@ ${screen.elementsText}
841
1069
  });
842
1070
  if (!approved) {
843
1071
  logger.info('AgentRuntime', `🛑 User rejected "${toolName}" on "${label}"`);
844
- return `❌ User rejected "${toolName}" action on "${label}". Ask what they would like to do instead.`;
1072
+ this.emitTrace('confirmation_rejected', {
1073
+ tool: toolName,
1074
+ elementLabel: label,
1075
+ response: 'cancel'
1076
+ }, stepIndex);
1077
+ return ACTION_NOT_APPROVED_MESSAGE;
845
1078
  }
1079
+ logger.info('AgentRuntime', `✅ User approved "${toolName}" on "${label}" via native Alert`);
1080
+ this.emitTrace('confirmation_approved', {
1081
+ tool: toolName,
1082
+ elementLabel: label,
1083
+ response: 'continue'
1084
+ }, stepIndex);
846
1085
  return null;
847
1086
  }
848
1087
 
@@ -852,7 +1091,8 @@ ${screen.elementsText}
852
1091
  return {
853
1092
  interactiveBlacklist: this.config.interactiveBlacklist,
854
1093
  interactiveWhitelist: this.config.interactiveWhitelist,
855
- screenName: this.getCurrentScreenName()
1094
+ screenName: this.getCurrentScreenName(),
1095
+ interceptNativeAlerts: this.config.interceptNativeAlerts
856
1096
  };
857
1097
  }
858
1098
 
@@ -1020,8 +1260,11 @@ ${screen.elementsText}
1020
1260
  this.isRunning = true;
1021
1261
  this.isCancelRequested = false;
1022
1262
  this.history = [];
1263
+ this.currentTraceId = generateTraceId();
1023
1264
  this.observations = [];
1024
1265
  this.lastScreenName = '';
1266
+ // Reset app-action approval gate for each new task
1267
+ this.appActionApproved = false;
1025
1268
  const maxSteps = this.config.maxSteps || DEFAULT_MAX_STEPS;
1026
1269
  const stepDelay = this.config.stepDelay ?? 300;
1027
1270
 
@@ -1044,7 +1287,19 @@ ${screen.elementsText}
1044
1287
  // Lifecycle: onBeforeTask
1045
1288
  await this.config.onBeforeTask?.();
1046
1289
  try {
1047
- // ── Start error suppression (3 layers) ──────────────────
1290
+ this.emitTrace('task_started', {
1291
+ message: userMessage,
1292
+ contextualMessage,
1293
+ maxSteps,
1294
+ interactionMode: this.config.interactionMode || 'copilot',
1295
+ enableUIControl: this.config.enableUIControl !== false,
1296
+ chatHistoryLength: chatHistory?.length ?? 0
1297
+ });
1298
+
1299
+ // ── Start interceptors & error suppression ──────────────────
1300
+ if (this.config.interceptNativeAlerts) {
1301
+ installAlertInterceptor();
1302
+ }
1048
1303
  this._startErrorSuppression();
1049
1304
 
1050
1305
  // ─── Knowledge-only fast path ─────────────────────────────────
@@ -1120,6 +1375,9 @@ ${screen.elementsText}
1120
1375
  // ── Cancel check ──
1121
1376
  if (this.isCancelRequested) {
1122
1377
  logger.info('AgentRuntime', `Task cancelled by user at step ${step + 1}`);
1378
+ this.emitTrace('task_cancelled', {
1379
+ reason: 'user_cancelled'
1380
+ }, step);
1123
1381
  const cancelResult = {
1124
1382
  success: false,
1125
1383
  message: 'Task was cancelled.',
@@ -1130,6 +1388,11 @@ ${screen.elementsText}
1130
1388
  return cancelResult;
1131
1389
  }
1132
1390
  logger.info('AgentRuntime', `===== Step ${step + 1}/${maxSteps} =====`);
1391
+ this.emitTrace('step_started', {
1392
+ maxSteps,
1393
+ historyLength: this.history.length
1394
+ }, step);
1395
+ logger.info('AgentRuntime', `âš™ī¸ Effective mode: interactionMode=${this.config.interactionMode || 'copilot(default)'} | onAskUser=${!!this.config.onAskUser} | enableUIControl=${this.config.enableUIControl !== false}`);
1133
1396
 
1134
1397
  // Lifecycle: onBeforeStep
1135
1398
  await this.config.onBeforeStep?.(step);
@@ -1141,8 +1404,12 @@ ${screen.elementsText}
1141
1404
 
1142
1405
  // Store root for tooling access (e.g., GuideTool measuring)
1143
1406
  this.lastDehydratedRoot = screen;
1407
+ this.emitTrace('screen_dehydrated', {
1408
+ screenName: screen.screenName,
1409
+ elementCount: screen.elements.length,
1410
+ elementsTextLength: screen.elementsText.length
1411
+ }, step);
1144
1412
  logger.info('AgentRuntime', `Screen: ${screen.screenName}`);
1145
- logger.debug('AgentRuntime', `Dehydrated:\n${screen.elementsText}`);
1146
1413
 
1147
1414
  // 2. Apply transformScreenContent
1148
1415
  let screenContent = screen.elementsText;
@@ -1160,7 +1427,7 @@ ${screen.elementsText}
1160
1427
  const screenshot = await this.captureScreenshot();
1161
1428
 
1162
1429
  // 5. Send to AI provider
1163
- this.config.onStatusUpdate?.('Analyzing screen...');
1430
+ this.config.onStatusUpdate?.('Thinking...');
1164
1431
  const hasKnowledge = !!this.knowledgeService;
1165
1432
  const isCopilot = this.config.interactionMode !== 'autopilot';
1166
1433
  const systemPrompt = buildSystemPrompt('en', hasKnowledge, isCopilot);
@@ -1169,6 +1436,19 @@ ${screen.elementsText}
1169
1436
  logger.debug('AgentRuntime', 'System prompt length:', systemPrompt.length);
1170
1437
  logger.debug('AgentRuntime', 'User context message:', contextMessage.substring(0, 300));
1171
1438
  const response = await this.provider.generateContent(systemPrompt, contextMessage, tools, this.history, screenshot);
1439
+ this.emitTrace('provider_response', {
1440
+ text: response.text,
1441
+ toolCalls: response.toolCalls,
1442
+ tokenUsage: response.tokenUsage
1443
+ }, step);
1444
+ logger.info('AgentRuntime', `🤖 Provider response: textLength=${response.text?.length || 0} toolCalls=${response.toolCalls?.length || 0}`);
1445
+ if (response.toolCalls?.length) {
1446
+ response.toolCalls.forEach((toolCall, idx) => {
1447
+ logger.info('AgentRuntime', `🤖 Tool call[${idx}]: ${toolCall.name}(${JSON.stringify(toolCall.args)})`);
1448
+ });
1449
+ } else if (response.text) {
1450
+ logger.info('AgentRuntime', `🤖 Provider text response: ${response.text}`);
1451
+ }
1172
1452
 
1173
1453
  // Accumulate token usage
1174
1454
  if (response.tokenUsage) {
@@ -1182,6 +1462,11 @@ ${screen.elementsText}
1182
1462
  // ── Budget Guards ──────────────────────────────────────
1183
1463
  if (this.config.maxTokenBudget && sessionUsage.totalTokens >= this.config.maxTokenBudget) {
1184
1464
  logger.warn('AgentRuntime', `Token budget exceeded: ${sessionUsage.totalTokens} >= ${this.config.maxTokenBudget}`);
1465
+ this.emitTrace('task_stopped_budget', {
1466
+ budgetType: 'tokens',
1467
+ used: sessionUsage.totalTokens,
1468
+ limit: this.config.maxTokenBudget
1469
+ }, step);
1185
1470
  const budgetResult = {
1186
1471
  success: false,
1187
1472
  message: `Task stopped: token budget exceeded (used ${sessionUsage.totalTokens.toLocaleString()} of ${this.config.maxTokenBudget.toLocaleString()} tokens)`,
@@ -1193,6 +1478,11 @@ ${screen.elementsText}
1193
1478
  }
1194
1479
  if (this.config.maxCostUSD && sessionUsage.estimatedCostUSD >= this.config.maxCostUSD) {
1195
1480
  logger.warn('AgentRuntime', `Cost budget exceeded: $${sessionUsage.estimatedCostUSD.toFixed(4)} >= $${this.config.maxCostUSD}`);
1481
+ this.emitTrace('task_stopped_budget', {
1482
+ budgetType: 'cost_usd',
1483
+ used: sessionUsage.estimatedCostUSD,
1484
+ limit: this.config.maxCostUSD
1485
+ }, step);
1196
1486
  const budgetResult = {
1197
1487
  success: false,
1198
1488
  message: `Task stopped: cost budget exceeded ($${sessionUsage.estimatedCostUSD.toFixed(4)} of $${this.config.maxCostUSD.toFixed(2)} max)`,
@@ -1206,6 +1496,9 @@ ${screen.elementsText}
1206
1496
  // 6. Process tool calls
1207
1497
  if (!response.toolCalls || response.toolCalls.length === 0) {
1208
1498
  logger.warn('AgentRuntime', 'No tool calls in response. Text:', response.text);
1499
+ this.emitTrace('task_completed_without_tool', {
1500
+ responseText: response.text
1501
+ }, step);
1209
1502
  const result = {
1210
1503
  success: true,
1211
1504
  message: response.text || 'Task completed.',
@@ -1232,6 +1525,14 @@ ${screen.elementsText}
1232
1525
  logger.warn('AgentRuntime', `AI returned ${response.toolCalls.length} tool calls, executing only the first one.`);
1233
1526
  }
1234
1527
  logger.info('AgentRuntime', `Tool: ${toolCall.name}(${JSON.stringify(toolCall.args)})`);
1528
+ this.emitTrace('tool_selected', {
1529
+ tool: toolCall.name,
1530
+ args: toolCall.args,
1531
+ reasoning
1532
+ }, step);
1533
+ if (toolCall.name !== 'ask_user' && this.config.interactionMode !== 'autopilot') {
1534
+ logger.info('AgentRuntime', `đŸ›Ąī¸ Tool "${toolCall.name}" chosen without prompt-level pause; relying on model plan-following and aiConfirm safeguards if present`);
1535
+ }
1235
1536
 
1236
1537
  // Dynamic status update based on tool being executed + Reasoning
1237
1538
  const statusLabel = this.getToolStatusLabel(toolCall.name, toolCall.args);
@@ -1243,11 +1544,30 @@ ${screen.elementsText}
1243
1544
  const tool = this.tools.get(toolCall.name) || this.buildToolsForProvider().find(t => t.name === toolCall.name);
1244
1545
  let output;
1245
1546
  if (tool) {
1246
- output = await this.executeToolSafely(tool, toolCall.args, toolCall.name);
1547
+ output = await this.executeToolSafely(tool, toolCall.args, toolCall.name, step);
1247
1548
  } else {
1549
+ this.emitTrace('tool_unknown', {
1550
+ tool: toolCall.name,
1551
+ args: toolCall.args
1552
+ }, step);
1248
1553
  output = `❌ Unknown tool: ${toolCall.name}`;
1249
1554
  }
1250
1555
  logger.info('AgentRuntime', `Result: ${output}`);
1556
+ this.emitTrace('tool_result', {
1557
+ tool: toolCall.name,
1558
+ args: toolCall.args,
1559
+ output
1560
+ }, step);
1561
+ if (output === APPROVAL_ALREADY_DONE_TOKEN) {
1562
+ const result = {
1563
+ success: true,
1564
+ message: USER_ALREADY_COMPLETED_MESSAGE,
1565
+ steps: this.history,
1566
+ tokenUsage: sessionUsage
1567
+ };
1568
+ await this.config.onAfterTask?.(result);
1569
+ return result;
1570
+ }
1251
1571
 
1252
1572
  // Record step with structured reasoning
1253
1573
  const agentStep = {
@@ -1268,24 +1588,37 @@ ${screen.elementsText}
1268
1588
  if (toolCall.name === 'done') {
1269
1589
  const result = {
1270
1590
  success: toolCall.args.success !== false,
1271
- message: toolCall.args.text || output,
1591
+ message: toolCall.args.text || toolCall.args.message || output || reasoning.plan || (toolCall.args.success === false ? 'Action stopped.' : 'Action completed.'),
1272
1592
  steps: this.history,
1273
1593
  tokenUsage: sessionUsage
1274
1594
  };
1275
1595
  logger.info('AgentRuntime', `Task completed: ${result.message}`);
1596
+ this.emitTrace('task_completed', {
1597
+ success: result.success,
1598
+ message: result.message,
1599
+ steps: this.history.length,
1600
+ tokenUsage: sessionUsage
1601
+ }, step);
1276
1602
  await this.config.onAfterTask?.(result);
1277
1603
  return result;
1278
1604
  }
1279
1605
 
1280
1606
  // Check if asking user (legacy path — only breaks loop when onAskUser is NOT set)
1281
1607
  if (toolCall.name === 'ask_user' && !this.config.onAskUser) {
1282
- this.lastAskUserQuestion = toolCall.args.question || output;
1608
+ let rawQuestion = toolCall.args.question || output || '';
1609
+ if (typeof rawQuestion === 'string') {
1610
+ rawQuestion = rawQuestion.replace(/\[\d+\]/g, '').replace(/ +/g, ' ').trim();
1611
+ }
1612
+ this.lastAskUserQuestion = rawQuestion;
1283
1613
  const result = {
1284
1614
  success: true,
1285
- message: output,
1615
+ message: this.lastAskUserQuestion || '',
1286
1616
  steps: this.history,
1287
1617
  tokenUsage: sessionUsage
1288
1618
  };
1619
+ this.emitTrace('task_paused_for_user', {
1620
+ question: this.lastAskUserQuestion || ''
1621
+ }, step);
1289
1622
  await this.config.onAfterTask?.(result);
1290
1623
  return result;
1291
1624
  }
@@ -1306,10 +1639,19 @@ ${screen.elementsText}
1306
1639
  if (__DEV__ && this.config.interactionMode !== 'autopilot') {
1307
1640
  logger.info('AgentRuntime', 'â„šī¸ Copilot mode active. Tip: Add aiConfirm={true} to critical buttons (e.g. "Place Order", "Delete") for extra safety.');
1308
1641
  }
1642
+ this.emitTrace('task_failed_max_steps', {
1643
+ success: false,
1644
+ steps: this.history.length,
1645
+ message: result.message
1646
+ });
1309
1647
  await this.config.onAfterTask?.(result);
1310
1648
  return result;
1311
1649
  } catch (error) {
1312
1650
  logger.error('AgentRuntime', 'Execution error:', error);
1651
+ this.emitTrace('task_failed_error', {
1652
+ error: error?.message ?? String(error),
1653
+ steps: this.history.length
1654
+ });
1313
1655
  const result = {
1314
1656
  success: false,
1315
1657
  message: `Error: ${error.message}`,
@@ -1320,6 +1662,10 @@ ${screen.elementsText}
1320
1662
  return result;
1321
1663
  } finally {
1322
1664
  this.isRunning = false;
1665
+ this.currentTraceId = null;
1666
+ if (this.config.interceptNativeAlerts) {
1667
+ uninstallAlertInterceptor();
1668
+ }
1323
1669
  // ── Grace period: keep error suppression for delayed side-effects ──
1324
1670
  // useEffect callbacks, PagerView onPageSelected, scrollToIndex, etc.
1325
1671
  // can fire AFTER execute() returns. Keep suppression active for 10s.