@ddwang/magnitude-core 0.3.1-ddwang.2 → 0.3.1-ddwang.3
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/browserAgent.js +2 -2
- package/dist/agent/errors.d.ts +2 -0
- package/dist/agent/errors.js +1 -1
- package/dist/agent/index.d.ts +16 -3
- package/dist/agent/index.js +150 -79
- package/dist/ai/modelHarness.js +4 -1
- package/dist/ai/multiModelHarness.js +4 -2
- package/dist/common/events.d.ts +3 -0
- package/dist/common/operation.d.ts +53 -2
- package/dist/common/operation.js +136 -8
- package/dist/common/operation.test.js +37 -1
- package/dist/common/retry.js +2 -2
- package/dist/common/util.js +2 -2
- package/dist/connectors/browserConnector.js +2 -2
- package/dist/index.cjs +325 -125
- package/dist/index.d.cts +52 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +324 -124
- package/dist/memory/image.d.ts +1 -1
- package/dist/memory/image.js +3 -2
- package/dist/memory/rendering/renderJsonParts.js +2 -2
- package/dist/memory/rendering/renderXmlParts.d.ts +1 -1
- package/dist/memory/rendering/renderXmlParts.js +3 -3
- package/dist/web/harness.js +27 -39
- package/dist/web/recovery.d.ts +2 -0
- package/dist/web/visualizer/cursor.d.ts +2 -1
- package/dist/web/visualizer/cursor.js +15 -14
- package/dist/web/visualizer/cursor.test.d.ts +1 -0
- package/dist/web/visualizer/cursor.test.js +63 -0
- package/dist/web/visualizer/index.d.ts +2 -0
- package/dist/web/visualizer/index.js +10 -6
- package/package.json +1 -1
package/dist/memory/image.d.ts
CHANGED
package/dist/memory/image.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
// Default import avoids Node 18/22's incomplete CJS named-export detection for Image.
|
|
2
|
+
import baml from '@boundaryml/baml';
|
|
2
3
|
import sharp from 'sharp';
|
|
3
4
|
export class Image {
|
|
4
5
|
/**
|
|
@@ -60,7 +61,7 @@ export class Image {
|
|
|
60
61
|
}
|
|
61
62
|
async toBaml() {
|
|
62
63
|
const { format, base64 } = await this.toJson();
|
|
63
|
-
return
|
|
64
|
+
return baml.Image.fromBase64(`image/${format}`, base64);
|
|
64
65
|
}
|
|
65
66
|
async saveToFile(filepath) {
|
|
66
67
|
await this.img.clone().toFile(filepath);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Image } from "@/memory/image";
|
|
2
|
-
import
|
|
2
|
+
import baml from '@boundaryml/baml';
|
|
3
3
|
async function buildJsonPartsRecursive(data, partsList, indent, currentLevel = 0, isRoot = false) {
|
|
4
4
|
if (data instanceof Image) {
|
|
5
5
|
const bamlImg = await data.toBaml();
|
|
@@ -8,7 +8,7 @@ async function buildJsonPartsRecursive(data, partsList, indent, currentLevel = 0
|
|
|
8
8
|
}
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
|
-
if (data instanceof
|
|
11
|
+
if (data instanceof baml.Image) {
|
|
12
12
|
partsList.push(data);
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Convert observations to form that can be passed as BAML context, rendered as a custom XML-like structure.
|
|
3
3
|
*/
|
|
4
|
-
import { Image as BamlImage } from '@boundaryml/baml';
|
|
4
|
+
import { type Image as BamlImage } from '@boundaryml/baml';
|
|
5
5
|
import { RenderableContent } from '@/memory/observation';
|
|
6
6
|
export type MultiMediaContentPart = BamlImage | string;
|
|
7
7
|
export declare function renderXmlParts(data: RenderableContent): Promise<MultiMediaContentPart[]>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Convert observations to form that can be passed as BAML context, rendered as a custom XML-like structure.
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
4
|
+
import baml from '@boundaryml/baml';
|
|
5
5
|
import { Image } from '@/memory/image';
|
|
6
6
|
//export type BamlRenderable = BamlImage | string;
|
|
7
7
|
// export type BamlRenderable = {
|
|
@@ -17,7 +17,7 @@ async function buildXmlPartsRecursive(data, indentLevel, partsList, isInsideList
|
|
|
17
17
|
}
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
if (data instanceof
|
|
20
|
+
if (data instanceof baml.Image) {
|
|
21
21
|
partsList.push(data);
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
@@ -66,7 +66,7 @@ async function buildXmlPartsRecursive(data, indentLevel, partsList, isInsideList
|
|
|
66
66
|
if (currentStr)
|
|
67
67
|
mergedValueParts.push(currentStr);
|
|
68
68
|
// Apply styling rules
|
|
69
|
-
if (mergedValueParts.length === 1 && mergedValueParts[0] instanceof
|
|
69
|
+
if (mergedValueParts.length === 1 && mergedValueParts[0] instanceof baml.Image) {
|
|
70
70
|
partsList.push(`${indent}<${tagName}>`);
|
|
71
71
|
partsList.push(mergedValueParts[0]); // The BamlImage
|
|
72
72
|
partsList.push(`</${tagName}>`);
|
package/dist/web/harness.js
CHANGED
|
@@ -6,7 +6,7 @@ import { TabManager } from "./tabs";
|
|
|
6
6
|
import { DOMTransformer } from "./transformer";
|
|
7
7
|
import { Image } from '@/memory/image';
|
|
8
8
|
import EventEmitter from "eventemitter3";
|
|
9
|
-
import { checkOperation, drainAll, operationSleep } from '@/common/operation';
|
|
9
|
+
import { checkOperation, drainAll, measureOperation, operationSleep } from '@/common/operation';
|
|
10
10
|
export class WebHarness {
|
|
11
11
|
/**
|
|
12
12
|
* Executes web actions on a page
|
|
@@ -77,47 +77,35 @@ export class WebHarness {
|
|
|
77
77
|
return this.tabs.getActivePage();
|
|
78
78
|
}
|
|
79
79
|
async screenshot(options = {}) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
let buffer;
|
|
86
|
-
const retries = 3;
|
|
87
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
88
|
-
checkOperation();
|
|
89
|
-
try {
|
|
90
|
-
dpr = await this.page.evaluate(() => window.devicePixelRatio);
|
|
91
|
-
checkOperation();
|
|
92
|
-
buffer = await this.page.screenshot({ type: 'png', ...options });
|
|
93
|
-
checkOperation();
|
|
94
|
-
break; // Success! Exit the retry loop
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
80
|
+
return measureOperation('screenshot', async () => {
|
|
81
|
+
let dpr;
|
|
82
|
+
let buffer;
|
|
83
|
+
const retries = 3;
|
|
84
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
97
85
|
checkOperation();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// Irrecoverable, no point in retrying
|
|
105
|
-
throw new Error("Attempted to take screenshot but page, context or browser is closed");
|
|
86
|
+
try {
|
|
87
|
+
dpr = await this.page.evaluate(() => window.devicePixelRatio);
|
|
88
|
+
checkOperation();
|
|
89
|
+
buffer = await this.page.screenshot({ type: 'png', ...options });
|
|
90
|
+
checkOperation();
|
|
91
|
+
break;
|
|
106
92
|
}
|
|
107
|
-
|
|
108
|
-
|
|
93
|
+
catch (err) {
|
|
94
|
+
checkOperation();
|
|
95
|
+
const error = err;
|
|
96
|
+
if (error.message.includes('Target page, context or browser has been closed')) {
|
|
97
|
+
throw new Error("Attempted to take screenshot but page, context or browser is closed");
|
|
98
|
+
}
|
|
99
|
+
if (attempt >= retries) {
|
|
100
|
+
throw new Error(`Unable to capture screenshot after retries, error: ${error.message}`);
|
|
101
|
+
}
|
|
109
102
|
}
|
|
110
103
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// (2) More importantly, clicks happen in the standard resolution space, so need to do this for coordinates to be correct
|
|
117
|
-
// for any agent not using a virtual screen space (e.g. those that aren't Claude)
|
|
118
|
-
const { width, height } = await image.getDimensions();
|
|
119
|
-
const rescaledImage = await image.resize(width / dpr, height / dpr);
|
|
120
|
-
return rescaledImage;
|
|
104
|
+
const image = Image.fromBase64(buffer.toString('base64'));
|
|
105
|
+
// Match browser coordinate space while avoiding high-DPR image tokens.
|
|
106
|
+
const { width, height } = await image.getDimensions();
|
|
107
|
+
return await image.resize(width / dpr, height / dpr);
|
|
108
|
+
});
|
|
121
109
|
}
|
|
122
110
|
// async goto(url: string) {
|
|
123
111
|
// // No need to redraw here anymore, the 'load' event listener handles it
|
|
@@ -395,7 +383,7 @@ export class WebHarness {
|
|
|
395
383
|
}
|
|
396
384
|
async waitForStability(timeout) {
|
|
397
385
|
checkOperation();
|
|
398
|
-
await this.stability.waitForStability(timeout);
|
|
386
|
+
await measureOperation('stability', () => this.stability.waitForStability(timeout));
|
|
399
387
|
checkOperation();
|
|
400
388
|
}
|
|
401
389
|
}
|
package/dist/web/recovery.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Action } from '@/actions/types';
|
|
2
|
+
import type { OperationDiagnostics } from '@/common/operation';
|
|
2
3
|
export type BlockReason = 'rate_limit' | 'subscription' | 'authentication' | 'no_progress';
|
|
3
4
|
export interface BrowserBlock {
|
|
4
5
|
reason: BlockReason;
|
|
@@ -7,6 +8,7 @@ export interface BrowserBlock {
|
|
|
7
8
|
}
|
|
8
9
|
export declare class BrowserBlockedError extends Error {
|
|
9
10
|
readonly block: BrowserBlock;
|
|
11
|
+
readonly operation?: OperationDiagnostics;
|
|
10
12
|
constructor(block: BrowserBlock);
|
|
11
13
|
}
|
|
12
14
|
export interface HttpDiagnostic {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Page } from "playwright";
|
|
2
2
|
export declare class CursorVisual {
|
|
3
|
+
private animate;
|
|
3
4
|
/**
|
|
4
5
|
* Manages the visual indicator for actions on a page
|
|
5
6
|
*/
|
|
6
7
|
private page;
|
|
7
8
|
private visualElementId;
|
|
8
9
|
private lastPosition;
|
|
9
|
-
constructor();
|
|
10
|
+
constructor(animate?: boolean);
|
|
10
11
|
setActivePage(page: Page): Promise<void>;
|
|
11
12
|
move(x: number, y: number): Promise<void>;
|
|
12
13
|
setupOnPage(): Promise<void>;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { retryOnErrorIsSuccess } from "@/common";
|
|
2
2
|
import logger from "@/logger";
|
|
3
|
-
import { operationSleep } from '@/common/operation';
|
|
3
|
+
import { measureOperation, operationSleep } from '@/common/operation';
|
|
4
4
|
export class CursorVisual {
|
|
5
|
+
animate;
|
|
5
6
|
/**
|
|
6
7
|
* Manages the visual indicator for actions on a page
|
|
7
8
|
*/
|
|
8
9
|
page;
|
|
9
10
|
visualElementId = 'action-visual-indicator';
|
|
10
11
|
lastPosition = null;
|
|
11
|
-
constructor() {
|
|
12
|
-
|
|
12
|
+
constructor(animate = true) {
|
|
13
|
+
this.animate = animate;
|
|
13
14
|
}
|
|
14
15
|
async setActivePage(page) {
|
|
15
16
|
this.page = page;
|
|
@@ -19,13 +20,12 @@ export class CursorVisual {
|
|
|
19
20
|
await retryOnErrorIsSuccess(this.setupOnPage.bind(this), { mode: 'retry_all', delayMs: 200, retryLimit: 5 });
|
|
20
21
|
}
|
|
21
22
|
async move(x, y) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
await operationSleep(300);
|
|
23
|
+
await measureOperation('cursor', async () => {
|
|
24
|
+
this.lastPosition = { x, y };
|
|
25
|
+
await this._drawVisual(x, y, false);
|
|
26
|
+
if (this.animate)
|
|
27
|
+
await operationSleep(300);
|
|
28
|
+
});
|
|
29
29
|
}
|
|
30
30
|
async setupOnPage() {
|
|
31
31
|
if (this.lastPosition) {
|
|
@@ -36,7 +36,7 @@ export class CursorVisual {
|
|
|
36
36
|
// Internal method to handle the actual drawing logic
|
|
37
37
|
async _drawVisual(x, y, showClickEffect) {
|
|
38
38
|
try {
|
|
39
|
-
await this.page.evaluate(({ x, y, id, showClickEffect }) => {
|
|
39
|
+
await this.page.evaluate(({ x, y, id, showClickEffect, animate }) => {
|
|
40
40
|
// Use viewport coordinates directly (no scroll adjustment for fixed positioning)
|
|
41
41
|
const viewportX = x;
|
|
42
42
|
const viewportY = y;
|
|
@@ -83,8 +83,6 @@ export class CursorVisual {
|
|
|
83
83
|
pointerElement.style.height = '32px';
|
|
84
84
|
pointerElement.style.zIndex = '2147483647'; // Max z-index
|
|
85
85
|
pointerElement.style.pointerEvents = 'none'; // Don't interfere with actual clicks
|
|
86
|
-
// Notice that transition is 300ms
|
|
87
|
-
pointerElement.style.transition = 'left 0.3s cubic-bezier(0.25, 0.1, 0.25, 1), top 0.3s cubic-bezier(0.25, 0.1, 0.25, 1)';
|
|
88
86
|
// Create SVG using DOM methods to avoid Trusted Types issues
|
|
89
87
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
90
88
|
svg.setAttribute('width', '32');
|
|
@@ -109,13 +107,16 @@ export class CursorVisual {
|
|
|
109
107
|
document.body.appendChild(pointerElement);
|
|
110
108
|
}
|
|
111
109
|
//pointerElement.style.display = 'none';
|
|
110
|
+
pointerElement.style.transition = animate
|
|
111
|
+
? 'left 0.3s cubic-bezier(0.25, 0.1, 0.25, 1), top 0.3s cubic-bezier(0.25, 0.1, 0.25, 1)'
|
|
112
|
+
: 'none';
|
|
112
113
|
// Update position - use viewport coordinates for fixed positioning
|
|
113
114
|
// Set the top-left corner to (viewportX, viewportY) and then translate by (-1px, -3px)
|
|
114
115
|
// to align the pointer tip (approx. at 1.27, 4.17 within the SVG) with the click point.
|
|
115
116
|
pointerElement.style.left = `${viewportX}px`;
|
|
116
117
|
pointerElement.style.top = `${viewportY}px`;
|
|
117
118
|
pointerElement.style.transform = 'translate(-1px, -3px)';
|
|
118
|
-
}, { x, y, id: this.visualElementId, showClickEffect });
|
|
119
|
+
}, { x, y, id: this.visualElementId, showClickEffect, animate: this.animate });
|
|
119
120
|
}
|
|
120
121
|
catch (error) {
|
|
121
122
|
// For example when:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
import { ActionVisualizer } from './index';
|
|
3
|
+
import { CursorVisual } from './cursor';
|
|
4
|
+
import { Operation } from '@/common/operation';
|
|
5
|
+
import { OperationCancelledError } from '@/agent/errors';
|
|
6
|
+
function pageFixture() {
|
|
7
|
+
const evaluations = [];
|
|
8
|
+
const page = {
|
|
9
|
+
on: () => { },
|
|
10
|
+
evaluate: async (_fn, arg) => { evaluations.push(arg); },
|
|
11
|
+
};
|
|
12
|
+
return { page, evaluations };
|
|
13
|
+
}
|
|
14
|
+
test('hidden cursor does not draw, register cursor timings, or wait', async () => {
|
|
15
|
+
const { page, evaluations } = pageFixture();
|
|
16
|
+
const visualizer = new ActionVisualizer({}, { showCursor: false });
|
|
17
|
+
await visualizer.setActivePage(page);
|
|
18
|
+
evaluations.length = 0;
|
|
19
|
+
const operation = new Operation({}, {});
|
|
20
|
+
await operation.run(async () => {
|
|
21
|
+
await visualizer.moveVirtualCursor(12, 34);
|
|
22
|
+
await visualizer.hideAll();
|
|
23
|
+
await visualizer.showAll();
|
|
24
|
+
});
|
|
25
|
+
operation.finish();
|
|
26
|
+
expect(evaluations).toEqual([]);
|
|
27
|
+
expect(operation.snapshot().timings.cursor).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
test('animation can be disabled while preserving cursor position and visibility controls', async () => {
|
|
30
|
+
const { page, evaluations } = pageFixture();
|
|
31
|
+
const visualizer = new ActionVisualizer({}, { animateCursor: false });
|
|
32
|
+
await visualizer.setActivePage(page);
|
|
33
|
+
evaluations.length = 0;
|
|
34
|
+
const operation = new Operation({}, {});
|
|
35
|
+
// No timer is needed: the move finishes before the next timers phase.
|
|
36
|
+
const nextTimer = new Promise(resolve => setTimeout(() => resolve('timer'), 0));
|
|
37
|
+
const move = operation.run(() => visualizer.moveVirtualCursor(12, 34)).then(() => 'move');
|
|
38
|
+
expect(await Promise.race([move, nextTimer])).toBe('move');
|
|
39
|
+
await visualizer.hideAll();
|
|
40
|
+
await visualizer.showAll();
|
|
41
|
+
operation.finish();
|
|
42
|
+
expect(evaluations).toEqual([
|
|
43
|
+
{ x: 12, y: 34, id: 'action-visual-indicator', showClickEffect: false, animate: false },
|
|
44
|
+
'action-visual-indicator', 'action-visual-indicator',
|
|
45
|
+
]);
|
|
46
|
+
expect(operation.snapshot().timings.cursor?.count).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
test('default animation still waits and its wait is cancellable', async () => {
|
|
49
|
+
const { page, evaluations } = pageFixture();
|
|
50
|
+
const cursor = new CursorVisual();
|
|
51
|
+
await cursor.setActivePage(page);
|
|
52
|
+
const operation = new Operation({}, {});
|
|
53
|
+
const move = operation.run(() => cursor.move(12, 34));
|
|
54
|
+
// Drawing is synchronous in this fixture; allow the animation wait to start.
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
56
|
+
expect(evaluations[0]).toMatchObject({ animate: true });
|
|
57
|
+
let complete = false;
|
|
58
|
+
void move.then(() => { complete = true; }, () => { });
|
|
59
|
+
expect(complete).toBe(false);
|
|
60
|
+
operation.cancel();
|
|
61
|
+
await expect(move).rejects.toBeInstanceOf(OperationCancelledError);
|
|
62
|
+
operation.finish();
|
|
63
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { BrowserContext, Page } from "playwright";
|
|
2
2
|
export interface ActionVisualizerOptions {
|
|
3
3
|
showCursor?: boolean;
|
|
4
|
+
/** Set false to skip the 300 ms movement animation. Defaults to true. */
|
|
5
|
+
animateCursor?: boolean;
|
|
4
6
|
showHoverCircle?: boolean;
|
|
5
7
|
showClickRipple?: boolean;
|
|
6
8
|
showDragLine?: boolean;
|
|
@@ -12,12 +12,13 @@ export class ActionVisualizer {
|
|
|
12
12
|
this.context = context;
|
|
13
13
|
this.options = {
|
|
14
14
|
showCursor: options.showCursor ?? true,
|
|
15
|
+
animateCursor: options.animateCursor ?? true,
|
|
15
16
|
showHoverCircle: options.showHoverCircle ?? false,
|
|
16
17
|
showClickRipple: options.showClickRipple ?? true,
|
|
17
18
|
showDragLine: options.showDragLine ?? false,
|
|
18
19
|
showTypeEffects: options.showTypeEffects ?? true
|
|
19
20
|
};
|
|
20
|
-
this.cursor = new CursorVisual();
|
|
21
|
+
this.cursor = new CursorVisual(this.options.animateCursor);
|
|
21
22
|
this.mouseEffects = new MouseEffectVisual(this.options);
|
|
22
23
|
this.typeEffects = new TypeEffectVisual();
|
|
23
24
|
}
|
|
@@ -30,18 +31,21 @@ export class ActionVisualizer {
|
|
|
30
31
|
}
|
|
31
32
|
async setActivePage(page) {
|
|
32
33
|
this.page = page;
|
|
33
|
-
|
|
34
|
+
if (this.options.showCursor)
|
|
35
|
+
await this.cursor.setActivePage(page);
|
|
34
36
|
await this.mouseEffects.setActivePage(page);
|
|
35
37
|
await this.typeEffects.setActivePage(page);
|
|
36
38
|
}
|
|
37
39
|
async moveVirtualCursor(x, y) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
if (this.options.showCursor)
|
|
41
|
+
await this.cursor.move(x, y);
|
|
40
42
|
}
|
|
41
43
|
async hideAll() {
|
|
42
|
-
|
|
44
|
+
if (this.options.showCursor)
|
|
45
|
+
await this.cursor.hide();
|
|
43
46
|
}
|
|
44
47
|
async showAll() {
|
|
45
|
-
|
|
48
|
+
if (this.options.showCursor)
|
|
49
|
+
await this.cursor.show();
|
|
46
50
|
}
|
|
47
51
|
}
|