@mobileai/react-native 0.9.26 → 0.9.27

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.
@@ -400,7 +400,7 @@ export function AIAgent({
400
400
  if (knowledgeBase) return knowledgeBase;
401
401
  if (!analyticsKey) return undefined;
402
402
  return createMobileAIKnowledgeRetriever({
403
- publishableKey: analyticsKey,
403
+ analyticsKey: analyticsKey,
404
404
  baseUrl: analyticsProxyUrl ?? ENDPOINTS.escalation,
405
405
  headers: analyticsProxyHeaders
406
406
  });
@@ -1143,7 +1143,29 @@ export function AIAgent({
1143
1143
  useEffect(() => {
1144
1144
  logger.info('AIAgent', `⚙️ Runtime config recomputed: mode=${mode} interactionMode=${interactionMode || 'copilot(default)'} onAskUser=${mode !== 'voice'} mergedTools=${Object.keys(mergedCustomTools).join(', ') || '(none)'}`);
1145
1145
  }, [mode, interactionMode, mergedCustomTools]);
1146
- const provider = useMemo(() => createProvider(providerName, apiKey, model, proxyUrl, proxyHeaders), [providerName, apiKey, model, proxyUrl, proxyHeaders]);
1146
+ const effectiveProxyHeaders = useMemo(() => {
1147
+ if (!analyticsKey) return proxyHeaders;
1148
+ const isAuthMissing = !proxyHeaders || !Object.keys(proxyHeaders).some(k => k.toLowerCase() === 'authorization');
1149
+ if (isAuthMissing) {
1150
+ return {
1151
+ ...proxyHeaders,
1152
+ Authorization: `Bearer ${analyticsKey}`
1153
+ };
1154
+ }
1155
+ return proxyHeaders;
1156
+ }, [proxyHeaders, analyticsKey]);
1157
+ const effectiveVoiceProxyHeaders = useMemo(() => {
1158
+ if (!analyticsKey) return voiceProxyHeaders;
1159
+ const isAuthMissing = !voiceProxyHeaders || !Object.keys(voiceProxyHeaders).some(k => k.toLowerCase() === 'authorization');
1160
+ if (isAuthMissing) {
1161
+ return {
1162
+ ...voiceProxyHeaders,
1163
+ Authorization: `Bearer ${analyticsKey}`
1164
+ };
1165
+ }
1166
+ return voiceProxyHeaders;
1167
+ }, [voiceProxyHeaders, analyticsKey]);
1168
+ const provider = useMemo(() => createProvider(providerName, apiKey, model, proxyUrl, effectiveProxyHeaders), [providerName, apiKey, model, proxyUrl, effectiveProxyHeaders]);
1147
1169
  const runtime = useMemo(() => new AgentRuntime(provider, config, rootViewRef.current, navRef),
1148
1170
  // eslint-disable-next-line react-hooks/exhaustive-deps
1149
1171
  [provider, config]);
@@ -1348,7 +1370,7 @@ export function AIAgent({
1348
1370
  voiceServiceRef.current = new VoiceService({
1349
1371
  apiKey,
1350
1372
  proxyUrl: voiceProxyUrl || proxyUrl,
1351
- proxyHeaders: voiceProxyHeaders || proxyHeaders,
1373
+ proxyHeaders: effectiveVoiceProxyHeaders || effectiveProxyHeaders,
1352
1374
  systemPrompt: voicePrompt,
1353
1375
  tools: runtimeTools,
1354
1376
  language: 'en'
@@ -788,7 +788,7 @@ export function AgentChatBar({
788
788
  style: styles.approvalPanel,
789
789
  children: [/*#__PURE__*/_jsx(Text, {
790
790
  style: styles.approvalHint,
791
- children: "The AI agent is requesting permission to perform this action. Tap \"Do it\" to approve, or \"Don\u2019t do it\" to cancel."
791
+ children: "The AI agent is requesting permission to perform this action. Tap \"Allow\" to approve, or \"Don\u2019t Allow\" to cancel."
792
792
  }), /*#__PURE__*/_jsxs(View, {
793
793
  style: styles.approvalActions,
794
794
  children: [/*#__PURE__*/_jsx(Pressable, {
@@ -796,14 +796,14 @@ export function AgentChatBar({
796
796
  onPress: () => onPendingApprovalAction('reject'),
797
797
  children: /*#__PURE__*/_jsx(Text, {
798
798
  style: [styles.approvalActionText, styles.approvalActionSecondaryText],
799
- children: "Don\u2019t do it"
799
+ children: "Don\u2019t Allow"
800
800
  })
801
801
  }), /*#__PURE__*/_jsx(Pressable, {
802
802
  style: [styles.approvalActionBtn, styles.approvalActionPrimary],
803
803
  onPress: () => onPendingApprovalAction('approve'),
804
804
  children: /*#__PURE__*/_jsx(Text, {
805
805
  style: [styles.approvalActionText, styles.approvalActionPrimaryText],
806
- children: "Do it"
806
+ children: "Allow"
807
807
  })
808
808
  })]
809
809
  })]
@@ -28,7 +28,6 @@ const APPROVAL_REJECTED_TOKEN = '__APPROVAL_REJECTED__';
28
28
  const APPROVAL_ALREADY_DONE_TOKEN = '__APPROVAL_ALREADY_DONE__';
29
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
30
  const ACTION_NOT_APPROVED_MESSAGE = "Okay — I won't do that. If you'd like, I can help with something else instead.";
31
-
32
31
  // ─── Agent Runtime ─────────────────────────────────────────────
33
32
 
34
33
  export class AgentRuntime {
@@ -51,15 +50,71 @@ export class AgentRuntime {
51
50
  originalReportErrorsAsExceptions = undefined;
52
51
 
53
52
  // ─── 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']);
53
+ // Copilot uses a workflow-scoped approval model:
54
+ // - none: routine UI actions are blocked
55
+ // - workflow: routine UI actions are allowed for the current task
56
+ // Final irreversible commits are still protected separately by prompt rules
57
+ // and aiConfirm-based confirmation checks.
58
+ appActionApprovalScope = 'none';
59
+ appActionApprovalSource = 'none';
60
+ // Tools that physically alter the app — must be gated by workflow approval
61
+ static APP_ACTION_TOOLS = new Set(['tap', 'type', 'scroll', 'navigate', 'long_press', 'adjust_slider', 'select_picker', 'set_date', 'dismiss_keyboard']);
60
62
  getConfig() {
61
63
  return this.config;
62
64
  }
65
+ resetAppActionApproval(reason) {
66
+ this.appActionApprovalScope = 'none';
67
+ this.appActionApprovalSource = 'none';
68
+ logger.info('AgentRuntime', `🔒 Workflow approval cleared (${reason})`);
69
+ }
70
+ grantWorkflowApproval(source, reason) {
71
+ this.appActionApprovalScope = 'workflow';
72
+ this.appActionApprovalSource = source;
73
+ logger.info('AgentRuntime', `✅ Workflow approval granted via ${source} (${reason})`);
74
+ }
75
+ hasWorkflowApproval() {
76
+ return this.appActionApprovalScope === 'workflow' && this.appActionApprovalSource !== 'none';
77
+ }
78
+ debugLogChunked(label, text, chunkSize = 1600) {
79
+ if (!text) {
80
+ logger.debug('AgentRuntime', `${label}: (empty)`);
81
+ return;
82
+ }
83
+ logger.debug('AgentRuntime', `${label} (length=${text.length})`);
84
+ for (let start = 0; start < text.length; start += chunkSize) {
85
+ const end = Math.min(start + chunkSize, text.length);
86
+ const chunkIndex = Math.floor(start / chunkSize) + 1;
87
+ const chunkCount = Math.ceil(text.length / chunkSize);
88
+ logger.debug('AgentRuntime', `${label} [chunk ${chunkIndex}/${chunkCount}]`, text.slice(start, end));
89
+ }
90
+ }
91
+ formatInteractiveForDebug(element) {
92
+ const props = element.props || {};
93
+ const stateParts = [];
94
+ if (props.accessibilityRole) stateParts.push(`role=${String(props.accessibilityRole)}`);
95
+ if (props.value !== undefined && typeof props.value !== 'function') stateParts.push(`value=${String(props.value)}`);
96
+ if (props.checked !== undefined && typeof props.checked !== 'function') stateParts.push(`checked=${String(props.checked)}`);
97
+ if (props.selected !== undefined && typeof props.selected !== 'function') stateParts.push(`selected=${String(props.selected)}`);
98
+ if (props.enabled !== undefined && typeof props.enabled !== 'function') stateParts.push(`enabled=${String(props.enabled)}`);
99
+ if (props.disabled === true) stateParts.push('disabled=true');
100
+ if (element.aiPriority) stateParts.push(`aiPriority=${element.aiPriority}`);
101
+ if (element.zoneId) stateParts.push(`zoneId=${element.zoneId}`);
102
+ if (element.requiresConfirmation) stateParts.push('requiresConfirmation=true');
103
+ const summary = `[${element.index}] <${element.type}> "${element.label}"`;
104
+ return stateParts.length > 0 ? `${summary} | ${stateParts.join(' | ')}` : summary;
105
+ }
106
+ debugScreenSnapshot(screenName, elements, rawElementsText, transformedScreenContent, contextMessage) {
107
+ const interactiveSummary = elements.length > 0 ? elements.map(element => this.formatInteractiveForDebug(element)).join('\n') : '(no interactive elements)';
108
+ logger.debug('AgentRuntime', `Screen snapshot for "${screenName}" | interactiveCount=${elements.length}`);
109
+ this.debugLogChunked('Interactive inventory', interactiveSummary);
110
+ this.debugLogChunked('Raw dehydrated elementsText', rawElementsText);
111
+ if (transformedScreenContent !== rawElementsText) {
112
+ this.debugLogChunked('Transformed screen content', transformedScreenContent);
113
+ }
114
+ if (contextMessage) {
115
+ this.debugLogChunked('Full provider context message', contextMessage);
116
+ }
117
+ }
63
118
  constructor(provider, config, rootRef, navRef) {
64
119
  this.provider = provider;
65
120
  this.config = config;
@@ -242,7 +297,7 @@ export class AgentRuntime {
242
297
  // ask_user — ask for clarification
243
298
  this.tools.set('ask_user', {
244
299
  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.',
300
+ 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
301
  parameters: {
247
302
  question: {
248
303
  type: 'string',
@@ -253,6 +308,11 @@ export class AgentRuntime {
253
308
  type: 'boolean',
254
309
  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
310
  required: true
311
+ },
312
+ grants_workflow_approval: {
313
+ type: 'boolean',
314
+ 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.',
315
+ required: false
256
316
  }
257
317
  },
258
318
  execute: async args => {
@@ -261,12 +321,16 @@ export class AgentRuntime {
261
321
  if (typeof cleanQuestion === 'string') {
262
322
  cleanQuestion = cleanQuestion.replace(/\[\d+\]/g, '').replace(/ +/g, ' ').trim();
263
323
  }
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');
324
+ const wantsExplicitAppApproval = args.request_app_action === true;
325
+ const grantsWorkflowApproval = args.grants_workflow_approval === true;
326
+ const kind = wantsExplicitAppApproval ? 'approval' : 'freeform';
327
+
328
+ // Mark that an explicit approval checkpoint is now pending.
329
+ if (wantsExplicitAppApproval) {
330
+ this.resetAppActionApproval('explicit approval requested');
331
+ logger.info('AgentRuntime', '🔒 App action gate: explicit approval requested, UI tools now BLOCKED until granted');
332
+ } else if (grantsWorkflowApproval) {
333
+ logger.info('AgentRuntime', '📝 ask_user will grant workflow approval if the user answers with routine action data');
270
334
  }
271
335
  logger.info('AgentRuntime', `❓ ask_user emitted (kind=${kind}): "${cleanQuestion}"`);
272
336
  if (this.config.onAskUser) {
@@ -281,13 +345,14 @@ export class AgentRuntime {
281
345
 
282
346
  // Resolve approval gate based on button response
283
347
  if (answer === '__APPROVAL_GRANTED__') {
284
- this.appActionApproved = true;
285
- logger.info('AgentRuntime', '✅ App action gate: APPROVED — UI tools unblocked');
348
+ this.grantWorkflowApproval('explicit_button', 'user tapped Allow');
286
349
  } else if (answer === '__APPROVAL_REJECTED__') {
287
- this.appActionApproved = false;
350
+ this.resetAppActionApproval('explicit approval rejected');
288
351
  logger.info('AgentRuntime', '🚫 App action gate: REJECTED — UI tools remain blocked');
352
+ } else if (grantsWorkflowApproval && typeof answer === 'string' && answer.trim().length > 0) {
353
+ this.grantWorkflowApproval('user_input', 'user supplied requested workflow data');
289
354
  }
290
- // Any other text answer (conversational interruption) leaves appActionApproved as-is
355
+ // Any other text answer leaves workflow approval unchanged.
291
356
 
292
357
  return `User answered: ${answer}`;
293
358
  }
@@ -820,8 +885,8 @@ ${screen.elementsText}
820
885
  // Mandate explicit ask_user approval for all UI-altering tools ONLY if we are in
821
886
  // copilot mode AND the host app has provided an onAskUser callback.
822
887
  // 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).`;
888
+ if (this.config.interactionMode !== 'autopilot' && this.config.onAskUser && AgentRuntime.APP_ACTION_TOOLS.has(toolName) && !this.hasWorkflowApproval()) {
889
+ 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
890
  logger.warn('AgentRuntime', blockedMsg);
826
891
  this.emitTrace('app_action_gate_blocked', {
827
892
  tool: toolName,
@@ -1263,8 +1328,8 @@ ${screen.elementsText}
1263
1328
  this.currentTraceId = generateTraceId();
1264
1329
  this.observations = [];
1265
1330
  this.lastScreenName = '';
1266
- // Reset app-action approval gate for each new task
1267
- this.appActionApproved = false;
1331
+ // Reset workflow approval for each new task
1332
+ this.resetAppActionApproval('new task');
1268
1333
  const maxSteps = this.config.maxSteps || DEFAULT_MAX_STEPS;
1269
1334
  const stepDelay = this.config.stepDelay ?? 300;
1270
1335
 
@@ -1422,6 +1487,7 @@ ${screen.elementsText}
1422
1487
 
1423
1488
  // 4. Assemble structured user prompt
1424
1489
  const contextMessage = this.assembleUserPrompt(step, maxSteps, contextualMessage, screenName, screenContent, chatHistory);
1490
+ this.debugScreenSnapshot(screen.screenName, screen.elements, screen.elementsText, screenContent, contextMessage);
1425
1491
 
1426
1492
  // 4.5. Capture screenshot for Gemini vision (optional)
1427
1493
  const screenshot = await this.captureScreenshot();
@@ -1434,7 +1500,7 @@ ${screen.elementsText}
1434
1500
  const tools = this.buildToolsForProvider();
1435
1501
  logger.info('AgentRuntime', `Sending to AI with ${tools.length} tools...`);
1436
1502
  logger.debug('AgentRuntime', 'System prompt length:', systemPrompt.length);
1437
- logger.debug('AgentRuntime', 'User context message:', contextMessage.substring(0, 300));
1503
+ logger.debug('AgentRuntime', 'User context preview:', contextMessage.substring(0, 300));
1438
1504
  const response = await this.provider.generateContent(systemPrompt, contextMessage, tools, this.history, screenshot);
1439
1505
  this.emitTrace('provider_response', {
1440
1506
  text: response.text,