@mobileai/react-native 0.9.25 → 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.
- package/README.md +10 -10
- package/bin/generate-map.cjs +511 -120
- package/lib/module/components/AIAgent.js +25 -3
- package/lib/module/components/AgentChatBar.js +3 -3
- package/lib/module/config/endpoints.js +1 -1
- package/lib/module/core/AgentRuntime.js +89 -23
- package/lib/module/core/FiberTreeWalker.js +312 -34
- package/lib/module/core/systemPrompt.js +30 -19
- package/lib/module/services/MobileAIKnowledgeRetriever.js +1 -1
- package/lib/module/services/telemetry/MobileAI.js +1 -1
- package/lib/module/tools/tapTool.js +77 -6
- package/lib/typescript/src/core/AgentRuntime.d.ts +8 -1
- package/lib/typescript/src/core/types.d.ts +2 -1
- package/lib/typescript/src/services/MobileAIKnowledgeRetriever.d.ts +1 -1
- package/lib/typescript/src/tools/tapTool.d.ts +3 -2
- package/lib/typescript/test-tree.d.ts +2 -0
- package/package.json +1 -1
|
@@ -400,7 +400,7 @@ export function AIAgent({
|
|
|
400
400
|
if (knowledgeBase) return knowledgeBase;
|
|
401
401
|
if (!analyticsKey) return undefined;
|
|
402
402
|
return createMobileAIKnowledgeRetriever({
|
|
403
|
-
|
|
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
|
|
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:
|
|
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 \"
|
|
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
|
|
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: "
|
|
806
|
+
children: "Allow"
|
|
807
807
|
})
|
|
808
808
|
})]
|
|
809
809
|
})]
|
|
@@ -10,7 +10,7 @@
|
|
|
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://
|
|
13
|
+
const MOBILEAI_BASE = process.env.EXPO_PUBLIC_MOBILEAI_BASE_URL || process.env.NEXT_PUBLIC_MOBILEAI_BASE_URL || 'https://mobileai.cloud';
|
|
14
14
|
export const ENDPOINTS = {
|
|
15
15
|
/** Telemetry event ingest — receives batched SDK events */
|
|
16
16
|
telemetryIngest: `${MOBILEAI_BASE}/api/v1/events`,
|
|
@@ -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
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
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,
|
|
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
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
824
|
-
const blockedMsg = `🚫 APP ACTION BLOCKED: You are attempting to use "${toolName}"
|
|
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
|
|
1267
|
-
this.
|
|
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
|
|
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,
|