@minded-ai/mindedjs 3.1.24 → 3.1.26
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/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +6 -1
- package/dist/agent.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/nodes/addPromptNode.d.ts.map +1 -1
- package/dist/nodes/addPromptNode.js +26 -2
- package/dist/nodes/addPromptNode.js.map +1 -1
- package/dist/nodes/addToolRunNode.d.ts.map +1 -1
- package/dist/nodes/addToolRunNode.js +40 -5
- package/dist/nodes/addToolRunNode.js.map +1 -1
- package/dist/platform/config.d.ts +2 -0
- package/dist/platform/config.d.ts.map +1 -1
- package/dist/platform/config.js +8 -0
- package/dist/platform/config.js.map +1 -1
- package/dist/platform/mindedConnection.d.ts.map +1 -1
- package/dist/platform/mindedConnection.js +2 -1
- package/dist/platform/mindedConnection.js.map +1 -1
- package/dist/platform/mindedConnectionTypes.d.ts +33 -0
- package/dist/platform/mindedConnectionTypes.d.ts.map +1 -1
- package/dist/platform/mindedConnectionTypes.js +4 -0
- package/dist/platform/mindedConnectionTypes.js.map +1 -1
- package/dist/platform/toolExecutor.d.ts.map +1 -1
- package/dist/platform/toolExecutor.js +21 -1
- package/dist/platform/toolExecutor.js.map +1 -1
- package/dist/toolsLibrary/withBrowserSession.d.ts +28 -4
- package/dist/toolsLibrary/withBrowserSession.d.ts.map +1 -1
- package/dist/toolsLibrary/withBrowserSession.js +253 -61
- package/dist/toolsLibrary/withBrowserSession.js.map +1 -1
- package/dist/types/Flows.types.d.ts +1 -0
- package/dist/types/Flows.types.d.ts.map +1 -1
- package/dist/types/Flows.types.js.map +1 -1
- package/package.json +2 -2
- package/src/agent.ts +7 -2
- package/src/index.ts +2 -0
- package/src/nodes/addPromptNode.ts +28 -2
- package/src/nodes/addToolRunNode.ts +49 -7
- package/src/platform/config.ts +10 -0
- package/src/platform/mindedConnection.ts +2 -1
- package/src/platform/mindedConnectionTypes.ts +36 -0
- package/src/platform/toolExecutor.ts +24 -1
- package/src/toolsLibrary/withBrowserSession.ts +345 -63
- package/src/types/Flows.types.ts +1 -0
|
@@ -4,13 +4,55 @@ import { createBrowserSession, destroyBrowserSession } from '../browserTask/exec
|
|
|
4
4
|
import { logger } from '../utils/logger';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
-
import { randomUUID } from 'crypto';
|
|
8
7
|
import { awaitEmit, emit, isConnected } from '../platform/mindedConnection';
|
|
9
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
BrowserRunEventRequest,
|
|
10
|
+
BrowserRunEventType,
|
|
11
|
+
mindedConnectionSocketMessageType,
|
|
12
|
+
RpaBreakpointHitMessage,
|
|
13
|
+
GetActiveRpaBreakpointResponse,
|
|
14
|
+
} from '../platform/mindedConnectionTypes';
|
|
10
15
|
import { ProxyConfig } from '../types/Tools.types';
|
|
16
|
+
import { randomUUID } from 'crypto';
|
|
11
17
|
import { getConfig } from '../platform/config';
|
|
12
18
|
import { fetchSessionDataViaSocket, saveSessionDataViaSocket } from './browserSessionPersistence';
|
|
13
19
|
|
|
20
|
+
// Type for RPA breakpoint info (returned from backend)
|
|
21
|
+
export interface RpaBreakpointInfo {
|
|
22
|
+
toolName: string;
|
|
23
|
+
stepNumber: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Type for breakpoint hit result
|
|
27
|
+
export interface BreakpointHitResult {
|
|
28
|
+
toolCallId: string;
|
|
29
|
+
toolName: string;
|
|
30
|
+
stepNumber: number;
|
|
31
|
+
cdpUrl: string;
|
|
32
|
+
logs: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Type for withBrowserSession result
|
|
36
|
+
export interface BrowserSessionResult<T> {
|
|
37
|
+
result: T;
|
|
38
|
+
breakpointHit?: boolean;
|
|
39
|
+
breakpointInfo?: BreakpointHitResult;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Custom error thrown when an RPA breakpoint is hit
|
|
44
|
+
* This is used to cleanly exit the RPA task and keep the browser session alive
|
|
45
|
+
*/
|
|
46
|
+
export class RpaBreakpointHitError extends Error {
|
|
47
|
+
constructor(
|
|
48
|
+
message: string,
|
|
49
|
+
public readonly breakpointInfo: BreakpointHitResult,
|
|
50
|
+
) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = 'RpaBreakpointHitError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
14
56
|
/**
|
|
15
57
|
* Sends a browser run analytics event to the backend (for local browsers only).
|
|
16
58
|
* Events are stored individually and joined by browser_run_id in worker aggregations.
|
|
@@ -100,7 +142,6 @@ function stripHtmlContent(html: string): string {
|
|
|
100
142
|
class ScreenshotCapture {
|
|
101
143
|
private sessionId: string;
|
|
102
144
|
private toolCallId: string;
|
|
103
|
-
private stepCounter = 0;
|
|
104
145
|
|
|
105
146
|
constructor(sessionId: string, toolCallId: string) {
|
|
106
147
|
this.sessionId = sessionId;
|
|
@@ -112,7 +153,7 @@ class ScreenshotCapture {
|
|
|
112
153
|
});
|
|
113
154
|
}
|
|
114
155
|
|
|
115
|
-
async captureScreenshot(page: Page, phase: 'before' | 'after'): Promise<void> {
|
|
156
|
+
async captureScreenshot(page: Page, phase: 'before' | 'after', stepNumber: number): Promise<void> {
|
|
116
157
|
try {
|
|
117
158
|
const timestamp = new Date().toISOString();
|
|
118
159
|
|
|
@@ -125,7 +166,7 @@ class ScreenshotCapture {
|
|
|
125
166
|
type: mindedConnectionSocketMessageType.RPA_SCREENSHOT_UPLOAD,
|
|
126
167
|
sessionId: this.sessionId,
|
|
127
168
|
toolCallId: this.toolCallId,
|
|
128
|
-
stepNumber
|
|
169
|
+
stepNumber,
|
|
129
170
|
phase,
|
|
130
171
|
screenshotData: screenshotBase64,
|
|
131
172
|
timestamp,
|
|
@@ -135,6 +176,7 @@ class ScreenshotCapture {
|
|
|
135
176
|
sessionId: this.sessionId,
|
|
136
177
|
toolCallId: this.toolCallId,
|
|
137
178
|
phase,
|
|
179
|
+
stepNumber,
|
|
138
180
|
error,
|
|
139
181
|
});
|
|
140
182
|
});
|
|
@@ -144,6 +186,7 @@ class ScreenshotCapture {
|
|
|
144
186
|
sessionId: this.sessionId,
|
|
145
187
|
toolCallId: this.toolCallId,
|
|
146
188
|
phase,
|
|
189
|
+
stepNumber,
|
|
147
190
|
error,
|
|
148
191
|
});
|
|
149
192
|
// Don't throw - continue execution
|
|
@@ -166,6 +209,9 @@ class LogsCapture {
|
|
|
166
209
|
this.toolCallId = toolCallId;
|
|
167
210
|
}
|
|
168
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Add a log entry
|
|
214
|
+
*/
|
|
169
215
|
async addLog(message: string): Promise<void> {
|
|
170
216
|
this.logEntries.push(message);
|
|
171
217
|
|
|
@@ -223,6 +269,117 @@ class LogsCapture {
|
|
|
223
269
|
}
|
|
224
270
|
}
|
|
225
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Context for breakpoint checking
|
|
274
|
+
*/
|
|
275
|
+
interface BreakpointContext {
|
|
276
|
+
toolCallId: string;
|
|
277
|
+
toolName: string;
|
|
278
|
+
sessionId: string;
|
|
279
|
+
cdpUrl: string;
|
|
280
|
+
checkBreakpoints: boolean; // Only check breakpoints in dev environment with socket connection
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Fetch active breakpoint from backend via socket
|
|
285
|
+
* Returns null if no breakpoint is set or if not connected
|
|
286
|
+
*/
|
|
287
|
+
const fetchActiveBreakpoint = async (toolName: string): Promise<RpaBreakpointInfo | null> => {
|
|
288
|
+
if (!isConnected()) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const response = await awaitEmit<
|
|
294
|
+
{ type: string; toolName: string },
|
|
295
|
+
GetActiveRpaBreakpointResponse
|
|
296
|
+
>(
|
|
297
|
+
mindedConnectionSocketMessageType.GET_ACTIVE_RPA_BREAKPOINT,
|
|
298
|
+
{
|
|
299
|
+
type: mindedConnectionSocketMessageType.GET_ACTIVE_RPA_BREAKPOINT,
|
|
300
|
+
toolName,
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
if (response.error) {
|
|
305
|
+
logger.warn({ msg: 'Error fetching active breakpoint from backend', error: response.error });
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return response.breakpoint;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
logger.warn({ msg: 'Failed to fetch active breakpoint from backend', error });
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Check if we've hit a breakpoint and handle it
|
|
318
|
+
* Queries backend for active breakpoint (only in dev environment)
|
|
319
|
+
* Throws RpaBreakpointHitError if breakpoint is hit
|
|
320
|
+
*/
|
|
321
|
+
const checkBreakpoint = async (
|
|
322
|
+
stepNumber: number,
|
|
323
|
+
logsCapture: LogsCapture,
|
|
324
|
+
page: Page,
|
|
325
|
+
context: BreakpointContext,
|
|
326
|
+
): Promise<void> => {
|
|
327
|
+
const { toolCallId, toolName, sessionId, cdpUrl, checkBreakpoints } = context;
|
|
328
|
+
|
|
329
|
+
// Skip breakpoint checking if not enabled (non-dev environment or no socket connection)
|
|
330
|
+
if (!checkBreakpoints) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Fetch active breakpoint from backend
|
|
335
|
+
const activeBreakpoint = await fetchActiveBreakpoint(toolName);
|
|
336
|
+
|
|
337
|
+
if (!activeBreakpoint) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check if we've hit the breakpoint (match by toolName and stepNumber)
|
|
342
|
+
if (activeBreakpoint.stepNumber === stepNumber) {
|
|
343
|
+
logger.info({
|
|
344
|
+
msg: 'RPA breakpoint hit!',
|
|
345
|
+
toolName,
|
|
346
|
+
stepNumber,
|
|
347
|
+
sessionId,
|
|
348
|
+
cdpUrl,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Emit breakpoint hit message to backend (agentId is added by backend from socket connection)
|
|
352
|
+
const breakpointMessage: RpaBreakpointHitMessage = {
|
|
353
|
+
type: mindedConnectionSocketMessageType.RPA_BREAKPOINT_HIT,
|
|
354
|
+
sessionId,
|
|
355
|
+
toolCallId,
|
|
356
|
+
toolName,
|
|
357
|
+
stepNumber,
|
|
358
|
+
cdpUrl,
|
|
359
|
+
logs: logsCapture.getLogs(),
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
await awaitEmit(mindedConnectionSocketMessageType.RPA_BREAKPOINT_HIT, breakpointMessage);
|
|
364
|
+
logger.info({ msg: 'RPA breakpoint hit message sent to backend', toolCallId, stepNumber });
|
|
365
|
+
} catch (error) {
|
|
366
|
+
logger.error({ msg: 'Failed to send breakpoint hit message', error });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Throw error to gracefully stop execution (will be caught by withBrowserSession)
|
|
370
|
+
throw new RpaBreakpointHitError(
|
|
371
|
+
`RPA breakpoint hit at step ${stepNumber} for tool ${toolName}`,
|
|
372
|
+
{
|
|
373
|
+
toolCallId,
|
|
374
|
+
toolName,
|
|
375
|
+
stepNumber,
|
|
376
|
+
cdpUrl,
|
|
377
|
+
logs: logsCapture.getLogs(),
|
|
378
|
+
},
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
226
383
|
/**
|
|
227
384
|
* Create a proxy locator that intercepts actions and captures screenshots and logs
|
|
228
385
|
*/
|
|
@@ -231,30 +388,76 @@ const createProxyLocator = (
|
|
|
231
388
|
page: Page,
|
|
232
389
|
screenshotCapture: ScreenshotCapture,
|
|
233
390
|
logsCapture: LogsCapture,
|
|
391
|
+
breakpointContext: BreakpointContext,
|
|
392
|
+
getNextStepNumber: () => number,
|
|
234
393
|
selector?: string,
|
|
235
394
|
): Locator => {
|
|
236
395
|
// List of methods to intercept on locators
|
|
237
|
-
const locatorMethodsToIntercept = [
|
|
396
|
+
const locatorMethodsToIntercept = [
|
|
397
|
+
'click',
|
|
398
|
+
'fill',
|
|
399
|
+
'type',
|
|
400
|
+
'selectOption',
|
|
401
|
+
'check',
|
|
402
|
+
'uncheck',
|
|
403
|
+
'setInputFiles',
|
|
404
|
+
'waitFor',
|
|
405
|
+
'evaluate',
|
|
406
|
+
'evaluateAll',
|
|
407
|
+
];
|
|
408
|
+
// Methods that return locators and need special handling
|
|
409
|
+
const locatorMethodsThatReturnLocators = [
|
|
410
|
+
'getByRole',
|
|
411
|
+
'getByText',
|
|
412
|
+
'getByLabel',
|
|
413
|
+
'getByPlaceholder',
|
|
414
|
+
'getByAltText',
|
|
415
|
+
'getByTitle',
|
|
416
|
+
'getByTestId',
|
|
417
|
+
'filter',
|
|
418
|
+
];
|
|
238
419
|
|
|
239
420
|
const proxyLocator = new Proxy(locator, {
|
|
240
421
|
get(target, prop, receiver) {
|
|
241
422
|
const originalValue = Reflect.get(target, prop, receiver);
|
|
242
423
|
|
|
424
|
+
// Handle methods that return locators - return a proxied locator
|
|
425
|
+
if (typeof prop === 'string' && locatorMethodsThatReturnLocators.includes(prop)) {
|
|
426
|
+
return function (this: any, ...args: any[]) {
|
|
427
|
+
const newLocator = originalValue.apply(target, args);
|
|
428
|
+
// For getBy* methods, try to extract a description from args
|
|
429
|
+
const description = args.length > 0 ? JSON.stringify(args[0]).substring(0, 50) : selector;
|
|
430
|
+
return createProxyLocator(
|
|
431
|
+
newLocator,
|
|
432
|
+
page,
|
|
433
|
+
screenshotCapture,
|
|
434
|
+
logsCapture,
|
|
435
|
+
breakpointContext,
|
|
436
|
+
getNextStepNumber,
|
|
437
|
+
description,
|
|
438
|
+
);
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
243
442
|
// If it's one of the methods we want to intercept
|
|
244
443
|
if (typeof prop === 'string' && locatorMethodsToIntercept.includes(prop)) {
|
|
245
444
|
return async function (this: any, ...args: any[]) {
|
|
445
|
+
// Get the step number for this action
|
|
446
|
+
const stepNumber = getNextStepNumber();
|
|
447
|
+
|
|
246
448
|
// Log the action (this will trigger immediate upload)
|
|
247
449
|
const actionDescription = formatLocatorActionLog(prop, selector, args);
|
|
248
450
|
await logsCapture.addLog(actionDescription);
|
|
249
451
|
|
|
250
|
-
// Capture "before" screenshot
|
|
251
|
-
await screenshotCapture.captureScreenshot(page, 'before');
|
|
252
452
|
|
|
253
453
|
// Call the original method
|
|
254
454
|
const result = await originalValue.apply(target, args);
|
|
255
455
|
|
|
256
456
|
// Capture "after" screenshot
|
|
257
|
-
await screenshotCapture.captureScreenshot(page, 'after');
|
|
457
|
+
await screenshotCapture.captureScreenshot(page, 'after', stepNumber);
|
|
458
|
+
|
|
459
|
+
// Check for breakpoint after executing the action
|
|
460
|
+
await checkBreakpoint(stepNumber, logsCapture, page, breakpointContext);
|
|
258
461
|
|
|
259
462
|
return result;
|
|
260
463
|
};
|
|
@@ -270,7 +473,13 @@ const createProxyLocator = (
|
|
|
270
473
|
/**
|
|
271
474
|
* Create a proxy page that intercepts actions and captures screenshots and logs
|
|
272
475
|
*/
|
|
273
|
-
const createProxyPage = (
|
|
476
|
+
const createProxyPage = (
|
|
477
|
+
page: Page,
|
|
478
|
+
screenshotCapture: ScreenshotCapture,
|
|
479
|
+
logsCapture: LogsCapture,
|
|
480
|
+
breakpointContext: BreakpointContext,
|
|
481
|
+
getNextStepNumber: () => number,
|
|
482
|
+
): Page => {
|
|
274
483
|
// List of methods to intercept for screenshot capture and logging
|
|
275
484
|
const methodsToIntercept = [
|
|
276
485
|
'click',
|
|
@@ -283,9 +492,19 @@ const createProxyPage = (page: Page, screenshotCapture: ScreenshotCapture, logsC
|
|
|
283
492
|
'setInputFiles',
|
|
284
493
|
'waitForTimeout',
|
|
285
494
|
'evaluate',
|
|
495
|
+
'waitForSelector',
|
|
286
496
|
];
|
|
287
497
|
// Methods that return locators and need special handling
|
|
288
|
-
const locatorMethods = [
|
|
498
|
+
const locatorMethods = [
|
|
499
|
+
'locator',
|
|
500
|
+
'getByRole',
|
|
501
|
+
'getByText',
|
|
502
|
+
'getByLabel',
|
|
503
|
+
'getByPlaceholder',
|
|
504
|
+
'getByAltText',
|
|
505
|
+
'getByTitle',
|
|
506
|
+
'getByTestId',
|
|
507
|
+
];
|
|
289
508
|
|
|
290
509
|
const proxyPage = new Proxy(page, {
|
|
291
510
|
get(target, prop, receiver) {
|
|
@@ -295,27 +514,38 @@ const createProxyPage = (page: Page, screenshotCapture: ScreenshotCapture, logsC
|
|
|
295
514
|
if (typeof prop === 'string' && locatorMethods.includes(prop)) {
|
|
296
515
|
return function (this: any, ...args: any[]) {
|
|
297
516
|
const locator = originalValue.apply(target, args);
|
|
298
|
-
// Extract selector from args
|
|
299
|
-
|
|
300
|
-
|
|
517
|
+
// Extract selector/description from args
|
|
518
|
+
let selector: string | undefined;
|
|
519
|
+
if (prop === 'locator' && args[0] && typeof args[0] === 'string') {
|
|
520
|
+
selector = args[0];
|
|
521
|
+
} else if (prop.startsWith('getBy')) {
|
|
522
|
+
// For getBy* methods, create a descriptive string
|
|
523
|
+
const methodName = prop.replace('getBy', '');
|
|
524
|
+
const argDescription = args.length > 0 ? JSON.stringify(args[0]).substring(0, 50) : '';
|
|
525
|
+
selector = `${methodName}(${argDescription})`;
|
|
526
|
+
}
|
|
527
|
+
return createProxyLocator(locator, target, screenshotCapture, logsCapture, breakpointContext, getNextStepNumber, selector);
|
|
301
528
|
};
|
|
302
529
|
}
|
|
303
530
|
|
|
304
531
|
// If it's one of the methods we want to intercept with screenshots
|
|
305
532
|
if (typeof prop === 'string' && methodsToIntercept.includes(prop)) {
|
|
306
533
|
return async function (this: any, ...args: any[]) {
|
|
534
|
+
// Get the step number for this action
|
|
535
|
+
const stepNumber = getNextStepNumber();
|
|
536
|
+
|
|
307
537
|
// Log the action (this will trigger immediate upload)
|
|
308
538
|
const actionDescription = formatActionLog(prop, args);
|
|
309
539
|
await logsCapture.addLog(actionDescription);
|
|
310
540
|
|
|
311
|
-
// Capture "before" screenshot
|
|
312
|
-
await screenshotCapture.captureScreenshot(target, 'before');
|
|
313
|
-
|
|
314
541
|
// Call the original method
|
|
315
542
|
const result = await originalValue.apply(target, args);
|
|
316
543
|
|
|
317
544
|
// Capture "after" screenshot
|
|
318
|
-
await screenshotCapture.captureScreenshot(target, 'after');
|
|
545
|
+
await screenshotCapture.captureScreenshot(target, 'after', stepNumber);
|
|
546
|
+
|
|
547
|
+
// Check for breakpoint before executing the action
|
|
548
|
+
await checkBreakpoint(stepNumber, logsCapture, target, breakpointContext);
|
|
319
549
|
|
|
320
550
|
return result;
|
|
321
551
|
};
|
|
@@ -351,6 +581,8 @@ const formatActionLog = (action: string, args: any[]): string => {
|
|
|
351
581
|
return `Set files for "${args[0]}"`;
|
|
352
582
|
case 'waitForTimeout':
|
|
353
583
|
return `Wait for timeout: ${args[0]}ms`;
|
|
584
|
+
case 'waitForSelector':
|
|
585
|
+
return `Wait for selector: ${args[0]}${args[1] ? ` (timeout: ${args[1].timeout || 'default'}ms)` : ''}`;
|
|
354
586
|
case 'evaluate':
|
|
355
587
|
return `Evaluate: ${typeof args[0] === 'function' ? 'function' : String(args[0]).substring(0, 100)}`;
|
|
356
588
|
default:
|
|
@@ -379,6 +611,14 @@ const formatLocatorActionLog = (action: string, selector: string | undefined, ar
|
|
|
379
611
|
return `Uncheck locator: ${locatorDescription}`;
|
|
380
612
|
case 'setInputFiles':
|
|
381
613
|
return `Set files for locator "${locatorDescription}"`;
|
|
614
|
+
case 'waitFor': {
|
|
615
|
+
const waitForOptions = args[0] || {};
|
|
616
|
+
return `Wait for locator "${locatorDescription}"${waitForOptions.state ? ` (state: ${waitForOptions.state})` : ''}${waitForOptions.timeout ? ` (timeout: ${waitForOptions.timeout}ms)` : ''}`;
|
|
617
|
+
}
|
|
618
|
+
case 'evaluate':
|
|
619
|
+
return `Evaluate on locator "${locatorDescription}": ${typeof args[0] === 'function' ? 'function' : String(args[0]).substring(0, 100)}`;
|
|
620
|
+
case 'evaluateAll':
|
|
621
|
+
return `Evaluate all on locator "${locatorDescription}": ${typeof args[0] === 'function' ? 'function' : String(args[0]).substring(0, 100)}`;
|
|
382
622
|
default:
|
|
383
623
|
return `Locator ${action}: ${JSON.stringify(args[0])}`;
|
|
384
624
|
}
|
|
@@ -392,6 +632,7 @@ export const withBrowserSession = async <T>(
|
|
|
392
632
|
proxyConfig,
|
|
393
633
|
toolName,
|
|
394
634
|
persistSession = true,
|
|
635
|
+
checkBreakpoints = false,
|
|
395
636
|
}: {
|
|
396
637
|
sessionId: string;
|
|
397
638
|
browserTaskMode: BrowserTaskMode;
|
|
@@ -399,9 +640,11 @@ export const withBrowserSession = async <T>(
|
|
|
399
640
|
proxyConfig?: ProxyConfig;
|
|
400
641
|
toolName: string;
|
|
401
642
|
persistSession?: boolean;
|
|
643
|
+
/** Enable breakpoint checking - only works in dev environment with socket connection */
|
|
644
|
+
checkBreakpoints?: boolean;
|
|
402
645
|
},
|
|
403
646
|
callback: (page: Page) => Promise<T>,
|
|
404
|
-
): Promise<
|
|
647
|
+
): Promise<BrowserSessionResult<T>> => {
|
|
405
648
|
const isLocalBrowser = browserTaskMode === BrowserTaskMode.LOCAL;
|
|
406
649
|
|
|
407
650
|
// For local browsers: generate browserRunId locally
|
|
@@ -468,7 +711,24 @@ export const withBrowserSession = async <T>(
|
|
|
468
711
|
// Initialize screenshot and log capture if enabled
|
|
469
712
|
const screenshotCapture = new ScreenshotCapture(sessionId, toolCallId);
|
|
470
713
|
const logsCapture = new LogsCapture(sessionId, toolCallId);
|
|
471
|
-
|
|
714
|
+
|
|
715
|
+
// Create shared step counter for consistent step numbering across logs and screenshots
|
|
716
|
+
let stepCounter = 0;
|
|
717
|
+
const getNextStepNumber = (): number => {
|
|
718
|
+
return stepCounter++;
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
// Create breakpoint context for checking during execution
|
|
722
|
+
// Breakpoints are checked by querying the backend via socket (only in dev environment)
|
|
723
|
+
const breakpointContext: BreakpointContext = {
|
|
724
|
+
toolCallId,
|
|
725
|
+
toolName,
|
|
726
|
+
sessionId,
|
|
727
|
+
cdpUrl,
|
|
728
|
+
checkBreakpoints: checkBreakpoints && isConnected(), // Only check if enabled and connected
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
page = createProxyPage(page, screenshotCapture, logsCapture, breakpointContext, getNextStepNumber);
|
|
472
732
|
|
|
473
733
|
// Load session data (cookies + localStorage) once at startup (if persistence is enabled)
|
|
474
734
|
if (persistSession) {
|
|
@@ -552,6 +812,8 @@ export const withBrowserSession = async <T>(
|
|
|
552
812
|
}
|
|
553
813
|
|
|
554
814
|
const startTime = Date.now();
|
|
815
|
+
let breakpointHitResult: BreakpointHitResult | null = null;
|
|
816
|
+
|
|
555
817
|
try {
|
|
556
818
|
const result = await callback(page);
|
|
557
819
|
// Finalize logs upload (wait for any pending uploads)
|
|
@@ -560,6 +822,24 @@ export const withBrowserSession = async <T>(
|
|
|
560
822
|
}
|
|
561
823
|
return { result };
|
|
562
824
|
} catch (e) {
|
|
825
|
+
// Check if this is a breakpoint hit - handle gracefully instead of throwing
|
|
826
|
+
if (e instanceof RpaBreakpointHitError) {
|
|
827
|
+
logger.info({
|
|
828
|
+
msg: 'RPA breakpoint hit - returning gracefully to stop execution',
|
|
829
|
+
toolCallId: e.breakpointInfo.toolCallId,
|
|
830
|
+
stepNumber: e.breakpointInfo.stepNumber,
|
|
831
|
+
});
|
|
832
|
+
breakpointHitResult = e.breakpointInfo;
|
|
833
|
+
// Finalize logs upload before returning
|
|
834
|
+
await logsCapture.finalize().catch(() => { });
|
|
835
|
+
// Return with breakpoint hit info - result is null since we stopped early
|
|
836
|
+
return {
|
|
837
|
+
result: null as unknown as T,
|
|
838
|
+
breakpointHit: true,
|
|
839
|
+
breakpointInfo: breakpointHitResult,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
563
843
|
const errorMessage = e instanceof Error ? e.message : JSON.stringify(e);
|
|
564
844
|
await logsCapture.addLog(`Error: ${errorMessage}`);
|
|
565
845
|
// Finalize logs upload (wait for any pending uploads)
|
|
@@ -612,54 +892,56 @@ export const withBrowserSession = async <T>(
|
|
|
612
892
|
const endTime = Date.now();
|
|
613
893
|
logger.info({ msg: 'Browser session execution time', toolName, toolCallId, sessionId, executionTimeMs: endTime - startTime });
|
|
614
894
|
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
895
|
+
// If breakpoint was hit, don't destroy the session - keep it alive for Copilot
|
|
896
|
+
if (breakpointHitResult) {
|
|
897
|
+
logger.info({
|
|
898
|
+
msg: 'Breakpoint hit - keeping browser session alive for Copilot debugging',
|
|
899
|
+
sessionId,
|
|
900
|
+
cdpUrl: breakpointHitResult.cdpUrl,
|
|
901
|
+
});
|
|
902
|
+
// Skip cleanup - don't run any of the code below
|
|
903
|
+
} else {
|
|
904
|
+
if (logsCapture) {
|
|
905
|
+
try {
|
|
906
|
+
await logsCapture.finalize();
|
|
907
|
+
logger.info({ msg: 'Final logs uploaded', sessionId });
|
|
908
|
+
} catch (error) {
|
|
909
|
+
logger.warn({ msg: 'Failed to upload final logs', sessionId, error });
|
|
910
|
+
}
|
|
629
911
|
}
|
|
630
|
-
}
|
|
631
912
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
913
|
+
// Save cookies and localStorage before destroying session (if persistence is enabled)
|
|
914
|
+
if (persistSession) {
|
|
915
|
+
try {
|
|
916
|
+
await saveSessionDataViaSocket(context, page, sessionId);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
logger.warn({ message: 'Failed to save session data', sessionId, error });
|
|
919
|
+
}
|
|
638
920
|
}
|
|
639
|
-
}
|
|
640
921
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
browserRunId,
|
|
647
|
-
success: true,
|
|
648
|
-
}).catch(() => {});
|
|
649
|
-
|
|
650
|
-
// For local browsers: send completion event
|
|
651
|
-
// For managed browsers: backend handles tracking via destroyBrowserSession
|
|
652
|
-
if (isLocalBrowser) {
|
|
653
|
-
await sendBrowserRunEvent({
|
|
654
|
-
eventType: 'completed',
|
|
922
|
+
// Destroy browser session (pass analytics fields for backend to track managed browsers)
|
|
923
|
+
destroyBrowserSession({
|
|
924
|
+
sessionId,
|
|
925
|
+
localRun: browserTaskMode === BrowserTaskMode.LOCAL,
|
|
926
|
+
onPrem: browserTaskMode === BrowserTaskMode.ON_PREM,
|
|
655
927
|
browserRunId,
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
928
|
+
success: true,
|
|
929
|
+
}).catch(() => { });
|
|
930
|
+
|
|
931
|
+
// For local browsers: send completion event
|
|
932
|
+
// For managed browsers: backend handles tracking via destroyBrowserSession
|
|
933
|
+
if (isLocalBrowser) {
|
|
934
|
+
await sendBrowserRunEvent({
|
|
935
|
+
eventType: 'completed',
|
|
936
|
+
browserRunId,
|
|
937
|
+
browserSessionId: sessionId,
|
|
938
|
+
toolCallId,
|
|
939
|
+
toolName,
|
|
940
|
+
proxyConfig,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
661
943
|
}
|
|
662
|
-
}
|
|
944
|
+
};
|
|
663
945
|
};
|
|
664
946
|
|
|
665
|
-
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
947
|
+
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|