@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.
Files changed (44) hide show
  1. package/dist/agent.d.ts.map +1 -1
  2. package/dist/agent.js +6 -1
  3. package/dist/agent.js.map +1 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/nodes/addPromptNode.d.ts.map +1 -1
  8. package/dist/nodes/addPromptNode.js +26 -2
  9. package/dist/nodes/addPromptNode.js.map +1 -1
  10. package/dist/nodes/addToolRunNode.d.ts.map +1 -1
  11. package/dist/nodes/addToolRunNode.js +40 -5
  12. package/dist/nodes/addToolRunNode.js.map +1 -1
  13. package/dist/platform/config.d.ts +2 -0
  14. package/dist/platform/config.d.ts.map +1 -1
  15. package/dist/platform/config.js +8 -0
  16. package/dist/platform/config.js.map +1 -1
  17. package/dist/platform/mindedConnection.d.ts.map +1 -1
  18. package/dist/platform/mindedConnection.js +2 -1
  19. package/dist/platform/mindedConnection.js.map +1 -1
  20. package/dist/platform/mindedConnectionTypes.d.ts +33 -0
  21. package/dist/platform/mindedConnectionTypes.d.ts.map +1 -1
  22. package/dist/platform/mindedConnectionTypes.js +4 -0
  23. package/dist/platform/mindedConnectionTypes.js.map +1 -1
  24. package/dist/platform/toolExecutor.d.ts.map +1 -1
  25. package/dist/platform/toolExecutor.js +21 -1
  26. package/dist/platform/toolExecutor.js.map +1 -1
  27. package/dist/toolsLibrary/withBrowserSession.d.ts +28 -4
  28. package/dist/toolsLibrary/withBrowserSession.d.ts.map +1 -1
  29. package/dist/toolsLibrary/withBrowserSession.js +253 -61
  30. package/dist/toolsLibrary/withBrowserSession.js.map +1 -1
  31. package/dist/types/Flows.types.d.ts +1 -0
  32. package/dist/types/Flows.types.d.ts.map +1 -1
  33. package/dist/types/Flows.types.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/agent.ts +7 -2
  36. package/src/index.ts +2 -0
  37. package/src/nodes/addPromptNode.ts +28 -2
  38. package/src/nodes/addToolRunNode.ts +49 -7
  39. package/src/platform/config.ts +10 -0
  40. package/src/platform/mindedConnection.ts +2 -1
  41. package/src/platform/mindedConnectionTypes.ts +36 -0
  42. package/src/platform/toolExecutor.ts +24 -1
  43. package/src/toolsLibrary/withBrowserSession.ts +345 -63
  44. 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 { mindedConnectionSocketMessageType, BrowserRunEventRequest, BrowserRunEventType } from '../platform/mindedConnectionTypes';
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: this.stepCounter++,
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 = ['click', 'fill', 'type', 'selectOption', 'check', 'uncheck', 'setInputFiles'];
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 = (page: Page, screenshotCapture: ScreenshotCapture, logsCapture: LogsCapture): Page => {
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 = ['locator'];
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 if available (first argument is usually the selector)
299
- const selector = args[0] && typeof args[0] === 'string' ? args[0] : undefined;
300
- return createProxyLocator(locator, target, screenshotCapture, logsCapture, selector);
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<{ result: T }> => {
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
- page = createProxyPage(page, screenshotCapture, logsCapture);
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
- // Capture final screenshot and upload final logs before session ends
616
- try {
617
- await screenshotCapture.captureScreenshot(page, 'after');
618
- logger.info({ msg: 'Final screenshot captured', sessionId });
619
- } catch (error) {
620
- logger.warn({ msg: 'Failed to capture final screenshot', sessionId, error });
621
- }
622
-
623
- if (logsCapture) {
624
- try {
625
- await logsCapture.finalize();
626
- logger.info({ msg: 'Final logs uploaded', sessionId });
627
- } catch (error) {
628
- logger.warn({ msg: 'Failed to upload final logs', sessionId, error });
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
- // Save cookies and localStorage before destroying session (if persistence is enabled)
633
- if (persistSession) {
634
- try {
635
- await saveSessionDataViaSocket(context, page, sessionId);
636
- } catch (error) {
637
- logger.warn({ message: 'Failed to save session data', sessionId, error });
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
- // Destroy browser session (pass analytics fields for backend to track managed browsers)
642
- destroyBrowserSession({
643
- sessionId,
644
- localRun: browserTaskMode === BrowserTaskMode.LOCAL,
645
- onPrem: browserTaskMode === BrowserTaskMode.ON_PREM,
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
- browserSessionId: sessionId,
657
- toolCallId,
658
- toolName,
659
- proxyConfig,
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));
@@ -310,6 +310,7 @@ export enum ActionInputParamType {
310
310
  }
311
311
 
312
312
  export interface Flow {
313
+ id: string;
313
314
  name: string;
314
315
  nodes: Node[];
315
316
  edges: Edge[];