@mobileai/react-native 0.9.26 → 0.9.28

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 (67) hide show
  1. package/README.md +28 -15
  2. package/android/build.gradle +17 -0
  3. package/android/src/main/java/com/mobileai/overlay/FloatingOverlayDialogRootViewGroup.kt +243 -0
  4. package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +281 -87
  5. package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +52 -17
  6. package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +49 -2
  7. package/bin/generate-map.cjs +556 -126
  8. package/ios/Podfile +63 -0
  9. package/ios/Podfile.lock +2290 -0
  10. package/ios/Podfile.properties.json +4 -0
  11. package/ios/mobileaireactnative/AppDelegate.swift +69 -0
  12. package/ios/mobileaireactnative/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
  13. package/ios/mobileaireactnative/Images.xcassets/Contents.json +6 -0
  14. package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +21 -0
  15. package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/SplashScreenLegacy.png +0 -0
  16. package/ios/mobileaireactnative/Info.plist +55 -0
  17. package/ios/mobileaireactnative/PrivacyInfo.xcprivacy +48 -0
  18. package/ios/mobileaireactnative/SplashScreen.storyboard +47 -0
  19. package/ios/mobileaireactnative/Supporting/Expo.plist +6 -0
  20. package/ios/mobileaireactnative/mobileaireactnative-Bridging-Header.h +3 -0
  21. package/ios/mobileaireactnative.xcodeproj/project.pbxproj +547 -0
  22. package/ios/mobileaireactnative.xcodeproj/xcshareddata/xcschemes/mobileaireactnative.xcscheme +88 -0
  23. package/ios/mobileaireactnative.xcworkspace/contents.xcworkspacedata +10 -0
  24. package/lib/module/components/AIAgent.js +407 -148
  25. package/lib/module/components/AgentChatBar.js +253 -62
  26. package/lib/module/components/FloatingOverlayWrapper.js +68 -32
  27. package/lib/module/config/endpoints.js +22 -1
  28. package/lib/module/core/AgentRuntime.js +192 -24
  29. package/lib/module/core/FiberTreeWalker.js +410 -34
  30. package/lib/module/core/OutcomeVerifier.js +149 -0
  31. package/lib/module/core/systemPrompt.js +126 -44
  32. package/lib/module/providers/GeminiProvider.js +9 -3
  33. package/lib/module/services/MobileAIKnowledgeRetriever.js +1 -1
  34. package/lib/module/services/telemetry/MobileAI.js +1 -1
  35. package/lib/module/services/telemetry/TelemetryService.js +21 -2
  36. package/lib/module/services/telemetry/TouchAutoCapture.js +45 -35
  37. package/lib/module/specs/FloatingOverlayNativeComponent.ts +7 -1
  38. package/lib/module/support/supportPrompt.js +22 -7
  39. package/lib/module/support/supportStyle.js +55 -0
  40. package/lib/module/support/types.js +2 -0
  41. package/lib/module/tools/tapTool.js +77 -6
  42. package/lib/module/tools/typeTool.js +20 -0
  43. package/lib/module/utils/humanizeScreenName.js +49 -0
  44. package/lib/typescript/src/components/AIAgent.d.ts +6 -2
  45. package/lib/typescript/src/components/AgentChatBar.d.ts +15 -1
  46. package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +22 -10
  47. package/lib/typescript/src/config/endpoints.d.ts +4 -0
  48. package/lib/typescript/src/core/AgentRuntime.d.ts +17 -1
  49. package/lib/typescript/src/core/FiberTreeWalker.d.ts +12 -1
  50. package/lib/typescript/src/core/OutcomeVerifier.d.ts +46 -0
  51. package/lib/typescript/src/core/systemPrompt.d.ts +3 -10
  52. package/lib/typescript/src/core/types.d.ts +37 -1
  53. package/lib/typescript/src/index.d.ts +1 -0
  54. package/lib/typescript/src/services/MobileAIKnowledgeRetriever.d.ts +1 -1
  55. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +7 -1
  56. package/lib/typescript/src/services/telemetry/types.d.ts +1 -1
  57. package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +5 -0
  58. package/lib/typescript/src/support/index.d.ts +1 -0
  59. package/lib/typescript/src/support/supportStyle.d.ts +9 -0
  60. package/lib/typescript/src/support/types.d.ts +3 -0
  61. package/lib/typescript/src/tools/tapTool.d.ts +3 -2
  62. package/lib/typescript/src/utils/humanizeScreenName.d.ts +6 -0
  63. package/lib/typescript/test-tree.d.ts +2 -0
  64. package/package.json +5 -2
  65. package/src/specs/FloatingOverlayNativeComponent.ts +7 -1
  66. package/ios/MobileAIFloatingOverlayComponentView.mm +0 -73
  67. package/ios/MobileAIPilotIntents.swift +0 -51
@@ -10,8 +10,29 @@
10
10
  * to route telemetry through your own backend without touching this file.
11
11
  */
12
12
 
13
- const MOBILEAI_BASE = process.env.EXPO_PUBLIC_MOBILEAI_BASE_URL || process.env.NEXT_PUBLIC_MOBILEAI_BASE_URL || 'https://mobileai.cloud';
13
+ import { Platform } from 'react-native';
14
+ function resolveMobileAIBase() {
15
+ const configuredBase = process.env.EXPO_PUBLIC_MOBILEAI_BASE_URL || process.env.NEXT_PUBLIC_MOBILEAI_BASE_URL || 'https://mobileai.cloud';
16
+
17
+ // Android emulators cannot reach the host machine via localhost/127.0.0.1.
18
+ // Translate those hostnames to 10.0.2.2 so the Expo example can talk to the
19
+ // local dashboard/backend without affecting iOS.
20
+ if (Platform.OS === 'android') {
21
+ return configuredBase.replace(/^http:\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/, 'http://10.0.2.2');
22
+ }
23
+ return configuredBase;
24
+ }
25
+ const MOBILEAI_BASE = resolveMobileAIBase();
26
+ function toWebSocketBase(url) {
27
+ if (url.startsWith('https://')) return `wss://${url.slice('https://'.length)}`;
28
+ if (url.startsWith('http://')) return `ws://${url.slice('http://'.length)}`;
29
+ return url;
30
+ }
14
31
  export const ENDPOINTS = {
32
+ /** Hosted MobileAI text proxy — used by default when analyticsKey is set */
33
+ hostedTextProxy: `${MOBILEAI_BASE}/api/v1/hosted-proxy/text`,
34
+ /** Hosted MobileAI voice proxy — used by default when analyticsKey is set */
35
+ hostedVoiceProxy: `${toWebSocketBase(MOBILEAI_BASE)}/ws/hosted-proxy/voice`,
15
36
  /** Telemetry event ingest — receives batched SDK events */
16
37
  telemetryIngest: `${MOBILEAI_BASE}/api/v1/events`,
17
38
  /** Feature flag sync — fetches remote flags for this analyticsKey */
@@ -15,10 +15,12 @@ import { logger } from "../utils/logger.js";
15
15
  import { walkFiberTree } from "./FiberTreeWalker.js";
16
16
  import { dehydrateScreen } from "./ScreenDehydrator.js";
17
17
  import { buildSystemPrompt, buildKnowledgeOnlyPrompt } from "./systemPrompt.js";
18
+ import { buildVerificationAction, createVerificationSnapshot, OutcomeVerifier } from "./OutcomeVerifier.js";
18
19
  import { KnowledgeBaseService } from "../services/KnowledgeBaseService.js";
19
20
  import { installAlertInterceptor, uninstallAlertInterceptor } from "./NativeAlertInterceptor.js";
20
21
  import { createTapTool, createLongPressTool, createTypeTool, createScrollTool, createSliderTool, createPickerTool, createDatePickerTool, createKeyboardTool, createGuideTool, createSimplifyTool, createRestoreTool } from "../tools/index.js";
21
22
  import { actionRegistry } from "./ActionRegistry.js";
23
+ import { createProvider } from "../providers/ProviderFactory.js";
22
24
  const DEFAULT_MAX_STEPS = 25;
23
25
  function generateTraceId() {
24
26
  return `trace_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
@@ -28,7 +30,6 @@ const APPROVAL_REJECTED_TOKEN = '__APPROVAL_REJECTED__';
28
30
  const APPROVAL_ALREADY_DONE_TOKEN = '__APPROVAL_ALREADY_DONE__';
29
31
  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
32
  const ACTION_NOT_APPROVED_MESSAGE = "Okay — I won't do that. If you'd like, I can help with something else instead.";
31
-
32
33
  // ─── Agent Runtime ─────────────────────────────────────────────
33
34
 
34
35
  export class AgentRuntime {
@@ -40,6 +41,10 @@ export class AgentRuntime {
40
41
  knowledgeService = null;
41
42
  lastDehydratedRoot = null;
42
43
  currentTraceId = null;
44
+ currentUserGoal = '';
45
+ verifierProvider = null;
46
+ outcomeVerifier = null;
47
+ pendingCriticalVerification = null;
43
48
 
44
49
  // ─── Task-scoped error suppression ──────────────────────────
45
50
  // Installed once at execute() start, removed after grace period.
@@ -51,15 +56,71 @@ export class AgentRuntime {
51
56
  originalReportErrorsAsExceptions = undefined;
52
57
 
53
58
  // ─── 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']);
59
+ // Copilot uses a workflow-scoped approval model:
60
+ // - none: routine UI actions are blocked
61
+ // - workflow: routine UI actions are allowed for the current task
62
+ // Final irreversible commits are still protected separately by prompt rules
63
+ // and aiConfirm-based confirmation checks.
64
+ appActionApprovalScope = 'none';
65
+ appActionApprovalSource = 'none';
66
+ // Tools that physically alter the app — must be gated by workflow approval
67
+ static APP_ACTION_TOOLS = new Set(['tap', 'type', 'scroll', 'navigate', 'long_press', 'adjust_slider', 'select_picker', 'set_date', 'dismiss_keyboard']);
60
68
  getConfig() {
61
69
  return this.config;
62
70
  }
71
+ resetAppActionApproval(reason) {
72
+ this.appActionApprovalScope = 'none';
73
+ this.appActionApprovalSource = 'none';
74
+ logger.info('AgentRuntime', `🔒 Workflow approval cleared (${reason})`);
75
+ }
76
+ grantWorkflowApproval(source, reason) {
77
+ this.appActionApprovalScope = 'workflow';
78
+ this.appActionApprovalSource = source;
79
+ logger.info('AgentRuntime', `✅ Workflow approval granted via ${source} (${reason})`);
80
+ }
81
+ hasWorkflowApproval() {
82
+ return this.appActionApprovalScope === 'workflow' && this.appActionApprovalSource !== 'none';
83
+ }
84
+ debugLogChunked(label, text, chunkSize = 1600) {
85
+ if (!text) {
86
+ logger.debug('AgentRuntime', `${label}: (empty)`);
87
+ return;
88
+ }
89
+ logger.debug('AgentRuntime', `${label} (length=${text.length})`);
90
+ for (let start = 0; start < text.length; start += chunkSize) {
91
+ const end = Math.min(start + chunkSize, text.length);
92
+ const chunkIndex = Math.floor(start / chunkSize) + 1;
93
+ const chunkCount = Math.ceil(text.length / chunkSize);
94
+ logger.debug('AgentRuntime', `${label} [chunk ${chunkIndex}/${chunkCount}]`, text.slice(start, end));
95
+ }
96
+ }
97
+ formatInteractiveForDebug(element) {
98
+ const props = element.props || {};
99
+ const stateParts = [];
100
+ if (props.accessibilityRole) stateParts.push(`role=${String(props.accessibilityRole)}`);
101
+ if (props.value !== undefined && typeof props.value !== 'function') stateParts.push(`value=${String(props.value)}`);
102
+ if (props.checked !== undefined && typeof props.checked !== 'function') stateParts.push(`checked=${String(props.checked)}`);
103
+ if (props.selected !== undefined && typeof props.selected !== 'function') stateParts.push(`selected=${String(props.selected)}`);
104
+ if (props.enabled !== undefined && typeof props.enabled !== 'function') stateParts.push(`enabled=${String(props.enabled)}`);
105
+ if (props.disabled === true) stateParts.push('disabled=true');
106
+ if (element.aiPriority) stateParts.push(`aiPriority=${element.aiPriority}`);
107
+ if (element.zoneId) stateParts.push(`zoneId=${element.zoneId}`);
108
+ if (element.requiresConfirmation) stateParts.push('requiresConfirmation=true');
109
+ const summary = `[${element.index}] <${element.type}> "${element.label}"`;
110
+ return stateParts.length > 0 ? `${summary} | ${stateParts.join(' | ')}` : summary;
111
+ }
112
+ debugScreenSnapshot(screenName, elements, rawElementsText, transformedScreenContent, contextMessage) {
113
+ const interactiveSummary = elements.length > 0 ? elements.map(element => this.formatInteractiveForDebug(element)).join('\n') : '(no interactive elements)';
114
+ logger.debug('AgentRuntime', `Screen snapshot for "${screenName}" | interactiveCount=${elements.length}`);
115
+ this.debugLogChunked('Interactive inventory', interactiveSummary);
116
+ this.debugLogChunked('Raw dehydrated elementsText', rawElementsText);
117
+ if (transformedScreenContent !== rawElementsText) {
118
+ this.debugLogChunked('Transformed screen content', transformedScreenContent);
119
+ }
120
+ if (contextMessage) {
121
+ this.debugLogChunked('Full provider context message', contextMessage);
122
+ }
123
+ }
63
124
  constructor(provider, config, rootRef, navRef) {
64
125
  this.provider = provider;
65
126
  this.config = config;
@@ -92,6 +153,77 @@ export class AgentRuntime {
92
153
  }
93
154
  }
94
155
  }
156
+ getVerifier() {
157
+ if (this.config.verifier?.enabled === false) {
158
+ return null;
159
+ }
160
+ if (!this.outcomeVerifier) {
161
+ const verifierConfig = this.config.verifier;
162
+ if (verifierConfig?.provider || verifierConfig?.model || verifierConfig?.proxyUrl || verifierConfig?.proxyHeaders) {
163
+ this.verifierProvider = createProvider(verifierConfig.provider || this.config.provider || 'gemini', this.config.apiKey, verifierConfig.model || this.config.model, verifierConfig.proxyUrl || this.config.proxyUrl, verifierConfig.proxyHeaders || this.config.proxyHeaders);
164
+ } else {
165
+ this.verifierProvider = this.provider;
166
+ }
167
+ this.outcomeVerifier = new OutcomeVerifier(this.verifierProvider, this.config);
168
+ }
169
+ return this.outcomeVerifier;
170
+ }
171
+ createCurrentVerificationSnapshot(screenName, screenContent, elements, screenshot) {
172
+ return createVerificationSnapshot(screenName, screenContent, elements, screenshot);
173
+ }
174
+ async updateCriticalVerification(screenName, screenContent, elements, screenshot, stepIndex) {
175
+ if (!this.pendingCriticalVerification) return;
176
+ const verifier = this.getVerifier();
177
+ if (!verifier) {
178
+ this.pendingCriticalVerification = null;
179
+ return;
180
+ }
181
+ const postAction = this.createCurrentVerificationSnapshot(screenName, screenContent, elements, screenshot);
182
+ this.pendingCriticalVerification.followupSteps += 1;
183
+ const result = await verifier.verify({
184
+ goal: this.pendingCriticalVerification.goal,
185
+ action: this.pendingCriticalVerification.action,
186
+ preAction: this.pendingCriticalVerification.preAction,
187
+ postAction
188
+ });
189
+ this.emitTrace('critical_action_verified', {
190
+ action: this.pendingCriticalVerification.action.toolName,
191
+ label: this.pendingCriticalVerification.action.label,
192
+ status: result.status,
193
+ failureKind: result.failureKind,
194
+ evidence: result.evidence,
195
+ source: result.source,
196
+ followupSteps: this.pendingCriticalVerification.followupSteps
197
+ }, stepIndex);
198
+ if (result.status === 'success') {
199
+ this.pendingCriticalVerification = null;
200
+ return;
201
+ }
202
+ if (result.status === 'error') {
203
+ this.observations.push(`Outcome verifier: The previous action "${this.pendingCriticalVerification.action.label}" did NOT complete successfully. ${result.evidence} Treat this as a ${result.failureKind} failure, do not claim success, and either recover or explain the issue clearly.`);
204
+ return;
205
+ }
206
+ const maxFollowupSteps = verifier.getMaxFollowupSteps();
207
+ const ageNote = this.pendingCriticalVerification.followupSteps >= maxFollowupSteps ? ` This critical action is still unverified after ${this.pendingCriticalVerification.followupSteps} follow-up checks.` : '';
208
+ this.observations.push(`Outcome verifier: The previous action "${this.pendingCriticalVerification.action.label}" is still unverified. ${result.evidence}${ageNote} Before calling done(success=true), keep checking for success or error evidence on the current screen.`);
209
+ }
210
+ maybeStartCriticalVerification(toolName, args, preAction) {
211
+ const verifier = this.getVerifier();
212
+ if (!verifier) return;
213
+ const action = buildVerificationAction(toolName, args, preAction.elements, this.getToolStatusLabel(toolName, args));
214
+ if (!verifier.isCriticalAction(action)) {
215
+ return;
216
+ }
217
+ this.pendingCriticalVerification = {
218
+ goal: this.currentUserGoal,
219
+ action,
220
+ preAction,
221
+ followupSteps: 0
222
+ };
223
+ }
224
+ shouldBlockSuccessCompletion() {
225
+ return this.pendingCriticalVerification !== null;
226
+ }
95
227
 
96
228
  // ─── Tool Registration ─────────────────────────────────────
97
229
 
@@ -242,7 +374,7 @@ export class AgentRuntime {
242
374
  // ask_user — ask for clarification
243
375
  this.tools.set('ask_user', {
244
376
  name: 'ask_user',
245
- description: 'Communicate with the user. Use this to ask questions, request permission for app actions, OR answer a question the user asked.',
377
+ description: 'Communicate with the user. Use this to ask questions, request explicit permission for app actions, answer a direct user question, or collect missing low-risk workflow data that can authorize routine in-flow steps.',
246
378
  parameters: {
247
379
  question: {
248
380
  type: 'string',
@@ -253,6 +385,11 @@ export class AgentRuntime {
253
385
  type: 'boolean',
254
386
  description: 'Set to true when requesting permission to take an action in the app (navigate, tap, investigate). Shows explicit approval buttons to the user.',
255
387
  required: true
388
+ },
389
+ grants_workflow_approval: {
390
+ type: 'boolean',
391
+ description: 'Optional. Set to true only when asking for missing low-risk input or a low-risk selection that you will directly apply in the current action workflow. If the user answers, their answer authorizes routine in-flow actions like typing/selecting/toggling, but NOT irreversible final commits or support investigations.',
392
+ required: false
256
393
  }
257
394
  },
258
395
  execute: async args => {
@@ -261,12 +398,16 @@ export class AgentRuntime {
261
398
  if (typeof cleanQuestion === 'string') {
262
399
  cleanQuestion = cleanQuestion.replace(/\[\d+\]/g, '').replace(/ +/g, ' ').trim();
263
400
  }
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');
401
+ const wantsExplicitAppApproval = args.request_app_action === true;
402
+ const grantsWorkflowApproval = args.grants_workflow_approval === true;
403
+ const kind = wantsExplicitAppApproval ? 'approval' : 'freeform';
404
+
405
+ // Mark that an explicit approval checkpoint is now pending.
406
+ if (wantsExplicitAppApproval) {
407
+ this.resetAppActionApproval('explicit approval requested');
408
+ logger.info('AgentRuntime', '🔒 App action gate: explicit approval requested, UI tools now BLOCKED until granted');
409
+ } else if (grantsWorkflowApproval) {
410
+ logger.info('AgentRuntime', '📝 ask_user will grant workflow approval if the user answers with routine action data');
270
411
  }
271
412
  logger.info('AgentRuntime', `❓ ask_user emitted (kind=${kind}): "${cleanQuestion}"`);
272
413
  if (this.config.onAskUser) {
@@ -281,13 +422,14 @@ export class AgentRuntime {
281
422
 
282
423
  // Resolve approval gate based on button response
283
424
  if (answer === '__APPROVAL_GRANTED__') {
284
- this.appActionApproved = true;
285
- logger.info('AgentRuntime', '✅ App action gate: APPROVED — UI tools unblocked');
425
+ this.grantWorkflowApproval('explicit_button', 'user tapped Allow');
286
426
  } else if (answer === '__APPROVAL_REJECTED__') {
287
- this.appActionApproved = false;
427
+ this.resetAppActionApproval('explicit approval rejected');
288
428
  logger.info('AgentRuntime', '🚫 App action gate: REJECTED — UI tools remain blocked');
429
+ } else if (grantsWorkflowApproval && typeof answer === 'string' && answer.trim().length > 0) {
430
+ this.grantWorkflowApproval('user_input', 'user supplied requested workflow data');
289
431
  }
290
- // Any other text answer (conversational interruption) leaves appActionApproved as-is
432
+ // Any other text answer leaves workflow approval unchanged.
291
433
 
292
434
  return `User answered: ${answer}`;
293
435
  }
@@ -820,8 +962,8 @@ ${screen.elementsText}
820
962
  // Mandate explicit ask_user approval for all UI-altering tools ONLY if we are in
821
963
  // copilot mode AND the host app has provided an onAskUser callback.
822
964
  // 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).`;
965
+ if (this.config.interactionMode !== 'autopilot' && this.config.onAskUser && AgentRuntime.APP_ACTION_TOOLS.has(toolName) && !this.hasWorkflowApproval()) {
966
+ const blockedMsg = `🚫 APP ACTION BLOCKED: You are attempting to use "${toolName}" without workflow approval. Before routine UI actions, either (1) call ask_user(request_app_action=true) and wait for the user to tap 'Allow', or (2) if you are collecting missing low-risk input/selection for the current action workflow, call ask_user(grants_workflow_approval=true) so the user's answer authorizes routine in-flow actions. Never use option (2) for support investigations or irreversible final commits.`;
825
967
  logger.warn('AgentRuntime', blockedMsg);
826
968
  this.emitTrace('app_action_gate_blocked', {
827
969
  tool: toolName,
@@ -1263,8 +1405,12 @@ ${screen.elementsText}
1263
1405
  this.currentTraceId = generateTraceId();
1264
1406
  this.observations = [];
1265
1407
  this.lastScreenName = '';
1266
- // Reset app-action approval gate for each new task
1267
- this.appActionApproved = false;
1408
+ this.pendingCriticalVerification = null;
1409
+ this.outcomeVerifier = null;
1410
+ this.verifierProvider = null;
1411
+ this.currentUserGoal = userMessage;
1412
+ // Reset workflow approval for each new task
1413
+ this.resetAppActionApproval('new task');
1268
1414
  const maxSteps = this.config.maxSteps || DEFAULT_MAX_STEPS;
1269
1415
  const stepDelay = this.config.stepDelay ?? 300;
1270
1416
 
@@ -1282,6 +1428,7 @@ ${screen.elementsText}
1282
1428
  contextualMessage = `(Note: You just asked the user: "${this.lastAskUserQuestion}")\n\nUser replied: ${userMessage}`;
1283
1429
  this.lastAskUserQuestion = null; // Consume the question
1284
1430
  }
1431
+ this.currentUserGoal = contextualMessage;
1285
1432
  logger.info('AgentRuntime', `Starting execution: "${contextualMessage}"`);
1286
1433
 
1287
1434
  // Lifecycle: onBeforeTask
@@ -1422,19 +1569,21 @@ ${screen.elementsText}
1422
1569
 
1423
1570
  // 4. Assemble structured user prompt
1424
1571
  const contextMessage = this.assembleUserPrompt(step, maxSteps, contextualMessage, screenName, screenContent, chatHistory);
1572
+ this.debugScreenSnapshot(screen.screenName, screen.elements, screen.elementsText, screenContent, contextMessage);
1425
1573
 
1426
1574
  // 4.5. Capture screenshot for Gemini vision (optional)
1427
1575
  const screenshot = await this.captureScreenshot();
1576
+ await this.updateCriticalVerification(screenName, screenContent, screen.elements, screenshot, step);
1428
1577
 
1429
1578
  // 5. Send to AI provider
1430
1579
  this.config.onStatusUpdate?.('Thinking...');
1431
1580
  const hasKnowledge = !!this.knowledgeService;
1432
1581
  const isCopilot = this.config.interactionMode !== 'autopilot';
1433
- const systemPrompt = buildSystemPrompt('en', hasKnowledge, isCopilot);
1582
+ const systemPrompt = buildSystemPrompt('en', hasKnowledge, isCopilot, this.config.supportStyle);
1434
1583
  const tools = this.buildToolsForProvider();
1435
1584
  logger.info('AgentRuntime', `Sending to AI with ${tools.length} tools...`);
1436
1585
  logger.debug('AgentRuntime', 'System prompt length:', systemPrompt.length);
1437
- logger.debug('AgentRuntime', 'User context message:', contextMessage.substring(0, 300));
1586
+ logger.debug('AgentRuntime', 'User context preview:', contextMessage.substring(0, 300));
1438
1587
  const response = await this.provider.generateContent(systemPrompt, contextMessage, tools, this.history, screenshot);
1439
1588
  this.emitTrace('provider_response', {
1440
1589
  text: response.text,
@@ -1495,6 +1644,13 @@ ${screen.elementsText}
1495
1644
 
1496
1645
  // 6. Process tool calls
1497
1646
  if (!response.toolCalls || response.toolCalls.length === 0) {
1647
+ if (this.shouldBlockSuccessCompletion()) {
1648
+ this.emitTrace('task_completion_blocked_needs_verification', {
1649
+ responseText: response.text,
1650
+ pendingVerification: this.pendingCriticalVerification
1651
+ }, step);
1652
+ continue;
1653
+ }
1498
1654
  logger.warn('AgentRuntime', 'No tool calls in response. Text:', response.text);
1499
1655
  this.emitTrace('task_completed_without_tool', {
1500
1656
  responseText: response.text
@@ -1539,6 +1695,7 @@ ${screen.elementsText}
1539
1695
  // Prefer the human-readable plan over the raw tool status if available to avoid double statuses
1540
1696
  const statusDisplay = reasoning.plan || statusLabel;
1541
1697
  this.config.onStatusUpdate?.(statusDisplay);
1698
+ const preActionSnapshot = this.createCurrentVerificationSnapshot(screenName, screenContent, screen.elements, screenshot);
1542
1699
 
1543
1700
  // Find and execute the tool
1544
1701
  const tool = this.tools.get(toolCall.name) || this.buildToolsForProvider().find(t => t.name === toolCall.name);
@@ -1558,6 +1715,11 @@ ${screen.elementsText}
1558
1715
  args: toolCall.args,
1559
1716
  output
1560
1717
  }, step);
1718
+ if (output.startsWith('✅')) {
1719
+ this.maybeStartCriticalVerification(toolCall.name, toolCall.args, preActionSnapshot);
1720
+ } else if (toolCall.name !== 'done') {
1721
+ this.pendingCriticalVerification = null;
1722
+ }
1561
1723
  if (output === APPROVAL_ALREADY_DONE_TOKEN) {
1562
1724
  const result = {
1563
1725
  success: true,
@@ -1586,6 +1748,12 @@ ${screen.elementsText}
1586
1748
 
1587
1749
  // Check if done
1588
1750
  if (toolCall.name === 'done') {
1751
+ if (toolCall.args.success !== false && this.shouldBlockSuccessCompletion()) {
1752
+ this.emitTrace('done_blocked_needs_verification', {
1753
+ pendingVerification: this.pendingCriticalVerification
1754
+ }, step);
1755
+ continue;
1756
+ }
1589
1757
  const result = {
1590
1758
  success: toolCall.args.success !== false,
1591
1759
  message: toolCall.args.text || toolCall.args.message || output || reasoning.plan || (toolCall.args.success === false ? 'Action stopped.' : 'Action completed.'),