@loadmill/droid-cua 2.10.0 → 2.10.1-rc.9
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/build/index.js +11 -0
- package/build/src/core/aim-assist.js +1 -1
- package/build/src/core/execution-engine.js +191 -358
- package/build/src/core/prompts.js +1 -1
- 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 +101 -8
- package/build/src/modes/design-mode-ink.js +16 -2
- package/build/src/modes/design-mode.js +16 -2
- package/build/src/modes/execution-mode.js +42 -234
- package/build/src/prompts/base.js +20 -21
- package/build/src/prompts/design.js +52 -11
- package/build/src/prompts/editor.js +1 -1
- package/build/src/prompts/execution.js +9 -5
- 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,311 +49,81 @@ function extractActionableOutputItems(items) {
|
|
|
42
49
|
}
|
|
43
50
|
return entries;
|
|
44
51
|
}
|
|
45
|
-
function buildPreviewClickValidationError(argumentsText) {
|
|
46
|
-
if (!argumentsText || typeof argumentsText !== "string") {
|
|
47
|
-
return "Invalid preview_click arguments: expected a JSON object with numeric x and y.";
|
|
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 buildDebugContext(context = null, stepContext = null) {
|
|
105
|
-
return {
|
|
106
|
-
scope: context?.sessionId ? "design" : "execution",
|
|
107
|
-
runId: context?.runId,
|
|
108
|
-
sessionId: context?.sessionId,
|
|
109
|
-
stepId: stepContext?.stepId,
|
|
110
|
-
instructionIndex: stepContext?.instructionIndex
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
52
|
function collectPendingSafetyChecks(items = []) {
|
|
114
53
|
return items
|
|
115
54
|
.filter((item) => item.type === "pending_safety_check")
|
|
116
55
|
.map((item) => ({ id: item.id }));
|
|
117
56
|
}
|
|
118
|
-
function normalizeArtifactPaths(result) {
|
|
119
|
-
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
120
|
-
if (Array.isArray(result.annotationSourcePaths)) {
|
|
121
|
-
return normalizeArtifactPaths(result.annotationSourcePaths);
|
|
122
|
-
}
|
|
123
|
-
return normalizeArtifactPaths(result.artifactPaths);
|
|
124
|
-
}
|
|
125
|
-
if (Array.isArray(result)) {
|
|
126
|
-
return result.filter((value) => typeof value === "string" && value.trim());
|
|
127
|
-
}
|
|
128
|
-
if (typeof result === "string" && result.trim()) {
|
|
129
|
-
return [result];
|
|
130
|
-
}
|
|
131
|
-
return [];
|
|
132
|
-
}
|
|
133
|
-
function screenshotPathFromRecorderResult(result) {
|
|
134
|
-
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
135
|
-
return typeof result.screenshotPath === "string" && result.screenshotPath.trim()
|
|
136
|
-
? result.screenshotPath
|
|
137
|
-
: null;
|
|
138
|
-
}
|
|
139
|
-
return typeof result === "string" && result.trim() ? result : null;
|
|
140
|
-
}
|
|
141
|
-
function screenshotDataUrlFromRecorderResult(result) {
|
|
142
|
-
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
143
|
-
return typeof result.screenshotDataUrl === "string" && result.screenshotDataUrl.trim()
|
|
144
|
-
? result.screenshotDataUrl
|
|
145
|
-
: null;
|
|
146
|
-
}
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
57
|
export class ExecutionEngine {
|
|
150
58
|
constructor(session, options = {}) {
|
|
151
59
|
this.session = session;
|
|
152
60
|
this.recordScreenshots = options.recordScreenshots || false;
|
|
153
61
|
this.screenshotDir = options.screenshotDir || null;
|
|
154
62
|
this.screenshotRecorder = options.screenshotRecorder || null;
|
|
63
|
+
this.screenshotStore = options.screenshotStore || new ScreenshotStore({
|
|
64
|
+
recordScreenshots: this.recordScreenshots,
|
|
65
|
+
screenshotDir: this.screenshotDir,
|
|
66
|
+
screenshotRecorder: this.screenshotRecorder,
|
|
67
|
+
});
|
|
155
68
|
this.stepDelayMs = options.stepDelayMs ?? getConfiguredStepDelayMs();
|
|
156
|
-
this.reportedScreenshotWriteError = false;
|
|
157
|
-
this.reportedScreenshotAnnotateError = false;
|
|
158
|
-
this.lastRecordedScreenshotPaths = [];
|
|
159
69
|
this.getScreenshotAsBase64 = options.getScreenshotAsBase64 || getScreenshotAsBase64;
|
|
160
70
|
this.handleModelAction = options.handleModelAction || handleModelAction;
|
|
161
71
|
this.sendCUARequest = options.sendCUARequest || sendCUARequest;
|
|
162
72
|
this.reviseTestScript = options.reviseTestScript || null;
|
|
163
73
|
this.getCurrentPlatform = options.getCurrentPlatform || getCurrentPlatform;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const previousRecordedScreenshotPaths = updateLastRecordedScreenshotPaths
|
|
171
|
-
? null
|
|
172
|
-
: [...this.lastRecordedScreenshotPaths];
|
|
173
|
-
try {
|
|
174
|
-
if (this.screenshotRecorder?.saveScreenshot) {
|
|
175
|
-
const result = await this.screenshotRecorder.saveScreenshot(screenshotBase64, metadata);
|
|
176
|
-
if (updateLastRecordedScreenshotPaths) {
|
|
177
|
-
this.lastRecordedScreenshotPaths = normalizeArtifactPaths(result);
|
|
178
|
-
}
|
|
179
|
-
return result;
|
|
180
|
-
}
|
|
181
|
-
if (this.recordScreenshots && this.screenshotDir) {
|
|
182
|
-
const framePath = path.join(this.screenshotDir, `frame_${String(Date.now())}.png`);
|
|
183
|
-
await writeFile(framePath, Buffer.from(screenshotBase64, "base64"));
|
|
184
|
-
if (updateLastRecordedScreenshotPaths) {
|
|
185
|
-
this.lastRecordedScreenshotPaths = [framePath];
|
|
186
|
-
}
|
|
187
|
-
return framePath;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
if (updateLastRecordedScreenshotPaths) {
|
|
192
|
-
this.lastRecordedScreenshotPaths = [];
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
this.lastRecordedScreenshotPaths = previousRecordedScreenshotPaths;
|
|
196
|
-
}
|
|
197
|
-
if (!this.reportedScreenshotWriteError) {
|
|
198
|
-
this.reportedScreenshotWriteError = true;
|
|
199
|
-
const scope = typeof metadata.sessionId === "string" ? "design" : "execution";
|
|
200
|
-
const ids = scope === "design"
|
|
201
|
-
? {
|
|
202
|
-
sessionId: metadata.sessionId,
|
|
203
|
-
stepId: metadata.stepId,
|
|
204
|
-
instructionIndex: metadata.instructionIndex
|
|
205
|
-
}
|
|
206
|
-
: {
|
|
207
|
-
runId: metadata.runId,
|
|
208
|
-
stepId: metadata.stepId,
|
|
209
|
-
instructionIndex: metadata.instructionIndex
|
|
210
|
-
};
|
|
211
|
-
emitDesktopDebug("run.screenshot_write.error", scope, ids, {
|
|
212
|
-
captureSource: metadata.captureSource ?? null,
|
|
213
|
-
message: error instanceof Error ? error.message : "Failed to persist screenshot"
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return null;
|
|
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;
|
|
218
80
|
}
|
|
219
81
|
async recordScreenshot(screenshotBase64, metadata = {}) {
|
|
220
|
-
return await this.
|
|
82
|
+
return await this.screenshotStore.saveScreenshot(screenshotBase64, metadata);
|
|
221
83
|
}
|
|
222
|
-
async
|
|
223
|
-
|
|
224
|
-
updateLastRecordedScreenshotPaths: false,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
async annotateLatestScreenshot(action, metadata = {}) {
|
|
228
|
-
if (!(action?.type === "click" || action?.type === "double_click") || this.lastRecordedScreenshotPaths.length === 0) {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
const marker = {
|
|
233
|
-
x: action.x,
|
|
234
|
-
y: action.y,
|
|
235
|
-
style: "click",
|
|
236
|
-
};
|
|
237
|
-
const sourcePath = this.lastRecordedScreenshotPaths.find((filePath) => typeof filePath === "string" && filePath.trim());
|
|
238
|
-
if (sourcePath && this.screenshotRecorder?.saveAnnotatedScreenshot) {
|
|
239
|
-
return await this.screenshotRecorder.saveAnnotatedScreenshot(sourcePath, marker, metadata);
|
|
240
|
-
}
|
|
241
|
-
await Promise.all(this.lastRecordedScreenshotPaths.map((filePath) => annotateScreenshotArtifact(filePath, marker)));
|
|
84
|
+
async annotateScreenshot(currentScreenshot, action, metadata = {}) {
|
|
85
|
+
if (!(action?.type === "click" || action?.type === "double_click")) {
|
|
242
86
|
return null;
|
|
243
87
|
}
|
|
244
|
-
|
|
245
|
-
if (!this.reportedScreenshotAnnotateError) {
|
|
246
|
-
this.reportedScreenshotAnnotateError = true;
|
|
247
|
-
const scope = typeof metadata.sessionId === "string" ? "design" : "execution";
|
|
248
|
-
const ids = scope === "design"
|
|
249
|
-
? {
|
|
250
|
-
sessionId: metadata.sessionId,
|
|
251
|
-
stepId: metadata.stepId,
|
|
252
|
-
instructionIndex: metadata.instructionIndex
|
|
253
|
-
}
|
|
254
|
-
: {
|
|
255
|
-
runId: metadata.runId,
|
|
256
|
-
stepId: metadata.stepId,
|
|
257
|
-
instructionIndex: metadata.instructionIndex
|
|
258
|
-
};
|
|
259
|
-
emitDesktopDebug("run.screenshot_annotate.error", scope, ids, {
|
|
260
|
-
actionType: action?.type ?? null,
|
|
261
|
-
x: action?.x ?? null,
|
|
262
|
-
y: action?.y ?? null,
|
|
263
|
-
message: error instanceof Error ? error.message : "Failed to annotate screenshot"
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return null;
|
|
88
|
+
return await this.screenshotStore.annotateScreenshot(currentScreenshot, { x: action.x, y: action.y, style: "click" }, { ...metadata, actionType: action.type });
|
|
268
89
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
eventId: previewEventId,
|
|
294
|
-
captureSource: CAPTURE_SOURCES.PREVIEW_CLICK,
|
|
295
|
-
});
|
|
296
|
-
const previewText = buildPreviewOutputText({
|
|
297
|
-
requestedX: previewResult.marker.requestedX,
|
|
298
|
-
requestedY: previewResult.marker.requestedY,
|
|
299
|
-
renderedX: previewResult.marker.x,
|
|
300
|
-
renderedY: previewResult.marker.y,
|
|
301
|
-
wasClamped: previewResult.marker.wasClamped,
|
|
302
|
-
});
|
|
303
|
-
const addOutput = context?.addOutput || null;
|
|
304
|
-
if (addOutput) {
|
|
305
|
-
const previewScreenshotPath = screenshotPathFromRecorderResult(previewScreenshotResult);
|
|
306
|
-
const previewScreenshotDataUrl = context?.includeScreenshotDataUrl
|
|
307
|
-
? `data:image/png;base64,${previewResult.screenshotBase64}`
|
|
308
|
-
: null;
|
|
309
|
-
addOutput({
|
|
310
|
-
type: "action",
|
|
311
|
-
text: "Click",
|
|
312
|
-
eventType: "tool_call",
|
|
313
|
-
runId: context?.runId,
|
|
314
|
-
stepId: stepContext?.stepId,
|
|
315
|
-
instructionIndex: stepContext?.instructionIndex,
|
|
316
|
-
payload: {
|
|
317
|
-
platform: this.session.deviceInfo?.platform,
|
|
318
|
-
toolName: "preview_click",
|
|
319
|
-
captureSource: CAPTURE_SOURCES.PREVIEW_CLICK,
|
|
320
|
-
requestedX: previewResult.marker.requestedX,
|
|
321
|
-
requestedY: previewResult.marker.requestedY,
|
|
322
|
-
renderedX: previewResult.marker.x,
|
|
323
|
-
renderedY: previewResult.marker.y,
|
|
324
|
-
wasClamped: previewResult.marker.wasClamped,
|
|
325
|
-
...(previewScreenshotPath ? { screenshotPath: previewScreenshotPath } : {}),
|
|
326
|
-
...(previewScreenshotDataUrl ? { screenshotDataUrl: previewScreenshotDataUrl } : {})
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
return buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
|
|
331
|
-
text: previewText,
|
|
332
|
-
annotatedScreenshotBase64: previewResult.screenshotBase64,
|
|
333
|
-
}));
|
|
334
|
-
}
|
|
335
|
-
catch (error) {
|
|
336
|
-
return buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
|
|
337
|
-
text: `preview_click failed: ${error instanceof Error ? error.message : "unable to capture or annotate the current screen."}`,
|
|
338
|
-
}));
|
|
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
|
+
};
|
|
339
114
|
}
|
|
340
115
|
}
|
|
341
|
-
async requestNextTurn(messages, previousResponseId, context = null, stepContext = null) {
|
|
116
|
+
async requestNextTurn(messages, previousResponseId, context = null, stepContext = null, options = {}) {
|
|
342
117
|
return await this.sendCUARequest({
|
|
343
118
|
messages,
|
|
344
119
|
previousResponseId,
|
|
345
120
|
deviceInfo: this.session.deviceInfo,
|
|
346
|
-
debugContext: buildDebugContext(context, stepContext)
|
|
121
|
+
debugContext: options.debugContextOverride ?? buildDebugContext(context, stepContext),
|
|
122
|
+
toolCapabilities: options.toolCapabilitiesOverride ?? this.toolCapabilities,
|
|
123
|
+
includeComputerTool: options.includeComputerToolOverride ?? true,
|
|
347
124
|
});
|
|
348
125
|
}
|
|
349
|
-
async executeActionBatch(actions, shouldStop, context = null, stepContext = null, trackAction = null) {
|
|
126
|
+
async executeActionBatch(actions, currentScreenshot, shouldStop, context = null, stepContext = null, trackAction = null) {
|
|
350
127
|
let sawExplicitScreenshotAction = false;
|
|
351
128
|
for (const action of actions) {
|
|
352
129
|
const actionEventId = randomUUID();
|
|
@@ -357,7 +134,7 @@ export class ExecutionEngine {
|
|
|
357
134
|
sawExplicitScreenshotAction = true;
|
|
358
135
|
continue;
|
|
359
136
|
}
|
|
360
|
-
const
|
|
137
|
+
const annotatedScreenshot = await this.annotateScreenshot(currentScreenshot, action, {
|
|
361
138
|
runId: context?.runId,
|
|
362
139
|
sessionId: context?.sessionId,
|
|
363
140
|
stepId: stepContext?.stepId,
|
|
@@ -365,8 +142,6 @@ export class ExecutionEngine {
|
|
|
365
142
|
eventId: actionEventId,
|
|
366
143
|
captureSource: CAPTURE_SOURCES.ACTION_CLICK_ANNOTATION
|
|
367
144
|
});
|
|
368
|
-
const annotatedScreenshotPath = screenshotPathFromRecorderResult(annotatedScreenshotResult);
|
|
369
|
-
const annotatedScreenshotDataUrl = screenshotDataUrlFromRecorderResult(annotatedScreenshotResult);
|
|
370
145
|
await this.handleModelAction(this.session.deviceId, action, this.session.deviceInfo.scale, {
|
|
371
146
|
...context,
|
|
372
147
|
shouldStop,
|
|
@@ -374,8 +149,8 @@ export class ExecutionEngine {
|
|
|
374
149
|
instructionIndex: stepContext?.instructionIndex,
|
|
375
150
|
eventId: actionEventId,
|
|
376
151
|
captureSource: CAPTURE_SOURCES.ACTION_CLICK_ANNOTATION,
|
|
377
|
-
...(
|
|
378
|
-
...(context?.includeScreenshotDataUrl &&
|
|
152
|
+
...(annotatedScreenshot?.screenshotPath ? { screenshotPath: annotatedScreenshot.screenshotPath } : {}),
|
|
153
|
+
...(context?.includeScreenshotDataUrl && annotatedScreenshot?.screenshotDataUrl ? { screenshotDataUrl: annotatedScreenshot.screenshotDataUrl } : {})
|
|
379
154
|
});
|
|
380
155
|
if (trackAction) {
|
|
381
156
|
trackAction(action);
|
|
@@ -404,7 +179,7 @@ export class ExecutionEngine {
|
|
|
404
179
|
});
|
|
405
180
|
const screenshotEventId = randomUUID();
|
|
406
181
|
const captureSource = sawExplicitScreenshotAction ? CAPTURE_SOURCES.CALL_OUTPUT_EXPLICIT_ACTION : CAPTURE_SOURCES.CALL_OUTPUT_POST_ACTION;
|
|
407
|
-
const
|
|
182
|
+
const currentScreenshot = await this.recordScreenshot(screenshotBase64, {
|
|
408
183
|
runId: context?.runId,
|
|
409
184
|
sessionId: context?.sessionId,
|
|
410
185
|
stepId: stepContext?.stepId,
|
|
@@ -412,7 +187,6 @@ export class ExecutionEngine {
|
|
|
412
187
|
eventId: screenshotEventId,
|
|
413
188
|
captureSource
|
|
414
189
|
});
|
|
415
|
-
const screenshotPath = screenshotPathFromRecorderResult(screenshotResult);
|
|
416
190
|
addOutput({
|
|
417
191
|
type: "info",
|
|
418
192
|
text: "Capturing screen",
|
|
@@ -423,35 +197,102 @@ export class ExecutionEngine {
|
|
|
423
197
|
eventId: screenshotEventId,
|
|
424
198
|
captureSource,
|
|
425
199
|
source: sawExplicitScreenshotAction ? "explicit_action" : "post_action",
|
|
426
|
-
...(
|
|
200
|
+
...(currentScreenshot?.screenshotPath ? { screenshotPath: currentScreenshot.screenshotPath } : {}),
|
|
427
201
|
...(context?.includeScreenshotDataUrl
|
|
428
202
|
? { screenshotDataUrl: `data:image/png;base64,${screenshotBase64}` }
|
|
429
203
|
: {})
|
|
430
204
|
}
|
|
431
205
|
})
|
|
432
206
|
});
|
|
433
|
-
return
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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") {
|
|
252
|
+
addOutput({
|
|
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
|
+
}),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
442
266
|
}
|
|
443
267
|
/**
|
|
444
|
-
* Run a full turn with the CUA model
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
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.
|
|
450
281
|
*/
|
|
451
|
-
async runFullTurn(response, trackAction = null, context = null, stepContext = null) {
|
|
282
|
+
async runFullTurn(response, trackAction = null, context = null, stepContext = null, options = {}) {
|
|
452
283
|
const addOutput = context?.addOutput || printCliOutput;
|
|
453
284
|
let newResponseId = response.id;
|
|
285
|
+
let currentScreenshot = options.currentScreenshot ?? null;
|
|
454
286
|
const shouldStop = () => Boolean(trackAction?.());
|
|
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
|
+
};
|
|
455
296
|
const eventMeta = (extra = {}) => ({
|
|
456
297
|
runId: context?.runId,
|
|
457
298
|
stepId: stepContext?.stepId,
|
|
@@ -461,53 +302,12 @@ export class ExecutionEngine {
|
|
|
461
302
|
while (true) {
|
|
462
303
|
// Check for interruption before processing next batch of actions
|
|
463
304
|
if (shouldStop()) {
|
|
464
|
-
return
|
|
305
|
+
return finalize();
|
|
465
306
|
}
|
|
466
307
|
const items = response.output || [];
|
|
467
308
|
const actionableItems = extractActionableOutputItems(items);
|
|
468
309
|
const pendingSafetyChecks = collectPendingSafetyChecks(items);
|
|
469
|
-
|
|
470
|
-
for (const item of items) {
|
|
471
|
-
if (item.type === "reasoning") {
|
|
472
|
-
for (const entry of item.summary) {
|
|
473
|
-
if (entry.type === "summary_text") {
|
|
474
|
-
addOutput({
|
|
475
|
-
type: 'reasoning',
|
|
476
|
-
text: entry.text,
|
|
477
|
-
eventType: 'reasoning',
|
|
478
|
-
...eventMeta()
|
|
479
|
-
});
|
|
480
|
-
this.session.addToTranscript(`[Reasoning] ${entry.text}`);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
else if (item.type === "message") {
|
|
485
|
-
const textPart = item.content.find(c => c.type === "output_text");
|
|
486
|
-
if (textPart) {
|
|
487
|
-
addOutput({
|
|
488
|
-
type: 'assistant',
|
|
489
|
-
text: textPart.text,
|
|
490
|
-
eventType: 'assistant_message',
|
|
491
|
-
...eventMeta()
|
|
492
|
-
});
|
|
493
|
-
this.session.addToTranscript(`[Assistant] ${textPart.text}`);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
else if (item.type === "pending_safety_check") {
|
|
497
|
-
addOutput({
|
|
498
|
-
type: 'warning',
|
|
499
|
-
text: `⚠️ Safety check: ${item.code} - ${item.message}`,
|
|
500
|
-
eventType: 'system_message',
|
|
501
|
-
...eventMeta({
|
|
502
|
-
payload: {
|
|
503
|
-
id: item.id,
|
|
504
|
-
code: item.code,
|
|
505
|
-
message: item.message
|
|
506
|
-
}
|
|
507
|
-
})
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
}
|
|
310
|
+
this.emitResponseOutputItems(items, addOutput, eventMeta, options);
|
|
511
311
|
if (actionableItems.length === 0) {
|
|
512
312
|
// No actions = turn complete
|
|
513
313
|
break;
|
|
@@ -516,27 +316,60 @@ export class ExecutionEngine {
|
|
|
516
316
|
if (!actionableItem?.call_id) {
|
|
517
317
|
break;
|
|
518
318
|
}
|
|
519
|
-
if (actionableItem.type === "
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
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;
|
|
327
|
+
}
|
|
523
328
|
if (shouldStop()) {
|
|
524
|
-
return
|
|
329
|
+
return finalize();
|
|
525
330
|
}
|
|
526
|
-
|
|
331
|
+
if (toolResult.kind === "fail") {
|
|
332
|
+
summary.failed = true;
|
|
333
|
+
summary.error = toolResult.error;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
if (toolResult.kind === "retry_instruction") {
|
|
337
|
+
summary.retryInstruction = true;
|
|
338
|
+
newResponseId = null;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
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,
|
|
355
|
+
});
|
|
527
356
|
newResponseId = response.id;
|
|
357
|
+
if (toolResult.kind === "close_turn") {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
528
360
|
continue;
|
|
529
361
|
}
|
|
530
362
|
const { call_id, actions } = actionableItem;
|
|
531
|
-
const actionBatchResult = await this.executeActionBatch(actions, shouldStop, context, stepContext, trackAction);
|
|
363
|
+
const actionBatchResult = await this.executeActionBatch(actions, currentScreenshot, shouldStop, context, stepContext, trackAction);
|
|
532
364
|
if (actionBatchResult.stopped) {
|
|
533
|
-
return
|
|
365
|
+
return finalize();
|
|
534
366
|
}
|
|
535
|
-
const
|
|
367
|
+
const followUp = await this.buildActionFollowUp(call_id, pendingSafetyChecks, actionBatchResult.sawExplicitScreenshotAction, addOutput, eventMeta, context, stepContext);
|
|
536
368
|
if (shouldStop()) {
|
|
537
|
-
return
|
|
369
|
+
return finalize();
|
|
538
370
|
}
|
|
539
|
-
|
|
371
|
+
currentScreenshot = followUp.currentScreenshot ?? currentScreenshot;
|
|
372
|
+
response = await this.requestNextTurn(followUp.input, newResponseId, context, stepContext);
|
|
540
373
|
newResponseId = response.id;
|
|
541
374
|
}
|
|
542
375
|
// ── At end, if last output was only reasoning ──
|
|
@@ -549,6 +382,6 @@ export class ExecutionEngine {
|
|
|
549
382
|
...eventMeta()
|
|
550
383
|
});
|
|
551
384
|
}
|
|
552
|
-
return
|
|
385
|
+
return finalize();
|
|
553
386
|
}
|
|
554
387
|
}
|