@mobileai/react-native 0.1.0 → 0.3.0
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 +78 -7
- package/lib/module/components/AIAgent.js +40 -4
- package/lib/module/components/AIAgent.js.map +1 -1
- package/lib/module/components/AgentChatBar.js +177 -29
- package/lib/module/components/AgentChatBar.js.map +1 -1
- package/lib/module/core/AgentRuntime.js +268 -126
- package/lib/module/core/AgentRuntime.js.map +1 -1
- package/lib/module/core/FiberTreeWalker.js +74 -20
- package/lib/module/core/FiberTreeWalker.js.map +1 -1
- package/lib/module/core/systemPrompt.js +164 -0
- package/lib/module/core/systemPrompt.js.map +1 -0
- package/lib/module/providers/GeminiProvider.js +189 -73
- package/lib/module/providers/GeminiProvider.js.map +1 -1
- package/lib/typescript/src/components/AIAgent.d.ts +9 -1
- package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
- package/lib/typescript/src/components/AgentChatBar.d.ts +4 -3
- package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -1
- package/lib/typescript/src/core/AgentRuntime.d.ts +16 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -1
- package/lib/typescript/src/core/FiberTreeWalker.d.ts +5 -0
- package/lib/typescript/src/core/FiberTreeWalker.d.ts.map +1 -1
- package/lib/typescript/src/core/systemPrompt.d.ts +9 -0
- package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -0
- package/lib/typescript/src/core/types.d.ts +51 -13
- package/lib/typescript/src/core/types.d.ts.map +1 -1
- package/lib/typescript/src/providers/GeminiProvider.d.ts +33 -13
- package/lib/typescript/src/providers/GeminiProvider.d.ts.map +1 -1
- package/package.json +16 -14
- package/src/components/AIAgent.tsx +41 -1
- package/src/components/AgentChatBar.tsx +150 -28
- package/src/core/AgentRuntime.ts +287 -131
- package/src/core/FiberTreeWalker.ts +74 -19
- package/src/core/systemPrompt.ts +162 -0
- package/src/core/types.ts +58 -10
- package/src/providers/GeminiProvider.ts +174 -101
package/src/core/AgentRuntime.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { logger } from '../utils/logger';
|
|
|
13
13
|
import { walkFiberTree } from './FiberTreeWalker';
|
|
14
14
|
import type { WalkConfig } from './FiberTreeWalker';
|
|
15
15
|
import { dehydrateScreen } from './ScreenDehydrator';
|
|
16
|
+
import { buildSystemPrompt } from './systemPrompt';
|
|
16
17
|
import type {
|
|
17
18
|
AIProvider,
|
|
18
19
|
AgentConfig,
|
|
@@ -24,51 +25,6 @@ import type {
|
|
|
24
25
|
|
|
25
26
|
const DEFAULT_MAX_STEPS = 10;
|
|
26
27
|
|
|
27
|
-
// ─── System Prompt ─────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
function buildSystemPrompt(language: string): string {
|
|
30
|
-
const isArabic = language === 'ar';
|
|
31
|
-
|
|
32
|
-
return `You are an AI agent that controls a React Native mobile app. You operate in an iterative loop to accomplish user requests.
|
|
33
|
-
|
|
34
|
-
${isArabic ? 'Respond to the user in Arabic.' : 'Respond to the user in English.'}
|
|
35
|
-
|
|
36
|
-
<input>
|
|
37
|
-
At every step you receive:
|
|
38
|
-
1. <screen_state>: Current screen name, available screens, and interactive elements indexed for actions.
|
|
39
|
-
2. <agent_history>: Your previous steps and their results.
|
|
40
|
-
3. <user_request>: The user's original request.
|
|
41
|
-
</input>
|
|
42
|
-
|
|
43
|
-
<screen_state>
|
|
44
|
-
Interactive elements are listed as [index]<type attrs>label</>
|
|
45
|
-
- index: numeric identifier for interaction
|
|
46
|
-
- type: element type (pressable, text-input, switch)
|
|
47
|
-
- label: visible text content of the element
|
|
48
|
-
|
|
49
|
-
Only elements with [index] are interactive. Use the index to tap or type into them.
|
|
50
|
-
</screen_state>
|
|
51
|
-
|
|
52
|
-
<tools>
|
|
53
|
-
Available tools:
|
|
54
|
-
- tap(index): Tap an interactive element by its index. This triggers its onPress handler.
|
|
55
|
-
- type(index, text): Type text into a text-input element by its index.
|
|
56
|
-
- navigate(screen, params): Navigate to a specific screen. params is optional JSON object.
|
|
57
|
-
- done(text, success): Complete the task. text is your response to the user.
|
|
58
|
-
- ask_user(question): Ask the user for clarification if needed.
|
|
59
|
-
</tools>
|
|
60
|
-
|
|
61
|
-
<rules>
|
|
62
|
-
- Only interact with elements that have an [index].
|
|
63
|
-
- After tapping an element, the screen may change. Wait for the next step to see updated elements.
|
|
64
|
-
- If the current screen doesn't have what you need, use navigate() to go to another screen.
|
|
65
|
-
- If you're stuck or need more info, use ask_user().
|
|
66
|
-
- When the task is complete, ALWAYS call done() with a summary.
|
|
67
|
-
- Be efficient — complete tasks in as few steps as possible.
|
|
68
|
-
- If a tap navigates to another screen, the next step will show the new screen's elements.
|
|
69
|
-
</rules>`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
28
|
// ─── Agent Runtime ─────────────────────────────────────────────
|
|
73
29
|
|
|
74
30
|
export class AgentRuntime {
|
|
@@ -112,10 +68,10 @@ export class AgentRuntime {
|
|
|
112
68
|
// ─── Tool Registration ─────────────────────────────────────
|
|
113
69
|
|
|
114
70
|
private registerBuiltInTools(): void {
|
|
115
|
-
// tap —
|
|
71
|
+
// tap — universal interaction (mirrors RNTL's dispatchEvent pattern)
|
|
116
72
|
this.tools.set('tap', {
|
|
117
73
|
name: 'tap',
|
|
118
|
-
description: 'Tap an interactive element by its index
|
|
74
|
+
description: 'Tap an interactive element by its index. Works universally on buttons, switches, and custom components.',
|
|
119
75
|
parameters: {
|
|
120
76
|
index: { type: 'number', description: 'The index of the element to tap', required: true },
|
|
121
77
|
},
|
|
@@ -125,17 +81,48 @@ export class AgentRuntime {
|
|
|
125
81
|
if (!element) {
|
|
126
82
|
return `❌ Element with index ${args.index} not found. Available indexes: ${elements.map(e => e.index).join(', ')}`;
|
|
127
83
|
}
|
|
128
|
-
|
|
129
|
-
|
|
84
|
+
|
|
85
|
+
// Strategy 1: Switch — call onValueChange (like RNTL's fireEvent('valueChange'))
|
|
86
|
+
if (element.type === 'switch' && element.props.onValueChange) {
|
|
87
|
+
try {
|
|
88
|
+
element.props.onValueChange(!element.props.value);
|
|
89
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
90
|
+
return `✅ Toggled [${args.index}] "${element.label}" to ${!element.props.value}`;
|
|
91
|
+
} catch (error: any) {
|
|
92
|
+
return `❌ Error toggling [${args.index}]: ${error.message}`;
|
|
93
|
+
}
|
|
130
94
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
95
|
+
|
|
96
|
+
// Strategy 2: Direct onPress (covers Pressable, Button, custom components)
|
|
97
|
+
if (element.props.onPress) {
|
|
98
|
+
try {
|
|
99
|
+
element.props.onPress();
|
|
100
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
101
|
+
return `✅ Tapped [${args.index}] "${element.label}"`;
|
|
102
|
+
} catch (error: any) {
|
|
103
|
+
return `❌ Error tapping [${args.index}]: ${error.message}`;
|
|
104
|
+
}
|
|
138
105
|
}
|
|
106
|
+
|
|
107
|
+
// Strategy 3: Bubble up Fiber tree (like RNTL's findEventHandler → element.parent)
|
|
108
|
+
let fiber = element.fiberNode?.return;
|
|
109
|
+
let bubbleDepth = 0;
|
|
110
|
+
while (fiber && bubbleDepth < 5) {
|
|
111
|
+
const parentProps = fiber.memoizedProps || {};
|
|
112
|
+
if (parentProps.onPress && typeof parentProps.onPress === 'function') {
|
|
113
|
+
try {
|
|
114
|
+
parentProps.onPress();
|
|
115
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
116
|
+
return `✅ Tapped parent of [${args.index}] "${element.label}"`;
|
|
117
|
+
} catch (error: any) {
|
|
118
|
+
return `❌ Error tapping parent of [${args.index}]: ${error.message}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
fiber = fiber.return;
|
|
122
|
+
bubbleDepth++;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return `❌ Element [${args.index}] "${element.label}" has no tap handler (no onPress or onValueChange found).`;
|
|
139
126
|
},
|
|
140
127
|
});
|
|
141
128
|
|
|
@@ -165,25 +152,35 @@ export class AgentRuntime {
|
|
|
165
152
|
},
|
|
166
153
|
});
|
|
167
154
|
|
|
168
|
-
// navigate — navigate to a screen
|
|
155
|
+
// navigate — navigate to a screen (supports React Navigation + Expo Router)
|
|
169
156
|
this.tools.set('navigate', {
|
|
170
157
|
name: 'navigate',
|
|
171
158
|
description: 'Navigate to a specific screen in the app.',
|
|
172
159
|
parameters: {
|
|
173
|
-
screen: { type: 'string', description: 'Screen name to navigate to', required: true },
|
|
160
|
+
screen: { type: 'string', description: 'Screen name or path to navigate to', required: true },
|
|
174
161
|
params: { type: 'string', description: 'Optional JSON params object', required: false },
|
|
175
162
|
},
|
|
176
163
|
execute: async (args) => {
|
|
164
|
+
// Expo Router path: use router.push()
|
|
165
|
+
if (this.config.router) {
|
|
166
|
+
try {
|
|
167
|
+
const path = args.screen.startsWith('/') ? args.screen : `/${args.screen}`;
|
|
168
|
+
this.config.router.push(path);
|
|
169
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
170
|
+
return `✅ Navigated to "${path}"`;
|
|
171
|
+
} catch (error: any) {
|
|
172
|
+
return `❌ Navigation error: ${error.message}`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// React Navigation path: use navRef.navigate()
|
|
177
177
|
if (!this.navRef) {
|
|
178
178
|
return '❌ Navigation ref not available.';
|
|
179
179
|
}
|
|
180
|
-
// Per React Navigation docs: must check isReady() before navigate
|
|
181
|
-
// https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization
|
|
182
180
|
if (!this.navRef.isReady()) {
|
|
183
|
-
// Wait a bit and retry — navigator may still be mounting
|
|
184
181
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
185
182
|
if (!this.navRef.isReady()) {
|
|
186
|
-
return '❌ Navigation is not ready yet.
|
|
183
|
+
return '❌ Navigation is not ready yet.';
|
|
187
184
|
}
|
|
188
185
|
}
|
|
189
186
|
try {
|
|
@@ -210,14 +207,21 @@ export class AgentRuntime {
|
|
|
210
207
|
},
|
|
211
208
|
});
|
|
212
209
|
|
|
213
|
-
// ask_user — ask for clarification
|
|
210
|
+
// ask_user — ask for clarification (mirrors page-agent: blocks until user responds)
|
|
214
211
|
this.tools.set('ask_user', {
|
|
215
212
|
name: 'ask_user',
|
|
216
|
-
description: 'Ask the user for
|
|
213
|
+
description: 'Ask the user a question and wait for their answer. Use this if you need more information or clarification.',
|
|
217
214
|
parameters: {
|
|
218
215
|
question: { type: 'string', description: 'Question to ask the user', required: true },
|
|
219
216
|
},
|
|
220
217
|
execute: async (args) => {
|
|
218
|
+
if (this.config.onAskUser) {
|
|
219
|
+
// Page-agent pattern: block until user responds, then continue the loop
|
|
220
|
+
this.config.onStatusUpdate?.('Waiting for your answer...');
|
|
221
|
+
const answer = await this.config.onAskUser(args.question);
|
|
222
|
+
return `User answered: ${answer}`;
|
|
223
|
+
}
|
|
224
|
+
// Legacy fallback: break the loop (context will be lost)
|
|
221
225
|
return `❓ ${args.question}`;
|
|
222
226
|
},
|
|
223
227
|
});
|
|
@@ -236,30 +240,85 @@ export class AgentRuntime {
|
|
|
236
240
|
|
|
237
241
|
// ─── Navigation Helpers ────────────────────────────────────
|
|
238
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Recursively collect ALL screen names from the navigation state tree.
|
|
245
|
+
* This handles tabs, drawers, and nested stacks.
|
|
246
|
+
*/
|
|
239
247
|
private getRouteNames(): string[] {
|
|
240
248
|
try {
|
|
241
249
|
if (!this.navRef?.isReady?.()) return [];
|
|
242
250
|
const state = this.navRef?.getRootState?.() || this.navRef?.getState?.();
|
|
243
|
-
if (state
|
|
244
|
-
|
|
245
|
-
return [];
|
|
251
|
+
if (!state) return [];
|
|
252
|
+
return this.collectRouteNames(state);
|
|
246
253
|
} catch {
|
|
247
254
|
return [];
|
|
248
255
|
}
|
|
249
256
|
}
|
|
250
257
|
|
|
258
|
+
private collectRouteNames(state: any): string[] {
|
|
259
|
+
const names: string[] = [];
|
|
260
|
+
if (state?.routes) {
|
|
261
|
+
for (const route of state.routes) {
|
|
262
|
+
names.push(route.name);
|
|
263
|
+
// Recurse into nested navigator states
|
|
264
|
+
if (route.state) {
|
|
265
|
+
names.push(...this.collectRouteNames(route.state));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return [...new Set(names)];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Recursively find the deepest active screen name.
|
|
274
|
+
* For tabs: follows active tab → active screen inside that tab.
|
|
275
|
+
*/
|
|
251
276
|
private getCurrentScreenName(): string {
|
|
277
|
+
// Expo Router: use pathname
|
|
278
|
+
if (this.config.pathname) {
|
|
279
|
+
const segments = this.config.pathname.split('/').filter(Boolean);
|
|
280
|
+
return segments[segments.length - 1] || 'Unknown';
|
|
281
|
+
}
|
|
282
|
+
|
|
252
283
|
try {
|
|
253
284
|
if (!this.navRef?.isReady?.()) return 'Unknown';
|
|
254
285
|
const state = this.navRef?.getRootState?.() || this.navRef?.getState?.();
|
|
255
286
|
if (!state) return 'Unknown';
|
|
256
|
-
|
|
257
|
-
return route?.name || 'Unknown';
|
|
287
|
+
return this.getDeepestScreenName(state);
|
|
258
288
|
} catch {
|
|
259
289
|
return 'Unknown';
|
|
260
290
|
}
|
|
261
291
|
}
|
|
262
292
|
|
|
293
|
+
private getDeepestScreenName(state: any): string {
|
|
294
|
+
if (!state?.routes || state.index == null) return 'Unknown';
|
|
295
|
+
const route = state.routes[state.index];
|
|
296
|
+
if (!route) return 'Unknown';
|
|
297
|
+
// If this route has a nested state, recurse deeper
|
|
298
|
+
if (route.state) {
|
|
299
|
+
return this.getDeepestScreenName(route.state);
|
|
300
|
+
}
|
|
301
|
+
return route.name || 'Unknown';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Maps a tool call to a user-friendly status label for the loading overlay. */
|
|
305
|
+
private getToolStatusLabel(toolName: string, args: Record<string, any>): string {
|
|
306
|
+
switch (toolName) {
|
|
307
|
+
case 'tap':
|
|
308
|
+
return `Tapping element ${args.index ?? ''}...`;
|
|
309
|
+
case 'type':
|
|
310
|
+
return `Typing into field...`;
|
|
311
|
+
case 'navigate':
|
|
312
|
+
return `Navigating to ${args.screen || 'screen'}...`;
|
|
313
|
+
case 'done':
|
|
314
|
+
return 'Wrapping up...';
|
|
315
|
+
case 'ask_user':
|
|
316
|
+
return 'Asking you a question...';
|
|
317
|
+
default:
|
|
318
|
+
return `Running ${toolName}...`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
263
322
|
// ─── Build Tools Array for Provider ────────────────────────
|
|
264
323
|
|
|
265
324
|
private buildToolsForProvider(): ToolDefinition[] {
|
|
@@ -324,7 +383,87 @@ export class AgentRuntime {
|
|
|
324
383
|
return result ? `<instructions>\n${result}</instructions>\n\n` : '';
|
|
325
384
|
}
|
|
326
385
|
|
|
327
|
-
// ───
|
|
386
|
+
// ─── Observation System (mirrors PageAgentCore.#handleObservations) ──
|
|
387
|
+
|
|
388
|
+
private observations: string[] = [];
|
|
389
|
+
private lastScreenName: string = '';
|
|
390
|
+
|
|
391
|
+
private handleObservations(step: number, maxSteps: number, screenName: string): void {
|
|
392
|
+
// Screen change detection
|
|
393
|
+
if (this.lastScreenName && screenName !== this.lastScreenName) {
|
|
394
|
+
this.observations.push(`Screen navigated to → ${screenName}`);
|
|
395
|
+
}
|
|
396
|
+
this.lastScreenName = screenName;
|
|
397
|
+
|
|
398
|
+
// Remaining steps warning
|
|
399
|
+
const remaining = maxSteps - step;
|
|
400
|
+
if (remaining === 5) {
|
|
401
|
+
this.observations.push(
|
|
402
|
+
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
|
|
403
|
+
);
|
|
404
|
+
} else if (remaining === 2) {
|
|
405
|
+
this.observations.push(
|
|
406
|
+
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ─── User Prompt Assembly (mirrors PageAgentCore.#assembleUserPrompt) ──
|
|
412
|
+
|
|
413
|
+
private assembleUserPrompt(
|
|
414
|
+
step: number,
|
|
415
|
+
maxSteps: number,
|
|
416
|
+
contextualMessage: string,
|
|
417
|
+
screenName: string,
|
|
418
|
+
screenContent: string,
|
|
419
|
+
): string {
|
|
420
|
+
let prompt = '';
|
|
421
|
+
|
|
422
|
+
// 1. <instructions> (optional system/screen instructions)
|
|
423
|
+
prompt += this.getInstructions(screenName);
|
|
424
|
+
|
|
425
|
+
// 2. <agent_state> — user request + step info (mirrors page-agent)
|
|
426
|
+
prompt += '<agent_state>\n';
|
|
427
|
+
prompt += '<user_request>\n';
|
|
428
|
+
prompt += `${contextualMessage}\n`;
|
|
429
|
+
prompt += '</user_request>\n';
|
|
430
|
+
prompt += '<step_info>\n';
|
|
431
|
+
prompt += `Step ${step + 1} of ${maxSteps} max possible steps\n`;
|
|
432
|
+
prompt += '</step_info>\n';
|
|
433
|
+
prompt += '</agent_state>\n\n';
|
|
434
|
+
|
|
435
|
+
// 3. <agent_history> — structured per-step history (mirrors page-agent)
|
|
436
|
+
prompt += '<agent_history>\n';
|
|
437
|
+
|
|
438
|
+
let stepIndex = 0;
|
|
439
|
+
for (const event of this.history) {
|
|
440
|
+
stepIndex++;
|
|
441
|
+
prompt += `<step_${stepIndex}>\n`;
|
|
442
|
+
prompt += `Previous Goal Eval: ${event.reflection.previousGoalEval}\n`;
|
|
443
|
+
prompt += `Memory: ${event.reflection.memory}\n`;
|
|
444
|
+
prompt += `Plan: ${event.reflection.plan}\n`;
|
|
445
|
+
prompt += `Action Result: ${event.action.output}\n`;
|
|
446
|
+
prompt += `</step_${stepIndex}>\n`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Inject system observations
|
|
450
|
+
for (const obs of this.observations) {
|
|
451
|
+
prompt += `<sys>${obs}</sys>\n`;
|
|
452
|
+
}
|
|
453
|
+
this.observations = [];
|
|
454
|
+
|
|
455
|
+
prompt += '</agent_history>\n\n';
|
|
456
|
+
|
|
457
|
+
// 4. <screen_state> — dehydrated screen content
|
|
458
|
+
prompt += '<screen_state>\n';
|
|
459
|
+
prompt += `Current Screen: ${screenName}\n`;
|
|
460
|
+
prompt += screenContent + '\n';
|
|
461
|
+
prompt += '</screen_state>\n';
|
|
462
|
+
|
|
463
|
+
return prompt;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ─── Main Execution Loop ──────────────────────────────────────
|
|
328
467
|
|
|
329
468
|
async execute(userMessage: string): Promise<ExecutionResult> {
|
|
330
469
|
if (this.isRunning) {
|
|
@@ -333,6 +472,8 @@ export class AgentRuntime {
|
|
|
333
472
|
|
|
334
473
|
this.isRunning = true;
|
|
335
474
|
this.history = [];
|
|
475
|
+
this.observations = [];
|
|
476
|
+
this.lastScreenName = '';
|
|
336
477
|
const maxSteps = this.config.maxSteps || DEFAULT_MAX_STEPS;
|
|
337
478
|
const stepDelay = this.config.stepDelay ?? 300;
|
|
338
479
|
|
|
@@ -374,13 +515,16 @@ export class AgentRuntime {
|
|
|
374
515
|
screenContent = await this.config.transformScreenContent(screenContent);
|
|
375
516
|
}
|
|
376
517
|
|
|
377
|
-
// 3.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
518
|
+
// 3. Handle observations (mirrors page-agent #handleObservations)
|
|
519
|
+
this.handleObservations(step, maxSteps, screenName);
|
|
520
|
+
|
|
521
|
+
// 4. Assemble structured user prompt (mirrors page-agent #assembleUserPrompt)
|
|
522
|
+
const contextMessage = this.assembleUserPrompt(
|
|
523
|
+
step, maxSteps, contextualMessage, screenName, screenContent,
|
|
524
|
+
);
|
|
382
525
|
|
|
383
|
-
//
|
|
526
|
+
// 5. Send to AI provider
|
|
527
|
+
this.config.onStatusUpdate?.('Analyzing screen...');
|
|
384
528
|
const systemPrompt = buildSystemPrompt(this.config.language || 'en');
|
|
385
529
|
const tools = this.buildToolsForProvider();
|
|
386
530
|
|
|
@@ -393,7 +537,7 @@ export class AgentRuntime {
|
|
|
393
537
|
this.history,
|
|
394
538
|
);
|
|
395
539
|
|
|
396
|
-
//
|
|
540
|
+
// 6. Process tool calls
|
|
397
541
|
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
398
542
|
logger.warn('AgentRuntime', 'No tool calls in response. Text:', response.text);
|
|
399
543
|
const result: ExecutionResult = {
|
|
@@ -405,65 +549,77 @@ export class AgentRuntime {
|
|
|
405
549
|
return result;
|
|
406
550
|
}
|
|
407
551
|
|
|
408
|
-
|
|
409
|
-
|
|
552
|
+
// 7. Structured reasoning from provider (no regex parsing needed)
|
|
553
|
+
const { reasoning } = response;
|
|
554
|
+
logger.info('AgentRuntime', `🧠 Plan: ${reasoning.plan}`);
|
|
555
|
+
if (reasoning.memory) {
|
|
556
|
+
logger.debug('AgentRuntime', `💾 Memory: ${reasoning.memory}`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Only process the FIRST tool call per step (one action per step).
|
|
560
|
+
// After one action, the loop re-reads the screen with fresh indexes.
|
|
561
|
+
const toolCall = response.toolCalls[0]!;
|
|
562
|
+
if (response.toolCalls.length > 1) {
|
|
563
|
+
logger.warn('AgentRuntime', `AI returned ${response.toolCalls.length} tool calls, executing only the first one.`);
|
|
564
|
+
}
|
|
410
565
|
|
|
411
|
-
|
|
412
|
-
const tool = this.tools.get(toolCall.name) ||
|
|
413
|
-
this.buildToolsForProvider().find(t => t.name === toolCall.name);
|
|
566
|
+
logger.info('AgentRuntime', `Tool: ${toolCall.name}(${JSON.stringify(toolCall.args)})`);
|
|
414
567
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
} else {
|
|
419
|
-
output = `❌ Unknown tool: ${toolCall.name}`;
|
|
420
|
-
}
|
|
568
|
+
// Dynamic status update based on tool being executed
|
|
569
|
+
const statusLabel = this.getToolStatusLabel(toolCall.name, toolCall.args);
|
|
570
|
+
this.config.onStatusUpdate?.(statusLabel);
|
|
421
571
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
572
|
+
// Find and execute the tool
|
|
573
|
+
const tool = this.tools.get(toolCall.name) ||
|
|
574
|
+
this.buildToolsForProvider().find(t => t.name === toolCall.name);
|
|
575
|
+
|
|
576
|
+
let output: string;
|
|
577
|
+
if (tool) {
|
|
578
|
+
output = await tool.execute(toolCall.args);
|
|
579
|
+
} else {
|
|
580
|
+
output = `❌ Unknown tool: ${toolCall.name}`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
logger.info('AgentRuntime', `Result: ${output}`);
|
|
584
|
+
|
|
585
|
+
// Record step with structured reasoning
|
|
586
|
+
const agentStep: AgentStep = {
|
|
587
|
+
stepIndex: step,
|
|
588
|
+
reflection: reasoning,
|
|
589
|
+
action: {
|
|
590
|
+
name: toolCall.name,
|
|
591
|
+
input: toolCall.args,
|
|
592
|
+
output,
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
this.history.push(agentStep);
|
|
596
|
+
|
|
597
|
+
// Lifecycle: onAfterStep (mirrors page-agent)
|
|
598
|
+
await this.config.onAfterStep?.(this.history);
|
|
599
|
+
|
|
600
|
+
// Check if done
|
|
601
|
+
if (toolCall.name === 'done') {
|
|
602
|
+
const result: ExecutionResult = {
|
|
603
|
+
success: toolCall.args.success !== false,
|
|
604
|
+
message: toolCall.args.text || output,
|
|
605
|
+
steps: this.history,
|
|
437
606
|
};
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// Check if done
|
|
444
|
-
if (toolCall.name === 'done') {
|
|
445
|
-
const result: ExecutionResult = {
|
|
446
|
-
success: toolCall.args.success !== false,
|
|
447
|
-
message: output,
|
|
448
|
-
steps: this.history,
|
|
449
|
-
};
|
|
450
|
-
logger.info('AgentRuntime', `Task completed: ${output}`);
|
|
451
|
-
await this.config.onAfterTask?.(result);
|
|
452
|
-
return result;
|
|
453
|
-
}
|
|
607
|
+
logger.info('AgentRuntime', `Task completed: ${result.message}`);
|
|
608
|
+
await this.config.onAfterTask?.(result);
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
454
611
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
612
|
+
// Check if asking user (legacy path — only breaks loop when onAskUser is NOT set)
|
|
613
|
+
if (toolCall.name === 'ask_user' && !this.config.onAskUser) {
|
|
614
|
+
this.lastAskUserQuestion = toolCall.args.question || output;
|
|
615
|
+
|
|
616
|
+
const result: ExecutionResult = {
|
|
617
|
+
success: true,
|
|
618
|
+
message: output,
|
|
619
|
+
steps: this.history,
|
|
620
|
+
};
|
|
621
|
+
await this.config.onAfterTask?.(result);
|
|
622
|
+
return result;
|
|
467
623
|
}
|
|
468
624
|
|
|
469
625
|
// Step delay (mirrors page-agent stepDelay)
|