@loadmill/droid-cua 2.8.0 → 2.10.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 +7 -6
- package/build/index.js +89 -71
- package/build/src/brand/loadmill-mark.js +20 -0
- package/build/src/browser/playwright/connection.js +160 -12
- package/build/src/browser/playwright/welcome-page.js +161 -0
- package/build/src/cli/headless-execution-config.js +18 -58
- package/build/src/cli/headless-target.js +0 -9
- package/build/src/cli/ink-shell.js +5 -2
- package/build/src/commands/help.js +5 -8
- package/build/src/commands/loadmill.js +10 -1
- package/build/src/commands/run.js +17 -28
- package/build/src/core/aim-assist.js +5 -0
- package/build/src/core/app-context.js +57 -33
- package/build/src/core/context-optimization.js +1 -1
- package/build/src/core/cua-models.js +9 -0
- package/build/src/core/execution-engine.js +121 -199
- package/build/src/core/runtime-config.js +95 -0
- package/build/src/device/loadmill.js +15 -9
- package/build/src/device/openai.js +76 -142
- package/build/src/integrations/loadmill/client.js +149 -131
- package/build/src/integrations/loadmill/executor.js +17 -11
- package/build/src/integrations/loadmill/index.js +1 -1
- package/build/src/integrations/loadmill/interpreter.js +4 -14
- package/build/src/modes/design-mode-ink.js +5 -6
- package/build/src/modes/design-mode.js +5 -6
- package/build/src/modes/execution-mode.js +26 -0
- package/build/src/prompts/base.js +44 -30
- package/build/src/prompts/design.js +3 -4
- package/build/src/prompts/execution.js +13 -110
- package/build/src/utils/console-output.js +8 -0
- package/build/src/utils/cua-debug-tracer.js +0 -1
- package/build/src/utils/structured-debug-log-manager.js +4 -1
- package/package.json +1 -1
|
@@ -9,7 +9,6 @@ import { getConfiguredStepDelayMs } from "../utils/step-delay.js";
|
|
|
9
9
|
import { printCliOutput } from "../utils/console-output.js";
|
|
10
10
|
import { annotateScreenshotArtifact, createAnnotatedScreenshotFromBase64 } from "../utils/run-screenshot-recorder.js";
|
|
11
11
|
import { CAPTURE_SOURCES } from "./capture-sources.js";
|
|
12
|
-
const VALID_CUA_MODELS = new Set(["gpt-5.4", "gpt-5.4-mini", "computer-use-preview"]);
|
|
13
12
|
function extractActionableOutputItems(items) {
|
|
14
13
|
const entries = [];
|
|
15
14
|
for (const item of items) {
|
|
@@ -102,55 +101,20 @@ function buildPreviewOutputContent({ text, annotatedScreenshotBase64 = null }) {
|
|
|
102
101
|
}
|
|
103
102
|
return content;
|
|
104
103
|
}
|
|
105
|
-
function
|
|
106
|
-
const scope = context?.sessionId ? "design" : "execution";
|
|
107
|
-
const ids = scope === "design"
|
|
108
|
-
? {
|
|
109
|
-
sessionId: context?.sessionId,
|
|
110
|
-
stepId: stepContext?.stepId,
|
|
111
|
-
instructionIndex: stepContext?.instructionIndex
|
|
112
|
-
}
|
|
113
|
-
: {
|
|
114
|
-
runId: context?.runId,
|
|
115
|
-
stepId: stepContext?.stepId,
|
|
116
|
-
instructionIndex: stepContext?.instructionIndex
|
|
117
|
-
};
|
|
118
|
-
return { scope, ids };
|
|
119
|
-
}
|
|
120
|
-
function buildStrictModePlan(actions = [], strictMode = false) {
|
|
121
|
-
if (!strictMode || actions.length === 0) {
|
|
122
|
-
return {
|
|
123
|
-
actionsToExecute: actions,
|
|
124
|
-
droppedActions: [],
|
|
125
|
-
truncationReason: null,
|
|
126
|
-
runtimeNote: null
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
const actionsToExecute = [actions[0]];
|
|
130
|
-
const droppedActions = actions.slice(1);
|
|
131
|
-
if (droppedActions.length === 0) {
|
|
132
|
-
return {
|
|
133
|
-
actionsToExecute,
|
|
134
|
-
droppedActions,
|
|
135
|
-
truncationReason: null,
|
|
136
|
-
runtimeNote: null
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
const leadingActionType = actionsToExecute[0]?.type;
|
|
140
|
-
const truncationReason = leadingActionType === "screenshot"
|
|
141
|
-
? "leading_screenshot_reobserve"
|
|
142
|
-
: "post_first_action_reobserve";
|
|
143
|
-
const droppedActionTypes = droppedActions
|
|
144
|
-
.map((action) => action?.type)
|
|
145
|
-
.filter(Boolean);
|
|
146
|
-
const runtimeNote = `Strict Mode: I executed only the first ${leadingActionType === "screenshot" ? "screenshot request" : "action"} from your previous chain and intentionally skipped the remaining ${droppedActions.length} action${droppedActions.length === 1 ? "" : "s"} (${droppedActionTypes.join(", ")}) so I could re-observe the device before continuing. Base your next step only on what is visible now.`;
|
|
104
|
+
function buildDebugContext(context = null, stepContext = null) {
|
|
147
105
|
return {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
106
|
+
scope: context?.sessionId ? "design" : "execution",
|
|
107
|
+
runId: context?.runId,
|
|
108
|
+
sessionId: context?.sessionId,
|
|
109
|
+
stepId: stepContext?.stepId,
|
|
110
|
+
instructionIndex: stepContext?.instructionIndex
|
|
152
111
|
};
|
|
153
112
|
}
|
|
113
|
+
function collectPendingSafetyChecks(items = []) {
|
|
114
|
+
return items
|
|
115
|
+
.filter((item) => item.type === "pending_safety_check")
|
|
116
|
+
.map((item) => ({ id: item.id }));
|
|
117
|
+
}
|
|
154
118
|
function normalizeArtifactPaths(result) {
|
|
155
119
|
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
156
120
|
if (Array.isArray(result.annotationSourcePaths)) {
|
|
@@ -188,14 +152,14 @@ export class ExecutionEngine {
|
|
|
188
152
|
this.recordScreenshots = options.recordScreenshots || false;
|
|
189
153
|
this.screenshotDir = options.screenshotDir || null;
|
|
190
154
|
this.screenshotRecorder = options.screenshotRecorder || null;
|
|
191
|
-
this.
|
|
192
|
-
this.stepDelayMs = getConfiguredStepDelayMs();
|
|
155
|
+
this.stepDelayMs = options.stepDelayMs ?? getConfiguredStepDelayMs();
|
|
193
156
|
this.reportedScreenshotWriteError = false;
|
|
194
157
|
this.reportedScreenshotAnnotateError = false;
|
|
195
158
|
this.lastRecordedScreenshotPaths = [];
|
|
196
159
|
this.getScreenshotAsBase64 = options.getScreenshotAsBase64 || getScreenshotAsBase64;
|
|
197
160
|
this.handleModelAction = options.handleModelAction || handleModelAction;
|
|
198
161
|
this.sendCUARequest = options.sendCUARequest || sendCUARequest;
|
|
162
|
+
this.reviseTestScript = options.reviseTestScript || null;
|
|
199
163
|
this.getCurrentPlatform = options.getCurrentPlatform || getCurrentPlatform;
|
|
200
164
|
}
|
|
201
165
|
async saveScreenshotArtifact(screenshotBase64, metadata = {}, options = {}) {
|
|
@@ -374,6 +338,108 @@ export class ExecutionEngine {
|
|
|
374
338
|
}));
|
|
375
339
|
}
|
|
376
340
|
}
|
|
341
|
+
async requestNextTurn(messages, previousResponseId, context = null, stepContext = null) {
|
|
342
|
+
return await this.sendCUARequest({
|
|
343
|
+
messages,
|
|
344
|
+
previousResponseId,
|
|
345
|
+
deviceInfo: this.session.deviceInfo,
|
|
346
|
+
debugContext: buildDebugContext(context, stepContext)
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
async executeActionBatch(actions, shouldStop, context = null, stepContext = null, trackAction = null) {
|
|
350
|
+
let sawExplicitScreenshotAction = false;
|
|
351
|
+
for (const action of actions) {
|
|
352
|
+
const actionEventId = randomUUID();
|
|
353
|
+
if (shouldStop()) {
|
|
354
|
+
return { stopped: true, sawExplicitScreenshotAction };
|
|
355
|
+
}
|
|
356
|
+
if (action.type === "screenshot") {
|
|
357
|
+
sawExplicitScreenshotAction = true;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const annotatedScreenshotResult = await this.annotateLatestScreenshot(action, {
|
|
361
|
+
runId: context?.runId,
|
|
362
|
+
sessionId: context?.sessionId,
|
|
363
|
+
stepId: stepContext?.stepId,
|
|
364
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
365
|
+
eventId: actionEventId,
|
|
366
|
+
captureSource: CAPTURE_SOURCES.ACTION_CLICK_ANNOTATION
|
|
367
|
+
});
|
|
368
|
+
const annotatedScreenshotPath = screenshotPathFromRecorderResult(annotatedScreenshotResult);
|
|
369
|
+
const annotatedScreenshotDataUrl = screenshotDataUrlFromRecorderResult(annotatedScreenshotResult);
|
|
370
|
+
await this.handleModelAction(this.session.deviceId, action, this.session.deviceInfo.scale, {
|
|
371
|
+
...context,
|
|
372
|
+
shouldStop,
|
|
373
|
+
stepId: stepContext?.stepId,
|
|
374
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
375
|
+
eventId: actionEventId,
|
|
376
|
+
captureSource: CAPTURE_SOURCES.ACTION_CLICK_ANNOTATION,
|
|
377
|
+
...(annotatedScreenshotPath ? { screenshotPath: annotatedScreenshotPath } : {}),
|
|
378
|
+
...(context?.includeScreenshotDataUrl && annotatedScreenshotDataUrl ? { screenshotDataUrl: annotatedScreenshotDataUrl } : {})
|
|
379
|
+
});
|
|
380
|
+
if (trackAction) {
|
|
381
|
+
trackAction(action);
|
|
382
|
+
}
|
|
383
|
+
if (shouldStop()) {
|
|
384
|
+
return { stopped: true, sawExplicitScreenshotAction };
|
|
385
|
+
}
|
|
386
|
+
if (action.type !== "wait") {
|
|
387
|
+
await new Promise((resolve) => setTimeout(resolve, this.stepDelayMs));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return { stopped: false, sawExplicitScreenshotAction };
|
|
391
|
+
}
|
|
392
|
+
async buildActionFollowUp(callId, pendingSafetyChecks, sawExplicitScreenshotAction, addOutput, eventMeta, context = null, stepContext = null) {
|
|
393
|
+
const screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
|
|
394
|
+
emitDesktopDebug("device.screenshot", "device", {
|
|
395
|
+
runId: context?.runId,
|
|
396
|
+
stepId: stepContext?.stepId,
|
|
397
|
+
instructionIndex: stepContext?.instructionIndex
|
|
398
|
+
}, {
|
|
399
|
+
source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
|
|
400
|
+
callId,
|
|
401
|
+
width: this.session.deviceInfo?.scaled_width,
|
|
402
|
+
height: this.session.deviceInfo?.scaled_height,
|
|
403
|
+
base64Length: screenshotBase64.length
|
|
404
|
+
});
|
|
405
|
+
const screenshotEventId = randomUUID();
|
|
406
|
+
const captureSource = sawExplicitScreenshotAction ? CAPTURE_SOURCES.CALL_OUTPUT_EXPLICIT_ACTION : CAPTURE_SOURCES.CALL_OUTPUT_POST_ACTION;
|
|
407
|
+
const screenshotResult = await this.recordScreenshot(screenshotBase64, {
|
|
408
|
+
runId: context?.runId,
|
|
409
|
+
sessionId: context?.sessionId,
|
|
410
|
+
stepId: stepContext?.stepId,
|
|
411
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
412
|
+
eventId: screenshotEventId,
|
|
413
|
+
captureSource
|
|
414
|
+
});
|
|
415
|
+
const screenshotPath = screenshotPathFromRecorderResult(screenshotResult);
|
|
416
|
+
addOutput({
|
|
417
|
+
type: "info",
|
|
418
|
+
text: "Capturing screen",
|
|
419
|
+
eventType: "screenshot_captured",
|
|
420
|
+
actionType: "screenshot",
|
|
421
|
+
...eventMeta({
|
|
422
|
+
payload: {
|
|
423
|
+
eventId: screenshotEventId,
|
|
424
|
+
captureSource,
|
|
425
|
+
source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
|
|
426
|
+
...(typeof screenshotPath === "string" && screenshotPath ? { screenshotPath } : {}),
|
|
427
|
+
...(context?.includeScreenshotDataUrl
|
|
428
|
+
? { screenshotDataUrl: `data:image/png;base64,${screenshotBase64}` }
|
|
429
|
+
: {})
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
});
|
|
433
|
+
return [{
|
|
434
|
+
type: "computer_call_output",
|
|
435
|
+
call_id: callId,
|
|
436
|
+
output: {
|
|
437
|
+
type: "computer_screenshot",
|
|
438
|
+
image_url: `data:image/png;base64,${screenshotBase64}`,
|
|
439
|
+
},
|
|
440
|
+
...(pendingSafetyChecks.length > 0 ? { acknowledged_safety_checks: pendingSafetyChecks } : {})
|
|
441
|
+
}];
|
|
442
|
+
}
|
|
377
443
|
/**
|
|
378
444
|
* Run a full turn with the CUA model
|
|
379
445
|
* Executes actions until the model stops requesting actions
|
|
@@ -386,7 +452,6 @@ export class ExecutionEngine {
|
|
|
386
452
|
const addOutput = context?.addOutput || printCliOutput;
|
|
387
453
|
let newResponseId = response.id;
|
|
388
454
|
const shouldStop = () => Boolean(trackAction?.());
|
|
389
|
-
const { scope, ids } = getScopeAndIds(context, stepContext);
|
|
390
455
|
const eventMeta = (extra = {}) => ({
|
|
391
456
|
runId: context?.runId,
|
|
392
457
|
stepId: stepContext?.stepId,
|
|
@@ -400,10 +465,7 @@ export class ExecutionEngine {
|
|
|
400
465
|
}
|
|
401
466
|
const items = response.output || [];
|
|
402
467
|
const actionableItems = extractActionableOutputItems(items);
|
|
403
|
-
|
|
404
|
-
const pendingSafetyChecks = items
|
|
405
|
-
.filter(item => item.type === "pending_safety_check")
|
|
406
|
-
.map(item => ({ id: item.id }));
|
|
468
|
+
const pendingSafetyChecks = collectPendingSafetyChecks(items);
|
|
407
469
|
// ── Print assistant output ──
|
|
408
470
|
for (const item of items) {
|
|
409
471
|
if (item.type === "reasoning") {
|
|
@@ -461,160 +523,20 @@ export class ExecutionEngine {
|
|
|
461
523
|
if (shouldStop()) {
|
|
462
524
|
return newResponseId;
|
|
463
525
|
}
|
|
464
|
-
response = await this.
|
|
465
|
-
messages: input,
|
|
466
|
-
previousResponseId: newResponseId,
|
|
467
|
-
deviceInfo: this.session.deviceInfo,
|
|
468
|
-
debugContext: {
|
|
469
|
-
scope,
|
|
470
|
-
runId: context?.runId,
|
|
471
|
-
sessionId: context?.sessionId,
|
|
472
|
-
stepId: stepContext?.stepId,
|
|
473
|
-
instructionIndex: stepContext?.instructionIndex
|
|
474
|
-
}
|
|
475
|
-
});
|
|
526
|
+
response = await this.requestNextTurn(input, newResponseId, context, stepContext);
|
|
476
527
|
newResponseId = response.id;
|
|
477
528
|
continue;
|
|
478
529
|
}
|
|
479
530
|
const { call_id, actions } = actionableItem;
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
for (const action of actionsToExecute) {
|
|
483
|
-
const actionEventId = randomUUID();
|
|
484
|
-
if (shouldStop()) {
|
|
485
|
-
return newResponseId;
|
|
486
|
-
}
|
|
487
|
-
if (action.type === "screenshot") {
|
|
488
|
-
sawExplicitScreenshotAction = true;
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
const annotatedScreenshotResult = await this.annotateLatestScreenshot(action, {
|
|
492
|
-
runId: context?.runId,
|
|
493
|
-
sessionId: context?.sessionId,
|
|
494
|
-
stepId: stepContext?.stepId,
|
|
495
|
-
instructionIndex: stepContext?.instructionIndex,
|
|
496
|
-
eventId: actionEventId,
|
|
497
|
-
captureSource: CAPTURE_SOURCES.ACTION_CLICK_ANNOTATION
|
|
498
|
-
});
|
|
499
|
-
const annotatedScreenshotPath = screenshotPathFromRecorderResult(annotatedScreenshotResult);
|
|
500
|
-
const annotatedScreenshotDataUrl = screenshotDataUrlFromRecorderResult(annotatedScreenshotResult);
|
|
501
|
-
await this.handleModelAction(this.session.deviceId, action, this.session.deviceInfo.scale, {
|
|
502
|
-
...context,
|
|
503
|
-
shouldStop,
|
|
504
|
-
stepId: stepContext?.stepId,
|
|
505
|
-
instructionIndex: stepContext?.instructionIndex,
|
|
506
|
-
eventId: actionEventId,
|
|
507
|
-
captureSource: CAPTURE_SOURCES.ACTION_CLICK_ANNOTATION,
|
|
508
|
-
...(annotatedScreenshotPath ? { screenshotPath: annotatedScreenshotPath } : {}),
|
|
509
|
-
...(context?.includeScreenshotDataUrl && annotatedScreenshotDataUrl ? { screenshotDataUrl: annotatedScreenshotDataUrl } : {})
|
|
510
|
-
});
|
|
511
|
-
if (trackAction) {
|
|
512
|
-
trackAction(action);
|
|
513
|
-
}
|
|
514
|
-
if (shouldStop()) {
|
|
515
|
-
return newResponseId;
|
|
516
|
-
}
|
|
517
|
-
if (action.type !== "wait") {
|
|
518
|
-
await new Promise(resolve => setTimeout(resolve, this.stepDelayMs));
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
if (shouldStop()) {
|
|
531
|
+
const actionBatchResult = await this.executeActionBatch(actions, shouldStop, context, stepContext, trackAction);
|
|
532
|
+
if (actionBatchResult.stopped) {
|
|
523
533
|
return newResponseId;
|
|
524
534
|
}
|
|
525
|
-
const
|
|
526
|
-
emitDesktopDebug("device.screenshot", "device", {
|
|
527
|
-
runId: context?.runId,
|
|
528
|
-
stepId: stepContext?.stepId,
|
|
529
|
-
instructionIndex: stepContext?.instructionIndex
|
|
530
|
-
}, {
|
|
531
|
-
source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
|
|
532
|
-
callId: call_id,
|
|
533
|
-
width: this.session.deviceInfo?.scaled_width,
|
|
534
|
-
height: this.session.deviceInfo?.scaled_height,
|
|
535
|
-
base64Length: screenshotBase64.length
|
|
536
|
-
});
|
|
537
|
-
const screenshotEventId = randomUUID();
|
|
538
|
-
const captureSource = sawExplicitScreenshotAction ? CAPTURE_SOURCES.CALL_OUTPUT_EXPLICIT_ACTION : CAPTURE_SOURCES.CALL_OUTPUT_POST_ACTION;
|
|
539
|
-
const screenshotResult = await this.recordScreenshot(screenshotBase64, {
|
|
540
|
-
runId: context?.runId,
|
|
541
|
-
sessionId: context?.sessionId,
|
|
542
|
-
stepId: stepContext?.stepId,
|
|
543
|
-
instructionIndex: stepContext?.instructionIndex,
|
|
544
|
-
eventId: screenshotEventId,
|
|
545
|
-
captureSource
|
|
546
|
-
});
|
|
547
|
-
const screenshotPath = screenshotPathFromRecorderResult(screenshotResult);
|
|
548
|
-
addOutput({
|
|
549
|
-
type: 'info',
|
|
550
|
-
text: 'Capturing screen',
|
|
551
|
-
eventType: 'screenshot_captured',
|
|
552
|
-
actionType: 'screenshot',
|
|
553
|
-
...eventMeta({
|
|
554
|
-
payload: {
|
|
555
|
-
eventId: screenshotEventId,
|
|
556
|
-
captureSource,
|
|
557
|
-
source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
|
|
558
|
-
...(typeof screenshotPath === "string" && screenshotPath ? { screenshotPath } : {}),
|
|
559
|
-
...(context?.includeScreenshotDataUrl
|
|
560
|
-
? { screenshotDataUrl: `data:image/png;base64,${screenshotBase64}` }
|
|
561
|
-
: {})
|
|
562
|
-
}
|
|
563
|
-
})
|
|
564
|
-
});
|
|
565
|
-
if (runtimeNote) {
|
|
566
|
-
const executedActionTypes = actionsToExecute
|
|
567
|
-
.map((action) => action?.type)
|
|
568
|
-
.filter(Boolean);
|
|
569
|
-
const droppedActionTypes = droppedActions
|
|
570
|
-
.map((action) => action?.type)
|
|
571
|
-
.filter(Boolean);
|
|
572
|
-
emitDesktopDebug("cua.strict_mode.truncation", scope, ids, {
|
|
573
|
-
callId: call_id,
|
|
574
|
-
executedCount: actionsToExecute.length,
|
|
575
|
-
droppedCount: droppedActions.length,
|
|
576
|
-
executedActionTypes,
|
|
577
|
-
droppedActionTypes,
|
|
578
|
-
reason: truncationReason,
|
|
579
|
-
note: runtimeNote
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
const selectedCuaModel = VALID_CUA_MODELS.has(process.env.OPENAI_CUA_MODEL)
|
|
583
|
-
? process.env.OPENAI_CUA_MODEL
|
|
584
|
-
: "gpt-5.4";
|
|
585
|
-
const input = [{
|
|
586
|
-
type: "computer_call_output",
|
|
587
|
-
call_id,
|
|
588
|
-
output: {
|
|
589
|
-
type: "computer_screenshot",
|
|
590
|
-
image_url: `data:image/png;base64,${screenshotBase64}`,
|
|
591
|
-
},
|
|
592
|
-
...(selectedCuaModel === "computer-use-preview"
|
|
593
|
-
? { current_url: this.getCurrentPlatform() === "ios" ? "ios://simulator" : "android://device" }
|
|
594
|
-
: {}),
|
|
595
|
-
...(pendingSafetyChecks.length > 0 ? { acknowledged_safety_checks: pendingSafetyChecks } : {})
|
|
596
|
-
}];
|
|
597
|
-
if (runtimeNote) {
|
|
598
|
-
input.push({
|
|
599
|
-
role: "user",
|
|
600
|
-
content: runtimeNote
|
|
601
|
-
});
|
|
602
|
-
}
|
|
535
|
+
const input = await this.buildActionFollowUp(call_id, pendingSafetyChecks, actionBatchResult.sawExplicitScreenshotAction, addOutput, eventMeta, context, stepContext);
|
|
603
536
|
if (shouldStop()) {
|
|
604
537
|
return newResponseId;
|
|
605
538
|
}
|
|
606
|
-
response = await this.
|
|
607
|
-
messages: input,
|
|
608
|
-
previousResponseId: newResponseId,
|
|
609
|
-
deviceInfo: this.session.deviceInfo,
|
|
610
|
-
debugContext: {
|
|
611
|
-
scope,
|
|
612
|
-
runId: context?.runId,
|
|
613
|
-
sessionId: context?.sessionId,
|
|
614
|
-
stepId: stepContext?.stepId,
|
|
615
|
-
instructionIndex: stepContext?.instructionIndex
|
|
616
|
-
}
|
|
617
|
-
});
|
|
539
|
+
response = await this.requestNextTurn(input, newResponseId, context, stepContext);
|
|
618
540
|
newResponseId = response.id;
|
|
619
541
|
}
|
|
620
542
|
// ── At end, if last output was only reasoning ──
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { readContextOptimizationEnv, normalizeContextOptimizationEnabled, normalizeContextOptimizationThreshold, } from "./context-optimization.js";
|
|
3
|
+
import { resolveCuaModel } from "./cua-models.js";
|
|
4
|
+
import { normalizeStepDelayMs } from "../utils/step-delay.js";
|
|
5
|
+
export function createEmptyPromptCustomizations() {
|
|
6
|
+
return {
|
|
7
|
+
basePromptInstructions: "",
|
|
8
|
+
designModeInstructions: "",
|
|
9
|
+
executionModeInstructions: "",
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function resolvePromptCustomizations(rawValue = null) {
|
|
13
|
+
const resolved = createEmptyPromptCustomizations();
|
|
14
|
+
if (!rawValue || typeof rawValue !== "object" || Array.isArray(rawValue)) {
|
|
15
|
+
return resolved;
|
|
16
|
+
}
|
|
17
|
+
for (const key of Object.keys(resolved)) {
|
|
18
|
+
if (typeof rawValue[key] === "string") {
|
|
19
|
+
resolved[key] = rawValue[key];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return resolved;
|
|
23
|
+
}
|
|
24
|
+
function trimNullableString(value) {
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
return trimmed || null;
|
|
30
|
+
}
|
|
31
|
+
export function resolveRuntimeCredentials({ openAiApiKey = null, loadmillApiToken = null, loadmillBaseUrl = null, } = {}) {
|
|
32
|
+
return {
|
|
33
|
+
openAiApiKey: trimNullableString(openAiApiKey),
|
|
34
|
+
loadmillApiToken: trimNullableString(loadmillApiToken),
|
|
35
|
+
loadmillBaseUrl: trimNullableString(loadmillBaseUrl),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function resolveRuntimeConfig({ cuaModel = null, stepDelayMs = undefined, contextOptimizationEnabled = undefined, contextOptimizationThreshold = undefined, promptCustomizations = null, appContext = null, } = {}) {
|
|
39
|
+
return {
|
|
40
|
+
cuaModel: resolveCuaModel(cuaModel),
|
|
41
|
+
stepDelayMs: normalizeStepDelayMs(Number(stepDelayMs)),
|
|
42
|
+
contextOptimizationEnabled: normalizeContextOptimizationEnabled(contextOptimizationEnabled),
|
|
43
|
+
contextOptimizationThreshold: normalizeContextOptimizationThreshold(contextOptimizationThreshold),
|
|
44
|
+
promptCustomizations: resolvePromptCustomizations(promptCustomizations),
|
|
45
|
+
appContext: appContext && typeof appContext === "object"
|
|
46
|
+
? {
|
|
47
|
+
enabled: appContext.enabled !== false,
|
|
48
|
+
path: trimNullableString(appContext.path ?? appContext.contextPath),
|
|
49
|
+
status: typeof appContext.status === "string" ? appContext.status : null,
|
|
50
|
+
text: typeof appContext.text === "string" ? appContext.text : "",
|
|
51
|
+
message: trimNullableString(appContext.message),
|
|
52
|
+
}
|
|
53
|
+
: {
|
|
54
|
+
enabled: true,
|
|
55
|
+
path: null,
|
|
56
|
+
status: null,
|
|
57
|
+
text: "",
|
|
58
|
+
message: null,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function resolveRuntimeConfigFromEnv(env = process.env, overrides = {}) {
|
|
63
|
+
const contextOptimization = readContextOptimizationEnv(env);
|
|
64
|
+
return resolveRuntimeConfig({
|
|
65
|
+
cuaModel: env.OPENAI_CUA_MODEL,
|
|
66
|
+
stepDelayMs: env.DROID_CUA_STEP_DELAY_MS,
|
|
67
|
+
contextOptimizationEnabled: contextOptimization.enabled,
|
|
68
|
+
contextOptimizationThreshold: contextOptimization.threshold,
|
|
69
|
+
...overrides,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export function resolveRuntimeCredentialsFromEnv(env = process.env, overrides = {}) {
|
|
73
|
+
return resolveRuntimeCredentials({
|
|
74
|
+
openAiApiKey: env.OPENAI_API_KEY,
|
|
75
|
+
loadmillApiToken: env.LOADMILL_API_TOKEN,
|
|
76
|
+
loadmillBaseUrl: env.LOADMILL_BASE_URL,
|
|
77
|
+
...overrides,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export function createOpenAiClient(apiKey) {
|
|
81
|
+
return new OpenAI({
|
|
82
|
+
apiKey: trimNullableString(apiKey) || undefined,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export function createRunContext({ config, credentials }) {
|
|
86
|
+
const resolvedConfig = resolveRuntimeConfig(config);
|
|
87
|
+
const resolvedCredentials = resolveRuntimeCredentials(credentials);
|
|
88
|
+
return {
|
|
89
|
+
config: resolvedConfig,
|
|
90
|
+
credentials: resolvedCredentials,
|
|
91
|
+
clients: {
|
|
92
|
+
openai: createOpenAiClient(resolvedCredentials.openAiApiKey),
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { executeLoadmillCommand } from "../integrations/loadmill/index.js";
|
|
5
5
|
import { printCliOutput } from "../utils/console-output.js";
|
|
6
|
-
function getLoadmillSiteBaseUrl() {
|
|
7
|
-
const rawBaseUrl =
|
|
6
|
+
function getLoadmillSiteBaseUrl(loadmillClient = null) {
|
|
7
|
+
const rawBaseUrl = loadmillClient?.getBaseUrl?.() || "https://app.loadmill.com/api";
|
|
8
8
|
return rawBaseUrl.replace(/\/api\/?$/, "");
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
@@ -45,6 +45,8 @@ export function extractLoadmillCommand(userInput) {
|
|
|
45
45
|
*/
|
|
46
46
|
export async function executeLoadmillInstruction(command, isHeadlessMode, context, stepContext = null) {
|
|
47
47
|
const addOutput = context?.addOutput || printCliOutput;
|
|
48
|
+
const loadmillClient = context?.loadmillClient || null;
|
|
49
|
+
const openAiClient = context?.openAiClient || null;
|
|
48
50
|
const meta = {
|
|
49
51
|
runId: context?.runId,
|
|
50
52
|
stepId: stepContext?.stepId,
|
|
@@ -54,14 +56,16 @@ export async function executeLoadmillInstruction(command, isHeadlessMode, contex
|
|
|
54
56
|
type: 'info',
|
|
55
57
|
text: `[Loadmill] Executing: ${command}`,
|
|
56
58
|
eventType: 'system_message',
|
|
57
|
-
payload: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() },
|
|
59
|
+
payload: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) },
|
|
58
60
|
...meta
|
|
59
61
|
});
|
|
60
62
|
const result = await executeLoadmillCommand(command, {
|
|
63
|
+
loadmillClient,
|
|
64
|
+
openAiClient,
|
|
61
65
|
onProgress: ({ message, runId }) => {
|
|
62
66
|
const payload = runId
|
|
63
|
-
? { loadmillSuiteRunId: runId, loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() }
|
|
64
|
-
: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() };
|
|
67
|
+
? { loadmillSuiteRunId: runId, loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) }
|
|
68
|
+
: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) };
|
|
65
69
|
addOutput({ type: 'info', text: `[Loadmill] ${message}`, eventType: 'system_message', payload, ...meta });
|
|
66
70
|
}
|
|
67
71
|
});
|
|
@@ -84,14 +88,15 @@ export async function executeLoadmillInstruction(command, isHeadlessMode, contex
|
|
|
84
88
|
*/
|
|
85
89
|
export async function handleLoadmillFailure(command, error, isHeadlessMode, context, stepContext = null, suiteRunId = null) {
|
|
86
90
|
const addOutput = context?.addOutput || printCliOutput;
|
|
91
|
+
const loadmillClient = context?.loadmillClient || null;
|
|
87
92
|
const meta = {
|
|
88
93
|
runId: context?.runId,
|
|
89
94
|
stepId: stepContext?.stepId,
|
|
90
95
|
instructionIndex: stepContext?.instructionIndex
|
|
91
96
|
};
|
|
92
97
|
const payload = suiteRunId
|
|
93
|
-
? { loadmillSuiteRunId: suiteRunId, loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() }
|
|
94
|
-
: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() };
|
|
98
|
+
? { loadmillSuiteRunId: suiteRunId, loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) }
|
|
99
|
+
: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) };
|
|
95
100
|
addOutput({ type: 'error', text: '[Loadmill] FAILED', eventType: 'error', payload, ...meta });
|
|
96
101
|
addOutput({ type: 'error', text: `Command: ${command}`, eventType: 'error', payload, ...meta });
|
|
97
102
|
addOutput({ type: 'error', text: `Error: ${error}`, eventType: 'error', payload, ...meta });
|
|
@@ -148,14 +153,15 @@ export async function handleLoadmillFailure(command, error, isHeadlessMode, cont
|
|
|
148
153
|
*/
|
|
149
154
|
export function handleLoadmillSuccess(command, result, context, stepContext = null) {
|
|
150
155
|
const addOutput = context?.addOutput || printCliOutput;
|
|
156
|
+
const loadmillClient = context?.loadmillClient || null;
|
|
151
157
|
const meta = {
|
|
152
158
|
runId: context?.runId,
|
|
153
159
|
stepId: stepContext?.stepId,
|
|
154
160
|
instructionIndex: stepContext?.instructionIndex
|
|
155
161
|
};
|
|
156
162
|
const payload = result.runId
|
|
157
|
-
? { loadmillSuiteRunId: result.runId, loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() }
|
|
158
|
-
: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() };
|
|
163
|
+
? { loadmillSuiteRunId: result.runId, loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) }
|
|
164
|
+
: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) };
|
|
159
165
|
if (result.action === "search") {
|
|
160
166
|
addOutput({ type: 'success', text: `[Loadmill] Found ${result.result.flows.length} flow(s)`, eventType: 'system_message', payload, ...meta });
|
|
161
167
|
result.result.flows.forEach((flow, i) => {
|