illuma-agents 1.0.20 → 1.0.22
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/cjs/graphs/Graph.cjs +3 -3
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +18 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/run.cjs +137 -3
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/BrowserInterruptTools.cjs +431 -0
- package/dist/cjs/tools/BrowserInterruptTools.cjs.map +1 -0
- package/dist/cjs/tools/BrowserTools.cjs +15 -10
- package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +3 -3
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/run.mjs +136 -4
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/BrowserInterruptTools.mjs +415 -0
- package/dist/esm/tools/BrowserInterruptTools.mjs.map +1 -0
- package/dist/esm/tools/BrowserTools.mjs +15 -10
- package/dist/esm/tools/BrowserTools.mjs.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/run.d.ts +47 -0
- package/dist/types/tools/BrowserInterruptTools.d.ts +282 -0
- package/dist/types/tools/BrowserTools.d.ts +2 -2
- package/dist/types/types/run.d.ts +8 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +3 -3
- package/src/index.ts +1 -0
- package/src/run.ts +176 -3
- package/src/specs/browser-interrupt-tools.test.ts +235 -0
- package/src/tools/BrowserInterruptTools.ts +571 -0
- package/src/tools/BrowserTools.test.ts +41 -6
- package/src/tools/BrowserTools.ts +15 -10
- package/src/types/run.ts +8 -0
package/src/run.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { CallbackHandler } from '@langfuse/langchain';
|
|
|
4
4
|
import { PromptTemplate } from '@langchain/core/prompts';
|
|
5
5
|
import { RunnableLambda } from '@langchain/core/runnables';
|
|
6
6
|
import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
|
|
7
|
+
import { MemorySaver } from '@langchain/langgraph-checkpoint';
|
|
8
|
+
import { Command, INTERRUPT, isInterrupted } from '@langchain/langgraph';
|
|
7
9
|
import type {
|
|
8
10
|
MessageContentComplex,
|
|
9
11
|
BaseMessage,
|
|
@@ -22,6 +24,7 @@ import { StandardGraph } from '@/graphs/Graph';
|
|
|
22
24
|
import { HandlerRegistry } from '@/events';
|
|
23
25
|
import { isOpenAILike } from '@/utils/llm';
|
|
24
26
|
import { isPresent } from '@/utils/misc';
|
|
27
|
+
import type { BrowserInterrupt, BrowserActionResult } from '@/tools/BrowserInterruptTools';
|
|
25
28
|
|
|
26
29
|
export const defaultOmitOptions = new Set([
|
|
27
30
|
'stream',
|
|
@@ -36,11 +39,35 @@ export const defaultOmitOptions = new Set([
|
|
|
36
39
|
'additionalModelRequestFields',
|
|
37
40
|
]);
|
|
38
41
|
|
|
42
|
+
/** Global checkpointer store for browser mode (keyed by runId) */
|
|
43
|
+
const browserCheckpointers = new Map<string, MemorySaver>();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get or create a checkpointer for browser mode
|
|
47
|
+
*/
|
|
48
|
+
export function getBrowserCheckpointer(runId: string): MemorySaver {
|
|
49
|
+
let checkpointer = browserCheckpointers.get(runId);
|
|
50
|
+
if (!checkpointer) {
|
|
51
|
+
checkpointer = new MemorySaver();
|
|
52
|
+
browserCheckpointers.set(runId, checkpointer);
|
|
53
|
+
}
|
|
54
|
+
return checkpointer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Clean up a browser checkpointer when done
|
|
59
|
+
*/
|
|
60
|
+
export function cleanupBrowserCheckpointer(runId: string): void {
|
|
61
|
+
browserCheckpointers.delete(runId);
|
|
62
|
+
}
|
|
63
|
+
|
|
39
64
|
export class Run<_T extends t.BaseGraphState> {
|
|
40
65
|
id: string;
|
|
41
66
|
private tokenCounter?: t.TokenCounter;
|
|
42
67
|
private handlerRegistry?: HandlerRegistry;
|
|
43
68
|
private indexTokenCountMap?: Record<string, number>;
|
|
69
|
+
/** Whether this run is in browser extension mode */
|
|
70
|
+
browserMode: boolean = false;
|
|
44
71
|
graphRunnable?: t.CompiledStateWorkflow;
|
|
45
72
|
Graph: StandardGraph | MultiAgentGraph | undefined;
|
|
46
73
|
returnContent: boolean = false;
|
|
@@ -54,6 +81,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
54
81
|
this.id = runId;
|
|
55
82
|
this.tokenCounter = config.tokenCounter;
|
|
56
83
|
this.indexTokenCountMap = config.indexTokenCountMap;
|
|
84
|
+
this.browserMode = config.browserMode ?? false;
|
|
57
85
|
|
|
58
86
|
const handlerRegistry = new HandlerRegistry();
|
|
59
87
|
|
|
@@ -73,16 +101,24 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
73
101
|
|
|
74
102
|
/** Handle different graph types */
|
|
75
103
|
if (config.graphConfig.type === 'multi-agent') {
|
|
104
|
+
// For multi-agent, browser mode checkpointer support not yet implemented
|
|
76
105
|
this.graphRunnable = this.createMultiAgentGraph(config.graphConfig);
|
|
77
106
|
if (this.Graph) {
|
|
78
107
|
this.Graph.handlerRegistry = handlerRegistry;
|
|
79
108
|
}
|
|
80
109
|
} else {
|
|
81
110
|
/** Default to legacy graph for 'standard' or undefined type */
|
|
82
|
-
|
|
111
|
+
// In browser mode, inject checkpointer into compileOptions BEFORE creating the graph
|
|
112
|
+
const graphConfig = { ...config.graphConfig };
|
|
113
|
+
if (this.browserMode) {
|
|
114
|
+
const checkpointer = getBrowserCheckpointer(runId);
|
|
115
|
+
graphConfig.compileOptions = {
|
|
116
|
+
...graphConfig.compileOptions,
|
|
117
|
+
checkpointer,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
this.graphRunnable = this.createLegacyGraph(graphConfig);
|
|
83
121
|
if (this.Graph) {
|
|
84
|
-
this.Graph.compileOptions =
|
|
85
|
-
config.graphConfig.compileOptions ?? this.Graph.compileOptions;
|
|
86
122
|
this.Graph.handlerRegistry = handlerRegistry;
|
|
87
123
|
}
|
|
88
124
|
}
|
|
@@ -453,4 +489,141 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
453
489
|
);
|
|
454
490
|
}
|
|
455
491
|
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Process stream with browser interrupt support.
|
|
495
|
+
* Uses regular stream() instead of streamEvents() to properly detect interrupts.
|
|
496
|
+
* Returns interrupt data when graph is paused waiting for browser action.
|
|
497
|
+
*/
|
|
498
|
+
async *processBrowserStream(
|
|
499
|
+
inputs: t.IState,
|
|
500
|
+
config: Partial<RunnableConfig> & { version: 'v1' | 'v2'; run_id?: string },
|
|
501
|
+
streamOptions?: t.EventStreamOptions
|
|
502
|
+
): AsyncGenerator<
|
|
503
|
+
| { type: 'event'; data: unknown }
|
|
504
|
+
| { type: 'interrupt'; data: BrowserInterrupt }
|
|
505
|
+
| { type: 'done'; data?: MessageContentComplex[] }
|
|
506
|
+
> {
|
|
507
|
+
if (this.graphRunnable == null) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
'Run not initialized. Make sure to use Run.create() to instantiate the Run.'
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
if (!this.Graph) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
'Graph not initialized. Make sure to use Run.create() to instantiate the Run.'
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
this.Graph.resetValues(streamOptions?.keepContent);
|
|
519
|
+
|
|
520
|
+
if (!this.id) {
|
|
521
|
+
throw new Error('Run ID not provided');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
config.run_id = this.id;
|
|
525
|
+
// Set up thread_id for checkpointing (required for interrupt/resume)
|
|
526
|
+
config.configurable = Object.assign(config.configurable ?? {}, {
|
|
527
|
+
run_id: this.id,
|
|
528
|
+
thread_id: this.id, // Use run ID as thread ID for browser sessions
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Use the values stream mode to detect interrupts
|
|
532
|
+
const stream = await this.graphRunnable.stream(inputs, {
|
|
533
|
+
...config,
|
|
534
|
+
streamMode: ['values', 'updates'],
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
for await (const chunk of stream) {
|
|
538
|
+
// Check if this chunk contains an interrupt
|
|
539
|
+
if (isInterrupted(chunk)) {
|
|
540
|
+
const interrupts = chunk[INTERRUPT] as Array<{ value: BrowserInterrupt }>;
|
|
541
|
+
if (interrupts.length > 0) {
|
|
542
|
+
// Emit the interrupt data to the client
|
|
543
|
+
for (const interrupt of interrupts) {
|
|
544
|
+
if (interrupt.value?.type === 'browser_interrupt') {
|
|
545
|
+
yield { type: 'interrupt', data: interrupt.value };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Stop yielding - graph is paused
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Emit regular events
|
|
554
|
+
yield { type: 'event', data: chunk };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Stream completed without interrupt
|
|
558
|
+
if (this.returnContent) {
|
|
559
|
+
yield { type: 'done', data: this.Graph.getContentParts() };
|
|
560
|
+
} else {
|
|
561
|
+
yield { type: 'done' };
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Resume a browser stream after interrupt.
|
|
567
|
+
* Call this with the result from the browser extension.
|
|
568
|
+
*/
|
|
569
|
+
async *resumeBrowserStream(
|
|
570
|
+
result: BrowserActionResult,
|
|
571
|
+
config: Partial<RunnableConfig> & { version: 'v1' | 'v2'; run_id?: string }
|
|
572
|
+
): AsyncGenerator<
|
|
573
|
+
| { type: 'event'; data: unknown }
|
|
574
|
+
| { type: 'interrupt'; data: BrowserInterrupt }
|
|
575
|
+
| { type: 'done'; data?: MessageContentComplex[] }
|
|
576
|
+
> {
|
|
577
|
+
if (this.graphRunnable == null) {
|
|
578
|
+
throw new Error(
|
|
579
|
+
'Run not initialized. Make sure to use Run.create() to instantiate the Run.'
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
if (!this.Graph) {
|
|
583
|
+
throw new Error(
|
|
584
|
+
'Graph not initialized. Make sure to use Run.create() to instantiate the Run.'
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (!this.id) {
|
|
589
|
+
throw new Error('Run ID not provided');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
config.run_id = this.id;
|
|
593
|
+
config.configurable = Object.assign(config.configurable ?? {}, {
|
|
594
|
+
run_id: this.id,
|
|
595
|
+
thread_id: this.id,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Use Command to resume with the browser result
|
|
599
|
+
const resumeCommand = new Command({ resume: result });
|
|
600
|
+
|
|
601
|
+
const stream = await this.graphRunnable.stream(resumeCommand, {
|
|
602
|
+
...config,
|
|
603
|
+
streamMode: ['values', 'updates'],
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
for await (const chunk of stream) {
|
|
607
|
+
// Check if this chunk contains another interrupt
|
|
608
|
+
if (isInterrupted(chunk)) {
|
|
609
|
+
const interrupts = chunk[INTERRUPT] as Array<{ value: BrowserInterrupt }>;
|
|
610
|
+
if (interrupts.length > 0) {
|
|
611
|
+
for (const interrupt of interrupts) {
|
|
612
|
+
if (interrupt.value?.type === 'browser_interrupt') {
|
|
613
|
+
yield { type: 'interrupt', data: interrupt.value };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
yield { type: 'event', data: chunk };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (this.returnContent) {
|
|
624
|
+
yield { type: 'done', data: this.Graph.getContentParts() };
|
|
625
|
+
} else {
|
|
626
|
+
yield { type: 'done' };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
456
629
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Browser Interrupt Tools
|
|
3
|
+
*
|
|
4
|
+
* These tests verify:
|
|
5
|
+
* 1. Tool creation and schema validation
|
|
6
|
+
* 2. Interrupt type exports and guards
|
|
7
|
+
* 3. Tool naming consistency
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import {
|
|
12
|
+
createBrowserInterruptTools,
|
|
13
|
+
createBrowserNavigateInterruptTool,
|
|
14
|
+
createBrowserClickInterruptTool,
|
|
15
|
+
createBrowserTypeInterruptTool,
|
|
16
|
+
createBrowserGetPageStateInterruptTool,
|
|
17
|
+
createBrowserScrollInterruptTool,
|
|
18
|
+
createBrowserExtractInterruptTool,
|
|
19
|
+
createBrowserHoverInterruptTool,
|
|
20
|
+
createBrowserWaitInterruptTool,
|
|
21
|
+
createBrowserGoBackInterruptTool,
|
|
22
|
+
createBrowserScreenshotInterruptTool,
|
|
23
|
+
isBrowserInterrupt,
|
|
24
|
+
isBrowserInterruptToolCall,
|
|
25
|
+
BROWSER_INTERRUPT_TOOL_NAMES,
|
|
26
|
+
EBrowserInterruptTools,
|
|
27
|
+
type BrowserInterrupt,
|
|
28
|
+
type BrowserActionResult,
|
|
29
|
+
} from '../tools/BrowserInterruptTools';
|
|
30
|
+
|
|
31
|
+
describe('BrowserInterruptTools', () => {
|
|
32
|
+
describe('createBrowserInterruptTools', () => {
|
|
33
|
+
it('should create all 10 browser tools', () => {
|
|
34
|
+
const tools = createBrowserInterruptTools();
|
|
35
|
+
expect(tools).toHaveLength(10);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should have unique tool names', () => {
|
|
39
|
+
const tools = createBrowserInterruptTools();
|
|
40
|
+
const names = tools.map(t => t.name);
|
|
41
|
+
const uniqueNames = new Set(names);
|
|
42
|
+
expect(uniqueNames.size).toBe(names.length);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should match BROWSER_INTERRUPT_TOOL_NAMES', () => {
|
|
46
|
+
const tools = createBrowserInterruptTools();
|
|
47
|
+
const names = tools.map(t => t.name);
|
|
48
|
+
expect(names.sort()).toEqual([...BROWSER_INTERRUPT_TOOL_NAMES].sort());
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('individual tool creation', () => {
|
|
53
|
+
it('should create navigate tool with correct schema', () => {
|
|
54
|
+
const tool = createBrowserNavigateInterruptTool();
|
|
55
|
+
expect(tool.name).toBe('browser_navigate');
|
|
56
|
+
expect(tool.schema).toBeDefined();
|
|
57
|
+
|
|
58
|
+
// Verify schema accepts valid input
|
|
59
|
+
const schema = tool.schema as z.ZodObject<any>;
|
|
60
|
+
const result = schema.safeParse({ url: 'https://example.com' });
|
|
61
|
+
expect(result.success).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should create click tool with correct schema', () => {
|
|
65
|
+
const tool = createBrowserClickInterruptTool();
|
|
66
|
+
expect(tool.name).toBe('browser_click');
|
|
67
|
+
|
|
68
|
+
const schema = tool.schema as z.ZodObject<any>;
|
|
69
|
+
// Click by index
|
|
70
|
+
expect(schema.safeParse({ index: 5 }).success).toBe(true);
|
|
71
|
+
// Click by coordinates
|
|
72
|
+
expect(schema.safeParse({ coordinates: { x: 100, y: 200 } }).success).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should create type tool with correct schema', () => {
|
|
76
|
+
const tool = createBrowserTypeInterruptTool();
|
|
77
|
+
expect(tool.name).toBe('browser_type');
|
|
78
|
+
|
|
79
|
+
const schema = tool.schema as z.ZodObject<any>;
|
|
80
|
+
expect(schema.safeParse({ index: 3, text: 'hello world' }).success).toBe(true);
|
|
81
|
+
expect(schema.safeParse({ index: 3, text: 'search', pressEnter: true }).success).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should create get_page_state tool with correct schema', () => {
|
|
85
|
+
const tool = createBrowserGetPageStateInterruptTool();
|
|
86
|
+
expect(tool.name).toBe('browser_get_page_state');
|
|
87
|
+
|
|
88
|
+
const schema = tool.schema as z.ZodObject<any>;
|
|
89
|
+
expect(schema.safeParse({}).success).toBe(true);
|
|
90
|
+
expect(schema.safeParse({ reason: 'checking elements' }).success).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should create scroll tool with correct schema', () => {
|
|
94
|
+
const tool = createBrowserScrollInterruptTool();
|
|
95
|
+
expect(tool.name).toBe('browser_scroll');
|
|
96
|
+
|
|
97
|
+
const schema = tool.schema as z.ZodObject<any>;
|
|
98
|
+
expect(schema.safeParse({ direction: 'down' }).success).toBe(true);
|
|
99
|
+
expect(schema.safeParse({ direction: 'up', amount: 500 }).success).toBe(true);
|
|
100
|
+
expect(schema.safeParse({ direction: 'invalid' }).success).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should create extract tool with correct schema', () => {
|
|
104
|
+
const tool = createBrowserExtractInterruptTool();
|
|
105
|
+
expect(tool.name).toBe('browser_extract');
|
|
106
|
+
|
|
107
|
+
const schema = tool.schema as z.ZodObject<any>;
|
|
108
|
+
expect(schema.safeParse({}).success).toBe(true);
|
|
109
|
+
expect(schema.safeParse({ query: 'product prices' }).success).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should create hover tool with correct schema', () => {
|
|
113
|
+
const tool = createBrowserHoverInterruptTool();
|
|
114
|
+
expect(tool.name).toBe('browser_hover');
|
|
115
|
+
|
|
116
|
+
const schema = tool.schema as z.ZodObject<any>;
|
|
117
|
+
expect(schema.safeParse({ index: 5 }).success).toBe(true);
|
|
118
|
+
expect(schema.safeParse({}).success).toBe(false); // index is required
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should create wait tool with correct schema', () => {
|
|
122
|
+
const tool = createBrowserWaitInterruptTool();
|
|
123
|
+
expect(tool.name).toBe('browser_wait');
|
|
124
|
+
|
|
125
|
+
const schema = tool.schema as z.ZodObject<any>;
|
|
126
|
+
expect(schema.safeParse({}).success).toBe(true);
|
|
127
|
+
expect(schema.safeParse({ duration: 2000 }).success).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should create back tool with correct schema', () => {
|
|
131
|
+
const tool = createBrowserGoBackInterruptTool();
|
|
132
|
+
expect(tool.name).toBe('browser_back');
|
|
133
|
+
|
|
134
|
+
const schema = tool.schema as z.ZodObject<any>;
|
|
135
|
+
expect(schema.safeParse({}).success).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should create screenshot tool with correct schema', () => {
|
|
139
|
+
const tool = createBrowserScreenshotInterruptTool();
|
|
140
|
+
expect(tool.name).toBe('browser_screenshot');
|
|
141
|
+
|
|
142
|
+
const schema = tool.schema as z.ZodObject<any>;
|
|
143
|
+
expect(schema.safeParse({}).success).toBe(true);
|
|
144
|
+
expect(schema.safeParse({ fullPage: true }).success).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('isBrowserInterrupt', () => {
|
|
149
|
+
it('should return true for valid browser interrupt', () => {
|
|
150
|
+
const interrupt: BrowserInterrupt = {
|
|
151
|
+
type: 'browser_interrupt',
|
|
152
|
+
action: { type: 'navigate', url: 'https://example.com' },
|
|
153
|
+
interruptId: 'test_123',
|
|
154
|
+
};
|
|
155
|
+
expect(isBrowserInterrupt(interrupt)).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should return false for null/undefined', () => {
|
|
159
|
+
expect(isBrowserInterrupt(null)).toBe(false);
|
|
160
|
+
expect(isBrowserInterrupt(undefined)).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should return false for non-object', () => {
|
|
164
|
+
expect(isBrowserInterrupt('string')).toBe(false);
|
|
165
|
+
expect(isBrowserInterrupt(123)).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should return false for wrong type field', () => {
|
|
169
|
+
expect(isBrowserInterrupt({ type: 'other_type' })).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('isBrowserInterruptToolCall', () => {
|
|
174
|
+
it('should return true for valid tool names', () => {
|
|
175
|
+
expect(isBrowserInterruptToolCall('browser_navigate')).toBe(true);
|
|
176
|
+
expect(isBrowserInterruptToolCall('browser_click')).toBe(true);
|
|
177
|
+
expect(isBrowserInterruptToolCall('browser_type')).toBe(true);
|
|
178
|
+
expect(isBrowserInterruptToolCall('browser_get_page_state')).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should return false for invalid tool names', () => {
|
|
182
|
+
expect(isBrowserInterruptToolCall('invalid_tool')).toBe(false);
|
|
183
|
+
expect(isBrowserInterruptToolCall('browser_invalid')).toBe(false);
|
|
184
|
+
expect(isBrowserInterruptToolCall('')).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('EBrowserInterruptTools enum', () => {
|
|
189
|
+
it('should have all expected tool names', () => {
|
|
190
|
+
expect(EBrowserInterruptTools.CLICK).toBe('browser_click');
|
|
191
|
+
expect(EBrowserInterruptTools.TYPE).toBe('browser_type');
|
|
192
|
+
expect(EBrowserInterruptTools.NAVIGATE).toBe('browser_navigate');
|
|
193
|
+
expect(EBrowserInterruptTools.SCROLL).toBe('browser_scroll');
|
|
194
|
+
expect(EBrowserInterruptTools.EXTRACT).toBe('browser_extract');
|
|
195
|
+
expect(EBrowserInterruptTools.HOVER).toBe('browser_hover');
|
|
196
|
+
expect(EBrowserInterruptTools.WAIT).toBe('browser_wait');
|
|
197
|
+
expect(EBrowserInterruptTools.BACK).toBe('browser_back');
|
|
198
|
+
expect(EBrowserInterruptTools.SCREENSHOT).toBe('browser_screenshot');
|
|
199
|
+
expect(EBrowserInterruptTools.GET_PAGE_STATE).toBe('browser_get_page_state');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('BrowserActionResult type', () => {
|
|
204
|
+
it('should accept success result', () => {
|
|
205
|
+
const result: BrowserActionResult = {
|
|
206
|
+
success: true,
|
|
207
|
+
};
|
|
208
|
+
expect(result.success).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should accept result with page state', () => {
|
|
212
|
+
const result: BrowserActionResult = {
|
|
213
|
+
success: true,
|
|
214
|
+
pageState: {
|
|
215
|
+
url: 'https://example.com',
|
|
216
|
+
title: 'Example',
|
|
217
|
+
elementList: '[0] <button>Click me</button>',
|
|
218
|
+
elementCount: 1,
|
|
219
|
+
scrollPosition: 0,
|
|
220
|
+
scrollHeight: 1000,
|
|
221
|
+
viewportHeight: 800,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
expect(result.pageState?.elementCount).toBe(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should accept error result', () => {
|
|
228
|
+
const result: BrowserActionResult = {
|
|
229
|
+
success: false,
|
|
230
|
+
error: 'Element not found',
|
|
231
|
+
};
|
|
232
|
+
expect(result.error).toBe('Element not found');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|