@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.
Files changed (33) hide show
  1. package/README.md +7 -6
  2. package/build/index.js +89 -71
  3. package/build/src/brand/loadmill-mark.js +20 -0
  4. package/build/src/browser/playwright/connection.js +160 -12
  5. package/build/src/browser/playwright/welcome-page.js +161 -0
  6. package/build/src/cli/headless-execution-config.js +18 -58
  7. package/build/src/cli/headless-target.js +0 -9
  8. package/build/src/cli/ink-shell.js +5 -2
  9. package/build/src/commands/help.js +5 -8
  10. package/build/src/commands/loadmill.js +10 -1
  11. package/build/src/commands/run.js +17 -28
  12. package/build/src/core/aim-assist.js +5 -0
  13. package/build/src/core/app-context.js +57 -33
  14. package/build/src/core/context-optimization.js +1 -1
  15. package/build/src/core/cua-models.js +9 -0
  16. package/build/src/core/execution-engine.js +121 -199
  17. package/build/src/core/runtime-config.js +95 -0
  18. package/build/src/device/loadmill.js +15 -9
  19. package/build/src/device/openai.js +76 -142
  20. package/build/src/integrations/loadmill/client.js +149 -131
  21. package/build/src/integrations/loadmill/executor.js +17 -11
  22. package/build/src/integrations/loadmill/index.js +1 -1
  23. package/build/src/integrations/loadmill/interpreter.js +4 -14
  24. package/build/src/modes/design-mode-ink.js +5 -6
  25. package/build/src/modes/design-mode.js +5 -6
  26. package/build/src/modes/execution-mode.js +26 -0
  27. package/build/src/prompts/base.js +44 -30
  28. package/build/src/prompts/design.js +3 -4
  29. package/build/src/prompts/execution.js +13 -110
  30. package/build/src/utils/console-output.js +8 -0
  31. package/build/src/utils/cua-debug-tracer.js +0 -1
  32. package/build/src/utils/structured-debug-log-manager.js +4 -1
  33. 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 getScopeAndIds(context = null, stepContext = null) {
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
- actionsToExecute,
149
- droppedActions,
150
- truncationReason,
151
- runtimeNote
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.strictMode = options.strictMode === true;
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
- // ── Collect pending safety checks ──
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.sendCUARequest({
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 { actionsToExecute, droppedActions, truncationReason, runtimeNote } = buildStrictModePlan(actions, this.strictMode);
481
- let sawExplicitScreenshotAction = false;
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 screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
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.sendCUARequest({
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 = process.env.LOADMILL_BASE_URL || "https://app.loadmill.com/api";
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) => {