@loadmill/droid-cua 2.9.0 → 2.10.1-rc.10
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 +0 -2
- package/build/index.js +24 -18
- package/build/src/brand/loadmill-mark.js +20 -0
- package/build/src/browser/playwright/connection.js +148 -11
- package/build/src/browser/playwright/welcome-page.js +161 -0
- package/build/src/cli/headless-execution-config.js +16 -24
- package/build/src/cli/headless-target.js +0 -9
- package/build/src/commands/help.js +3 -5
- package/build/src/commands/run.js +1 -1
- package/build/src/core/aim-assist.js +5 -0
- package/build/src/core/execution-engine.js +266 -505
- package/build/src/core/prompts.js +1 -1
- package/build/src/core/runtime-config.js +1 -2
- package/build/src/core/screenshot-store.js +153 -0
- package/build/src/core/tool-handlers/assert.js +155 -0
- package/build/src/core/tool-handlers/loadmill.js +29 -0
- package/build/src/core/tool-handlers/navigate.js +110 -0
- package/build/src/core/tool-handlers/preview-click.js +112 -0
- package/build/src/core/tool-handlers/test-design-complete.js +93 -0
- package/build/src/core/tool-handlers/utils.js +80 -0
- package/build/src/device/openai.js +103 -4
- package/build/src/modes/design-mode-ink.js +17 -5
- package/build/src/modes/design-mode.js +17 -5
- package/build/src/modes/execution-mode.js +42 -234
- package/build/src/prompts/base.js +44 -31
- package/build/src/prompts/design.js +55 -15
- package/build/src/prompts/editor.js +1 -1
- package/build/src/prompts/execution.js +15 -10
- package/build/src/utils/cua-debug-tracer.js +3 -1
- package/package.json +3 -4
- package/build/src/device/screenshot-resolution.js +0 -33
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { writeFile } from "fs/promises";
|
|
4
2
|
import { getScreenshotAsBase64, getCurrentPlatform } from "../device/connection.js";
|
|
3
|
+
import { getBrowserInfo, navigateToUrl as navigateBrowserToUrl } from "../browser/connection.js";
|
|
5
4
|
import { handleModelAction } from "../device/actions.js";
|
|
6
5
|
import { sendCUARequest } from "../device/openai.js";
|
|
6
|
+
import { executeLoadmillInstruction } from "../device/loadmill.js";
|
|
7
7
|
import { emitDesktopDebug } from "../utils/desktop-debug.js";
|
|
8
8
|
import { getConfiguredStepDelayMs } from "../utils/step-delay.js";
|
|
9
9
|
import { printCliOutput } from "../utils/console-output.js";
|
|
10
|
-
import { annotateScreenshotArtifact, createAnnotatedScreenshotFromBase64 } from "../utils/run-screenshot-recorder.js";
|
|
11
10
|
import { CAPTURE_SOURCES } from "./capture-sources.js";
|
|
11
|
+
import { ScreenshotStore } from "./screenshot-store.js";
|
|
12
|
+
import { handlePreviewClick } from "./tool-handlers/preview-click.js";
|
|
13
|
+
import { handleNavigateToolCall } from "./tool-handlers/navigate.js";
|
|
14
|
+
import { handleAssertionToolCall } from "./tool-handlers/assert.js";
|
|
15
|
+
import { handleLoadmillToolCall } from "./tool-handlers/loadmill.js";
|
|
16
|
+
import { handleTestDesignCompleteToolCall } from "./tool-handlers/test-design-complete.js";
|
|
17
|
+
import { buildDebugContext, buildTextToolOutputItem, } from "./tool-handlers/utils.js";
|
|
12
18
|
function extractActionableOutputItems(items) {
|
|
13
19
|
const entries = [];
|
|
14
20
|
for (const item of items) {
|
|
15
|
-
if (item?.type === "function_call"
|
|
21
|
+
if (item?.type === "function_call") {
|
|
16
22
|
entries.push({
|
|
17
|
-
type: "
|
|
23
|
+
type: "function_call",
|
|
18
24
|
call_id: item.call_id || item.id,
|
|
25
|
+
name: item.name,
|
|
19
26
|
arguments: item.arguments,
|
|
20
27
|
});
|
|
21
28
|
continue;
|
|
@@ -42,144 +49,10 @@ function extractActionableOutputItems(items) {
|
|
|
42
49
|
}
|
|
43
50
|
return entries;
|
|
44
51
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return `Invalid preview_click arguments: ${argumentsText}`;
|
|
50
|
-
}
|
|
51
|
-
function parsePreviewClickArguments(argumentsText) {
|
|
52
|
-
let parsedArguments;
|
|
53
|
-
try {
|
|
54
|
-
parsedArguments = JSON.parse(argumentsText);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return {
|
|
58
|
-
ok: false,
|
|
59
|
-
message: buildPreviewClickValidationError("arguments could not be parsed as JSON; expected numeric x and y."),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
const x = parsedArguments?.x;
|
|
63
|
-
const y = parsedArguments?.y;
|
|
64
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
65
|
-
return {
|
|
66
|
-
ok: false,
|
|
67
|
-
message: buildPreviewClickValidationError("x and y must both be numeric."),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
ok: true,
|
|
72
|
-
x,
|
|
73
|
-
y,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
function buildFunctionCallOutputItem(callId, output) {
|
|
77
|
-
return {
|
|
78
|
-
type: "function_call_output",
|
|
79
|
-
call_id: callId,
|
|
80
|
-
output,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
function buildPreviewOutputText({ requestedX, requestedY, renderedX, renderedY, wasClamped }) {
|
|
84
|
-
const baseText = `Marker placed at (${renderedX}, ${renderedY}) on the current screen. The click has not been executed.`;
|
|
85
|
-
if (!wasClamped) {
|
|
86
|
-
return baseText;
|
|
87
|
-
}
|
|
88
|
-
return `${baseText} Requested point (${requestedX}, ${requestedY}) was off-screen; marker rendered at (${renderedX}, ${renderedY}).`;
|
|
89
|
-
}
|
|
90
|
-
function buildPreviewOutputContent({ text, annotatedScreenshotBase64 = null }) {
|
|
91
|
-
const content = [{
|
|
92
|
-
type: "input_text",
|
|
93
|
-
text,
|
|
94
|
-
}];
|
|
95
|
-
if (annotatedScreenshotBase64) {
|
|
96
|
-
content.push({
|
|
97
|
-
type: "input_image",
|
|
98
|
-
image_url: `data:image/png;base64,${annotatedScreenshotBase64}`,
|
|
99
|
-
detail: "high",
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
return content;
|
|
103
|
-
}
|
|
104
|
-
function getScopeAndIds(context = null, stepContext = null) {
|
|
105
|
-
const scope = context?.sessionId ? "design" : "execution";
|
|
106
|
-
const ids = scope === "design"
|
|
107
|
-
? {
|
|
108
|
-
sessionId: context?.sessionId,
|
|
109
|
-
stepId: stepContext?.stepId,
|
|
110
|
-
instructionIndex: stepContext?.instructionIndex
|
|
111
|
-
}
|
|
112
|
-
: {
|
|
113
|
-
runId: context?.runId,
|
|
114
|
-
stepId: stepContext?.stepId,
|
|
115
|
-
instructionIndex: stepContext?.instructionIndex
|
|
116
|
-
};
|
|
117
|
-
return { scope, ids };
|
|
118
|
-
}
|
|
119
|
-
function buildStrictModePlan(actions = [], strictMode = false) {
|
|
120
|
-
if (!strictMode || actions.length === 0) {
|
|
121
|
-
return {
|
|
122
|
-
actionsToExecute: actions,
|
|
123
|
-
droppedActions: [],
|
|
124
|
-
truncationReason: null,
|
|
125
|
-
runtimeNote: null
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
const actionsToExecute = [actions[0]];
|
|
129
|
-
const droppedActions = actions.slice(1);
|
|
130
|
-
if (droppedActions.length === 0) {
|
|
131
|
-
return {
|
|
132
|
-
actionsToExecute,
|
|
133
|
-
droppedActions,
|
|
134
|
-
truncationReason: null,
|
|
135
|
-
runtimeNote: null
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
const leadingActionType = actionsToExecute[0]?.type;
|
|
139
|
-
const truncationReason = leadingActionType === "screenshot"
|
|
140
|
-
? "leading_screenshot_reobserve"
|
|
141
|
-
: "post_first_action_reobserve";
|
|
142
|
-
const droppedActionTypes = droppedActions
|
|
143
|
-
.map((action) => action?.type)
|
|
144
|
-
.filter(Boolean);
|
|
145
|
-
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.`;
|
|
146
|
-
return {
|
|
147
|
-
actionsToExecute,
|
|
148
|
-
droppedActions,
|
|
149
|
-
truncationReason,
|
|
150
|
-
runtimeNote
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
function normalizeArtifactPaths(result) {
|
|
154
|
-
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
155
|
-
if (Array.isArray(result.annotationSourcePaths)) {
|
|
156
|
-
return normalizeArtifactPaths(result.annotationSourcePaths);
|
|
157
|
-
}
|
|
158
|
-
return normalizeArtifactPaths(result.artifactPaths);
|
|
159
|
-
}
|
|
160
|
-
if (Array.isArray(result)) {
|
|
161
|
-
return result.filter((value) => typeof value === "string" && value.trim());
|
|
162
|
-
}
|
|
163
|
-
if (typeof result === "string" && result.trim()) {
|
|
164
|
-
return [result];
|
|
165
|
-
}
|
|
166
|
-
return [];
|
|
167
|
-
}
|
|
168
|
-
function screenshotPathFromRecorderResult(result) {
|
|
169
|
-
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
170
|
-
return typeof result.screenshotPath === "string" && result.screenshotPath.trim()
|
|
171
|
-
? result.screenshotPath
|
|
172
|
-
: null;
|
|
173
|
-
}
|
|
174
|
-
return typeof result === "string" && result.trim() ? result : null;
|
|
175
|
-
}
|
|
176
|
-
function screenshotDataUrlFromRecorderResult(result) {
|
|
177
|
-
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
178
|
-
return typeof result.screenshotDataUrl === "string" && result.screenshotDataUrl.trim()
|
|
179
|
-
? result.screenshotDataUrl
|
|
180
|
-
: null;
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
52
|
+
function collectPendingSafetyChecks(items = []) {
|
|
53
|
+
return items
|
|
54
|
+
.filter((item) => item.type === "pending_safety_check")
|
|
55
|
+
.map((item) => ({ id: item.id }));
|
|
183
56
|
}
|
|
184
57
|
export class ExecutionEngine {
|
|
185
58
|
constructor(session, options = {}) {
|
|
@@ -187,206 +60,239 @@ export class ExecutionEngine {
|
|
|
187
60
|
this.recordScreenshots = options.recordScreenshots || false;
|
|
188
61
|
this.screenshotDir = options.screenshotDir || null;
|
|
189
62
|
this.screenshotRecorder = options.screenshotRecorder || null;
|
|
190
|
-
this.
|
|
63
|
+
this.screenshotStore = options.screenshotStore || new ScreenshotStore({
|
|
64
|
+
recordScreenshots: this.recordScreenshots,
|
|
65
|
+
screenshotDir: this.screenshotDir,
|
|
66
|
+
screenshotRecorder: this.screenshotRecorder,
|
|
67
|
+
});
|
|
191
68
|
this.stepDelayMs = options.stepDelayMs ?? getConfiguredStepDelayMs();
|
|
192
|
-
this.reportedScreenshotWriteError = false;
|
|
193
|
-
this.reportedScreenshotAnnotateError = false;
|
|
194
|
-
this.lastRecordedScreenshotPaths = [];
|
|
195
69
|
this.getScreenshotAsBase64 = options.getScreenshotAsBase64 || getScreenshotAsBase64;
|
|
196
70
|
this.handleModelAction = options.handleModelAction || handleModelAction;
|
|
197
71
|
this.sendCUARequest = options.sendCUARequest || sendCUARequest;
|
|
198
72
|
this.reviseTestScript = options.reviseTestScript || null;
|
|
199
73
|
this.getCurrentPlatform = options.getCurrentPlatform || getCurrentPlatform;
|
|
74
|
+
this.getBrowserInfo = options.getBrowserInfo || getBrowserInfo;
|
|
75
|
+
this.navigateToUrl = options.navigateToUrl || navigateBrowserToUrl;
|
|
76
|
+
this.executeLoadmillInstruction = options.executeLoadmillInstruction || executeLoadmillInstruction;
|
|
77
|
+
this.toolCapabilities = options.toolCapabilities || {};
|
|
78
|
+
this.baseSystemPrompt = options.baseSystemPrompt || session?.systemPrompt || null;
|
|
79
|
+
this.isHeadlessMode = options.isHeadlessMode === true;
|
|
200
80
|
}
|
|
201
|
-
async
|
|
202
|
-
|
|
81
|
+
async recordScreenshot(screenshotBase64, metadata = {}) {
|
|
82
|
+
return await this.screenshotStore.saveScreenshot(screenshotBase64, metadata);
|
|
83
|
+
}
|
|
84
|
+
async annotateScreenshot(currentScreenshot, action, metadata = {}) {
|
|
85
|
+
if (!(action?.type === "click" || action?.type === "double_click")) {
|
|
203
86
|
return null;
|
|
204
87
|
}
|
|
205
|
-
|
|
206
|
-
const previousRecordedScreenshotPaths = updateLastRecordedScreenshotPaths
|
|
207
|
-
? null
|
|
208
|
-
: [...this.lastRecordedScreenshotPaths];
|
|
209
|
-
try {
|
|
210
|
-
if (this.screenshotRecorder?.saveScreenshot) {
|
|
211
|
-
const result = await this.screenshotRecorder.saveScreenshot(screenshotBase64, metadata);
|
|
212
|
-
if (updateLastRecordedScreenshotPaths) {
|
|
213
|
-
this.lastRecordedScreenshotPaths = normalizeArtifactPaths(result);
|
|
214
|
-
}
|
|
215
|
-
return result;
|
|
216
|
-
}
|
|
217
|
-
if (this.recordScreenshots && this.screenshotDir) {
|
|
218
|
-
const framePath = path.join(this.screenshotDir, `frame_${String(Date.now())}.png`);
|
|
219
|
-
await writeFile(framePath, Buffer.from(screenshotBase64, "base64"));
|
|
220
|
-
if (updateLastRecordedScreenshotPaths) {
|
|
221
|
-
this.lastRecordedScreenshotPaths = [framePath];
|
|
222
|
-
}
|
|
223
|
-
return framePath;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
if (updateLastRecordedScreenshotPaths) {
|
|
228
|
-
this.lastRecordedScreenshotPaths = [];
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
this.lastRecordedScreenshotPaths = previousRecordedScreenshotPaths;
|
|
232
|
-
}
|
|
233
|
-
if (!this.reportedScreenshotWriteError) {
|
|
234
|
-
this.reportedScreenshotWriteError = true;
|
|
235
|
-
const scope = typeof metadata.sessionId === "string" ? "design" : "execution";
|
|
236
|
-
const ids = scope === "design"
|
|
237
|
-
? {
|
|
238
|
-
sessionId: metadata.sessionId,
|
|
239
|
-
stepId: metadata.stepId,
|
|
240
|
-
instructionIndex: metadata.instructionIndex
|
|
241
|
-
}
|
|
242
|
-
: {
|
|
243
|
-
runId: metadata.runId,
|
|
244
|
-
stepId: metadata.stepId,
|
|
245
|
-
instructionIndex: metadata.instructionIndex
|
|
246
|
-
};
|
|
247
|
-
emitDesktopDebug("run.screenshot_write.error", scope, ids, {
|
|
248
|
-
captureSource: metadata.captureSource ?? null,
|
|
249
|
-
message: error instanceof Error ? error.message : "Failed to persist screenshot"
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return null;
|
|
88
|
+
return await this.screenshotStore.annotateScreenshot(currentScreenshot, { x: action.x, y: action.y, style: "click" }, { ...metadata, actionType: action.type });
|
|
254
89
|
}
|
|
255
|
-
|
|
256
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Dispatches a function-call tool to its handler. Each handler returns a
|
|
92
|
+
* discriminated outcome that runFullTurn interprets below. Continue and
|
|
93
|
+
* close_turn outcomes may also return currentScreenshot to update the
|
|
94
|
+
* parent turn's explicit screenshot state.
|
|
95
|
+
*/
|
|
96
|
+
async handleFunctionToolCall(toolName, callId, argumentsText, context = null, stepContext = null) {
|
|
97
|
+
const handlerArgs = { engine: this, callId, argumentsText, context, stepContext };
|
|
98
|
+
switch (toolName) {
|
|
99
|
+
case "preview_click":
|
|
100
|
+
return await handlePreviewClick(handlerArgs);
|
|
101
|
+
case "assert":
|
|
102
|
+
return await handleAssertionToolCall(handlerArgs);
|
|
103
|
+
case "navigate":
|
|
104
|
+
return await handleNavigateToolCall(handlerArgs);
|
|
105
|
+
case "loadmill":
|
|
106
|
+
return await handleLoadmillToolCall(handlerArgs);
|
|
107
|
+
case "test_design_complete":
|
|
108
|
+
return await handleTestDesignCompleteToolCall(handlerArgs);
|
|
109
|
+
default:
|
|
110
|
+
return {
|
|
111
|
+
kind: "continue",
|
|
112
|
+
outputItem: buildTextToolOutputItem(callId, `Unsupported tool call: ${toolName}`),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
257
115
|
}
|
|
258
|
-
async
|
|
259
|
-
return await this.
|
|
260
|
-
|
|
116
|
+
async requestNextTurn(messages, previousResponseId, context = null, stepContext = null, options = {}) {
|
|
117
|
+
return await this.sendCUARequest({
|
|
118
|
+
messages,
|
|
119
|
+
previousResponseId,
|
|
120
|
+
deviceInfo: this.session.deviceInfo,
|
|
121
|
+
debugContext: options.debugContextOverride ?? buildDebugContext(context, stepContext),
|
|
122
|
+
toolCapabilities: options.toolCapabilitiesOverride ?? this.toolCapabilities,
|
|
123
|
+
includeComputerTool: options.includeComputerToolOverride ?? true,
|
|
261
124
|
});
|
|
262
125
|
}
|
|
263
|
-
async
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
x: action.x,
|
|
270
|
-
y: action.y,
|
|
271
|
-
style: "click",
|
|
272
|
-
};
|
|
273
|
-
const sourcePath = this.lastRecordedScreenshotPaths.find((filePath) => typeof filePath === "string" && filePath.trim());
|
|
274
|
-
if (sourcePath && this.screenshotRecorder?.saveAnnotatedScreenshot) {
|
|
275
|
-
return await this.screenshotRecorder.saveAnnotatedScreenshot(sourcePath, marker, metadata);
|
|
276
|
-
}
|
|
277
|
-
await Promise.all(this.lastRecordedScreenshotPaths.map((filePath) => annotateScreenshotArtifact(filePath, marker)));
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
catch (error) {
|
|
281
|
-
if (!this.reportedScreenshotAnnotateError) {
|
|
282
|
-
this.reportedScreenshotAnnotateError = true;
|
|
283
|
-
const scope = typeof metadata.sessionId === "string" ? "design" : "execution";
|
|
284
|
-
const ids = scope === "design"
|
|
285
|
-
? {
|
|
286
|
-
sessionId: metadata.sessionId,
|
|
287
|
-
stepId: metadata.stepId,
|
|
288
|
-
instructionIndex: metadata.instructionIndex
|
|
289
|
-
}
|
|
290
|
-
: {
|
|
291
|
-
runId: metadata.runId,
|
|
292
|
-
stepId: metadata.stepId,
|
|
293
|
-
instructionIndex: metadata.instructionIndex
|
|
294
|
-
};
|
|
295
|
-
emitDesktopDebug("run.screenshot_annotate.error", scope, ids, {
|
|
296
|
-
actionType: action?.type ?? null,
|
|
297
|
-
x: action?.x ?? null,
|
|
298
|
-
y: action?.y ?? null,
|
|
299
|
-
message: error instanceof Error ? error.message : "Failed to annotate screenshot"
|
|
300
|
-
});
|
|
126
|
+
async executeActionBatch(actions, currentScreenshot, shouldStop, context = null, stepContext = null, trackAction = null) {
|
|
127
|
+
let sawExplicitScreenshotAction = false;
|
|
128
|
+
for (const action of actions) {
|
|
129
|
+
const actionEventId = randomUUID();
|
|
130
|
+
if (shouldStop()) {
|
|
131
|
+
return { stopped: true, sawExplicitScreenshotAction };
|
|
301
132
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
async handlePreviewClick(callId, argumentsText, context = null, stepContext = null) {
|
|
306
|
-
const parsedArguments = parsePreviewClickArguments(argumentsText);
|
|
307
|
-
if (!parsedArguments.ok) {
|
|
308
|
-
return buildFunctionCallOutputItem(callId, buildPreviewOutputContent({ text: parsedArguments.message }));
|
|
309
|
-
}
|
|
310
|
-
try {
|
|
311
|
-
const screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
|
|
312
|
-
const previewResult = await createAnnotatedScreenshotFromBase64(screenshotBase64, {
|
|
313
|
-
x: parsedArguments.x,
|
|
314
|
-
y: parsedArguments.y,
|
|
315
|
-
style: "preview",
|
|
316
|
-
});
|
|
317
|
-
if (!previewResult?.screenshotBase64 || !previewResult?.marker) {
|
|
318
|
-
return buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
|
|
319
|
-
text: "preview_click failed: could not annotate the current screenshot.",
|
|
320
|
-
}));
|
|
133
|
+
if (action.type === "screenshot") {
|
|
134
|
+
sawExplicitScreenshotAction = true;
|
|
135
|
+
continue;
|
|
321
136
|
}
|
|
322
|
-
const
|
|
323
|
-
const previewScreenshotResult = await this.recordPreviewScreenshot(previewResult.screenshotBase64, {
|
|
137
|
+
const annotatedScreenshot = await this.annotateScreenshot(currentScreenshot, action, {
|
|
324
138
|
runId: context?.runId,
|
|
325
139
|
sessionId: context?.sessionId,
|
|
326
140
|
stepId: stepContext?.stepId,
|
|
327
141
|
instructionIndex: stepContext?.instructionIndex,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
captureSource: CAPTURE_SOURCES.PREVIEW_CLICK,
|
|
142
|
+
eventId: actionEventId,
|
|
143
|
+
captureSource: CAPTURE_SOURCES.ACTION_CLICK_ANNOTATION
|
|
331
144
|
});
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
145
|
+
await this.handleModelAction(this.session.deviceId, action, this.session.deviceInfo.scale, {
|
|
146
|
+
...context,
|
|
147
|
+
shouldStop,
|
|
148
|
+
stepId: stepContext?.stepId,
|
|
149
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
150
|
+
eventId: actionEventId,
|
|
151
|
+
captureSource: CAPTURE_SOURCES.ACTION_CLICK_ANNOTATION,
|
|
152
|
+
...(annotatedScreenshot?.screenshotPath ? { screenshotPath: annotatedScreenshot.screenshotPath } : {}),
|
|
153
|
+
...(context?.includeScreenshotDataUrl && annotatedScreenshot?.screenshotDataUrl ? { screenshotDataUrl: annotatedScreenshot.screenshotDataUrl } : {})
|
|
338
154
|
});
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
155
|
+
if (trackAction) {
|
|
156
|
+
trackAction(action);
|
|
157
|
+
}
|
|
158
|
+
if (shouldStop()) {
|
|
159
|
+
return { stopped: true, sawExplicitScreenshotAction };
|
|
160
|
+
}
|
|
161
|
+
if (action.type !== "wait") {
|
|
162
|
+
await new Promise((resolve) => setTimeout(resolve, this.stepDelayMs));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { stopped: false, sawExplicitScreenshotAction };
|
|
166
|
+
}
|
|
167
|
+
async buildActionFollowUp(callId, pendingSafetyChecks, sawExplicitScreenshotAction, addOutput, eventMeta, context = null, stepContext = null) {
|
|
168
|
+
const screenshotBase64 = await this.getScreenshotAsBase64(this.session.deviceId, this.session.deviceInfo);
|
|
169
|
+
emitDesktopDebug("device.screenshot", "device", {
|
|
170
|
+
runId: context?.runId,
|
|
171
|
+
stepId: stepContext?.stepId,
|
|
172
|
+
instructionIndex: stepContext?.instructionIndex
|
|
173
|
+
}, {
|
|
174
|
+
source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
|
|
175
|
+
callId,
|
|
176
|
+
width: this.session.deviceInfo?.scaled_width,
|
|
177
|
+
height: this.session.deviceInfo?.scaled_height,
|
|
178
|
+
base64Length: screenshotBase64.length
|
|
179
|
+
});
|
|
180
|
+
const screenshotEventId = randomUUID();
|
|
181
|
+
const captureSource = sawExplicitScreenshotAction ? CAPTURE_SOURCES.CALL_OUTPUT_EXPLICIT_ACTION : CAPTURE_SOURCES.CALL_OUTPUT_POST_ACTION;
|
|
182
|
+
const currentScreenshot = await this.recordScreenshot(screenshotBase64, {
|
|
183
|
+
runId: context?.runId,
|
|
184
|
+
sessionId: context?.sessionId,
|
|
185
|
+
stepId: stepContext?.stepId,
|
|
186
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
187
|
+
eventId: screenshotEventId,
|
|
188
|
+
captureSource
|
|
189
|
+
});
|
|
190
|
+
addOutput({
|
|
191
|
+
type: "info",
|
|
192
|
+
text: "Capturing screen",
|
|
193
|
+
eventType: "screenshot_captured",
|
|
194
|
+
actionType: "screenshot",
|
|
195
|
+
...eventMeta({
|
|
196
|
+
payload: {
|
|
197
|
+
eventId: screenshotEventId,
|
|
198
|
+
captureSource,
|
|
199
|
+
source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
|
|
200
|
+
...(currentScreenshot?.screenshotPath ? { screenshotPath: currentScreenshot.screenshotPath } : {}),
|
|
201
|
+
...(context?.includeScreenshotDataUrl
|
|
202
|
+
? { screenshotDataUrl: `data:image/png;base64,${screenshotBase64}` }
|
|
203
|
+
: {})
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
});
|
|
207
|
+
return {
|
|
208
|
+
input: [{
|
|
209
|
+
type: "computer_call_output",
|
|
210
|
+
call_id: callId,
|
|
211
|
+
output: {
|
|
212
|
+
type: "computer_screenshot",
|
|
213
|
+
image_url: `data:image/png;base64,${screenshotBase64}`,
|
|
214
|
+
},
|
|
215
|
+
...(pendingSafetyChecks.length > 0 ? { acknowledged_safety_checks: pendingSafetyChecks } : {})
|
|
216
|
+
}],
|
|
217
|
+
currentScreenshot,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
emitResponseOutputItems(items, addOutput, eventMeta, options = {}) {
|
|
221
|
+
for (const item of items) {
|
|
222
|
+
if (item.type === "reasoning") {
|
|
223
|
+
for (const entry of item.summary) {
|
|
224
|
+
if (entry.type !== "summary_text")
|
|
225
|
+
continue;
|
|
226
|
+
addOutput({
|
|
227
|
+
type: "reasoning",
|
|
228
|
+
text: entry.text,
|
|
229
|
+
eventType: "reasoning",
|
|
230
|
+
...eventMeta(),
|
|
231
|
+
});
|
|
232
|
+
this.session.addToTranscript(`[Reasoning] ${entry.text}`);
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (item.type === "message") {
|
|
237
|
+
const textPart = item.content.find((contentItem) => contentItem.type === "output_text");
|
|
238
|
+
if (!textPart)
|
|
239
|
+
continue;
|
|
240
|
+
if (!options.suppressAssistantOutput) {
|
|
241
|
+
addOutput({
|
|
242
|
+
type: "assistant",
|
|
243
|
+
text: textPart.text,
|
|
244
|
+
eventType: "assistant_message",
|
|
245
|
+
...eventMeta(),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
this.session.addToTranscript(`[Assistant] ${textPart.text}`);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (item.type === "pending_safety_check") {
|
|
345
252
|
addOutput({
|
|
346
|
-
type: "
|
|
347
|
-
text:
|
|
348
|
-
eventType: "
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
requestedX: previewResult.marker.requestedX,
|
|
357
|
-
requestedY: previewResult.marker.requestedY,
|
|
358
|
-
renderedX: previewResult.marker.x,
|
|
359
|
-
renderedY: previewResult.marker.y,
|
|
360
|
-
wasClamped: previewResult.marker.wasClamped,
|
|
361
|
-
...(previewScreenshotPath ? { screenshotPath: previewScreenshotPath } : {}),
|
|
362
|
-
...(previewScreenshotDataUrl ? { screenshotDataUrl: previewScreenshotDataUrl } : {})
|
|
363
|
-
}
|
|
253
|
+
type: "warning",
|
|
254
|
+
text: `⚠️ Safety check: ${item.code} - ${item.message}`,
|
|
255
|
+
eventType: "system_message",
|
|
256
|
+
...eventMeta({
|
|
257
|
+
payload: {
|
|
258
|
+
id: item.id,
|
|
259
|
+
code: item.code,
|
|
260
|
+
message: item.message,
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
364
263
|
});
|
|
365
264
|
}
|
|
366
|
-
return buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
|
|
367
|
-
text: previewText,
|
|
368
|
-
annotatedScreenshotBase64: previewResult.screenshotBase64,
|
|
369
|
-
}));
|
|
370
|
-
}
|
|
371
|
-
catch (error) {
|
|
372
|
-
return buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
|
|
373
|
-
text: `preview_click failed: ${error instanceof Error ? error.message : "unable to capture or annotate the current screen."}`,
|
|
374
|
-
}));
|
|
375
265
|
}
|
|
376
266
|
}
|
|
377
267
|
/**
|
|
378
|
-
* Run a full turn with the CUA model
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
268
|
+
* Run a full turn with the CUA model. Executes actions until the model
|
|
269
|
+
* stops requesting actions, then returns { responseId, summary, currentScreenshot }.
|
|
270
|
+
*
|
|
271
|
+
* The summary's shape depends on which (if any) tool handler ran:
|
|
272
|
+
* - assertions: summary.assertionStatus = "passed" | "failed" | "failed_skipped" | "retry"
|
|
273
|
+
* - failures: summary.failed = true, summary.error = string
|
|
274
|
+
* - retries: summary.retryInstruction = true (caller should rerun the instruction)
|
|
275
|
+
* - navigation: summary.preserveSessionMessages = true (skip the usual message reset)
|
|
276
|
+
*
|
|
277
|
+
* @param {Object} response - The CUA response that opens this turn.
|
|
278
|
+
* @param {Function} trackAction - Optional callback for tracking actions / interrupting.
|
|
279
|
+
* @param {Object} context - Optional Ink context for output.
|
|
280
|
+
* @param {Object} options.currentScreenshot - Screenshot object to annotate during this turn.
|
|
384
281
|
*/
|
|
385
|
-
async runFullTurn(response, trackAction = null, context = null, stepContext = null) {
|
|
282
|
+
async runFullTurn(response, trackAction = null, context = null, stepContext = null, options = {}) {
|
|
386
283
|
const addOutput = context?.addOutput || printCliOutput;
|
|
387
284
|
let newResponseId = response.id;
|
|
285
|
+
let currentScreenshot = options.currentScreenshot ?? null;
|
|
388
286
|
const shouldStop = () => Boolean(trackAction?.());
|
|
389
|
-
const
|
|
287
|
+
const summary = {
|
|
288
|
+
responseId: newResponseId,
|
|
289
|
+
invokedFunctionTools: [],
|
|
290
|
+
preserveSessionMessages: false,
|
|
291
|
+
};
|
|
292
|
+
const finalize = () => {
|
|
293
|
+
summary.responseId = newResponseId;
|
|
294
|
+
return { responseId: newResponseId, summary, currentScreenshot };
|
|
295
|
+
};
|
|
390
296
|
const eventMeta = (extra = {}) => ({
|
|
391
297
|
runId: context?.runId,
|
|
392
298
|
stepId: stepContext?.stepId,
|
|
@@ -396,56 +302,12 @@ export class ExecutionEngine {
|
|
|
396
302
|
while (true) {
|
|
397
303
|
// Check for interruption before processing next batch of actions
|
|
398
304
|
if (shouldStop()) {
|
|
399
|
-
return
|
|
305
|
+
return finalize();
|
|
400
306
|
}
|
|
401
307
|
const items = response.output || [];
|
|
402
308
|
const actionableItems = extractActionableOutputItems(items);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
.filter(item => item.type === "pending_safety_check")
|
|
406
|
-
.map(item => ({ id: item.id }));
|
|
407
|
-
// ── Print assistant output ──
|
|
408
|
-
for (const item of items) {
|
|
409
|
-
if (item.type === "reasoning") {
|
|
410
|
-
for (const entry of item.summary) {
|
|
411
|
-
if (entry.type === "summary_text") {
|
|
412
|
-
addOutput({
|
|
413
|
-
type: 'reasoning',
|
|
414
|
-
text: entry.text,
|
|
415
|
-
eventType: 'reasoning',
|
|
416
|
-
...eventMeta()
|
|
417
|
-
});
|
|
418
|
-
this.session.addToTranscript(`[Reasoning] ${entry.text}`);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
else if (item.type === "message") {
|
|
423
|
-
const textPart = item.content.find(c => c.type === "output_text");
|
|
424
|
-
if (textPart) {
|
|
425
|
-
addOutput({
|
|
426
|
-
type: 'assistant',
|
|
427
|
-
text: textPart.text,
|
|
428
|
-
eventType: 'assistant_message',
|
|
429
|
-
...eventMeta()
|
|
430
|
-
});
|
|
431
|
-
this.session.addToTranscript(`[Assistant] ${textPart.text}`);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
else if (item.type === "pending_safety_check") {
|
|
435
|
-
addOutput({
|
|
436
|
-
type: 'warning',
|
|
437
|
-
text: `⚠️ Safety check: ${item.code} - ${item.message}`,
|
|
438
|
-
eventType: 'system_message',
|
|
439
|
-
...eventMeta({
|
|
440
|
-
payload: {
|
|
441
|
-
id: item.id,
|
|
442
|
-
code: item.code,
|
|
443
|
-
message: item.message
|
|
444
|
-
}
|
|
445
|
-
})
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
}
|
|
309
|
+
const pendingSafetyChecks = collectPendingSafetyChecks(items);
|
|
310
|
+
this.emitResponseOutputItems(items, addOutput, eventMeta, options);
|
|
449
311
|
if (actionableItems.length === 0) {
|
|
450
312
|
// No actions = turn complete
|
|
451
313
|
break;
|
|
@@ -454,161 +316,60 @@ export class ExecutionEngine {
|
|
|
454
316
|
if (!actionableItem?.call_id) {
|
|
455
317
|
break;
|
|
456
318
|
}
|
|
457
|
-
if (actionableItem.type === "
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
319
|
+
if (actionableItem.type === "function_call") {
|
|
320
|
+
const toolResult = await this.handleFunctionToolCall(actionableItem.name, actionableItem.call_id, actionableItem.arguments, context, stepContext);
|
|
321
|
+
summary.invokedFunctionTools.push(actionableItem.name);
|
|
322
|
+
if (toolResult.summaryUpdates) {
|
|
323
|
+
Object.assign(summary, toolResult.summaryUpdates);
|
|
324
|
+
}
|
|
325
|
+
if ((toolResult.kind === "continue" || toolResult.kind === "close_turn") && toolResult.currentScreenshot) {
|
|
326
|
+
currentScreenshot = toolResult.currentScreenshot;
|
|
463
327
|
}
|
|
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
|
-
});
|
|
476
|
-
newResponseId = response.id;
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
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
328
|
if (shouldStop()) {
|
|
485
|
-
return
|
|
329
|
+
return finalize();
|
|
486
330
|
}
|
|
487
|
-
if (
|
|
488
|
-
|
|
331
|
+
if (toolResult.kind === "fail") {
|
|
332
|
+
summary.failed = true;
|
|
333
|
+
summary.error = toolResult.error;
|
|
334
|
+
break;
|
|
489
335
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
}
|
|
336
|
+
if (toolResult.kind === "retry_instruction") {
|
|
337
|
+
summary.retryInstruction = true;
|
|
338
|
+
newResponseId = null;
|
|
339
|
+
break;
|
|
520
340
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
|
341
|
+
if (toolResult.kind === "preserve_session_and_stop") {
|
|
342
|
+
summary.preserveSessionMessages = true;
|
|
343
|
+
newResponseId = null;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
const nextTurnMessages = [toolResult.outputItem];
|
|
347
|
+
const nextTurnConfig = toolResult.nextTurnConfig ?? {};
|
|
348
|
+
if (Array.isArray(nextTurnConfig.followUpMessages) && nextTurnConfig.followUpMessages.length > 0) {
|
|
349
|
+
nextTurnMessages.push(...nextTurnConfig.followUpMessages);
|
|
350
|
+
}
|
|
351
|
+
response = await this.requestNextTurn(nextTurnMessages, newResponseId, context, stepContext, {
|
|
352
|
+
debugContextOverride: options.debugContextOverride,
|
|
353
|
+
toolCapabilitiesOverride: nextTurnConfig.toolCapabilitiesOverride,
|
|
354
|
+
includeComputerToolOverride: nextTurnConfig.includeComputerToolOverride,
|
|
580
355
|
});
|
|
356
|
+
newResponseId = response.id;
|
|
357
|
+
if (toolResult.kind === "close_turn") {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
581
361
|
}
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
type: "computer_screenshot",
|
|
587
|
-
image_url: `data:image/png;base64,${screenshotBase64}`,
|
|
588
|
-
},
|
|
589
|
-
...(pendingSafetyChecks.length > 0 ? { acknowledged_safety_checks: pendingSafetyChecks } : {})
|
|
590
|
-
}];
|
|
591
|
-
if (runtimeNote) {
|
|
592
|
-
input.push({
|
|
593
|
-
role: "user",
|
|
594
|
-
content: runtimeNote
|
|
595
|
-
});
|
|
362
|
+
const { call_id, actions } = actionableItem;
|
|
363
|
+
const actionBatchResult = await this.executeActionBatch(actions, currentScreenshot, shouldStop, context, stepContext, trackAction);
|
|
364
|
+
if (actionBatchResult.stopped) {
|
|
365
|
+
return finalize();
|
|
596
366
|
}
|
|
367
|
+
const followUp = await this.buildActionFollowUp(call_id, pendingSafetyChecks, actionBatchResult.sawExplicitScreenshotAction, addOutput, eventMeta, context, stepContext);
|
|
597
368
|
if (shouldStop()) {
|
|
598
|
-
return
|
|
369
|
+
return finalize();
|
|
599
370
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
previousResponseId: newResponseId,
|
|
603
|
-
deviceInfo: this.session.deviceInfo,
|
|
604
|
-
debugContext: {
|
|
605
|
-
scope,
|
|
606
|
-
runId: context?.runId,
|
|
607
|
-
sessionId: context?.sessionId,
|
|
608
|
-
stepId: stepContext?.stepId,
|
|
609
|
-
instructionIndex: stepContext?.instructionIndex
|
|
610
|
-
}
|
|
611
|
-
});
|
|
371
|
+
currentScreenshot = followUp.currentScreenshot ?? currentScreenshot;
|
|
372
|
+
response = await this.requestNextTurn(followUp.input, newResponseId, context, stepContext);
|
|
612
373
|
newResponseId = response.id;
|
|
613
374
|
}
|
|
614
375
|
// ── At end, if last output was only reasoning ──
|
|
@@ -621,6 +382,6 @@ export class ExecutionEngine {
|
|
|
621
382
|
...eventMeta()
|
|
622
383
|
});
|
|
623
384
|
}
|
|
624
|
-
return
|
|
385
|
+
return finalize();
|
|
625
386
|
}
|
|
626
387
|
}
|