@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.
- package/README.md +28 -15
- package/android/build.gradle +17 -0
- package/android/src/main/java/com/mobileai/overlay/FloatingOverlayDialogRootViewGroup.kt +243 -0
- package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +281 -87
- package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +52 -17
- package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +49 -2
- package/bin/generate-map.cjs +556 -126
- package/ios/Podfile +63 -0
- package/ios/Podfile.lock +2290 -0
- package/ios/Podfile.properties.json +4 -0
- package/ios/mobileaireactnative/AppDelegate.swift +69 -0
- package/ios/mobileaireactnative/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
- package/ios/mobileaireactnative/Images.xcassets/Contents.json +6 -0
- package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +21 -0
- package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/SplashScreenLegacy.png +0 -0
- package/ios/mobileaireactnative/Info.plist +55 -0
- package/ios/mobileaireactnative/PrivacyInfo.xcprivacy +48 -0
- package/ios/mobileaireactnative/SplashScreen.storyboard +47 -0
- package/ios/mobileaireactnative/Supporting/Expo.plist +6 -0
- package/ios/mobileaireactnative/mobileaireactnative-Bridging-Header.h +3 -0
- package/ios/mobileaireactnative.xcodeproj/project.pbxproj +547 -0
- package/ios/mobileaireactnative.xcodeproj/xcshareddata/xcschemes/mobileaireactnative.xcscheme +88 -0
- package/ios/mobileaireactnative.xcworkspace/contents.xcworkspacedata +10 -0
- package/lib/module/components/AIAgent.js +407 -148
- package/lib/module/components/AgentChatBar.js +253 -62
- package/lib/module/components/FloatingOverlayWrapper.js +68 -32
- package/lib/module/config/endpoints.js +22 -1
- package/lib/module/core/AgentRuntime.js +192 -24
- package/lib/module/core/FiberTreeWalker.js +410 -34
- package/lib/module/core/OutcomeVerifier.js +149 -0
- package/lib/module/core/systemPrompt.js +126 -44
- package/lib/module/providers/GeminiProvider.js +9 -3
- package/lib/module/services/MobileAIKnowledgeRetriever.js +1 -1
- package/lib/module/services/telemetry/MobileAI.js +1 -1
- package/lib/module/services/telemetry/TelemetryService.js +21 -2
- package/lib/module/services/telemetry/TouchAutoCapture.js +45 -35
- package/lib/module/specs/FloatingOverlayNativeComponent.ts +7 -1
- package/lib/module/support/supportPrompt.js +22 -7
- package/lib/module/support/supportStyle.js +55 -0
- package/lib/module/support/types.js +2 -0
- package/lib/module/tools/tapTool.js +77 -6
- package/lib/module/tools/typeTool.js +20 -0
- package/lib/module/utils/humanizeScreenName.js +49 -0
- package/lib/typescript/src/components/AIAgent.d.ts +6 -2
- package/lib/typescript/src/components/AgentChatBar.d.ts +15 -1
- package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +22 -10
- package/lib/typescript/src/config/endpoints.d.ts +4 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts +17 -1
- package/lib/typescript/src/core/FiberTreeWalker.d.ts +12 -1
- package/lib/typescript/src/core/OutcomeVerifier.d.ts +46 -0
- package/lib/typescript/src/core/systemPrompt.d.ts +3 -10
- package/lib/typescript/src/core/types.d.ts +37 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/services/MobileAIKnowledgeRetriever.d.ts +1 -1
- package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +7 -1
- package/lib/typescript/src/services/telemetry/types.d.ts +1 -1
- package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +5 -0
- package/lib/typescript/src/support/index.d.ts +1 -0
- package/lib/typescript/src/support/supportStyle.d.ts +9 -0
- package/lib/typescript/src/support/types.d.ts +3 -0
- package/lib/typescript/src/tools/tapTool.d.ts +3 -2
- package/lib/typescript/src/utils/humanizeScreenName.d.ts +6 -0
- package/lib/typescript/test-tree.d.ts +2 -0
- package/package.json +5 -2
- package/src/specs/FloatingOverlayNativeComponent.ts +7 -1
- package/ios/MobileAIFloatingOverlayComponentView.mm +0 -73
- 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
|
-
|
|
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
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
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,
|
|
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
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
824
|
-
const blockedMsg = `🚫 APP ACTION BLOCKED: You are attempting to use "${toolName}"
|
|
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
|
-
|
|
1267
|
-
this.
|
|
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
|
|
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.'),
|