@loadmill/droid-cua 2.14.0 → 2.16.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 +6 -3
- package/build/index.js +43 -148
- package/build/src/cli/headless-execution-config.js +28 -2
- package/build/src/cli/headless-instruction-target.js +64 -0
- package/build/src/cli/headless-runner.js +245 -0
- package/build/src/cli/ink-shell.js +1 -0
- package/build/src/commands/help.js +2 -1
- package/build/src/commands/loadmill.js +52 -41
- package/build/src/core/cua-backends.js +11 -0
- package/build/src/core/cua-models.js +47 -2
- package/build/src/core/runtime-config.js +252 -8
- package/build/src/device/loadmill.js +41 -78
- package/build/src/device/openai.js +31 -4
- package/build/src/integrations/loadmill/executor.js +38 -22
- package/build/src/integrations/loadmill/interpreter.js +4 -4
- package/build/src/modes/execution-mode.js +45 -0
- package/build/src/prompts/execution.js +1 -1
- package/build/src/prompts/loadmill.js +4 -8
- package/build/src/utils/cua-debug-tracer.js +15 -0
- package/package.json +6 -2
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { buildTestRevisionSystemPrompt } from "../prompts/editor.js";
|
|
2
|
-
import { createOpenAiClient, resolveRuntimeConfig } from "../core/runtime-config.js";
|
|
2
|
+
import { createOpenAiClient, LOADMILL_MODEL_ALIASES, resolveEffectiveHelperModel, resolveRuntimeConfig, } from "../core/runtime-config.js";
|
|
3
3
|
import { DEFAULT_AIM_ASSIST_MODE } from "../core/aim-assist.js";
|
|
4
4
|
import { DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD } from "../core/context-optimization.js";
|
|
5
5
|
import { logger } from "../utils/logger.js";
|
|
6
6
|
import { CuaDebugTracer } from "../utils/cua-debug-tracer.js";
|
|
7
7
|
const cuaDebugTracer = new CuaDebugTracer(logger);
|
|
8
|
+
function resolveOpenAiBaseUrl(client) {
|
|
9
|
+
return client?.baseURL || client?.baseUrl || client?._options?.baseURL || null;
|
|
10
|
+
}
|
|
11
|
+
function resolveOpenAiEndpointUrl(client, endpointPath) {
|
|
12
|
+
const baseURL = resolveOpenAiBaseUrl(client);
|
|
13
|
+
if (!baseURL) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return new URL(endpointPath.replace(/^\/+/, ''), `${String(baseURL).replace(/\/+$/, '')}/`).toString();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return `${String(baseURL).replace(/\/+$/, '')}/${endpointPath.replace(/^\/+/, '')}`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
8
23
|
function normalizeMessageContent(content) {
|
|
9
24
|
if (Array.isArray(content)) {
|
|
10
25
|
return content.map((item) => {
|
|
@@ -81,7 +96,7 @@ const closeCurrentTabTool = {
|
|
|
81
96
|
const loadmillTool = {
|
|
82
97
|
type: "function",
|
|
83
98
|
name: "loadmill",
|
|
84
|
-
description: "
|
|
99
|
+
description: "Search Loadmill for the requested flow, run the best match, and wait for completion. Use this when the instruction explicitly mentions running or using a Loadmill flow.",
|
|
85
100
|
strict: true,
|
|
86
101
|
parameters: {
|
|
87
102
|
type: "object",
|
|
@@ -225,14 +240,15 @@ export function buildCuaRequestParams({ cuaModel, aimAssistMode = DEFAULT_AIM_AS
|
|
|
225
240
|
* Revise a test script based on user feedback using simple chat completion
|
|
226
241
|
* @param {{
|
|
227
242
|
* client: import("openai").default,
|
|
243
|
+
* model?: string,
|
|
228
244
|
* originalScript: string,
|
|
229
245
|
* revisionRequest: string,
|
|
230
246
|
* }} options
|
|
231
247
|
* @returns {Promise<string>} - The revised test script
|
|
232
248
|
*/
|
|
233
|
-
export async function reviseTestScript({ client, originalScript, revisionRequest }) {
|
|
249
|
+
export async function reviseTestScript({ client, model = LOADMILL_MODEL_ALIASES.DEFAULT_AGENT_MODEL, originalScript, revisionRequest, }) {
|
|
234
250
|
const response = await client.chat.completions.create({
|
|
235
|
-
model
|
|
251
|
+
model,
|
|
236
252
|
messages: [{
|
|
237
253
|
role: "system",
|
|
238
254
|
content: buildTestRevisionSystemPrompt(originalScript, revisionRequest)
|
|
@@ -274,7 +290,14 @@ export async function sendCUARequest({ client, cuaModel, aimAssistMode = DEFAULT
|
|
|
274
290
|
debugContext,
|
|
275
291
|
previousResponseId
|
|
276
292
|
});
|
|
293
|
+
trace.endpoint = {
|
|
294
|
+
baseURL: resolveOpenAiBaseUrl(client),
|
|
295
|
+
responsesURL: resolveOpenAiEndpointUrl(client, "responses"),
|
|
296
|
+
model: requestParams.model,
|
|
297
|
+
previousResponseId: requestParams.previous_response_id ?? null,
|
|
298
|
+
};
|
|
277
299
|
logger.debug("CUA Request:", trace.requestLog);
|
|
300
|
+
logger.debug("CUA Request endpoint:", trace.endpoint);
|
|
278
301
|
try {
|
|
279
302
|
const response = await client.responses.create(requestParams);
|
|
280
303
|
cuaDebugTracer.onResponse(trace, response);
|
|
@@ -311,6 +334,10 @@ export function createOpenAiExecutor({ client, config }) {
|
|
|
311
334
|
async reviseTestScript(originalScript, revisionRequest) {
|
|
312
335
|
return await reviseTestScript({
|
|
313
336
|
client: openAiClient,
|
|
337
|
+
model: resolveEffectiveHelperModel({
|
|
338
|
+
llmProviderMode: runtimeConfig.llmProviderMode,
|
|
339
|
+
cuaModel: runtimeConfig.cuaModel,
|
|
340
|
+
}),
|
|
314
341
|
originalScript,
|
|
315
342
|
revisionRequest,
|
|
316
343
|
});
|
|
@@ -4,9 +4,30 @@
|
|
|
4
4
|
import { createLoadmillApiClient } from "./client.js";
|
|
5
5
|
import { interpretLoadmillCommand, selectBestFlow } from "./interpreter.js";
|
|
6
6
|
import { extractLoadmillRuntimeParameters, renderLoadmillParameterContextNote, selectLoadmillParameterCandidates, } from "./parameters.js";
|
|
7
|
+
import { LOADMILL_MODEL_ALIASES } from "../../core/runtime-config.js";
|
|
7
8
|
import { logger } from "../../utils/logger.js";
|
|
8
9
|
const POLL_INTERVAL_MS = 5000; // 5 seconds
|
|
9
|
-
const
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
|
|
11
|
+
function readTimeoutMsFromEnv() {
|
|
12
|
+
const raw = process.env.LOADMILL_RUN_TIMEOUT_MS;
|
|
13
|
+
if (!raw) {
|
|
14
|
+
return DEFAULT_TIMEOUT_MS;
|
|
15
|
+
}
|
|
16
|
+
const parsed = Number.parseInt(raw, 10);
|
|
17
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
logger.debug("Ignoring invalid LOADMILL_RUN_TIMEOUT_MS value", { value: raw });
|
|
21
|
+
return DEFAULT_TIMEOUT_MS;
|
|
22
|
+
}
|
|
23
|
+
function formatDuration(ms) {
|
|
24
|
+
const totalSeconds = Math.ceil(ms / 1000);
|
|
25
|
+
if (totalSeconds >= 60 && totalSeconds % 60 === 0) {
|
|
26
|
+
const minutes = totalSeconds / 60;
|
|
27
|
+
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
|
|
28
|
+
}
|
|
29
|
+
return `${totalSeconds} ${totalSeconds === 1 ? "second" : "seconds"}`;
|
|
30
|
+
}
|
|
10
31
|
function buildParameterSelectionMessages({ command, flowName, futureInstructions, parameters, }) {
|
|
11
32
|
const parameterList = parameters
|
|
12
33
|
.map((param) => `- ${param.key}`)
|
|
@@ -42,7 +63,7 @@ async function selectRelevantParameterNames({ openAiClient, command, flowName, f
|
|
|
42
63
|
}
|
|
43
64
|
try {
|
|
44
65
|
const response = await openAiClient.chat.completions.create({
|
|
45
|
-
model:
|
|
66
|
+
model: LOADMILL_MODEL_ALIASES.DEFAULT_AGENT_MODEL,
|
|
46
67
|
messages: buildParameterSelectionMessages({
|
|
47
68
|
command,
|
|
48
69
|
flowName,
|
|
@@ -127,9 +148,10 @@ function sleep(ms) {
|
|
|
127
148
|
* @param {Function} onStatusUpdate - Callback for status updates
|
|
128
149
|
* @returns {Promise<Object>} - Final run status
|
|
129
150
|
*/
|
|
130
|
-
async function pollForCompletion(runId, loadmillClient,
|
|
131
|
-
const
|
|
132
|
-
|
|
151
|
+
async function pollForCompletion(runId, loadmillClient, options = {}) {
|
|
152
|
+
const { onStatusUpdate = () => { }, timeoutMs = readTimeoutMsFromEnv(), pollIntervalMs = POLL_INTERVAL_MS, now = Date.now, sleepFn = sleep, } = options;
|
|
153
|
+
const startTime = now();
|
|
154
|
+
while (now() - startTime < timeoutMs) {
|
|
133
155
|
try {
|
|
134
156
|
const status = await loadmillClient.getTestRunStatus(runId);
|
|
135
157
|
onStatusUpdate(status);
|
|
@@ -158,7 +180,7 @@ async function pollForCompletion(runId, loadmillClient, onStatusUpdate = () => {
|
|
|
158
180
|
return { success: false, status: "stopped", result: status, error: "Test flow was stopped" };
|
|
159
181
|
}
|
|
160
182
|
// Still running, wait and poll again
|
|
161
|
-
await
|
|
183
|
+
await sleepFn(pollIntervalMs);
|
|
162
184
|
}
|
|
163
185
|
catch (err) {
|
|
164
186
|
logger.error("Error polling Loadmill run status", { runId, error: err.message });
|
|
@@ -166,7 +188,7 @@ async function pollForCompletion(runId, loadmillClient, onStatusUpdate = () => {
|
|
|
166
188
|
}
|
|
167
189
|
}
|
|
168
190
|
// Timeout
|
|
169
|
-
return { success: false, status: "timeout", error:
|
|
191
|
+
return { success: false, status: "timeout", error: `Test run timed out after ${formatDuration(timeoutMs)}` };
|
|
170
192
|
}
|
|
171
193
|
/**
|
|
172
194
|
* Execute a Loadmill command from natural language input
|
|
@@ -176,7 +198,7 @@ async function pollForCompletion(runId, loadmillClient, onStatusUpdate = () => {
|
|
|
176
198
|
* @returns {Promise<{success: boolean, error?: string, result?: Object}>}
|
|
177
199
|
*/
|
|
178
200
|
export async function executeLoadmillCommand(userInput, options = {}) {
|
|
179
|
-
const { onProgress = () => { }, loadmillClient = createLoadmillApiClient(), openAiClient = null, futureInstructions = [], } = options;
|
|
201
|
+
const { onProgress = () => { }, loadmillClient = createLoadmillApiClient(), openAiClient = null, futureInstructions = [], pollTimeoutMs, pollIntervalMs, now, sleepFn, } = options;
|
|
180
202
|
// Check for API token
|
|
181
203
|
if (!loadmillClient.getApiToken()) {
|
|
182
204
|
return {
|
|
@@ -214,18 +236,6 @@ export async function executeLoadmillCommand(userInput, options = {}) {
|
|
|
214
236
|
error: "Could not select a matching flow"
|
|
215
237
|
};
|
|
216
238
|
}
|
|
217
|
-
// If action is "search", just return the results
|
|
218
|
-
if (interpreted.action === "search") {
|
|
219
|
-
return {
|
|
220
|
-
success: true,
|
|
221
|
-
action: "search",
|
|
222
|
-
result: {
|
|
223
|
-
flows,
|
|
224
|
-
selectedFlow,
|
|
225
|
-
confidence
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
239
|
// Step 4: Run the flow
|
|
230
240
|
const flowName = selectedFlow.description || selectedFlow.name || "Unknown";
|
|
231
241
|
onProgress({
|
|
@@ -244,8 +254,14 @@ export async function executeLoadmillCommand(userInput, options = {}) {
|
|
|
244
254
|
}
|
|
245
255
|
// Step 5: Poll for completion
|
|
246
256
|
onProgress({ step: "polling", message: `Test started (ID: ${runId}). Waiting for completion...`, runId });
|
|
247
|
-
const finalResult = await pollForCompletion(runId, loadmillClient,
|
|
248
|
-
|
|
257
|
+
const finalResult = await pollForCompletion(runId, loadmillClient, {
|
|
258
|
+
timeoutMs: pollTimeoutMs,
|
|
259
|
+
pollIntervalMs,
|
|
260
|
+
now,
|
|
261
|
+
sleepFn,
|
|
262
|
+
onStatusUpdate: (status) => {
|
|
263
|
+
onProgress({ step: "polling", message: `Status: ${status.status}...`, runId });
|
|
264
|
+
},
|
|
249
265
|
});
|
|
250
266
|
const runtimeParameterContext = finalResult.success
|
|
251
267
|
? await buildRuntimeParameterContext({
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI-powered text interpretation for Loadmill commands
|
|
3
3
|
*/
|
|
4
|
+
import { LOADMILL_MODEL_ALIASES } from "../../core/runtime-config.js";
|
|
4
5
|
import { buildLoadmillCommandInterpretationMessages, buildLoadmillFlowSelectionMessages, } from "../../prompts/loadmill.js";
|
|
5
6
|
/**
|
|
6
7
|
* Interpret a natural language Loadmill command into structured data
|
|
7
8
|
* @param {string} userInput - Natural language command
|
|
8
|
-
* @returns {Promise<{searchQuery: string, parameters: Object
|
|
9
|
+
* @returns {Promise<{searchQuery: string, parameters: Object}>}
|
|
9
10
|
*/
|
|
10
11
|
export async function interpretLoadmillCommand(userInput, { client }) {
|
|
11
12
|
const response = await client.chat.completions.create({
|
|
12
|
-
model:
|
|
13
|
+
model: LOADMILL_MODEL_ALIASES.FAST,
|
|
13
14
|
messages: buildLoadmillCommandInterpretationMessages(userInput),
|
|
14
15
|
response_format: { type: "json_object" }
|
|
15
16
|
});
|
|
@@ -18,7 +19,6 @@ export async function interpretLoadmillCommand(userInput, { client }) {
|
|
|
18
19
|
return {
|
|
19
20
|
searchQuery: parsed.searchQuery || userInput,
|
|
20
21
|
parameters: parsed.parameters || {},
|
|
21
|
-
action: parsed.action || "run"
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
@@ -42,7 +42,7 @@ export async function selectBestFlow(flows, originalQuery, { client }) {
|
|
|
42
42
|
return `${i + 1}. ID: ${f.id}, Name: "${name}"${suite ? `, Suite: "${suite}"` : ""}`;
|
|
43
43
|
}).join("\n");
|
|
44
44
|
const response = await client.chat.completions.create({
|
|
45
|
-
model:
|
|
45
|
+
model: LOADMILL_MODEL_ALIASES.FAST,
|
|
46
46
|
messages: buildLoadmillFlowSelectionMessages(originalQuery, flowList),
|
|
47
47
|
response_format: { type: "json_object" }
|
|
48
48
|
});
|
|
@@ -8,6 +8,31 @@ import { isAssertion } from "../device/assertions.js";
|
|
|
8
8
|
import { logger } from "../utils/logger.js";
|
|
9
9
|
import { emitDesktopDebug } from "../utils/desktop-debug.js";
|
|
10
10
|
import { printCliOutput } from "../utils/console-output.js";
|
|
11
|
+
// Mirrors the codes emitted by the Loadmill LLM gateway (see services/app/private/llm
|
|
12
|
+
// in the server repo): a transient burst limit plus day/week/fixed-budget request and
|
|
13
|
+
// token quotas. Free plans run on the fixed-budget quota.
|
|
14
|
+
const LLM_USAGE_LIMIT_ERROR_CODES = new Set([
|
|
15
|
+
"llm_rate_limit_exceeded",
|
|
16
|
+
"llm_day_quota_exceeded",
|
|
17
|
+
"llm_week_quota_exceeded",
|
|
18
|
+
"llm_fixed_budget_quota_exceeded",
|
|
19
|
+
"llm_day_token_quota_exceeded",
|
|
20
|
+
"llm_week_token_quota_exceeded",
|
|
21
|
+
"llm_fixed_budget_token_quota_exceeded",
|
|
22
|
+
]);
|
|
23
|
+
const LOADMILL_UPGRADE_URL = "https://www.loadmill.com/book-a-demo";
|
|
24
|
+
function isLlmUsageLimitError(err) {
|
|
25
|
+
if (!err || typeof err !== "object") {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (typeof err.code === "string" && LLM_USAGE_LIMIT_ERROR_CODES.has(err.code)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return err.status === 429 && err.type === "rate_limit_error";
|
|
32
|
+
}
|
|
33
|
+
function getLlmUsageLimitMessage() {
|
|
34
|
+
return `Execution stopped: you've reached the AI usage limit for this account. Contact the Loadmill team to upgrade: ${LOADMILL_UPGRADE_URL}`;
|
|
35
|
+
}
|
|
11
36
|
/**
|
|
12
37
|
* Execution Mode - Run test scripts line-by-line
|
|
13
38
|
* Each instruction is executed in isolation (messages cleared after each turn)
|
|
@@ -267,6 +292,26 @@ export class ExecutionMode {
|
|
|
267
292
|
stack: err.stack
|
|
268
293
|
});
|
|
269
294
|
const addOutput = context.addOutput || printCliOutput;
|
|
295
|
+
if (isLlmUsageLimitError(err)) {
|
|
296
|
+
const stopMessage = getLlmUsageLimitMessage();
|
|
297
|
+
logger.debug('Stopping execution because an LLM usage limit was reached', {
|
|
298
|
+
instruction,
|
|
299
|
+
message: err.message,
|
|
300
|
+
status: err.status,
|
|
301
|
+
code: err.code,
|
|
302
|
+
type: err.type,
|
|
303
|
+
});
|
|
304
|
+
this.emit(addOutput, 'error', stopMessage, context, stepContext, {
|
|
305
|
+
eventType: 'error',
|
|
306
|
+
payload: {
|
|
307
|
+
message: stopMessage,
|
|
308
|
+
status: err.status,
|
|
309
|
+
code: err.code,
|
|
310
|
+
type: err.type,
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
return { success: false, error: stopMessage, usageLimitReached: true };
|
|
314
|
+
}
|
|
270
315
|
// Check if we've exceeded max retries
|
|
271
316
|
if (retryCount >= MAX_RETRIES) {
|
|
272
317
|
emitDesktopDebug("reconnect.attempt", "device", {
|
|
@@ -44,7 +44,7 @@ Your process:
|
|
|
44
44
|
- Before calling \`assert\`, make sure the current screen contains the evidence needed to validate the condition. If needed, perform only minimal preparation first, such as taking a screenshot, waiting briefly, closing the keyboard, scrolling within the relevant view, or opening the relevant menu.
|
|
45
45
|
- Once you call \`assert\`, validation is read-only. The assertion tool will not click, type, wait, scroll, request screenshots, or change the device state.
|
|
46
46
|
- Use \`navigate\` for direct browser navigation instead of trying to manipulate the address bar
|
|
47
|
-
- Use \`loadmill\`
|
|
47
|
+
- Use the \`loadmill\` tool whenever the instruction explicitly references running or using a Loadmill flow. Pass the requested flow name/intent and parameters as the command; the tool will search Loadmill, select the best matching flow, run it, and wait for completion. Do not handle Loadmill by typing in the app UI.
|
|
48
48
|
3. Execute the required actions
|
|
49
49
|
4. Before tapping a mutating action, dismiss the keyboard if it is open and not needed
|
|
50
50
|
5. After a mutating action, inspect the resulting screen for success cues such as cleared fields, reset buttons, changed status, refreshed content, or navigation
|
|
@@ -7,25 +7,21 @@ export function buildLoadmillCommandInterpretationMessages(userInput) {
|
|
|
7
7
|
Extract the following from the user's input:
|
|
8
8
|
1. searchQuery: The flow name or description to search for (required). FIX any obvious typos or misspellings.
|
|
9
9
|
2. parameters: Any key=value pairs mentioned (as an object)
|
|
10
|
-
3. action: Either "run" (if user wants to execute) or "search" (if user just wants to find flows)
|
|
11
10
|
|
|
12
11
|
Output JSON only, no markdown or explanation.
|
|
13
12
|
|
|
14
13
|
Examples:
|
|
15
14
|
Input: "run the checkout flow with user=test123"
|
|
16
|
-
Output: {"searchQuery": "checkout flow", "parameters": {"user": "test123"}
|
|
17
|
-
|
|
18
|
-
Input: "search for login test"
|
|
19
|
-
Output: {"searchQuery": "login test", "parameters": {}, "action": "search"}
|
|
15
|
+
Output: {"searchQuery": "checkout flow", "parameters": {"user": "test123"}}
|
|
20
16
|
|
|
21
17
|
Input: "run user authentication with email=test@example.com password=secret123"
|
|
22
|
-
Output: {"searchQuery": "user authentication", "parameters": {"email": "test@example.com", "password": "secret123"}
|
|
18
|
+
Output: {"searchQuery": "user authentication", "parameters": {"email": "test@example.com", "password": "secret123"}}
|
|
23
19
|
|
|
24
20
|
Input: "execute payment flow"
|
|
25
|
-
Output: {"searchQuery": "payment flow", "parameters": {}
|
|
21
|
+
Output: {"searchQuery": "payment flow", "parameters": {}}
|
|
26
22
|
|
|
27
23
|
Input: "create a transction with amount=200"
|
|
28
|
-
Output: {"searchQuery": "transaction", "parameters": {"amount": "200"}
|
|
24
|
+
Output: {"searchQuery": "transaction", "parameters": {"amount": "200"}}`
|
|
29
25
|
},
|
|
30
26
|
{
|
|
31
27
|
role: "user",
|
|
@@ -23,6 +23,17 @@ function safeHeadersSnapshot(headers) {
|
|
|
23
23
|
}
|
|
24
24
|
return out;
|
|
25
25
|
}
|
|
26
|
+
function summarizeErrorCause(error) {
|
|
27
|
+
const cause = error?.cause;
|
|
28
|
+
if (!cause || typeof cause !== "object") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
name: typeof cause.name === "string" ? cause.name : null,
|
|
33
|
+
message: typeof cause.message === "string" ? cause.message : String(cause),
|
|
34
|
+
code: typeof cause.code === "string" ? cause.code : null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
26
37
|
function summarizeContent(content) {
|
|
27
38
|
if (typeof content === "string") {
|
|
28
39
|
return truncateForDebug(content, 1600);
|
|
@@ -394,6 +405,7 @@ export class CuaDebugTracer {
|
|
|
394
405
|
: null;
|
|
395
406
|
const requestId = err?.request_id || responseError?.request_id || requestIdFromHeaders || null;
|
|
396
407
|
const headers = safeHeadersSnapshot(err?.headers);
|
|
408
|
+
const provider = typeof responseError?.provider === "string" ? responseError.provider : null;
|
|
397
409
|
emitDesktopDebug("device.error", "device", trace.ids, {
|
|
398
410
|
localRequestId: trace.localRequestId,
|
|
399
411
|
operation: "cua.request",
|
|
@@ -403,8 +415,11 @@ export class CuaDebugTracer {
|
|
|
403
415
|
code: err?.code ?? responseError?.code ?? null,
|
|
404
416
|
type: responseError?.type ?? null,
|
|
405
417
|
param: responseError?.param ?? null,
|
|
418
|
+
provider,
|
|
406
419
|
requestId,
|
|
407
420
|
headers,
|
|
421
|
+
endpoint: trace.endpoint || null,
|
|
422
|
+
cause: summarizeErrorCause(err),
|
|
408
423
|
chain: {
|
|
409
424
|
lastResponseId: trace.lastResponseMeta?.id ?? null,
|
|
410
425
|
lastResponseOutputTypes: trace.lastResponseMeta?.outputTypes ?? [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loadmill/droid-cua",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "AI-powered Android testing agent using OpenAI's computer-use model and ADB",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -15,15 +15,19 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"start": "tsx index.js",
|
|
18
|
-
"bench": "
|
|
18
|
+
"bench": "node scripts/benchmark.js",
|
|
19
19
|
"bench:preflight": "node scripts/benchmark-preflight.js",
|
|
20
20
|
"bench:run": "tsx index.js --platform android --instructions benchmarks/benchmark.dcua --context benchmarks/context.md --debug",
|
|
21
21
|
"bench:report": "node scripts/benchmark-report.js",
|
|
22
|
+
"bench:report:web": "node scripts/benchmark-report-web.js",
|
|
22
23
|
"execution:profile": "node scripts/execution-profile.js",
|
|
23
24
|
"test": "node --test unit-tests/*.test.js",
|
|
24
25
|
"build": "tsc",
|
|
25
26
|
"clean": "rm -rf build",
|
|
26
27
|
"typecheck": "tsc --noEmit",
|
|
28
|
+
"typecheck:desktop": "npm --prefix desktop-app run typecheck",
|
|
29
|
+
"typecheck:all": "npm run typecheck && npm run typecheck:desktop",
|
|
30
|
+
"prepare": "node scripts/install-git-hooks.mjs",
|
|
27
31
|
"prepublishOnly": "npm run build"
|
|
28
32
|
},
|
|
29
33
|
"repository": {
|