@loadmill/droid-cua 2.10.0 → 2.10.1-rc.9
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/build/index.js +11 -0
- package/build/src/core/aim-assist.js +1 -1
- package/build/src/core/execution-engine.js +191 -358
- package/build/src/core/prompts.js +1 -1
- package/build/src/core/screenshot-store.js +153 -0
- package/build/src/core/tool-handlers/assert.js +155 -0
- package/build/src/core/tool-handlers/loadmill.js +29 -0
- package/build/src/core/tool-handlers/navigate.js +110 -0
- package/build/src/core/tool-handlers/preview-click.js +112 -0
- package/build/src/core/tool-handlers/test-design-complete.js +93 -0
- package/build/src/core/tool-handlers/utils.js +80 -0
- package/build/src/device/openai.js +101 -8
- package/build/src/modes/design-mode-ink.js +16 -2
- package/build/src/modes/design-mode.js +16 -2
- package/build/src/modes/execution-mode.js +42 -234
- package/build/src/prompts/base.js +20 -21
- package/build/src/prompts/design.js +52 -11
- package/build/src/prompts/editor.js +1 -1
- package/build/src/prompts/execution.js +9 -5
- package/build/src/utils/cua-debug-tracer.js +3 -1
- package/package.json +3 -4
- package/build/src/device/screenshot-resolution.js +0 -33
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for tool handlers. These are pure functions — no engine
|
|
3
|
+
* state. Anything that touches the engine (screenshots, network requests,
|
|
4
|
+
* the session) belongs in the handler files instead.
|
|
5
|
+
*/
|
|
6
|
+
export function parseJsonArguments(argumentsText) {
|
|
7
|
+
try {
|
|
8
|
+
return { ok: true, value: JSON.parse(argumentsText) };
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return { ok: false, value: null };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function parseStringToolArgument(argumentsText, key, toolName) {
|
|
15
|
+
const parsed = parseJsonArguments(argumentsText);
|
|
16
|
+
if (!parsed.ok || typeof parsed.value !== "object" || !parsed.value) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
message: `Invalid ${toolName} arguments: expected a JSON object with a string "${key}" field.`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const value = parsed.value[key];
|
|
23
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
message: `Invalid ${toolName} arguments: "${key}" must be a non-empty string.`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, value: value.trim() };
|
|
30
|
+
}
|
|
31
|
+
export function parseStringArray(value) {
|
|
32
|
+
if (!Array.isArray(value))
|
|
33
|
+
return [];
|
|
34
|
+
return value
|
|
35
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
export function buildFunctionCallOutputItem(callId, output) {
|
|
39
|
+
return {
|
|
40
|
+
type: "function_call_output",
|
|
41
|
+
call_id: callId,
|
|
42
|
+
output,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function buildTextToolOutputItem(callId, text) {
|
|
46
|
+
return buildFunctionCallOutputItem(callId, [{ type: "input_text", text }]);
|
|
47
|
+
}
|
|
48
|
+
export function buildPreviewOutputContent({ text, annotatedScreenshotBase64 = null }) {
|
|
49
|
+
const content = [{ type: "input_text", text }];
|
|
50
|
+
if (annotatedScreenshotBase64) {
|
|
51
|
+
content.push({
|
|
52
|
+
type: "input_image",
|
|
53
|
+
image_url: `data:image/png;base64,${annotatedScreenshotBase64}`,
|
|
54
|
+
detail: "high",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return content;
|
|
58
|
+
}
|
|
59
|
+
export function isDesignContext(context = null) {
|
|
60
|
+
return typeof context?.sessionId === "string" && context.sessionId.trim().length > 0;
|
|
61
|
+
}
|
|
62
|
+
export function buildDebugContext(context = null, stepContext = null) {
|
|
63
|
+
return {
|
|
64
|
+
scope: context?.sessionId ? "design" : "execution",
|
|
65
|
+
runId: context?.runId,
|
|
66
|
+
sessionId: context?.sessionId,
|
|
67
|
+
stepId: stepContext?.stepId,
|
|
68
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function buildNestedToolDebugContext(context, stepContext, toolName, callId) {
|
|
72
|
+
const debugContext = buildDebugContext(context, stepContext);
|
|
73
|
+
const baseChainId = debugContext.scope === "design"
|
|
74
|
+
? debugContext.sessionId || "unknown"
|
|
75
|
+
: debugContext.runId || "unknown";
|
|
76
|
+
return {
|
|
77
|
+
...debugContext,
|
|
78
|
+
chainId: `${debugContext.scope}:${baseChainId}:${toolName}:${callId}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -34,6 +34,99 @@ const previewClickTool = {
|
|
|
34
34
|
additionalProperties: false,
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
|
+
const assertTool = {
|
|
38
|
+
type: "function",
|
|
39
|
+
name: "assert",
|
|
40
|
+
description: "Validate whether a condition is true on the current screen. Use this for instructions like assert, verify, validate, ensure, or confirm. Returns a pass/fail result for the requested condition.",
|
|
41
|
+
strict: true,
|
|
42
|
+
parameters: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
condition: { type: "string" },
|
|
46
|
+
},
|
|
47
|
+
required: ["condition"],
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
const navigateTool = {
|
|
52
|
+
type: "function",
|
|
53
|
+
name: "navigate",
|
|
54
|
+
description: "Navigate the active browser session directly to a target URL or page. Use this instead of trying to click the browser address bar or simulating URL entry.",
|
|
55
|
+
strict: true,
|
|
56
|
+
parameters: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
target: { type: "string" },
|
|
60
|
+
},
|
|
61
|
+
required: ["target"],
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const loadmillTool = {
|
|
66
|
+
type: "function",
|
|
67
|
+
name: "loadmill",
|
|
68
|
+
description: "Run an explicit Loadmill command. Use this only when the instruction explicitly mentions Loadmill and asks for a Loadmill-specific action.",
|
|
69
|
+
strict: true,
|
|
70
|
+
parameters: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
command: { type: "string" },
|
|
74
|
+
},
|
|
75
|
+
required: ["command"],
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const testDesignCompleteTool = {
|
|
80
|
+
type: "function",
|
|
81
|
+
name: "test_design_complete",
|
|
82
|
+
description: "Signal that design exploration is complete and hand off a structured summary for final test generation. Use this only after the requested flow has been successfully demonstrated or validated. After calling this, stop exploring.",
|
|
83
|
+
strict: true,
|
|
84
|
+
parameters: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
suggestedTestName: { type: "string" },
|
|
88
|
+
goalSummary: { type: "string" },
|
|
89
|
+
finalState: { type: "string" },
|
|
90
|
+
importantSteps: {
|
|
91
|
+
type: "array",
|
|
92
|
+
items: { type: "string" }
|
|
93
|
+
},
|
|
94
|
+
verificationPoints: {
|
|
95
|
+
type: "array",
|
|
96
|
+
items: { type: "string" }
|
|
97
|
+
},
|
|
98
|
+
notesForScript: { type: "string" },
|
|
99
|
+
},
|
|
100
|
+
required: [
|
|
101
|
+
"suggestedTestName",
|
|
102
|
+
"goalSummary",
|
|
103
|
+
"finalState",
|
|
104
|
+
"importantSteps",
|
|
105
|
+
"verificationPoints",
|
|
106
|
+
"notesForScript"
|
|
107
|
+
],
|
|
108
|
+
additionalProperties: false,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
export function buildFunctionTools({ aimAssistMode = DEFAULT_AIM_ASSIST_MODE, toolCapabilities = {}, } = {}) {
|
|
112
|
+
const tools = [];
|
|
113
|
+
if (aimAssistMode !== "off" && toolCapabilities.previewClick !== false) {
|
|
114
|
+
tools.push(previewClickTool);
|
|
115
|
+
}
|
|
116
|
+
if (toolCapabilities.assert !== false) {
|
|
117
|
+
tools.push(assertTool);
|
|
118
|
+
}
|
|
119
|
+
if (toolCapabilities.navigate) {
|
|
120
|
+
tools.push(navigateTool);
|
|
121
|
+
}
|
|
122
|
+
if (toolCapabilities.loadmill) {
|
|
123
|
+
tools.push(loadmillTool);
|
|
124
|
+
}
|
|
125
|
+
if (toolCapabilities.testDesignComplete) {
|
|
126
|
+
tools.push(testDesignCompleteTool);
|
|
127
|
+
}
|
|
128
|
+
return tools;
|
|
129
|
+
}
|
|
37
130
|
function appendInitialScreenshot(input, screenshotBase64) {
|
|
38
131
|
// Most callers end with the active user message, but some recovery flows can
|
|
39
132
|
// reset the conversation to system-only state before the next screenshot send.
|
|
@@ -91,7 +184,7 @@ function buildContextManagement(contextOptimizationEnabled, contextOptimizationT
|
|
|
91
184
|
}
|
|
92
185
|
return [{ type: "compaction", compact_threshold: contextOptimizationThreshold }];
|
|
93
186
|
}
|
|
94
|
-
export function buildCuaRequestParams({ cuaModel, aimAssistMode = DEFAULT_AIM_ASSIST_MODE, previousResponseId, input, contextOptimizationEnabled = true, contextOptimizationThreshold = DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD, }) {
|
|
187
|
+
export function buildCuaRequestParams({ cuaModel, aimAssistMode = DEFAULT_AIM_ASSIST_MODE, previousResponseId, input, includeComputerTool = true, contextOptimizationEnabled = true, contextOptimizationThreshold = DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD, toolCapabilities = {}, }) {
|
|
95
188
|
const contextManagement = buildContextManagement(contextOptimizationEnabled, contextOptimizationThreshold);
|
|
96
189
|
// Keep Responses API storage enabled on every turn so execution can chain
|
|
97
190
|
// reliably through previous_response_id across the whole run.
|
|
@@ -103,12 +196,10 @@ export function buildCuaRequestParams({ cuaModel, aimAssistMode = DEFAULT_AIM_AS
|
|
|
103
196
|
input,
|
|
104
197
|
store: true,
|
|
105
198
|
truncation: "auto",
|
|
106
|
-
tools:
|
|
107
|
-
? [{ type: "computer" }]
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
previewClickTool,
|
|
111
|
-
],
|
|
199
|
+
tools: [
|
|
200
|
+
...(includeComputerTool ? [{ type: "computer" }] : []),
|
|
201
|
+
...buildFunctionTools({ aimAssistMode, toolCapabilities }),
|
|
202
|
+
],
|
|
112
203
|
};
|
|
113
204
|
}
|
|
114
205
|
/**
|
|
@@ -144,15 +235,17 @@ export async function reviseTestScript({ client, originalScript, revisionRequest
|
|
|
144
235
|
* debugContext?: unknown,
|
|
145
236
|
* }} options
|
|
146
237
|
*/
|
|
147
|
-
export async function sendCUARequest({ client, cuaModel, aimAssistMode = DEFAULT_AIM_ASSIST_MODE, contextOptimizationEnabled = true, contextOptimizationThreshold = DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD, messages, screenshotBase64, previousResponseId, callId, deviceInfo, debugContext, }) {
|
|
238
|
+
export async function sendCUARequest({ client, cuaModel, aimAssistMode = DEFAULT_AIM_ASSIST_MODE, includeComputerTool = true, contextOptimizationEnabled = true, contextOptimizationThreshold = DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD, toolCapabilities = {}, messages, screenshotBase64, previousResponseId, callId, deviceInfo, debugContext, }) {
|
|
148
239
|
const input = buildCuaInput({ messages, screenshotBase64, previousResponseId, callId });
|
|
149
240
|
const requestParams = buildCuaRequestParams({
|
|
150
241
|
cuaModel,
|
|
151
242
|
aimAssistMode,
|
|
152
243
|
previousResponseId,
|
|
153
244
|
input,
|
|
245
|
+
includeComputerTool,
|
|
154
246
|
contextOptimizationEnabled,
|
|
155
247
|
contextOptimizationThreshold,
|
|
248
|
+
toolCapabilities,
|
|
156
249
|
});
|
|
157
250
|
const trace = cuaDebugTracer.startTurn({
|
|
158
251
|
requestParams,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getScreenshotAsBase64 } from "../device/connection.js";
|
|
2
|
+
import { CAPTURE_SOURCES } from "../core/capture-sources.js";
|
|
2
3
|
import { buildDesignModePrompt, buildDesignRecoveryPrompt } from "../core/prompts.js";
|
|
3
4
|
import { saveTest } from "../test-store/test-manager.js";
|
|
4
5
|
import { logger } from "../utils/logger.js";
|
|
@@ -35,6 +36,14 @@ export class DesignModeInk {
|
|
|
35
36
|
const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {});
|
|
36
37
|
this.baseDesignPrompt = designPrompt;
|
|
37
38
|
this.session.setSystemPrompt(designPrompt);
|
|
39
|
+
this.engine.toolCapabilities = {
|
|
40
|
+
assert: true,
|
|
41
|
+
navigate: this.session?.deviceInfo?.platform === "browser",
|
|
42
|
+
loadmill: false,
|
|
43
|
+
testDesignComplete: true,
|
|
44
|
+
};
|
|
45
|
+
this.engine.baseSystemPrompt = designPrompt;
|
|
46
|
+
this.engine.isHeadlessMode = false;
|
|
38
47
|
// Update UI
|
|
39
48
|
if (this.context.setMode) {
|
|
40
49
|
this.context.setMode('design');
|
|
@@ -182,14 +191,19 @@ export class DesignModeInk {
|
|
|
182
191
|
}
|
|
183
192
|
// Get screenshot and send to model
|
|
184
193
|
const screenshotBase64 = await getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
|
|
194
|
+
const currentScreenshot = await this.engine.recordScreenshot?.(screenshotBase64, {
|
|
195
|
+
sessionId: this.context?.sessionId,
|
|
196
|
+
captureSource: CAPTURE_SOURCES.DESIGN_INPUT,
|
|
197
|
+
});
|
|
185
198
|
const response = await this.sendCUARequest({
|
|
186
199
|
messages: this.session.messages,
|
|
187
200
|
screenshotBase64,
|
|
188
201
|
previousResponseId: this.session.previousResponseId,
|
|
189
202
|
deviceInfo: this.session.deviceInfo,
|
|
203
|
+
toolCapabilities: this.engine.toolCapabilities,
|
|
190
204
|
});
|
|
191
205
|
// Run turn with action tracking and interruption checking
|
|
192
|
-
const newResponseId = await this.engine.runFullTurn(response, (action) => {
|
|
206
|
+
const { responseId: newResponseId } = await this.engine.runFullTurn(response, (action) => {
|
|
193
207
|
// Check if user interrupted - stop immediately
|
|
194
208
|
if (this.inputQueue.length > 0) {
|
|
195
209
|
return true; // Signal to stop execution
|
|
@@ -205,7 +219,7 @@ export class DesignModeInk {
|
|
|
205
219
|
return true; // Stop for guidance
|
|
206
220
|
}
|
|
207
221
|
return false; // Continue execution
|
|
208
|
-
}, this.context);
|
|
222
|
+
}, this.context, null, { currentScreenshot });
|
|
209
223
|
this.session.updateResponseId(newResponseId);
|
|
210
224
|
this.consecutiveErrorCount = 0;
|
|
211
225
|
// Clear agent working status
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import readline from "readline";
|
|
2
2
|
import { getScreenshotAsBase64 } from "../device/connection.js";
|
|
3
|
+
import { CAPTURE_SOURCES } from "../core/capture-sources.js";
|
|
3
4
|
import { buildDesignModePrompt, buildDesignRecoveryPrompt } from "../core/prompts.js";
|
|
4
5
|
import { saveTest } from "../test-store/test-manager.js";
|
|
5
6
|
import { logger } from "../utils/logger.js";
|
|
@@ -32,6 +33,14 @@ export class DesignMode {
|
|
|
32
33
|
const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {});
|
|
33
34
|
this.baseDesignPrompt = designPrompt;
|
|
34
35
|
this.session.setSystemPrompt(designPrompt);
|
|
36
|
+
this.engine.toolCapabilities = {
|
|
37
|
+
assert: true,
|
|
38
|
+
navigate: this.session?.deviceInfo?.platform === "browser",
|
|
39
|
+
loadmill: false,
|
|
40
|
+
testDesignComplete: true,
|
|
41
|
+
};
|
|
42
|
+
this.engine.baseSystemPrompt = designPrompt;
|
|
43
|
+
this.engine.isHeadlessMode = false;
|
|
35
44
|
console.log(`\n=== Design Mode: Creating test "${this.testName}" ===`);
|
|
36
45
|
console.log("Describe what you want to test. The agent will explore autonomously.");
|
|
37
46
|
console.log('When ready, type "generate the script" to create the test.');
|
|
@@ -146,14 +155,19 @@ export class DesignMode {
|
|
|
146
155
|
const keypressHandler = this.setupEscDetection();
|
|
147
156
|
// Get screenshot and send to model
|
|
148
157
|
const screenshotBase64 = await getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
|
|
158
|
+
const currentScreenshot = await this.engine.recordScreenshot?.(screenshotBase64, {
|
|
159
|
+
sessionId: context?.sessionId,
|
|
160
|
+
captureSource: CAPTURE_SOURCES.DESIGN_INPUT,
|
|
161
|
+
});
|
|
149
162
|
const response = await this.sendCUARequest({
|
|
150
163
|
messages: this.session.messages,
|
|
151
164
|
screenshotBase64,
|
|
152
165
|
previousResponseId: this.session.previousResponseId,
|
|
153
166
|
deviceInfo: this.session.deviceInfo,
|
|
167
|
+
toolCapabilities: this.engine.toolCapabilities,
|
|
154
168
|
});
|
|
155
169
|
// Run turn with action tracking and interruption checking
|
|
156
|
-
const newResponseId = await this.engine.runFullTurn(response, (action) => {
|
|
170
|
+
const { responseId: newResponseId } = await this.engine.runFullTurn(response, (action) => {
|
|
157
171
|
// Check if user pressed ESC - stop immediately
|
|
158
172
|
if (this.escPressed) {
|
|
159
173
|
return true; // Signal to stop execution
|
|
@@ -170,7 +184,7 @@ export class DesignMode {
|
|
|
170
184
|
this.session._shouldStopForGuidance = true;
|
|
171
185
|
}
|
|
172
186
|
return false; // Continue execution
|
|
173
|
-
});
|
|
187
|
+
}, null, null, { currentScreenshot });
|
|
174
188
|
this.session.updateResponseId(newResponseId);
|
|
175
189
|
this.consecutiveErrorCount = 0;
|
|
176
190
|
// Cleanup ESC detection
|
|
@@ -1,40 +1,13 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { CAPTURE_SOURCES } from "../core/capture-sources.js";
|
|
3
2
|
import { getScreenshotAsBase64, connectToDevice, getDeviceInfo, getCurrentPlatform } from "../device/connection.js";
|
|
4
|
-
import { connectToBrowser, getBrowserInfo, getCurrentPlatform as getBrowserCurrentPlatform,
|
|
3
|
+
import { connectToBrowser, getBrowserInfo, getCurrentPlatform as getBrowserCurrentPlatform, } from "../browser/connection.js";
|
|
5
4
|
import { normalizeBrowserInfoForSession } from "../browser/normalize.js";
|
|
6
|
-
import { extractNavigateTarget, isNavigateInstruction } from "../browser/navigation.js";
|
|
7
5
|
import { sendCUARequest } from "../device/openai.js";
|
|
8
|
-
import {
|
|
9
|
-
import { isAssertion
|
|
10
|
-
import { isLoadmillInstruction, extractLoadmillCommand, executeLoadmillInstruction, } from "../device/loadmill.js";
|
|
6
|
+
import { buildExecutionRecoveryPrompt } from "../core/prompts.js";
|
|
7
|
+
import { isAssertion } from "../device/assertions.js";
|
|
11
8
|
import { logger } from "../utils/logger.js";
|
|
12
9
|
import { emitDesktopDebug } from "../utils/desktop-debug.js";
|
|
13
10
|
import { printCliOutput } from "../utils/console-output.js";
|
|
14
|
-
export function buildExecutionRequestPayload({ instruction, isAssertionStep, messages, previousResponseId }) {
|
|
15
|
-
return {
|
|
16
|
-
messagesToSend: previousResponseId && !isAssertionStep
|
|
17
|
-
? [{ role: "user", content: instruction }]
|
|
18
|
-
: messages,
|
|
19
|
-
previousResponseIdToSend: previousResponseId,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function screenshotPathFromRecorderResult(result) {
|
|
23
|
-
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
24
|
-
return typeof result.screenshotPath === "string" && result.screenshotPath.trim()
|
|
25
|
-
? result.screenshotPath
|
|
26
|
-
: null;
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
function screenshotDataUrlFromRecorderResult(result) {
|
|
31
|
-
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
32
|
-
return typeof result.screenshotDataUrl === "string" && result.screenshotDataUrl.trim()
|
|
33
|
-
? result.screenshotDataUrl
|
|
34
|
-
: null;
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
11
|
/**
|
|
39
12
|
* Execution Mode - Run test scripts line-by-line
|
|
40
13
|
* Each instruction is executed in isolation (messages cleared after each turn)
|
|
@@ -65,7 +38,15 @@ export class ExecutionMode {
|
|
|
65
38
|
executionEngine.getCurrentPlatform || (this.isBrowserSession ? getBrowserCurrentPlatform : getCurrentPlatform);
|
|
66
39
|
this.connectToBrowser = executionEngine.connectToBrowser || connectToBrowser;
|
|
67
40
|
this.getBrowserInfo = executionEngine.getBrowserInfo || getBrowserInfo;
|
|
68
|
-
|
|
41
|
+
// The Ink shell shares one engine across modes, so design mode may have
|
|
42
|
+
// changed these three fields. Reset them to the values execution needs.
|
|
43
|
+
this.engine.toolCapabilities = {
|
|
44
|
+
assert: true,
|
|
45
|
+
navigate: this.isBrowserSession,
|
|
46
|
+
loadmill: true,
|
|
47
|
+
};
|
|
48
|
+
this.engine.baseSystemPrompt = this.initialSystemText;
|
|
49
|
+
this.engine.isHeadlessMode = this.isHeadlessMode;
|
|
69
50
|
}
|
|
70
51
|
/**
|
|
71
52
|
* Format duration in human-readable format (Xm Ys)
|
|
@@ -142,7 +123,7 @@ export class ExecutionMode {
|
|
|
142
123
|
eventType: 'instruction_started',
|
|
143
124
|
payload: {
|
|
144
125
|
instruction,
|
|
145
|
-
isAssertion: isAssertion(instruction)
|
|
126
|
+
isAssertion: isAssertion(instruction),
|
|
146
127
|
}
|
|
147
128
|
});
|
|
148
129
|
// Check for exit command
|
|
@@ -213,165 +194,28 @@ export class ExecutionMode {
|
|
|
213
194
|
*/
|
|
214
195
|
async executeInstruction(instruction, context, retryCount = 0, stepContext = null) {
|
|
215
196
|
const MAX_RETRIES = 10;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (!this.isBrowserSession) {
|
|
219
|
-
const message = "navigate: instructions are only supported for web runs.";
|
|
220
|
-
this.emit(addOutput, "error", message, context, stepContext, {
|
|
221
|
-
eventType: "error",
|
|
222
|
-
payload: {
|
|
223
|
-
instructionType: "navigate",
|
|
224
|
-
message
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
return { success: false, error: message };
|
|
228
|
-
}
|
|
229
|
-
const navigationTarget = extractNavigateTarget(instruction);
|
|
230
|
-
this.session.addToTranscript(`[Navigate] ${navigationTarget}`);
|
|
231
|
-
emitDesktopDebug("runner.navigate.started", "execution", {
|
|
232
|
-
runId: context?.runId,
|
|
233
|
-
stepId: stepContext?.stepId,
|
|
234
|
-
instructionIndex: stepContext?.instructionIndex
|
|
235
|
-
}, {
|
|
236
|
-
instruction,
|
|
237
|
-
target: navigationTarget
|
|
238
|
-
});
|
|
239
|
-
this.emit(addOutput, "system", `Navigating to ${navigationTarget}...`, context, stepContext, {
|
|
240
|
-
eventType: "system_message",
|
|
241
|
-
payload: {
|
|
242
|
-
instructionType: "navigate",
|
|
243
|
-
target: navigationTarget,
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
try {
|
|
247
|
-
const normalizedUrl = await this.navigateToUrl(this.session.browserSessionId, navigationTarget);
|
|
248
|
-
const browserInfo = normalizeBrowserInfoForSession(await this.getBrowserInfo(this.session.browserSessionId));
|
|
249
|
-
this.session.browserInfo = browserInfo;
|
|
250
|
-
this.session.deviceInfo = browserInfo;
|
|
251
|
-
this.session.currentUrl = browserInfo.currentUrl || normalizedUrl;
|
|
252
|
-
this.session.browserName = browserInfo.browserName || this.session.currentUrl;
|
|
253
|
-
this.session.deviceName = this.session.browserName;
|
|
254
|
-
const transcriptContext = this.session.getTranscriptText();
|
|
255
|
-
const navigationRecoveryPrompt = buildExecutionNavigationRecoveryPrompt({
|
|
256
|
-
basePrompt: this.initialSystemText,
|
|
257
|
-
transcript: transcriptContext,
|
|
258
|
-
navigatedUrl: this.session.currentUrl,
|
|
259
|
-
});
|
|
260
|
-
this.session.installRecoverySystemPrompt(navigationRecoveryPrompt);
|
|
261
|
-
emitDesktopDebug("runner.navigate.completed", "execution", {
|
|
262
|
-
runId: context?.runId,
|
|
263
|
-
stepId: stepContext?.stepId,
|
|
264
|
-
instructionIndex: stepContext?.instructionIndex
|
|
265
|
-
}, {
|
|
266
|
-
instruction,
|
|
267
|
-
target: navigationTarget,
|
|
268
|
-
url: normalizedUrl
|
|
269
|
-
});
|
|
270
|
-
this.emit(addOutput, "success", `Navigation complete: ${normalizedUrl}`, context, stepContext, {
|
|
271
|
-
eventType: "system_message",
|
|
272
|
-
payload: {
|
|
273
|
-
instructionType: "navigate",
|
|
274
|
-
url: normalizedUrl,
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
return { success: true };
|
|
278
|
-
}
|
|
279
|
-
catch (err) {
|
|
280
|
-
const message = err instanceof Error ? err.message : "Navigation failed.";
|
|
281
|
-
logger.error("Execution navigation error", {
|
|
282
|
-
instruction,
|
|
283
|
-
retryCount,
|
|
284
|
-
message,
|
|
285
|
-
});
|
|
286
|
-
emitDesktopDebug("runner.navigate.failed", "execution", {
|
|
287
|
-
runId: context?.runId,
|
|
288
|
-
stepId: stepContext?.stepId,
|
|
289
|
-
instructionIndex: stepContext?.instructionIndex
|
|
290
|
-
}, {
|
|
291
|
-
instruction,
|
|
292
|
-
target: navigationTarget,
|
|
293
|
-
message
|
|
294
|
-
});
|
|
295
|
-
this.emit(addOutput, "error", `Navigation failed: ${message}`, context, stepContext, {
|
|
296
|
-
eventType: "error",
|
|
297
|
-
payload: {
|
|
298
|
-
instructionType: "navigate",
|
|
299
|
-
message,
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
return { success: false, error: message };
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
// ── Check for Loadmill instruction ──
|
|
306
|
-
if (isLoadmillInstruction(instruction)) {
|
|
307
|
-
const loadmillCommand = extractLoadmillCommand(instruction);
|
|
308
|
-
this.session.addToTranscript(`[Loadmill] ${loadmillCommand}`);
|
|
309
|
-
const result = await executeLoadmillInstruction(loadmillCommand, this.isHeadlessMode, context, stepContext);
|
|
310
|
-
// Handle retry request from interactive mode
|
|
311
|
-
if (result.retry) {
|
|
312
|
-
this.stats.retryCount++;
|
|
313
|
-
return await this.executeInstruction(instruction, context, 0, stepContext);
|
|
314
|
-
}
|
|
315
|
-
return result;
|
|
316
|
-
}
|
|
317
|
-
// ── Check for assertion ──
|
|
318
|
-
const isAssertionStep = isAssertion(instruction);
|
|
319
|
-
const assertionEventId = isAssertionStep ? randomUUID() : null;
|
|
320
|
-
let assertionPrompt = null;
|
|
321
|
-
const transcriptStartIndex = this.session.transcript.length;
|
|
322
|
-
if (isAssertionStep) {
|
|
323
|
-
assertionPrompt = extractAssertionPrompt(instruction);
|
|
324
|
-
const assertionSystemPrompt = buildAssertionSystemPrompt(this.initialSystemText, assertionPrompt);
|
|
325
|
-
this.session.clearMessages();
|
|
326
|
-
this.session.addMessage("system", assertionSystemPrompt);
|
|
327
|
-
this.session.addToTranscript(`[Assertion] ${assertionPrompt}`);
|
|
328
|
-
this.session.addMessage("user", `Validate this assertion: ${assertionPrompt}`);
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
this.session.addToTranscript(`[User] ${instruction}`);
|
|
332
|
-
this.session.addMessage("user", instruction);
|
|
333
|
-
}
|
|
197
|
+
this.session.addToTranscript(`[User] ${instruction}`);
|
|
198
|
+
this.session.addMessage("user", instruction);
|
|
334
199
|
try {
|
|
335
200
|
const screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
|
|
336
|
-
const
|
|
201
|
+
const currentScreenshot = await this.engine.recordScreenshot?.(screenshotBase64, {
|
|
337
202
|
runId: context?.runId,
|
|
338
203
|
stepId: stepContext?.stepId,
|
|
339
204
|
instructionIndex: stepContext?.instructionIndex,
|
|
340
|
-
|
|
341
|
-
captureSource: isAssertionStep ? CAPTURE_SOURCES.INSTRUCTION_INPUT_ASSERTION : CAPTURE_SOURCES.INSTRUCTION_INPUT
|
|
342
|
-
});
|
|
343
|
-
const assertionScreenshotPath = isAssertionStep ? screenshotPathFromRecorderResult(screenshotResult) : null;
|
|
344
|
-
const assertionScreenshotDataUrl = isAssertionStep
|
|
345
|
-
? screenshotDataUrlFromRecorderResult(screenshotResult) ||
|
|
346
|
-
(context?.includeScreenshotDataUrl && assertionScreenshotPath
|
|
347
|
-
? `data:image/png;base64,${screenshotBase64}`
|
|
348
|
-
: null)
|
|
349
|
-
: null;
|
|
350
|
-
const assertionEventPayload = isAssertionStep
|
|
351
|
-
? {
|
|
352
|
-
eventId: assertionEventId,
|
|
353
|
-
...(assertionScreenshotPath ? { screenshotPath: assertionScreenshotPath } : {}),
|
|
354
|
-
...(assertionScreenshotDataUrl ? { screenshotDataUrl: assertionScreenshotDataUrl } : {})
|
|
355
|
-
}
|
|
356
|
-
: {};
|
|
357
|
-
// Assertions rely on the prior response chain for earlier execution context.
|
|
358
|
-
const { messagesToSend, previousResponseIdToSend } = buildExecutionRequestPayload({
|
|
359
|
-
instruction,
|
|
360
|
-
isAssertionStep,
|
|
361
|
-
messages: this.session.messages,
|
|
362
|
-
previousResponseId: this.session.previousResponseId
|
|
205
|
+
captureSource: CAPTURE_SOURCES.INSTRUCTION_INPUT
|
|
363
206
|
});
|
|
364
207
|
const response = await this.sendCUARequest({
|
|
365
|
-
messages:
|
|
208
|
+
messages: this.session.messages,
|
|
366
209
|
screenshotBase64,
|
|
367
|
-
previousResponseId:
|
|
210
|
+
previousResponseId: this.session.previousResponseId,
|
|
368
211
|
deviceInfo: this.session.deviceInfo,
|
|
369
212
|
debugContext: {
|
|
370
213
|
scope: "execution",
|
|
371
214
|
runId: context?.runId,
|
|
372
215
|
stepId: stepContext?.stepId,
|
|
373
216
|
instructionIndex: stepContext?.instructionIndex
|
|
374
|
-
}
|
|
217
|
+
},
|
|
218
|
+
toolCapabilities: this.engine.toolCapabilities,
|
|
375
219
|
});
|
|
376
220
|
// Track actions for stats
|
|
377
221
|
const trackAction = (action = null) => {
|
|
@@ -380,65 +224,29 @@ export class ExecutionMode {
|
|
|
380
224
|
}
|
|
381
225
|
return this.shouldStop;
|
|
382
226
|
};
|
|
383
|
-
const newResponseId = await this.engine.runFullTurn(response, trackAction, context, stepContext);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
// Interactive mode - ask user what to do
|
|
398
|
-
this.emit(addOutput, 'system', 'What would you like to do? (retry/skip/stop)', context, stepContext, {
|
|
399
|
-
eventType: 'input_request',
|
|
400
|
-
payload: {
|
|
401
|
-
options: ['retry', 'skip', 'stop']
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
// Wait for user input
|
|
405
|
-
const userChoice = await new Promise((resolve) => {
|
|
406
|
-
if (context?.waitForUserInput) {
|
|
407
|
-
context.waitForUserInput().then(resolve);
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
// Fallback if waitForUserInput not available
|
|
411
|
-
resolve('stop');
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
const choice = userChoice.toLowerCase().trim();
|
|
415
|
-
if (choice === 'retry' || choice === 'r') {
|
|
416
|
-
// Retry the same instruction by recursing
|
|
417
|
-
this.stats.retryCount++;
|
|
418
|
-
return await this.executeInstruction(instruction, context, 0, stepContext);
|
|
419
|
-
}
|
|
420
|
-
else if (choice === 'skip' || choice === 's') {
|
|
421
|
-
// Continue to next instruction
|
|
422
|
-
this.stats.assertionsFailed++;
|
|
423
|
-
this.emit(addOutput, 'info', 'Skipping failed assertion and continuing...', context, stepContext, {
|
|
424
|
-
eventType: 'system_message'
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
// Stop execution
|
|
429
|
-
this.stats.assertionsFailed++;
|
|
430
|
-
return { success: false, error: `Assertion failed: ${assertionPrompt}` };
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
else if (result.passed) {
|
|
434
|
-
this.stats.assertionsPassed++;
|
|
435
|
-
handleAssertionSuccess(assertionPrompt, context, stepContext, assertionEventPayload);
|
|
227
|
+
const { responseId: newResponseId, summary: turnSummary } = await this.engine.runFullTurn(response, trackAction, context, stepContext, { currentScreenshot });
|
|
228
|
+
if (turnSummary.assertionStatus === "passed") {
|
|
229
|
+
this.stats.assertionsPassed++;
|
|
230
|
+
}
|
|
231
|
+
else if (turnSummary.assertionStatus === "failed" ||
|
|
232
|
+
turnSummary.assertionStatus === "failed_skipped") {
|
|
233
|
+
this.stats.assertionsFailed++;
|
|
234
|
+
}
|
|
235
|
+
if (turnSummary.retryInstruction) {
|
|
236
|
+
this.stats.retryCount++;
|
|
237
|
+
this.session.updateResponseId(turnSummary.responseId);
|
|
238
|
+
if (!turnSummary.preserveSessionMessages) {
|
|
239
|
+
this.session.clearMessages();
|
|
436
240
|
}
|
|
241
|
+
return await this.executeInstruction(instruction, context, 0, stepContext);
|
|
242
|
+
}
|
|
243
|
+
if (turnSummary.failed) {
|
|
244
|
+
return { success: false, error: turnSummary.error || "Instruction failed." };
|
|
245
|
+
}
|
|
246
|
+
this.session.updateResponseId(turnSummary.responseId ?? newResponseId);
|
|
247
|
+
if (!turnSummary.preserveSessionMessages) {
|
|
248
|
+
this.session.clearMessages();
|
|
437
249
|
}
|
|
438
|
-
// Clear messages after each turn but KEEP the response chain for context
|
|
439
|
-
// When continuing with previousResponseId, only send new user message (not system)
|
|
440
|
-
this.session.clearMessages();
|
|
441
|
-
// Note: we keep previousResponseId to maintain context across the test
|
|
442
250
|
return { success: true };
|
|
443
251
|
}
|
|
444
252
|
catch (err) {
|