@loadmill/droid-cua 2.17.0 → 2.18.0
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/README.md +18 -0
- package/build/index.js +16 -1
- package/build/src/browser/playwright/actions.js +17 -8
- package/build/src/cli/headless-execution-config.js +6 -0
- package/build/src/cli/headless-report.js +113 -0
- package/build/src/cli/headless-runner.js +40 -6
- package/build/src/commands/help.js +1 -0
- package/build/src/commands/run.js +2 -1
- package/build/src/core/execution-engine.js +57 -2
- package/build/src/core/prompts.js +1 -1
- package/build/src/core/report/generate-html-report.js +88 -0
- package/build/src/core/report/parse-run-report.js +222 -0
- package/build/src/core/report/render-html-report.js +317 -0
- package/build/src/core/report/report-file-name.js +31 -0
- package/build/src/core/report/run-log-model.js +144 -0
- package/build/src/core/report/run-report-model.js +143 -0
- package/build/src/core/secrets.js +116 -0
- package/build/src/core/tool-handlers/close-current-tab.js +3 -3
- package/build/src/core/tool-handlers/navigate.js +3 -3
- package/build/src/core/tool-handlers/preview-click.js +6 -6
- package/build/src/core/tool-handlers/type-sensitive-text.js +62 -0
- package/build/src/core/tool-handlers/utils.js +3 -3
- package/build/src/device/android/actions.js +13 -8
- package/build/src/device/cloud/actions.js +17 -8
- package/build/src/device/ios/actions.js +17 -8
- package/build/src/device/openai.js +17 -0
- package/build/src/device/sensitive-action.js +61 -0
- package/build/src/modes/design-mode-ink.js +3 -1
- package/build/src/modes/design-mode.js +3 -1
- package/build/src/modes/execution-mode.js +1 -0
- package/build/src/prompts/design.js +4 -3
- package/build/src/prompts/execution.js +15 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { logger } from "../../utils/logger.js";
|
|
|
2
2
|
import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js";
|
|
3
3
|
import { getConfiguredStepDelayMs } from "../../utils/step-delay.js";
|
|
4
4
|
import { buildActionEventMeta } from "../action-event-meta.js";
|
|
5
|
+
import { actionForErrorLog, buildActionErrorOutput, buildDebugTextField, buildSensitiveDebugFields, buildTypeActionPayload, buildTypeActionText, isSensitiveAction, } from "../sensitive-action.js";
|
|
5
6
|
import { buildMobileKeypressPayload, getCloudAndroidTransport, getIosTransport, normalizeMobileKeypress } from "../mobile-keypress.js";
|
|
6
7
|
import { getActiveSession, getDevicePixelRatio } from "./connection.js";
|
|
7
8
|
import { resolveScrollGesture } from "../scroll-gesture.js";
|
|
@@ -33,7 +34,8 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
|
|
|
33
34
|
deviceId,
|
|
34
35
|
actionType: action?.type,
|
|
35
36
|
scale,
|
|
36
|
-
text:
|
|
37
|
+
text: buildDebugTextField(action, truncateForDebug),
|
|
38
|
+
...buildSensitiveDebugFields(action),
|
|
37
39
|
keyCount: Array.isArray(action?.keys) ? action.keys.length : 0,
|
|
38
40
|
pathPoints: Array.isArray(action?.path) ? action.path.length : 0
|
|
39
41
|
});
|
|
@@ -48,7 +50,11 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
|
|
|
48
50
|
break;
|
|
49
51
|
}
|
|
50
52
|
case "type": {
|
|
51
|
-
addOutput({
|
|
53
|
+
addOutput({
|
|
54
|
+
type: "action",
|
|
55
|
+
text: buildTypeActionText(action, action.text),
|
|
56
|
+
...meta(buildTypeActionPayload(action, action.text))
|
|
57
|
+
});
|
|
52
58
|
await session.client.type(session.sessionId, action.text);
|
|
53
59
|
break;
|
|
54
60
|
}
|
|
@@ -133,12 +139,14 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
|
|
|
133
139
|
default:
|
|
134
140
|
addOutput({ type: "info", text: `Unknown action: ${JSON.stringify(action)}` });
|
|
135
141
|
}
|
|
142
|
+
return { success: true };
|
|
136
143
|
}
|
|
137
144
|
catch (error) {
|
|
145
|
+
const outputError = buildActionErrorOutput(action, error);
|
|
138
146
|
logger.error("Cloud action execution error", {
|
|
139
|
-
action,
|
|
140
|
-
message:
|
|
141
|
-
stack: error.stack,
|
|
147
|
+
action: actionForErrorLog(action),
|
|
148
|
+
message: outputError.message,
|
|
149
|
+
stack: isSensitiveAction(action) ? undefined : error.stack,
|
|
142
150
|
providerId: session.providerId,
|
|
143
151
|
platform
|
|
144
152
|
});
|
|
@@ -152,11 +160,11 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
|
|
|
152
160
|
source: session.providerId,
|
|
153
161
|
operation: "action.execute",
|
|
154
162
|
actionType: action?.type,
|
|
155
|
-
message:
|
|
163
|
+
message: outputError.message
|
|
156
164
|
});
|
|
157
165
|
addOutput({
|
|
158
166
|
type: "error",
|
|
159
|
-
text:
|
|
167
|
+
text: outputError.text,
|
|
160
168
|
eventType: "error",
|
|
161
169
|
actionType: action?.type,
|
|
162
170
|
runId: context?.runId,
|
|
@@ -164,11 +172,12 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
|
|
|
164
172
|
instructionIndex: context?.instructionIndex,
|
|
165
173
|
payload: {
|
|
166
174
|
eventId: context?.eventId,
|
|
167
|
-
message:
|
|
175
|
+
message: outputError.message,
|
|
168
176
|
platform,
|
|
169
177
|
source: session.providerId
|
|
170
178
|
}
|
|
171
179
|
});
|
|
172
180
|
addOutput({ type: "info", text: "Full error details have been logged to the debug log." });
|
|
181
|
+
return { success: false, error: outputError.message };
|
|
173
182
|
}
|
|
174
183
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import * as appium from "./appium-client.js";
|
|
7
7
|
import { buildActionEventMeta } from "../action-event-meta.js";
|
|
8
8
|
import { buildMobileKeypressPayload, getIosTransport, normalizeMobileKeypress } from "../mobile-keypress.js";
|
|
9
|
+
import { actionForErrorLog, buildActionErrorOutput, buildDebugTextField, buildSensitiveDebugFields, buildTypeActionPayload, buildTypeActionText, isSensitiveAction, } from "../sensitive-action.js";
|
|
9
10
|
import { getActiveSession, getDevicePixelRatio } from "./connection.js";
|
|
10
11
|
import { logger } from "../../utils/logger.js";
|
|
11
12
|
import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js";
|
|
@@ -41,7 +42,8 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
|
|
|
41
42
|
simulatorId,
|
|
42
43
|
actionType: action?.type,
|
|
43
44
|
scale,
|
|
44
|
-
text:
|
|
45
|
+
text: buildDebugTextField(action, truncateForDebug),
|
|
46
|
+
...buildSensitiveDebugFields(action),
|
|
45
47
|
keyCount: Array.isArray(action?.keys) ? action.keys.length : 0,
|
|
46
48
|
pathPoints: Array.isArray(action?.path) ? action.path.length : 0
|
|
47
49
|
});
|
|
@@ -59,7 +61,11 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
|
|
|
59
61
|
break;
|
|
60
62
|
}
|
|
61
63
|
case "type": {
|
|
62
|
-
addOutput({
|
|
64
|
+
addOutput({
|
|
65
|
+
type: "action",
|
|
66
|
+
text: buildTypeActionText(action, action.text),
|
|
67
|
+
...meta(buildTypeActionPayload(action, action.text))
|
|
68
|
+
});
|
|
63
69
|
await appium.type(session.sessionId, action.text);
|
|
64
70
|
break;
|
|
65
71
|
}
|
|
@@ -138,12 +144,14 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
|
|
|
138
144
|
default:
|
|
139
145
|
addOutput({ type: "info", text: `Unknown action: ${JSON.stringify(action)}` });
|
|
140
146
|
}
|
|
147
|
+
return { success: true };
|
|
141
148
|
}
|
|
142
149
|
catch (error) {
|
|
150
|
+
const outputError = buildActionErrorOutput(action, error);
|
|
143
151
|
logger.error("iOS action execution error", {
|
|
144
|
-
action,
|
|
145
|
-
message:
|
|
146
|
-
stack: error.stack,
|
|
152
|
+
action: actionForErrorLog(action),
|
|
153
|
+
message: outputError.message,
|
|
154
|
+
stack: isSensitiveAction(action) ? undefined : error.stack,
|
|
147
155
|
});
|
|
148
156
|
emitDesktopDebug("device.error", "device", {
|
|
149
157
|
runId: context?.runId,
|
|
@@ -154,11 +162,11 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
|
|
|
154
162
|
platform: "ios",
|
|
155
163
|
operation: "action.execute",
|
|
156
164
|
actionType: action?.type,
|
|
157
|
-
message:
|
|
165
|
+
message: outputError.message
|
|
158
166
|
});
|
|
159
167
|
addOutput({
|
|
160
168
|
type: "error",
|
|
161
|
-
text:
|
|
169
|
+
text: outputError.text,
|
|
162
170
|
eventType: "error",
|
|
163
171
|
actionType: action?.type,
|
|
164
172
|
runId: context?.runId,
|
|
@@ -166,10 +174,11 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
|
|
|
166
174
|
instructionIndex: context?.instructionIndex,
|
|
167
175
|
payload: {
|
|
168
176
|
eventId: context?.eventId,
|
|
169
|
-
message:
|
|
177
|
+
message: outputError.message,
|
|
170
178
|
platform: "ios"
|
|
171
179
|
}
|
|
172
180
|
});
|
|
173
181
|
addOutput({ type: "info", text: "Full error details have been logged to the debug log." });
|
|
182
|
+
return { success: false, error: outputError.message };
|
|
174
183
|
}
|
|
175
184
|
}
|
|
@@ -107,6 +107,20 @@ const loadmillTool = {
|
|
|
107
107
|
additionalProperties: false,
|
|
108
108
|
},
|
|
109
109
|
};
|
|
110
|
+
const typeSensitiveTextTool = {
|
|
111
|
+
type: "function",
|
|
112
|
+
name: "type_sensitive_text",
|
|
113
|
+
description: "Type a secret value by key without exposing the value to the model. Use this only for secret keys listed in the prompt.",
|
|
114
|
+
strict: true,
|
|
115
|
+
parameters: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
key: { type: "string" },
|
|
119
|
+
},
|
|
120
|
+
required: ["key"],
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
110
124
|
const testDesignCompleteTool = {
|
|
111
125
|
type: "function",
|
|
112
126
|
name: "test_design_complete",
|
|
@@ -156,6 +170,9 @@ export function buildFunctionTools({ aimAssistMode = DEFAULT_AIM_ASSIST_MODE, to
|
|
|
156
170
|
if (toolCapabilities.loadmill) {
|
|
157
171
|
tools.push(loadmillTool);
|
|
158
172
|
}
|
|
173
|
+
if (toolCapabilities.typeSensitiveText) {
|
|
174
|
+
tools.push(typeSensitiveTextTool);
|
|
175
|
+
}
|
|
159
176
|
if (toolCapabilities.testDesignComplete) {
|
|
160
177
|
tools.push(testDesignCompleteTool);
|
|
161
178
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function isSensitiveAction(action) {
|
|
2
|
+
return action?.sensitive === true;
|
|
3
|
+
}
|
|
4
|
+
export function getSensitiveKey(action) {
|
|
5
|
+
return typeof action?.sensitiveKey === "string" && action.sensitiveKey.trim()
|
|
6
|
+
? action.sensitiveKey.trim()
|
|
7
|
+
: "secret";
|
|
8
|
+
}
|
|
9
|
+
export function buildTypeActionText(action, text) {
|
|
10
|
+
if (isSensitiveAction(action)) {
|
|
11
|
+
return `Typing secret: ${getSensitiveKey(action)}`;
|
|
12
|
+
}
|
|
13
|
+
return `Typing text: ${text}`;
|
|
14
|
+
}
|
|
15
|
+
export function buildTypeActionPayload(action, text) {
|
|
16
|
+
if (isSensitiveAction(action)) {
|
|
17
|
+
return {
|
|
18
|
+
sensitive: true,
|
|
19
|
+
secretKey: getSensitiveKey(action),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return { text };
|
|
23
|
+
}
|
|
24
|
+
export function buildDebugTextField(action, truncate) {
|
|
25
|
+
if (isSensitiveAction(action)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return typeof action?.text === "string" ? truncate(action.text, 300) : undefined;
|
|
29
|
+
}
|
|
30
|
+
export function buildSensitiveDebugFields(action) {
|
|
31
|
+
if (!isSensitiveAction(action)) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
sensitive: true,
|
|
36
|
+
secretKey: getSensitiveKey(action),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function actionForErrorLog(action) {
|
|
40
|
+
if (!isSensitiveAction(action)) {
|
|
41
|
+
return action;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
type: action?.type,
|
|
45
|
+
sensitive: true,
|
|
46
|
+
secretKey: getSensitiveKey(action),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function buildActionErrorOutput(action, error) {
|
|
50
|
+
if (isSensitiveAction(action)) {
|
|
51
|
+
const key = getSensitiveKey(action);
|
|
52
|
+
return {
|
|
53
|
+
text: `Error typing secret: ${key}`,
|
|
54
|
+
message: `Sensitive typing failed for ${key}.`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
text: `Error executing action: ${error.message}`,
|
|
59
|
+
message: error.message,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -33,7 +33,8 @@ export class DesignModeInk {
|
|
|
33
33
|
async start() {
|
|
34
34
|
const addOutput = this.context.addOutput;
|
|
35
35
|
// Set design mode system prompt
|
|
36
|
-
const
|
|
36
|
+
const availableSecretKeys = this.engine.getAvailableSecretKeys();
|
|
37
|
+
const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {}, "", { availableSecretKeys });
|
|
37
38
|
this.baseDesignPrompt = designPrompt;
|
|
38
39
|
this.session.setSystemPrompt(designPrompt);
|
|
39
40
|
this.engine.toolCapabilities = {
|
|
@@ -41,6 +42,7 @@ export class DesignModeInk {
|
|
|
41
42
|
navigate: this.session?.deviceInfo?.platform === "browser",
|
|
42
43
|
closeCurrentTab: this.session?.deviceInfo?.platform === "browser",
|
|
43
44
|
loadmill: false,
|
|
45
|
+
typeSensitiveText: this.engine.hasSecrets(),
|
|
44
46
|
testDesignComplete: true,
|
|
45
47
|
};
|
|
46
48
|
this.engine.baseSystemPrompt = designPrompt;
|
|
@@ -30,7 +30,8 @@ export class DesignMode {
|
|
|
30
30
|
*/
|
|
31
31
|
async start(context) {
|
|
32
32
|
// Set design mode system prompt
|
|
33
|
-
const
|
|
33
|
+
const availableSecretKeys = this.engine.getAvailableSecretKeys();
|
|
34
|
+
const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {}, "", { availableSecretKeys });
|
|
34
35
|
this.baseDesignPrompt = designPrompt;
|
|
35
36
|
this.session.setSystemPrompt(designPrompt);
|
|
36
37
|
this.engine.toolCapabilities = {
|
|
@@ -38,6 +39,7 @@ export class DesignMode {
|
|
|
38
39
|
navigate: this.session?.deviceInfo?.platform === "browser",
|
|
39
40
|
closeCurrentTab: this.session?.deviceInfo?.platform === "browser",
|
|
40
41
|
loadmill: false,
|
|
42
|
+
typeSensitiveText: this.engine.hasSecrets(),
|
|
41
43
|
testDesignComplete: true,
|
|
42
44
|
};
|
|
43
45
|
this.engine.baseSystemPrompt = designPrompt;
|
|
@@ -70,6 +70,7 @@ export class ExecutionMode {
|
|
|
70
70
|
navigate: this.isBrowserSession,
|
|
71
71
|
closeCurrentTab: this.isBrowserSession,
|
|
72
72
|
loadmill: true,
|
|
73
|
+
typeSensitiveText: this.engine.hasSecrets(),
|
|
73
74
|
};
|
|
74
75
|
this.engine.baseSystemPrompt = this.initialSystemText;
|
|
75
76
|
this.engine.isHeadlessMode = this.isHeadlessMode;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { appendCustomSections, buildBaseSystemPrompt, isBrowserTarget } from "./base.js";
|
|
2
|
-
import { buildAppContextSection } from "./execution.js";
|
|
3
|
-
export function buildDesignModePrompt(deviceInfo, customInstructions = {}, appContextText = "") {
|
|
2
|
+
import { buildAppContextSection, buildSecretKeysContextSection } from "./execution.js";
|
|
3
|
+
export function buildDesignModePrompt(deviceInfo, customInstructions = {}, appContextText = "", options = {}) {
|
|
4
4
|
const designCustomText = typeof customInstructions.designModeInstructions === "string" ? customInstructions.designModeInstructions.trim() : "";
|
|
5
5
|
const basePrompt = buildBaseSystemPrompt(deviceInfo, customInstructions);
|
|
6
6
|
const appContextSection = buildAppContextSection(appContextText);
|
|
7
|
+
const secretKeysContextSection = buildSecretKeysContextSection(options.availableSecretKeys);
|
|
7
8
|
const browserTarget = isBrowserTarget(deviceInfo);
|
|
8
9
|
const prompt = `${basePrompt}
|
|
9
10
|
|
|
@@ -93,7 +94,7 @@ WRONG Example (DON'T DO THIS):
|
|
|
93
94
|
3. etc...
|
|
94
95
|
\`\`\`
|
|
95
96
|
|
|
96
|
-
Remember: You are autonomous. Explore confidently. Generate simple, executable test scripts.${appContextSection ? `\n\n${appContextSection}` : ""}
|
|
97
|
+
Remember: You are autonomous. Explore confidently. Generate simple, executable test scripts.${appContextSection ? `\n\n${appContextSection}` : ""}${secretKeysContextSection ? `\n\n${secretKeysContextSection}` : ""}
|
|
97
98
|
`;
|
|
98
99
|
return appendCustomSections(prompt, [
|
|
99
100
|
{ title: "Base Prompt Instructions", text: customInstructions.basePromptInstructions },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_AIM_ASSIST_MODE } from "../core/aim-assist.js";
|
|
2
|
+
import { normalizeSecretKeys } from "../core/secrets.js";
|
|
2
3
|
import { appendCustomSections, buildBaseSystemPrompt, isBrowserTarget } from "./base.js";
|
|
3
4
|
export function buildAppContextSection(text) {
|
|
4
5
|
const trimmed = typeof text === "string" ? text.trim() : "";
|
|
@@ -22,12 +23,25 @@ They do not replace or override the test instructions.
|
|
|
22
23
|
|
|
23
24
|
${trimmed}`;
|
|
24
25
|
}
|
|
26
|
+
export function buildSecretKeysContextSection(keys = []) {
|
|
27
|
+
const normalized = normalizeSecretKeys(keys);
|
|
28
|
+
if (normalized.length === 0) {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
return `SECRET KEYS FOR THIS RUN:
|
|
32
|
+
The following secret keys are available. You can see only the keys, not the values.
|
|
33
|
+
Use the \`type_sensitive_text\` tool when you need to enter passwords, OTPs, tokens, API keys, or other hidden credentials.
|
|
34
|
+
Never ask the user to reveal these values and never type placeholder text when a matching secret key is available.
|
|
35
|
+
|
|
36
|
+
${normalized.map((key) => `- ${key}`).join("\n")}`;
|
|
37
|
+
}
|
|
25
38
|
export function buildExecutionModePrompt(deviceInfo, customInstructions = {}, appContextText = "", options = {}) {
|
|
26
39
|
const executionCustomText = typeof customInstructions.executionModeInstructions === "string" ? customInstructions.executionModeInstructions.trim() : "";
|
|
27
40
|
const aimAssistMode = typeof options.aimAssistMode === "string" ? options.aimAssistMode : DEFAULT_AIM_ASSIST_MODE;
|
|
28
41
|
const basePrompt = buildBaseSystemPrompt(deviceInfo, { aimAssistMode });
|
|
29
42
|
const appContextSection = buildAppContextSection(appContextText);
|
|
30
43
|
const testDataContextSection = buildTestDataContextSection(options.testDataContext);
|
|
44
|
+
const secretKeysContextSection = buildSecretKeysContextSection(options.availableSecretKeys);
|
|
31
45
|
const browserTarget = isBrowserTarget(deviceInfo);
|
|
32
46
|
const prompt = `${basePrompt}
|
|
33
47
|
|
|
@@ -64,7 +78,7 @@ Your process:
|
|
|
64
78
|
7. Stop immediately - no commentary, no questions
|
|
65
79
|
|
|
66
80
|
Each instruction is independent. Do not reference previous instructions or ask about next steps.
|
|
67
|
-
${appContextSection ? `\n\n${appContextSection}` : ""}${testDataContextSection ? `\n\n${testDataContextSection}` : ""}`;
|
|
81
|
+
${appContextSection ? `\n\n${appContextSection}` : ""}${testDataContextSection ? `\n\n${testDataContextSection}` : ""}${secretKeysContextSection ? `\n\n${secretKeysContextSection}` : ""}`;
|
|
68
82
|
return appendCustomSections(prompt, [
|
|
69
83
|
{ title: "Base Prompt Instructions", text: customInstructions.basePromptInstructions },
|
|
70
84
|
{ title: "Execution Mode Instructions", text: executionCustomText }
|