@namzu/sdk 59.0.0 → 60.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -0
- package/dist/compaction/context-window.d.ts +53 -0
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +141 -2
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/execution/workflow/index.d.ts +3 -1
- package/dist/execution/workflow/index.d.ts.map +1 -1
- package/dist/execution/workflow/index.js +2 -1
- package/dist/execution/workflow/index.js.map +1 -1
- package/dist/execution/workflow/journal.d.ts +24 -4
- package/dist/execution/workflow/journal.d.ts.map +1 -1
- package/dist/execution/workflow/journal.js +131 -9
- package/dist/execution/workflow/journal.js.map +1 -1
- package/dist/execution/workflow/prelude.d.ts +2 -0
- package/dist/execution/workflow/prelude.d.ts.map +1 -1
- package/dist/execution/workflow/prelude.js +49 -8
- package/dist/execution/workflow/prelude.js.map +1 -1
- package/dist/execution/workflow/resume.d.ts +59 -0
- package/dist/execution/workflow/resume.d.ts.map +1 -0
- package/dist/execution/workflow/resume.js +273 -0
- package/dist/execution/workflow/resume.js.map +1 -0
- package/dist/execution/workflow/runtime.d.ts.map +1 -1
- package/dist/execution/workflow/runtime.js +112 -8
- package/dist/execution/workflow/runtime.js.map +1 -1
- package/dist/execution/workflow/types.d.ts +105 -1
- package/dist/execution/workflow/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin/compat.d.ts +24 -0
- package/dist/plugin/compat.d.ts.map +1 -0
- package/dist/plugin/compat.js +149 -0
- package/dist/plugin/compat.js.map +1 -0
- package/dist/plugin/loader.d.ts +2 -1
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +6 -0
- package/dist/plugin/loader.js.map +1 -1
- package/dist/pricing/index.d.ts.map +1 -1
- package/dist/pricing/index.js +2 -1
- package/dist/pricing/index.js.map +1 -1
- package/dist/process/kill-tree.d.ts +44 -0
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +102 -4
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +9 -1
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/model-window-suffix.d.ts +18 -0
- package/dist/provider/model-window-suffix.d.ts.map +1 -0
- package/dist/provider/model-window-suffix.js +77 -0
- package/dist/provider/model-window-suffix.js.map +1 -0
- package/dist/public-runtime.d.ts +5 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -4
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +1 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.d.ts +1 -4
- package/dist/registry/command/kernel-commands.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.js +0 -28
- package/dist/registry/command/kernel-commands.js.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
- package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.js +11 -0
- package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
- package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
- package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
- package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
- package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
- package/dist/runtime/jobs/liveness.d.ts +67 -0
- package/dist/runtime/jobs/liveness.d.ts.map +1 -0
- package/dist/runtime/jobs/liveness.js +94 -0
- package/dist/runtime/jobs/liveness.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +21 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +104 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/jobs/wake-budget.d.ts +35 -0
- package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
- package/dist/runtime/jobs/wake-budget.js +48 -0
- package/dist/runtime/jobs/wake-budget.js.map +1 -0
- package/dist/runtime/query/detached-calls.d.ts +65 -0
- package/dist/runtime/query/detached-calls.d.ts.map +1 -0
- package/dist/runtime/query/detached-calls.js +116 -0
- package/dist/runtime/query/detached-calls.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +4 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +90 -6
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +20 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +31 -5
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.js +11 -3
- package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +7 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/session-storage.d.ts.map +1 -1
- package/dist/runtime/query/session-storage.js +8 -3
- package/dist/runtime/query/session-storage.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +29 -3
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +58 -3
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/runtime/system-events.d.ts +5 -0
- package/dist/runtime/system-events.d.ts.map +1 -1
- package/dist/runtime/system-events.js +7 -1
- package/dist/runtime/system-events.js.map +1 -1
- package/dist/sandbox/denial.d.ts +11 -0
- package/dist/sandbox/denial.d.ts.map +1 -0
- package/dist/sandbox/denial.js +61 -0
- package/dist/sandbox/denial.js.map +1 -0
- package/dist/sandbox/file-walk-program.d.ts.map +1 -1
- package/dist/sandbox/file-walk-program.js +31 -13
- package/dist/sandbox/file-walk-program.js.map +1 -1
- package/dist/sandbox/file-walk.d.ts +2 -0
- package/dist/sandbox/file-walk.d.ts.map +1 -1
- package/dist/sandbox/file-walk.js +64 -13
- package/dist/sandbox/file-walk.js.map +1 -1
- package/dist/sandbox/ignore-matcher.d.ts +30 -0
- package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
- package/dist/sandbox/ignore-matcher.js +11 -0
- package/dist/sandbox/ignore-matcher.js.map +1 -0
- package/dist/sandbox/ignore-source.d.ts +26 -0
- package/dist/sandbox/ignore-source.d.ts.map +1 -0
- package/dist/sandbox/ignore-source.js +221 -0
- package/dist/sandbox/ignore-source.js.map +1 -0
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +11 -1
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +1 -0
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/session/log-hash.d.ts.map +1 -1
- package/dist/session/log-hash.js +18 -1
- package/dist/session/log-hash.js.map +1 -1
- package/dist/store/session-index/sqlite.d.ts.map +1 -1
- package/dist/store/session-index/sqlite.js +169 -16
- package/dist/store/session-index/sqlite.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +10 -4
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +35 -0
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +512 -67
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/glob.d.ts +1 -0
- package/dist/tools/builtins/glob.d.ts.map +1 -1
- package/dist/tools/builtins/glob.js +65 -28
- package/dist/tools/builtins/glob.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +2 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +94 -40
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/job.d.ts +1 -1
- package/dist/tools/builtins/job.d.ts.map +1 -1
- package/dist/tools/builtins/job.js +34 -7
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/search-support.d.ts +44 -0
- package/dist/tools/builtins/search-support.d.ts.map +1 -0
- package/dist/tools/builtins/search-support.js +89 -0
- package/dist/tools/builtins/search-support.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job.js +13 -2
- package/dist/tools/builtins/wait-for-job.js.map +1 -1
- package/dist/tools/command-shell.d.ts.map +1 -1
- package/dist/tools/command-shell.js +14 -5
- package/dist/tools/command-shell.js.map +1 -1
- package/dist/types/computer-use/index.d.ts +129 -13
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +24 -0
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +4 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +38 -1
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/session/records.d.ts +264 -0
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +32 -1
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/index.d.ts +30 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +8 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/compaction/context-window.ts +163 -2
- package/src/execution/workflow/index.ts +14 -1
- package/src/execution/workflow/journal.ts +161 -11
- package/src/execution/workflow/prelude.ts +51 -8
- package/src/execution/workflow/resume.ts +313 -0
- package/src/execution/workflow/runtime.ts +127 -9
- package/src/execution/workflow/types.ts +109 -1
- package/src/index.ts +17 -0
- package/src/plugin/compat.ts +173 -0
- package/src/plugin/loader.ts +7 -1
- package/src/pricing/index.ts +2 -1
- package/src/process/kill-tree.ts +106 -4
- package/src/prompt/coding-agent-doctrine.ts +9 -1
- package/src/provider/model-window-suffix.ts +84 -0
- package/src/public-runtime.ts +21 -2
- package/src/public-types.ts +1 -0
- package/src/registry/command/kernel-commands.ts +1 -32
- package/src/runtime/jobs/awaited-jobs.ts +11 -0
- package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
- package/src/runtime/jobs/liveness.ts +144 -0
- package/src/runtime/jobs/registry.ts +114 -2
- package/src/runtime/jobs/wake-budget.ts +55 -0
- package/src/runtime/query/detached-calls.ts +135 -0
- package/src/runtime/query/executor.ts +92 -10
- package/src/runtime/query/index.ts +50 -7
- package/src/runtime/query/iteration/outstanding-work.ts +12 -3
- package/src/runtime/query/iteration/phases/context.ts +7 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
- package/src/runtime/query/session-storage.ts +8 -3
- package/src/runtime/query/steering.ts +77 -6
- package/src/runtime/query/tooling.ts +2 -0
- package/src/runtime/system-events.ts +12 -1
- package/src/sandbox/denial.ts +79 -0
- package/src/sandbox/file-walk-program.ts +32 -13
- package/src/sandbox/file-walk.ts +75 -12
- package/src/sandbox/ignore-matcher.ts +46 -0
- package/src/sandbox/ignore-source.ts +220 -0
- package/src/sandbox/provider/local.ts +11 -1
- package/src/scheduler/completion-inbox.ts +1 -0
- package/src/session/log-hash.ts +21 -0
- package/src/store/session-index/sqlite.ts +171 -16
- package/src/tools/builtins/bash.ts +11 -4
- package/src/tools/builtins/computer-use.ts +593 -68
- package/src/tools/builtins/glob.ts +79 -32
- package/src/tools/builtins/grep.ts +107 -43
- package/src/tools/builtins/job.ts +42 -12
- package/src/tools/builtins/search-support.ts +107 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
- package/src/tools/builtins/wait-for-job.ts +14 -2
- package/src/tools/command-shell.ts +13 -5
- package/src/types/computer-use/index.ts +157 -15
- package/src/types/plugin/index.ts +8 -0
- package/src/types/sandbox/index.ts +40 -1
- package/src/types/session/records.ts +36 -2
- package/src/types/tool/index.ts +30 -0
- package/src/types/tool/presentation.ts +8 -0
|
@@ -10,9 +10,12 @@ export const COMPUTER_USE_TOOL_NAME = 'computer_use';
|
|
|
10
10
|
const DEFAULT_SETTLE_MS = 500;
|
|
11
11
|
const DEFAULT_MAX_BATCH_ACTIONS = 20;
|
|
12
12
|
const DEFAULT_MAX_WAIT_MS = 10_000;
|
|
13
|
+
const DEFAULT_WAIT_FOR_MS = 10_000;
|
|
13
14
|
const MAX_LISTED_WINDOWS = 50;
|
|
14
15
|
/** The most of a ui_snapshot the model is shown, in characters (about 4 000 tokens). */
|
|
15
16
|
const MAX_UI_SNAPSHOT_CHARS = 14_000;
|
|
17
|
+
/** The refs of earlier snapshots the tool still resolves for the host. */
|
|
18
|
+
const MAX_HELD_UI_REFS = 4_000;
|
|
16
19
|
/**
|
|
17
20
|
* Why `provider` cannot drive `computer_use`, or undefined when it can.
|
|
18
21
|
*
|
|
@@ -52,7 +55,10 @@ const regionSchema = z.object({
|
|
|
52
55
|
const mouseButtonSchema = z.enum(['left', 'right', 'middle']).default('left');
|
|
53
56
|
const screenshotIdSchema = z.string().min(1).optional();
|
|
54
57
|
const cursorPositionSchema = z.object({ type: z.literal('cursor_position') });
|
|
55
|
-
const mouseMoveSchema = z.object({
|
|
58
|
+
const mouseMoveSchema = z.object({
|
|
59
|
+
type: z.literal('mouse_move'),
|
|
60
|
+
to: pointSchema,
|
|
61
|
+
});
|
|
56
62
|
const mouseClickSchema = z.object({
|
|
57
63
|
type: z.literal('mouse_click'),
|
|
58
64
|
at: pointSchema,
|
|
@@ -87,7 +93,10 @@ const keySchema = z.object({
|
|
|
87
93
|
at: pointSchema.optional(),
|
|
88
94
|
delivery_mode: z.enum(['background', 'foreground']).optional(),
|
|
89
95
|
});
|
|
90
|
-
const waitSchema = z.object({
|
|
96
|
+
const waitSchema = z.object({
|
|
97
|
+
type: z.literal('wait'),
|
|
98
|
+
ms: z.number().int().nonnegative(),
|
|
99
|
+
});
|
|
91
100
|
const listWindowsSchema = z.object({ type: z.literal('list_windows') });
|
|
92
101
|
const focusWindowSchema = z.object({
|
|
93
102
|
type: z.literal('focus_window'),
|
|
@@ -106,6 +115,30 @@ const UI_ACTIONS = [
|
|
|
106
115
|
const uiSnapshotSchema = z.object({
|
|
107
116
|
type: z.literal('ui_snapshot'),
|
|
108
117
|
window_id: z.string().min(1).optional(),
|
|
118
|
+
/** A ref shown by an earlier ui_snapshot: read only that region or control. */
|
|
119
|
+
root: z.string().min(1).optional(),
|
|
120
|
+
/** With root: skip this many of the region's children (the next page of a long region). */
|
|
121
|
+
offset: z.number().int().min(0).max(1_000_000).optional(),
|
|
122
|
+
/** Only controls whose text contains this, with the elements above them. */
|
|
123
|
+
query: z.string().min(1).max(200).optional(),
|
|
124
|
+
});
|
|
125
|
+
const WAIT_FOR_CONDITIONS = [
|
|
126
|
+
'element_appears',
|
|
127
|
+
'element_gone',
|
|
128
|
+
'text_appears',
|
|
129
|
+
'text_gone',
|
|
130
|
+
'window_appears',
|
|
131
|
+
'window_closed',
|
|
132
|
+
];
|
|
133
|
+
const waitForSchema = z.object({
|
|
134
|
+
type: z.literal('wait_for'),
|
|
135
|
+
until: z.enum(WAIT_FOR_CONDITIONS),
|
|
136
|
+
/** A control's name, a text, or a window title: matched as a case-insensitive substring. */
|
|
137
|
+
match: z.string().min(1),
|
|
138
|
+
/** Narrows element_* to controls of this role, such as Button. */
|
|
139
|
+
role: z.string().min(1).optional(),
|
|
140
|
+
window_id: z.string().min(1).optional(),
|
|
141
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
109
142
|
});
|
|
110
143
|
const uiActSchema = z.object({
|
|
111
144
|
type: z.literal('ui_act'),
|
|
@@ -129,7 +162,10 @@ const batchItemSchema = z.discriminatedUnion('type', [
|
|
|
129
162
|
]);
|
|
130
163
|
const withFrame = { screenshot_id: screenshotIdSchema };
|
|
131
164
|
const actionSchema = z.discriminatedUnion('type', [
|
|
132
|
-
z.object({
|
|
165
|
+
z.object({
|
|
166
|
+
type: z.literal('screenshot'),
|
|
167
|
+
window_id: z.string().min(1).optional(),
|
|
168
|
+
}),
|
|
133
169
|
z.object({ type: z.literal('zoom'), region: regionSchema, ...withFrame }),
|
|
134
170
|
cursorPositionSchema.extend(withFrame),
|
|
135
171
|
mouseMoveSchema.extend(withFrame),
|
|
@@ -142,6 +178,7 @@ const actionSchema = z.discriminatedUnion('type', [
|
|
|
142
178
|
listWindowsSchema.extend(withFrame),
|
|
143
179
|
focusWindowSchema.extend(withFrame),
|
|
144
180
|
uiSnapshotSchema,
|
|
181
|
+
waitForSchema,
|
|
145
182
|
uiActSchema.extend(withFrame),
|
|
146
183
|
z.object({
|
|
147
184
|
type: z.literal('batch'),
|
|
@@ -173,6 +210,7 @@ const ALL_ACTIONS = [
|
|
|
173
210
|
'list_windows',
|
|
174
211
|
'focus_window',
|
|
175
212
|
'ui_snapshot',
|
|
213
|
+
'wait_for',
|
|
176
214
|
'ui_act',
|
|
177
215
|
'batch',
|
|
178
216
|
];
|
|
@@ -184,6 +222,7 @@ const READ_ONLY_ACTIONS = new Set([
|
|
|
184
222
|
'wait',
|
|
185
223
|
'list_windows',
|
|
186
224
|
'ui_snapshot',
|
|
225
|
+
'wait_for',
|
|
187
226
|
]);
|
|
188
227
|
const DESTRUCTIVE_ACTIONS = new Set([
|
|
189
228
|
'mouse_click',
|
|
@@ -194,16 +233,22 @@ const DESTRUCTIVE_ACTIONS = new Set([
|
|
|
194
233
|
'ui_act',
|
|
195
234
|
]);
|
|
196
235
|
/** Observations that show the model what is on the screen, on their own. */
|
|
197
|
-
const SCREEN_OBSERVATIONS = new Set([
|
|
236
|
+
const SCREEN_OBSERVATIONS = new Set([
|
|
237
|
+
'screenshot',
|
|
238
|
+
'zoom',
|
|
239
|
+
'list_windows',
|
|
240
|
+
'ui_snapshot',
|
|
241
|
+
'wait_for',
|
|
242
|
+
]);
|
|
198
243
|
/**
|
|
199
244
|
* Terminal applications, by the process name a host reports in
|
|
200
245
|
* `WindowInfo.app` (on Windows without `.exe`). Keys are not typed into these.
|
|
201
246
|
*/
|
|
202
247
|
const TERMINAL_APPS = /^(WindowsTerminal|OpenConsole|conhost|cmd|powershell|pwsh|wsl|mintty|wezterm(-gui)?|alacritty|Hyper|Tabby|kitty|ConEmu(64)?|putty|Terminal|iTerm2?|gnome-terminal(-server)?|konsole|xterm|xfce4-terminal|tilix|terminator|foot|ghostty|Warp)$/i;
|
|
203
248
|
/** Actions a batch cannot carry: the ones that return an image or a tree, and batch itself. */
|
|
204
|
-
const OUTSIDE_BATCH = new Set(['screenshot', 'zoom', 'ui_snapshot', 'batch']);
|
|
249
|
+
const OUTSIDE_BATCH = new Set(['screenshot', 'zoom', 'ui_snapshot', 'wait_for', 'batch']);
|
|
205
250
|
/** A single action's whole result when it only acted: `<label>: done`, then its screenshot. */
|
|
206
|
-
const ACKNOWLEDGEMENT = /^[^\n]*: done(\nScreenshot s\d+ \(\d+x\d+\)\.)?$/;
|
|
251
|
+
const ACKNOWLEDGEMENT = /^[^\n]*: (done(, verified[^\n]*)?|sent, not confirmed)(\nScreenshot s\d+ \(\d+x\d+\)\.)?$/;
|
|
207
252
|
function isRecord(value) {
|
|
208
253
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
209
254
|
}
|
|
@@ -249,6 +294,7 @@ function requiredCapability(type) {
|
|
|
249
294
|
case 'focus_window':
|
|
250
295
|
return 'windows';
|
|
251
296
|
case 'ui_snapshot':
|
|
297
|
+
case 'wait_for':
|
|
252
298
|
case 'ui_act':
|
|
253
299
|
return 'uiTree';
|
|
254
300
|
default:
|
|
@@ -297,6 +343,7 @@ function availableActions(host, caps) {
|
|
|
297
343
|
case 'focus_window':
|
|
298
344
|
return windows;
|
|
299
345
|
case 'ui_snapshot':
|
|
346
|
+
case 'wait_for':
|
|
300
347
|
case 'ui_act':
|
|
301
348
|
return uiTree;
|
|
302
349
|
case 'batch':
|
|
@@ -350,10 +397,12 @@ function buildDescription(host, caps, settings) {
|
|
|
350
397
|
}
|
|
351
398
|
if (!available.includes('screenshot'))
|
|
352
399
|
return finish(lines, caps, available);
|
|
353
|
-
|
|
400
|
+
if (available.includes('ui_snapshot'))
|
|
401
|
+
lines.push("Work from the window's controls first: list_windows, then ui_snapshot {window_id}, then ui_act on a ref. This reaches a window that is behind others, does not move the user's pointer or take their screen, and does not depend on coordinates. Confirm an action by the result it reports (a control read back), by wait_for, or by a window screenshot. Click coordinates only in a window whose tree has no usable controls; a first coordinate click in a window that has a tree comes with a reminder.");
|
|
402
|
+
lines.push('Coordinates: every x/y you send is a pixel of the most recent screenshot this tool returned — origin at its top-left, x to the right, y down — never a screen pixel. Each screenshot states its id and size (for example "s3: 1456x819"); stay inside it. Display screenshots map onto the display. To aim at an earlier display screenshot, pass its id as screenshot_id.', 'Take a screenshot before your first click on pixels.');
|
|
354
403
|
if (settings.screenshotAfterActions)
|
|
355
404
|
lines.push(`After any action that changes something, the tool waits ${settings.settleMs} ms and returns a new screenshot — do not call screenshot again after acting.`);
|
|
356
|
-
lines.push('zoom returns a closer, sharper view of region {x, y, width, height} of the screenshot; your coordinates still refer to the screenshot, not to the zoomed image.', `wait pauses for ms milliseconds (at most ${settings.maxWaitMs} per call, a batch's waits together) and then shows the screen
|
|
405
|
+
lines.push('zoom returns a closer, sharper view of region {x, y, width, height} of the screenshot; your coordinates still refer to the screenshot, not to the zoomed image.', `wait pauses for ms milliseconds (at most ${settings.maxWaitMs} per call, a batch's waits together) and then shows the screen.${available.includes('wait_for') ? ' Prefer wait_for to a fixed wait whenever you know what you are waiting for.' : ''}`);
|
|
357
406
|
if (available.includes('batch'))
|
|
358
407
|
lines.push(`batch: {"type":"batch","actions":[...]} runs up to ${settings.maxBatchActions} actions in order, stops at the first one that fails, and returns one screenshot at the end. Every call costs a model round trip of several seconds, so put the steps you can already predict into one batch (click a field, type, press ENTER) rather than one call each — but end the batch at any step that should bring up a new window, and look before typing into it. A batch cannot contain screenshot${available.includes('ui_snapshot') ? ', zoom or ui_snapshot' : ' or zoom'}.`);
|
|
359
408
|
if (caps.displayServer === 'win32' &&
|
|
@@ -363,9 +412,13 @@ function buildDescription(host, caps, settings) {
|
|
|
363
412
|
if (available.includes('list_windows'))
|
|
364
413
|
lines.push('list_windows names the open windows with their ids; focus_window brings one to the front.');
|
|
365
414
|
if (windowCaptureAvailable(host, caps))
|
|
366
|
-
lines.push(`When the user names an application or window, ${available.includes('list_windows') ? 'use list_windows then screenshot {window_id}' : 'use screenshot {window_id} with a host-provided window id'} to capture pixels for that window. Its x/y are pixels of that window image; clicks, drags, typing and keys target that window. Window pointer input
|
|
415
|
+
lines.push(`When the user names an application or window, ${available.includes('list_windows') ? 'use list_windows then screenshot {window_id}' : 'use screenshot {window_id} with a host-provided window id'} to capture pixels for that window. Its x/y are pixels of that window image; clicks, drags, typing and keys target that window. Window pointer input is sent in the background first. If a click or drag is refused, inspect a fresh image before any other action; where the host can use the screen it may repeat a refused click once on the screen, and the result says so. ${caps.windowScroll === false ? `Window pixel scrolling is unavailable on this host; for a browser page use key PAGE_DOWN or ARROW_DOWN with delivery_mode:"foreground"${hostActionAvailable(caps, 'screenshot') ? ', or deliberately take a display screenshot for display scrolling' : ''}. ` : ''}To type into a browser field, first mouse_click its point in the window image, then type_text with delivery_mode:"foreground" if background text is unavailable. This may briefly bring the verified window to the front: the host waits until the user has been still for a moment, puts their window back afterwards, and reports it as not delivered (safe to try again later) when the screen stays busy. ${host.beginTurn ? 'A result that says the user pressed Escape means stop: end your turn and say what is left undone. ' : ''}For browser shortcuts such as CTRL+L, use key with delivery_mode:"foreground". A stale or moved window is refused; take a fresh window screenshot. ${hostActionAvailable(caps, 'screenshot') ? 'Plain screenshot still shows the display. ' : 'screenshot requires window_id on this host. '}Browser permission bubbles outside the window may be absent. If the window image is blank or shows another window, do not click it; focus the target and capture it again.`);
|
|
367
416
|
if (available.includes('ui_snapshot'))
|
|
368
|
-
lines.push(`ui_snapshot {window_id} reads a window's controls (its accessibility tree) as text, each control you can act on with a ref such as e12; without window_id it reads the window in front. ui_act {ref, action, value} acts on one control: invoke (press a button, open a menu item), set_value (replace a field's text with value), toggle, select, expand, collapse.
|
|
417
|
+
lines.push(`ui_snapshot {window_id} reads a window's controls (its accessibility tree) as text, each control you can act on with a ref such as e12; without window_id it reads the window in front. A large window comes back as an outline: a region ends with "(+N more inside)" and has a ref; ui_snapshot {root: <that ref>} reads that region alone (a long one in pages: offset) and keeps every other ref usable, and ui_snapshot {query: <text>} lists only the controls whose text matches. ui_act {ref, action, value} acts on one control: invoke (press a button, open a menu item), set_value (replace a field's text with value), toggle, select, expand, collapse. A batch of ui_act steps (for example pressing several buttons) runs in one call. A ref names its control and stays usable after later snapshots and after you act: the control is found again by its id, its place and its name before anything is sent. If it cannot be found the step fails with STALE_REF, and if several controls fit it fails with AMBIGUOUS_TARGET; in both nothing was sent, so take a ui_snapshot and choose again.`, 'Each ui_act says what is known. "done, verified" means the control was read back and matches. "sent, not confirmed" means the input went out and nothing could check it: look (ui_snapshot, or a window screenshot) before repeating it, because repeating can act twice. A failure that says "not delivered" means nothing reached the control, so trying again, or another way, is safe. set_value on a control that does not take the value fails with that reason instead of being typed in; select on a list row selects it without opening it (invoke opens it).', ...(available.includes('wait_for')
|
|
418
|
+
? [
|
|
419
|
+
`wait_for {until, match, window_id, timeout_ms} looks for an element (element_appears, element_gone; role narrows it), a text (text_appears, text_gone) or a window (window_appears, window_closed) and returns the window's controls as soon as it holds (at most ${settings.maxWaitMs} ms). Use it after an action that opens a dialog or loads a page, instead of a fixed wait. It is its own call, not part of a batch.`,
|
|
420
|
+
]
|
|
421
|
+
: []), 'For a web page, prefer the browser tool when one is available; a page control that needs real focus (a drop-down list) may not take background input.');
|
|
369
422
|
lines.push(windowCaptureAvailable(host, caps)
|
|
370
423
|
? 'After a display screenshot, typing and keys go to whichever window has focus: confirm on that screenshot that the right window is in front before type_text or key. After a named-window screenshot, input targets the captured window.'
|
|
371
424
|
: 'Typing and keys go to whichever window has focus: confirm on a screenshot that the right window is in front before type_text or key.');
|
|
@@ -395,8 +448,14 @@ function pointModelSchema(description) {
|
|
|
395
448
|
type: 'object',
|
|
396
449
|
...(description ? { description } : {}),
|
|
397
450
|
properties: {
|
|
398
|
-
x: {
|
|
399
|
-
|
|
451
|
+
x: {
|
|
452
|
+
type: 'integer',
|
|
453
|
+
description: 'Horizontal pixel coordinate, from the left edge.',
|
|
454
|
+
},
|
|
455
|
+
y: {
|
|
456
|
+
type: 'integer',
|
|
457
|
+
description: 'Vertical pixel coordinate, from the top edge.',
|
|
458
|
+
},
|
|
400
459
|
},
|
|
401
460
|
required: ['x', 'y'],
|
|
402
461
|
additionalProperties: false,
|
|
@@ -415,7 +474,8 @@ const ACTION_REQUIREMENTS = {
|
|
|
415
474
|
wait: 'wait needs ms',
|
|
416
475
|
list_windows: 'list_windows needs no other fields',
|
|
417
476
|
focus_window: 'focus_window needs window_id',
|
|
418
|
-
ui_snapshot: 'ui_snapshot takes an optional window_id',
|
|
477
|
+
ui_snapshot: 'ui_snapshot takes an optional window_id, root (a ref to open) and query (text to find)',
|
|
478
|
+
wait_for: 'wait_for needs until and match; role, window_id and timeout_ms are optional',
|
|
419
479
|
ui_act: 'ui_act needs ref and action, and value for set_value',
|
|
420
480
|
batch: 'batch needs actions',
|
|
421
481
|
};
|
|
@@ -444,13 +504,20 @@ function hostModelSchema(host, caps, settings) {
|
|
|
444
504
|
to: pointModelSchema('Destination point: where mouse_move moves the cursor to, or where mouse_drag releases.'),
|
|
445
505
|
at: pointModelSchema('Point the action happens at: where to click, or where to scroll.'),
|
|
446
506
|
from: pointModelSchema('Drag start point, for mouse_drag.'),
|
|
447
|
-
button: {
|
|
507
|
+
button: {
|
|
508
|
+
type: 'string',
|
|
509
|
+
enum: buttonEnum,
|
|
510
|
+
description: 'Mouse button; left when omitted.',
|
|
511
|
+
},
|
|
448
512
|
direction: {
|
|
449
513
|
type: 'string',
|
|
450
514
|
enum: ['up', 'down', 'left', 'right'],
|
|
451
515
|
description: 'Which way to scroll.',
|
|
452
516
|
},
|
|
453
|
-
amount: {
|
|
517
|
+
amount: {
|
|
518
|
+
type: 'integer',
|
|
519
|
+
description: 'Positive integer scroll distance.',
|
|
520
|
+
},
|
|
454
521
|
text: { type: 'string', description: 'Literal text to type.' },
|
|
455
522
|
keys: {
|
|
456
523
|
type: 'string',
|
|
@@ -473,32 +540,81 @@ function hostModelSchema(host, caps, settings) {
|
|
|
473
540
|
if (present.includes('focus_window') ||
|
|
474
541
|
(!forItems &&
|
|
475
542
|
(present.includes('ui_snapshot') ||
|
|
543
|
+
present.includes('wait_for') ||
|
|
476
544
|
(present.includes('screenshot') && windowCaptureAvailable(host, caps)))))
|
|
477
545
|
fields.window_id = {
|
|
478
546
|
type: 'string',
|
|
479
547
|
description: 'A window id from list_windows or supplied by the host.',
|
|
480
548
|
};
|
|
549
|
+
if (!forItems && present.includes('ui_snapshot')) {
|
|
550
|
+
fields.root = {
|
|
551
|
+
type: 'string',
|
|
552
|
+
description: 'For ui_snapshot: a ref of a region the last outline marked "(+N more inside)". Reads only that region; other refs stay usable.',
|
|
553
|
+
};
|
|
554
|
+
fields.offset = {
|
|
555
|
+
type: 'integer',
|
|
556
|
+
description: 'For ui_snapshot with root: where to start in a long region, as the previous page reported ("next page: offset N").',
|
|
557
|
+
};
|
|
558
|
+
fields.query = {
|
|
559
|
+
type: 'string',
|
|
560
|
+
description: 'For ui_snapshot: list only the controls whose text contains this (case-insensitive), with the elements above them.',
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
if (!forItems && present.includes('wait_for')) {
|
|
564
|
+
fields.until = {
|
|
565
|
+
type: 'string',
|
|
566
|
+
enum: [...WAIT_FOR_CONDITIONS],
|
|
567
|
+
description: "For wait_for: what to wait for. element_* and text_* read the window's controls; window_* read the window list.",
|
|
568
|
+
};
|
|
569
|
+
fields.match = {
|
|
570
|
+
type: 'string',
|
|
571
|
+
description: 'For wait_for: the control name, text or window title to look for (case-insensitive, part of it is enough).',
|
|
572
|
+
};
|
|
573
|
+
fields.role = {
|
|
574
|
+
type: 'string',
|
|
575
|
+
description: 'For wait_for element_*: only controls of this role, such as Button or Edit.',
|
|
576
|
+
};
|
|
577
|
+
fields.timeout_ms = {
|
|
578
|
+
type: 'integer',
|
|
579
|
+
description: `For wait_for: how long to keep looking, at most ${settings.maxWaitMs}. Default ${Math.min(10_000, settings.maxWaitMs)}.`,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
481
582
|
if (present.includes('ui_act')) {
|
|
482
583
|
fields.ref = {
|
|
483
584
|
type: 'string',
|
|
484
|
-
description: 'A ref
|
|
585
|
+
description: 'A ref shown by a ui_snapshot, such as e12.',
|
|
485
586
|
};
|
|
486
587
|
fields.action = {
|
|
487
588
|
type: 'string',
|
|
488
589
|
enum: [...UI_ACTIONS],
|
|
489
590
|
description: 'What ui_act does to the control.',
|
|
490
591
|
};
|
|
491
|
-
fields.value = {
|
|
592
|
+
fields.value = {
|
|
593
|
+
type: 'string',
|
|
594
|
+
description: 'The text set_value puts in the control.',
|
|
595
|
+
};
|
|
492
596
|
}
|
|
493
597
|
if (!forItems && present.includes('zoom'))
|
|
494
598
|
fields.region = {
|
|
495
599
|
type: 'object',
|
|
496
600
|
description: 'Area of the screenshot to zoom into, in its pixels.',
|
|
497
601
|
properties: {
|
|
498
|
-
x: {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
602
|
+
x: {
|
|
603
|
+
type: 'integer',
|
|
604
|
+
description: 'Left edge of the region, in screenshot pixels.',
|
|
605
|
+
},
|
|
606
|
+
y: {
|
|
607
|
+
type: 'integer',
|
|
608
|
+
description: 'Top edge of the region, in screenshot pixels.',
|
|
609
|
+
},
|
|
610
|
+
width: {
|
|
611
|
+
type: 'integer',
|
|
612
|
+
description: 'Width of the region, in screenshot pixels.',
|
|
613
|
+
},
|
|
614
|
+
height: {
|
|
615
|
+
type: 'integer',
|
|
616
|
+
description: 'Height of the region, in screenshot pixels.',
|
|
617
|
+
},
|
|
502
618
|
},
|
|
503
619
|
required: ['x', 'y', 'width', 'height'],
|
|
504
620
|
additionalProperties: false,
|
|
@@ -614,9 +730,13 @@ function itemLabel(input, describe) {
|
|
|
614
730
|
case 'focus_window':
|
|
615
731
|
return `Focus window ${input.window_id}`;
|
|
616
732
|
case 'ui_snapshot':
|
|
733
|
+
if (input.root)
|
|
734
|
+
return `Open region ${input.root}${input.window_id ? ` of window ${input.window_id}` : ''}`;
|
|
617
735
|
return input.window_id
|
|
618
736
|
? `Read the controls of window ${input.window_id}`
|
|
619
737
|
: 'Read the controls of the front window';
|
|
738
|
+
case 'wait_for':
|
|
739
|
+
return `Wait until ${input.until.replace('_', ' ')} ${quotedText(input.match)}`;
|
|
620
740
|
case 'ui_act':
|
|
621
741
|
return uiActLabel(input, describe);
|
|
622
742
|
case 'batch':
|
|
@@ -674,11 +794,71 @@ function visibleWindowId(id) {
|
|
|
674
794
|
}
|
|
675
795
|
class StepFailure extends Error {
|
|
676
796
|
unknown;
|
|
677
|
-
|
|
797
|
+
delivery;
|
|
798
|
+
retrySafety;
|
|
799
|
+
constructor(message, unknown, delivery, retrySafety) {
|
|
678
800
|
super(message);
|
|
679
801
|
this.unknown = unknown;
|
|
802
|
+
this.delivery = delivery;
|
|
803
|
+
this.retrySafety = retrySafety;
|
|
680
804
|
}
|
|
681
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* A host's verdict on an action, in the words the model reads. "Done" is
|
|
808
|
+
* said only for a delivery the host checked; a call that merely returned is
|
|
809
|
+
* "sent, not confirmed", and one that did not reach its target fails.
|
|
810
|
+
*/
|
|
811
|
+
function deliveryOutcome(verdict, extra) {
|
|
812
|
+
const detail = [verdict.detail, extra].filter((part) => part && part.length > 0).join(' ');
|
|
813
|
+
const base = {
|
|
814
|
+
...(verdict.delivery ? { delivery: verdict.delivery } : {}),
|
|
815
|
+
...(verdict.retrySafety ? { retrySafety: verdict.retrySafety } : {}),
|
|
816
|
+
...(verdict.readBack ? { readBack: verdict.readBack } : {}),
|
|
817
|
+
};
|
|
818
|
+
switch (verdict.delivery) {
|
|
819
|
+
case 'delivered_verified': {
|
|
820
|
+
const observed = verdict.readBack?.observed;
|
|
821
|
+
const seen = observed !== undefined ? ` (reads ${quotedText(observed)})` : '';
|
|
822
|
+
return {
|
|
823
|
+
...base,
|
|
824
|
+
note: `done, verified${seen}${detail ? `. ${detail}` : ''}`,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
case 'delivered_unverified':
|
|
828
|
+
// An input the window provably did not react to can be sent again; one nobody could check cannot.
|
|
829
|
+
if (verdict.retrySafety === 'safe' && detail)
|
|
830
|
+
return {
|
|
831
|
+
...base,
|
|
832
|
+
note: `sent, no effect seen. ${detail} It is safe to send it again if it is still needed.`,
|
|
833
|
+
};
|
|
834
|
+
return {
|
|
835
|
+
...base,
|
|
836
|
+
note: detail
|
|
837
|
+
? `sent, not confirmed. ${detail} Do not repeat it before checking.`
|
|
838
|
+
: 'sent, not confirmed',
|
|
839
|
+
};
|
|
840
|
+
default:
|
|
841
|
+
return { ...base, note: detail ? `done (${detail})` : 'done' };
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/** The failure a verdict that did not take effect becomes. */
|
|
845
|
+
function deliveryFailure(verdict) {
|
|
846
|
+
const text = verdict.detail ?? 'the action did not take effect';
|
|
847
|
+
const advice = verdict.delivery === 'not_delivered'
|
|
848
|
+
? ' Not delivered: nothing reached the target, so trying again is safe once the cause is dealt with.'
|
|
849
|
+
: verdict.delivery === 'delivered_unverified'
|
|
850
|
+
? ' It may have taken effect: look at the window before repeating it.'
|
|
851
|
+
: '';
|
|
852
|
+
return new StepFailure(`${text}${advice}`, undefined, verdict.delivery, verdict.retrySafety);
|
|
853
|
+
}
|
|
854
|
+
/** A host's window or display verdict → a step outcome, or the failure it amounts to. */
|
|
855
|
+
function hostOutcome(verdict) {
|
|
856
|
+
if (!verdict || verdict.delivery === undefined)
|
|
857
|
+
return { note: 'done' };
|
|
858
|
+
if (verdict.delivery === 'not_delivered')
|
|
859
|
+
throw deliveryFailure(verdict);
|
|
860
|
+
return deliveryOutcome(verdict);
|
|
861
|
+
}
|
|
682
862
|
/** Whether a call sends the screen to the provider (see `ToolDefinition.capturesScreen`). */
|
|
683
863
|
function capturesScreenInput(input, screenshotAfterActions) {
|
|
684
864
|
const items = batchActions(input) ?? [input];
|
|
@@ -775,13 +955,19 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
775
955
|
const frames = new ScreenshotFrames();
|
|
776
956
|
const available = new Set(availableActions(host, caps));
|
|
777
957
|
// The controls of the latest ui_snapshot, by the ref the model was shown.
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
|
|
958
|
+
// A ref names a control, not one reading of it: the same host ref keeps the
|
|
959
|
+
// same number in every snapshot, and a number is never reused for another
|
|
960
|
+
// control. Whether the control is still there is the host's answer.
|
|
961
|
+
const uiRefs = new Map();
|
|
962
|
+
const uiRefByHost = new Map();
|
|
781
963
|
let uiSnapshots = 0;
|
|
782
964
|
let uiRefCount = 0;
|
|
783
965
|
let uiSnapshotWindowId;
|
|
784
966
|
let uiSnapshotFrame;
|
|
967
|
+
// Whether the latest ui_snapshot of a window had controls to act on, and which windows were
|
|
968
|
+
// already pointed at them after a first coordinate click.
|
|
969
|
+
const treeUsable = new Map();
|
|
970
|
+
const nudgedWindows = new Set();
|
|
785
971
|
const describeUiRef = (ref) => {
|
|
786
972
|
const entry = uiRefs.get(ref);
|
|
787
973
|
return entry ? `${uiElementText(entry.element)} (${ref})` : undefined;
|
|
@@ -800,12 +986,16 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
800
986
|
return `computer_use: action "${type}" is not supported on this host.${why}`;
|
|
801
987
|
return null;
|
|
802
988
|
};
|
|
989
|
+
// The cancellation of the call being served. A tool runs one call at a time
|
|
990
|
+
// (`concurrencySafe: false`), so one variable is enough; every host call below
|
|
991
|
+
// passes it so an interrupted turn stops waiting for the desktop.
|
|
992
|
+
let callSignal;
|
|
803
993
|
/** Capture the display or one explicitly named window, fit it, and number it. */
|
|
804
994
|
const capture = async (windowId) => {
|
|
805
995
|
if (windowId !== undefined) {
|
|
806
996
|
if (!windowCaptureAvailable(host, caps))
|
|
807
997
|
throw new StepFailure('this host cannot capture a named window; no desktop screenshot was taken');
|
|
808
|
-
const shot = await host.captureWindow(windowId);
|
|
998
|
+
const shot = await host.captureWindow(windowId, callSignal);
|
|
809
999
|
const image = await fitPng(shot.data, settings.limits);
|
|
810
1000
|
if (image.sourceWidth !== shot.width || image.sourceHeight !== shot.height)
|
|
811
1001
|
throw new StepFailure('the window PNG dimensions changed during capture; take a new screenshot');
|
|
@@ -813,7 +1003,7 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
813
1003
|
}
|
|
814
1004
|
if (!hostActionAvailable(caps, 'screenshot'))
|
|
815
1005
|
throw new StepFailure('this host requires window_id for screenshots; no display screenshot was taken');
|
|
816
|
-
const result = await host.execute({ type: 'screenshot' });
|
|
1006
|
+
const result = await host.execute({ type: 'screenshot' }, callSignal);
|
|
817
1007
|
if (result.type !== 'screenshot')
|
|
818
1008
|
throw new Error(`computer_use: the host answered a screenshot with "${result.type}"`);
|
|
819
1009
|
const shot = result.result;
|
|
@@ -858,17 +1048,17 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
858
1048
|
throw new StepFailure(`${action.type} has no window-scoped input path; use a display screenshot`);
|
|
859
1049
|
if (frames.latest() !== frame)
|
|
860
1050
|
throw new StepFailure(`window screenshot ${frame.id} is stale; capture the window again`);
|
|
861
|
-
await host.executeWindow(frame.window.captureId, action);
|
|
862
|
-
return
|
|
1051
|
+
const verdict = await host.executeWindow(frame.window.captureId, action, callSignal);
|
|
1052
|
+
return hostOutcome(verdict ?? undefined);
|
|
863
1053
|
}
|
|
864
|
-
const result = await host.execute(action);
|
|
1054
|
+
const result = await host.execute(action, callSignal);
|
|
865
1055
|
if (result.type === 'cursor_position') {
|
|
866
1056
|
const selected = frame ?? frames.latest();
|
|
867
1057
|
if (!selected)
|
|
868
1058
|
return `at display pixel ${pointLabel(result.point)}`;
|
|
869
1059
|
return `at ${pointLabel(toImagePoint(selected, result.point))} in ${selected.id}`;
|
|
870
1060
|
}
|
|
871
|
-
return 'done';
|
|
1061
|
+
return result.type === 'ok' ? hostOutcome(result) : 'done';
|
|
872
1062
|
}
|
|
873
1063
|
catch (error) {
|
|
874
1064
|
if (error instanceof StepFailure)
|
|
@@ -911,7 +1101,15 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
911
1101
|
throw new StepFailure(`computer_use: action "mouse_click" does not support button "${item.button}" on this host.`);
|
|
912
1102
|
const frame = frameFor(frameId);
|
|
913
1103
|
const at = mapPoint(frame, item.at, 'at');
|
|
914
|
-
return step(() =>
|
|
1104
|
+
return step(async () => {
|
|
1105
|
+
const outcome = await runHost({ type: 'mouse_click', at, button: item.button }, frame);
|
|
1106
|
+
const nudge = coordinateNudge(frame);
|
|
1107
|
+
if (!nudge)
|
|
1108
|
+
return outcome;
|
|
1109
|
+
return typeof outcome === 'string'
|
|
1110
|
+
? `${outcome}. ${nudge}`
|
|
1111
|
+
: { ...outcome, note: `${outcome.note}. ${nudge}` };
|
|
1112
|
+
});
|
|
915
1113
|
}
|
|
916
1114
|
case 'mouse_drag': {
|
|
917
1115
|
if (item.delivery_mode)
|
|
@@ -931,7 +1129,12 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
931
1129
|
if (frame.window && caps.windowScroll === false)
|
|
932
1130
|
throw new StepFailure('window pixel scrolling is unavailable on this host; use foreground PAGE_DOWN or ARROW_DOWN in this window, or take a deliberate display screenshot before display scrolling');
|
|
933
1131
|
const at = mapPoint(frame, item.at, 'at');
|
|
934
|
-
return step(() => runHost({
|
|
1132
|
+
return step(() => runHost({
|
|
1133
|
+
type: 'scroll',
|
|
1134
|
+
at,
|
|
1135
|
+
direction: item.direction,
|
|
1136
|
+
amount: item.amount,
|
|
1137
|
+
}, frame));
|
|
935
1138
|
}
|
|
936
1139
|
case 'type_text': {
|
|
937
1140
|
if (item.at)
|
|
@@ -976,13 +1179,21 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
976
1179
|
return step(async () => {
|
|
977
1180
|
let outcome;
|
|
978
1181
|
try {
|
|
979
|
-
outcome = await host.focusWindow(item.window_id);
|
|
1182
|
+
outcome = await host.focusWindow(item.window_id, callSignal);
|
|
980
1183
|
}
|
|
981
1184
|
catch (error) {
|
|
982
1185
|
throw new StepFailure(errorText(error));
|
|
983
1186
|
}
|
|
1187
|
+
if (!outcome.ok && outcome.delivery === 'not_delivered')
|
|
1188
|
+
throw deliveryFailure({
|
|
1189
|
+
delivery: outcome.delivery,
|
|
1190
|
+
retrySafety: outcome.retrySafety ?? 'safe',
|
|
1191
|
+
detail: outcome.detail,
|
|
1192
|
+
});
|
|
984
1193
|
if (!outcome.ok)
|
|
985
|
-
throw new StepFailure(`window ${item.window_id} could not be brought to the front; ${outcome.focusedId ? `window ${outcome.focusedId} is in front` : 'the window in front could not be read'}`);
|
|
1194
|
+
throw new StepFailure(`window ${item.window_id} could not be brought to the front; ${outcome.focusedId ? `window ${outcome.focusedId} is in front` : 'the window in front could not be read'}${outcome.detail ? `. ${outcome.detail}` : ''}`);
|
|
1195
|
+
if (outcome.detail)
|
|
1196
|
+
return { note: `done. ${outcome.detail}` };
|
|
986
1197
|
return 'done';
|
|
987
1198
|
});
|
|
988
1199
|
case 'ui_act': {
|
|
@@ -990,7 +1201,9 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
990
1201
|
if (!entry)
|
|
991
1202
|
throw new StepFailure(uiSnapshots === 0
|
|
992
1203
|
? `there is no ${item.ref}: no ui_snapshot has been taken yet; take one and use its refs`
|
|
993
|
-
: `${item.ref} is not a
|
|
1204
|
+
: `${item.ref} is not a ref any ui_snapshot showed; use a ref from a snapshot`);
|
|
1205
|
+
if ((entry.element.hiddenChildren ?? 0) > 0 && (entry.element.actions ?? []).length === 0)
|
|
1206
|
+
throw new StepFailure(`${describeUiRef(item.ref)} is a region with ${entry.element.hiddenChildren} controls not shown, not a control. Open it with ui_snapshot {root: "${item.ref}"} and act on a control inside it.`);
|
|
994
1207
|
const offered = entry.element.actions;
|
|
995
1208
|
if (offered && offered.length > 0 && !offered.includes(item.action))
|
|
996
1209
|
throw new StepFailure(`${describeUiRef(item.ref)} offers ${offered.join(', ')}, not ${item.action}`);
|
|
@@ -999,14 +1212,20 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
999
1212
|
return step(async () => {
|
|
1000
1213
|
let outcome;
|
|
1001
1214
|
try {
|
|
1002
|
-
outcome = await host.uiAct(entry.hostRef, item.action, item.value);
|
|
1215
|
+
outcome = await host.uiAct(entry.hostRef, item.action, item.value, callSignal);
|
|
1003
1216
|
}
|
|
1004
1217
|
catch (error) {
|
|
1005
1218
|
throw new StepFailure(errorText(error));
|
|
1006
1219
|
}
|
|
1007
1220
|
if (!outcome.ok)
|
|
1008
|
-
throw
|
|
1009
|
-
|
|
1221
|
+
throw deliveryFailure({
|
|
1222
|
+
...outcome,
|
|
1223
|
+
detail: outcome.code !== undefined
|
|
1224
|
+
? `${outcome.code}: ${outcome.detail ?? `${describeUiRef(item.ref)} could not be resolved`}`
|
|
1225
|
+
: (outcome.detail ??
|
|
1226
|
+
`${item.action} did not take effect on ${describeUiRef(item.ref)}`),
|
|
1227
|
+
});
|
|
1228
|
+
return deliveryOutcome(outcome);
|
|
1010
1229
|
});
|
|
1011
1230
|
}
|
|
1012
1231
|
}
|
|
@@ -1014,15 +1233,32 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1014
1233
|
/** Read one window's controls, number the ones the model can act on, and show them. */
|
|
1015
1234
|
const uiSnapshot = async (input) => {
|
|
1016
1235
|
let snapshot;
|
|
1236
|
+
let rootRef;
|
|
1237
|
+
if (input.root !== undefined) {
|
|
1238
|
+
rootRef = uiRefs.get(input.root);
|
|
1239
|
+
if (!rootRef)
|
|
1240
|
+
throw new StepFailure(`${input.root} is not a ref any ui_snapshot showed; take a ui_snapshot of the window and open a region it marks`);
|
|
1241
|
+
}
|
|
1242
|
+
const windowId = input.window_id ?? rootRef?.windowId;
|
|
1243
|
+
const options = {
|
|
1244
|
+
...(rootRef ? { root: rootRef.hostRef } : {}),
|
|
1245
|
+
...(rootRef && input.offset !== undefined ? { offset: input.offset } : {}),
|
|
1246
|
+
...(input.query !== undefined ? { query: input.query } : {}),
|
|
1247
|
+
};
|
|
1017
1248
|
try {
|
|
1018
|
-
snapshot = await host.uiSnapshot(
|
|
1249
|
+
snapshot = await host.uiSnapshot(windowId, options, callSignal);
|
|
1019
1250
|
}
|
|
1020
1251
|
catch (error) {
|
|
1021
1252
|
throw new StepFailure(errorText(error));
|
|
1022
1253
|
}
|
|
1254
|
+
return showUiSnapshot(snapshot, windowId, [], input.root);
|
|
1255
|
+
};
|
|
1256
|
+
/** Number a read tree's controls and show it; this snapshot's refs replace the last one's. */
|
|
1257
|
+
const showUiSnapshot = (snapshot, requestedWindowId, preface = [], openedRef) => {
|
|
1023
1258
|
uiSnapshots += 1;
|
|
1024
1259
|
const id = `u${uiSnapshots}`;
|
|
1025
1260
|
const refs = new Map();
|
|
1261
|
+
const windowId = snapshot.windowId ?? requestedWindowId;
|
|
1026
1262
|
const latest = frames.latest();
|
|
1027
1263
|
const frame = latest?.window ? undefined : latest;
|
|
1028
1264
|
const lines = [];
|
|
@@ -1038,8 +1274,11 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1038
1274
|
// children still stand, one level up.
|
|
1039
1275
|
const silent = !actionable && element.name.trim().length === 0 && element.value === undefined;
|
|
1040
1276
|
if (!silent && !cut) {
|
|
1041
|
-
|
|
1042
|
-
const
|
|
1277
|
+
// The same host ref keeps its number; a new one takes the next.
|
|
1278
|
+
const known = actionable ? uiRefByHost.get(element.ref) : undefined;
|
|
1279
|
+
const ref = actionable ? (known ?? `e${uiRefCount + 1}`) : undefined;
|
|
1280
|
+
const hidden = element.hiddenChildren ?? 0;
|
|
1281
|
+
const line = `${' '.repeat(depth)}${uiElementLine(element, ref, frame)}${hidden > 0 ? ` (+${hidden} more inside)` : ''}`;
|
|
1043
1282
|
if (chars + line.length + 1 > MAX_UI_SNAPSHOT_CHARS) {
|
|
1044
1283
|
cut = true;
|
|
1045
1284
|
}
|
|
@@ -1048,8 +1287,11 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1048
1287
|
chars += line.length + 1;
|
|
1049
1288
|
shown += 1;
|
|
1050
1289
|
if (ref) {
|
|
1051
|
-
|
|
1052
|
-
|
|
1290
|
+
if (known === undefined) {
|
|
1291
|
+
uiRefCount += 1;
|
|
1292
|
+
uiRefByHost.set(element.ref, ref);
|
|
1293
|
+
}
|
|
1294
|
+
refs.set(ref, { hostRef: element.ref, element, windowId });
|
|
1053
1295
|
}
|
|
1054
1296
|
}
|
|
1055
1297
|
}
|
|
@@ -1057,12 +1299,28 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1057
1299
|
visit(child, silent ? depth : depth + 1);
|
|
1058
1300
|
};
|
|
1059
1301
|
visit(snapshot.root, 0);
|
|
1060
|
-
|
|
1061
|
-
|
|
1302
|
+
// Refs shown now replace what they name; the rest stay as they were, so a
|
|
1303
|
+
// region opened by itself leaves the refs outside it usable.
|
|
1304
|
+
for (const [ref, entry] of refs)
|
|
1305
|
+
uiRefs.set(ref, entry);
|
|
1306
|
+
if (uiRefs.size > MAX_HELD_UI_REFS) {
|
|
1307
|
+
for (const ref of [...uiRefs.keys()].slice(0, uiRefs.size - MAX_HELD_UI_REFS)) {
|
|
1308
|
+
const gone = uiRefs.get(ref);
|
|
1309
|
+
uiRefs.delete(ref);
|
|
1310
|
+
if (gone)
|
|
1311
|
+
uiRefByHost.delete(gone.hostRef);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
uiSnapshotWindowId = snapshot.windowId ?? requestedWindowId;
|
|
1315
|
+
if (uiSnapshotWindowId !== undefined)
|
|
1316
|
+
treeUsable.set(uiSnapshotWindowId, refs.size > 0);
|
|
1062
1317
|
uiSnapshotFrame = frames.latest();
|
|
1063
1318
|
const window = snapshot.windowId && /^[\w.:-]{1,64}$/.test(snapshot.windowId) ? snapshot.windowId : undefined;
|
|
1319
|
+
const opened = openedRef !== undefined && /^e\d{1,6}$/.test(openedRef) ? openedRef : undefined;
|
|
1320
|
+
const summaryLine = `UI snapshot ${id}${window ? ` of window ${window}` : ''}${snapshot.region ? `${opened ? `, region ${opened}` : ', one region'}` : ''}: ${shown} controls shown${snapshot.total !== undefined && snapshot.total > shown ? ` of ${snapshot.total}` : ''}, ${refs.size} with a ref you can pass to ui_act. A ref names its control and stays usable after later snapshots; if the control has gone or several fit, ui_act says so (STALE_REF, AMBIGUOUS_TARGET) and nothing is sent.`;
|
|
1064
1321
|
const header = [
|
|
1065
|
-
|
|
1322
|
+
...preface,
|
|
1323
|
+
summaryLine,
|
|
1066
1324
|
...(frame && !frame.window
|
|
1067
1325
|
? [
|
|
1068
1326
|
`@(x, y) is a control's centre on screenshot ${frame.id}, for a click when ui_act cannot reach it.`,
|
|
@@ -1078,13 +1336,29 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1078
1336
|
: []),
|
|
1079
1337
|
];
|
|
1080
1338
|
const body = [...about, ...lines].join('\n');
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1339
|
+
const drill = 'Open a region with ui_snapshot {root: <its ref>}, or find a control by its text with ui_snapshot {query}.';
|
|
1340
|
+
const footer = [
|
|
1341
|
+
...(snapshot.next !== undefined && opened
|
|
1342
|
+
? [
|
|
1343
|
+
`This is one page of a long region. Next page: ui_snapshot {root: "${opened}", offset: ${snapshot.next}}.`,
|
|
1344
|
+
]
|
|
1345
|
+
: []),
|
|
1346
|
+
...(snapshot.outline
|
|
1347
|
+
? [
|
|
1348
|
+
`This is an outline: regions marked "(+N more inside)" hold controls that are not listed. ${drill}`,
|
|
1349
|
+
]
|
|
1350
|
+
: []),
|
|
1351
|
+
...(cut
|
|
1352
|
+
? [
|
|
1353
|
+
`The listing was cut at ${MAX_UI_SNAPSHOT_CHARS} characters after ${shown} of ${total} controls. Drill into the region you need: ${drill}`,
|
|
1354
|
+
]
|
|
1355
|
+
: []),
|
|
1356
|
+
...(snapshot.truncated
|
|
1357
|
+
? [
|
|
1358
|
+
`The host stopped reading the tree before it ended, so some controls are missing. Drill into the region you need: ${drill} A window screenshot shows the rest.`,
|
|
1359
|
+
]
|
|
1360
|
+
: []),
|
|
1361
|
+
];
|
|
1088
1362
|
const text = [
|
|
1089
1363
|
...header,
|
|
1090
1364
|
wrapUntrusted({
|
|
@@ -1096,7 +1370,7 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1096
1370
|
].join('\n');
|
|
1097
1371
|
return {
|
|
1098
1372
|
success: true,
|
|
1099
|
-
output:
|
|
1373
|
+
output: summaryLine,
|
|
1100
1374
|
content: [{ type: 'text', text }],
|
|
1101
1375
|
data: {
|
|
1102
1376
|
uiSnapshot: {
|
|
@@ -1105,10 +1379,130 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1105
1379
|
controls: shown,
|
|
1106
1380
|
refs: refs.size,
|
|
1107
1381
|
truncated: cut || snapshot.truncated === true,
|
|
1382
|
+
...(snapshot.outline ? { outline: true } : {}),
|
|
1383
|
+
...(snapshot.total !== undefined ? { total: snapshot.total } : {}),
|
|
1108
1384
|
},
|
|
1109
1385
|
},
|
|
1110
1386
|
};
|
|
1111
1387
|
};
|
|
1388
|
+
const WAIT_FOR_POLL_MS = 500;
|
|
1389
|
+
/** Whether a wait_for condition holds in what the host just showed. */
|
|
1390
|
+
const treeHolds = (input, snapshot) => {
|
|
1391
|
+
const needle = input.match.toLowerCase();
|
|
1392
|
+
const role = input.role?.toLowerCase();
|
|
1393
|
+
let found = false;
|
|
1394
|
+
const visit = (element) => {
|
|
1395
|
+
if (found)
|
|
1396
|
+
return;
|
|
1397
|
+
const name = element.name.toLowerCase();
|
|
1398
|
+
const value = (element.value ?? '').toLowerCase();
|
|
1399
|
+
if (input.until.startsWith('text_')) {
|
|
1400
|
+
if (name.includes(needle) || value.includes(needle))
|
|
1401
|
+
found = true;
|
|
1402
|
+
}
|
|
1403
|
+
else if (name.includes(needle) &&
|
|
1404
|
+
(role === undefined || element.role.toLowerCase() === role)) {
|
|
1405
|
+
found = true;
|
|
1406
|
+
}
|
|
1407
|
+
for (const child of element.children ?? [])
|
|
1408
|
+
visit(child);
|
|
1409
|
+
};
|
|
1410
|
+
let nodes = 0;
|
|
1411
|
+
const count = (element) => {
|
|
1412
|
+
nodes++;
|
|
1413
|
+
for (const child of element.children ?? [])
|
|
1414
|
+
count(child);
|
|
1415
|
+
};
|
|
1416
|
+
count(snapshot.root);
|
|
1417
|
+
visit(snapshot.root);
|
|
1418
|
+
if (input.until.endsWith('appears'))
|
|
1419
|
+
return found;
|
|
1420
|
+
// "Gone" is only proof on a complete read of a tree that has content: a
|
|
1421
|
+
// cut-off tree or a bare root (a web view whose tree has not woken yet)
|
|
1422
|
+
// may simply not show the element.
|
|
1423
|
+
return !found && snapshot.truncated !== true && nodes > 1;
|
|
1424
|
+
};
|
|
1425
|
+
/**
|
|
1426
|
+
* Look until a condition holds or the time is up. It replaces a fixed
|
|
1427
|
+
* wait: the model says what it is waiting for and gets the screen the
|
|
1428
|
+
* moment that is true. A read that fails is not an answer, so it is tried
|
|
1429
|
+
* again; only the last failure is reported if time runs out.
|
|
1430
|
+
*/
|
|
1431
|
+
const waitFor = async (input, context) => {
|
|
1432
|
+
const windowCondition = input.until.startsWith('window_');
|
|
1433
|
+
if (windowCondition && !available.has('list_windows'))
|
|
1434
|
+
throw new StepFailure('window conditions need list_windows, which this host does not offer');
|
|
1435
|
+
if (!windowCondition && input.window_id === undefined && uiSnapshotWindowId === undefined)
|
|
1436
|
+
throw new StepFailure('wait_for on a control or text needs window_id (or a ui_snapshot of the window first)');
|
|
1437
|
+
const timeout = Math.min(input.timeout_ms ?? Math.min(DEFAULT_WAIT_FOR_MS, settings.maxWaitMs), settings.maxWaitMs);
|
|
1438
|
+
const signal = context?.abortSignal;
|
|
1439
|
+
const started = Date.now();
|
|
1440
|
+
const needle = input.match.toLowerCase();
|
|
1441
|
+
const what = `${input.until.replace('_', ' ')} ${quotedText(input.match)}`;
|
|
1442
|
+
let lastError;
|
|
1443
|
+
let lastSnapshot;
|
|
1444
|
+
for (;;) {
|
|
1445
|
+
if (signal?.aborted)
|
|
1446
|
+
throw new StepFailure('cancelled');
|
|
1447
|
+
try {
|
|
1448
|
+
if (windowCondition) {
|
|
1449
|
+
const windows = await host.listWindows(callSignal);
|
|
1450
|
+
const open = windows.some((window) => window.title.toLowerCase().includes(needle));
|
|
1451
|
+
if (open === input.until.endsWith('appears')) {
|
|
1452
|
+
const seconds = ((Date.now() - started) / 1000).toFixed(1);
|
|
1453
|
+
return {
|
|
1454
|
+
success: true,
|
|
1455
|
+
output: `wait_for ${what}: satisfied after ${seconds} s.`,
|
|
1456
|
+
content: [
|
|
1457
|
+
{
|
|
1458
|
+
type: 'text',
|
|
1459
|
+
text: `wait_for ${what}: satisfied after ${seconds} s.`,
|
|
1460
|
+
},
|
|
1461
|
+
],
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
else {
|
|
1466
|
+
const snapshot = await host.uiSnapshot(input.window_id ?? uiSnapshotWindowId, undefined, callSignal);
|
|
1467
|
+
lastSnapshot = snapshot;
|
|
1468
|
+
if (treeHolds(input, snapshot)) {
|
|
1469
|
+
const seconds = ((Date.now() - started) / 1000).toFixed(1);
|
|
1470
|
+
return showUiSnapshot(snapshot, input.window_id, [
|
|
1471
|
+
`wait_for ${what}: satisfied after ${seconds} s.`,
|
|
1472
|
+
]);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
lastError = undefined;
|
|
1476
|
+
}
|
|
1477
|
+
catch (error) {
|
|
1478
|
+
lastError = errorText(error);
|
|
1479
|
+
}
|
|
1480
|
+
if (Date.now() - started >= timeout)
|
|
1481
|
+
break;
|
|
1482
|
+
await sleep(WAIT_FOR_POLL_MS, signal);
|
|
1483
|
+
}
|
|
1484
|
+
const message = `wait_for ${what}: not satisfied within ${Number((timeout / 1000).toFixed(1))} s${lastError ? `; the last read failed: ${lastError}` : ''}. Nothing was changed. Look at the window (ui_snapshot or a window screenshot) to see where it stands before waiting longer or trying something else.`;
|
|
1485
|
+
if (lastSnapshot && !lastError) {
|
|
1486
|
+
const shown = showUiSnapshot(lastSnapshot, input.window_id, [message]);
|
|
1487
|
+
return { ...shown, success: false, error: message };
|
|
1488
|
+
}
|
|
1489
|
+
return { success: false, output: '', error: message };
|
|
1490
|
+
};
|
|
1491
|
+
/** Said once per window, after its first coordinate click: the tree is the surer way in. */
|
|
1492
|
+
const coordinateNudge = (frame) => {
|
|
1493
|
+
if (!frame.window || !available.has('ui_snapshot'))
|
|
1494
|
+
return undefined;
|
|
1495
|
+
const id = frame.window.window.id;
|
|
1496
|
+
if (nudgedWindows.has(id))
|
|
1497
|
+
return undefined;
|
|
1498
|
+
const usable = treeUsable.get(id);
|
|
1499
|
+
if (usable === false)
|
|
1500
|
+
return undefined;
|
|
1501
|
+
nudgedWindows.add(id);
|
|
1502
|
+
return usable === true
|
|
1503
|
+
? 'This window has controls with refs: use ui_act on a ref where one fits; it does not depend on where the window is or what is in front.'
|
|
1504
|
+
: 'Read this window with ui_snapshot before clicking more pixels: if it has controls, ui_act by ref is surer than coordinates. Click pixels only where the window has no usable tree.';
|
|
1505
|
+
};
|
|
1112
1506
|
/**
|
|
1113
1507
|
* Keys and text go to the window in front, and a terminal there — the
|
|
1114
1508
|
* one running this agent, or the user's own — turns a model's typing into
|
|
@@ -1123,7 +1517,7 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1123
1517
|
return;
|
|
1124
1518
|
let windows;
|
|
1125
1519
|
try {
|
|
1126
|
-
windows = await host.listWindows();
|
|
1520
|
+
windows = await host.listWindows(callSignal);
|
|
1127
1521
|
}
|
|
1128
1522
|
catch (error) {
|
|
1129
1523
|
throw new StepFailure(`the window in front could not be read, so nothing was typed: ${errorText(error)}`);
|
|
@@ -1135,7 +1529,7 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1135
1529
|
const listWindows = async () => {
|
|
1136
1530
|
let windows;
|
|
1137
1531
|
try {
|
|
1138
|
-
windows = await host.listWindows();
|
|
1532
|
+
windows = await host.listWindows(callSignal);
|
|
1139
1533
|
}
|
|
1140
1534
|
catch (error) {
|
|
1141
1535
|
throw new StepFailure(errorText(error));
|
|
@@ -1225,7 +1619,7 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1225
1619
|
// name the controls.
|
|
1226
1620
|
const firstLookHint = (frame) => frame.id === 's1' && available.has('ui_snapshot')
|
|
1227
1621
|
? [
|
|
1228
|
-
'This host can
|
|
1622
|
+
'This host can read a window’s controls: list_windows, then ui_snapshot {window_id}, then ui_act by ref (a batch of them for several buttons). Prefer that to clicking pixels wherever the window has a tree; it works in the background.',
|
|
1229
1623
|
]
|
|
1230
1624
|
: [];
|
|
1231
1625
|
const screenshotResult = (shot) => ({
|
|
@@ -1234,7 +1628,10 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1234
1628
|
? `Screenshot ${shot.frame.id} captured (${shot.frame.imageWidth}x${shot.frame.imageHeight} of window ${visibleWindowId(shot.frame.window.window.id)}).`
|
|
1235
1629
|
: `Screenshot ${shot.frame.id} captured (${shot.frame.imageWidth}x${shot.frame.imageHeight} of the ${shot.frame.display.width}x${shot.frame.display.height} display).`,
|
|
1236
1630
|
content: [
|
|
1237
|
-
{
|
|
1631
|
+
{
|
|
1632
|
+
type: 'text',
|
|
1633
|
+
text: [describeFrame(shot.frame), ...firstLookHint(shot.frame)].join('\n'),
|
|
1634
|
+
},
|
|
1238
1635
|
imageBlock(shot.image),
|
|
1239
1636
|
],
|
|
1240
1637
|
data: { screenshot: frameData(shot.frame) },
|
|
@@ -1259,11 +1656,11 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1259
1656
|
fitted = await cropAndFitPng(frame.window.data, rect, settings.limits);
|
|
1260
1657
|
}
|
|
1261
1658
|
else if (caps.regionCapture === true && typeof host.captureRegion === 'function') {
|
|
1262
|
-
const piece = await host.captureRegion(rect);
|
|
1659
|
+
const piece = await host.captureRegion(rect, callSignal);
|
|
1263
1660
|
fitted = await fitPng(piece.data, settings.limits);
|
|
1264
1661
|
}
|
|
1265
1662
|
else {
|
|
1266
|
-
const result = await host.execute({ type: 'screenshot' });
|
|
1663
|
+
const result = await host.execute({ type: 'screenshot' }, callSignal);
|
|
1267
1664
|
if (result.type !== 'screenshot')
|
|
1268
1665
|
throw new Error(`computer_use: the host answered a screenshot with "${result.type}"`);
|
|
1269
1666
|
const full = result.result;
|
|
@@ -1361,7 +1758,11 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1361
1758
|
for (const [index, step] of planned.entries()) {
|
|
1362
1759
|
if (signal?.aborted) {
|
|
1363
1760
|
failure = { index, error: 'cancelled before it started' };
|
|
1364
|
-
records.push({
|
|
1761
|
+
records.push({
|
|
1762
|
+
label: step.label,
|
|
1763
|
+
status: 'failed',
|
|
1764
|
+
error: failure.error,
|
|
1765
|
+
});
|
|
1365
1766
|
break;
|
|
1366
1767
|
}
|
|
1367
1768
|
try {
|
|
@@ -1372,7 +1773,8 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1372
1773
|
}
|
|
1373
1774
|
if (step.item.type !== 'type_text')
|
|
1374
1775
|
frontChecked = false;
|
|
1375
|
-
const
|
|
1776
|
+
const ran = await step.run(signal);
|
|
1777
|
+
const outcome = typeof ran === 'string' ? { note: ran } : ran;
|
|
1376
1778
|
if (step.item.type === 'focus_window' && windowCaptureAvailable(host, caps))
|
|
1377
1779
|
postWindowId = step.item.window_id;
|
|
1378
1780
|
else if (step.item.type === 'ui_act' &&
|
|
@@ -1390,12 +1792,14 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1390
1792
|
else if (actionFrame?.window &&
|
|
1391
1793
|
['mouse_click', 'mouse_drag', 'scroll', 'type_text', 'key'].includes(step.item.type))
|
|
1392
1794
|
postWindowId = actionFrame.window.window.id;
|
|
1393
|
-
records.push({ label: step.label, status: 'done',
|
|
1795
|
+
records.push({ label: step.label, status: 'done', ...outcome });
|
|
1394
1796
|
if (step.mutating)
|
|
1395
1797
|
changed = true;
|
|
1396
1798
|
}
|
|
1397
1799
|
catch (error) {
|
|
1398
1800
|
const unknown = error instanceof StepFailure ? error.unknown : undefined;
|
|
1801
|
+
const delivery = error instanceof StepFailure ? error.delivery : undefined;
|
|
1802
|
+
const retrySafety = error instanceof StepFailure ? error.retrySafety : undefined;
|
|
1399
1803
|
const message = signal?.aborted ? 'cancelled' : errorText(error);
|
|
1400
1804
|
failure = { index, error: message, ...(unknown ? { unknown } : {}) };
|
|
1401
1805
|
records.push({
|
|
@@ -1403,7 +1807,12 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1403
1807
|
status: 'failed',
|
|
1404
1808
|
error: message,
|
|
1405
1809
|
...(unknown ? { unknown } : {}),
|
|
1810
|
+
...(delivery ? { delivery } : {}),
|
|
1811
|
+
...(retrySafety ? { retrySafety } : {}),
|
|
1406
1812
|
});
|
|
1813
|
+
// A step that may have acted changed the desktop even though it failed.
|
|
1814
|
+
if (delivery === 'delivered_unverified')
|
|
1815
|
+
changed = true;
|
|
1407
1816
|
if (unknown)
|
|
1408
1817
|
changed = true;
|
|
1409
1818
|
break;
|
|
@@ -1464,8 +1873,22 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1464
1873
|
].join('\n');
|
|
1465
1874
|
const data = {
|
|
1466
1875
|
steps: records.map((record) => record.status === 'failed'
|
|
1467
|
-
? {
|
|
1468
|
-
|
|
1876
|
+
? {
|
|
1877
|
+
label: record.label,
|
|
1878
|
+
status: record.status,
|
|
1879
|
+
error: record.error,
|
|
1880
|
+
...(record.delivery ? { delivery: record.delivery } : {}),
|
|
1881
|
+
...(record.retrySafety ? { retrySafety: record.retrySafety } : {}),
|
|
1882
|
+
}
|
|
1883
|
+
: record.status === 'done'
|
|
1884
|
+
? {
|
|
1885
|
+
label: record.label,
|
|
1886
|
+
status: record.status,
|
|
1887
|
+
...(record.delivery ? { delivery: record.delivery } : {}),
|
|
1888
|
+
...(record.retrySafety ? { retrySafety: record.retrySafety } : {}),
|
|
1889
|
+
...(record.readBack ? { readBack: record.readBack } : {}),
|
|
1890
|
+
}
|
|
1891
|
+
: { label: record.label, status: record.status }),
|
|
1469
1892
|
...(shot ? { screenshot: frameData(shot.frame) } : {}),
|
|
1470
1893
|
...(failure?.unknown
|
|
1471
1894
|
? {
|
|
@@ -1529,6 +1952,19 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1529
1952
|
const denied = refusal(input.type);
|
|
1530
1953
|
if (denied)
|
|
1531
1954
|
return { success: false, output: '', error: denied };
|
|
1955
|
+
// The host keeps per-turn state (an Escape stop, refused permissions); it
|
|
1956
|
+
// is told which turn this is and released when the turn is cut short.
|
|
1957
|
+
host.beginTurn?.(context.turnId);
|
|
1958
|
+
callSignal = context.abortSignal;
|
|
1959
|
+
if (host.endTurn) {
|
|
1960
|
+
const release = () => void Promise.resolve(host.endTurn?.()).catch(() => undefined);
|
|
1961
|
+
if (context.abortSignal.aborted)
|
|
1962
|
+
release();
|
|
1963
|
+
else
|
|
1964
|
+
context.abortSignal.addEventListener('abort', release, {
|
|
1965
|
+
once: true,
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1532
1968
|
try {
|
|
1533
1969
|
switch (input.type) {
|
|
1534
1970
|
case 'screenshot':
|
|
@@ -1537,6 +1973,8 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1537
1973
|
return await zoom(input);
|
|
1538
1974
|
case 'ui_snapshot':
|
|
1539
1975
|
return await uiSnapshot(input);
|
|
1976
|
+
case 'wait_for':
|
|
1977
|
+
return await waitFor(input, context);
|
|
1540
1978
|
case 'batch':
|
|
1541
1979
|
return await runSteps(input.actions, input.screenshot_id, context, false);
|
|
1542
1980
|
default: {
|
|
@@ -1547,9 +1985,16 @@ export function createComputerUseTool(host, options = {}) {
|
|
|
1547
1985
|
}
|
|
1548
1986
|
catch (error) {
|
|
1549
1987
|
if (error instanceof StepFailure)
|
|
1550
|
-
return {
|
|
1988
|
+
return {
|
|
1989
|
+
success: false,
|
|
1990
|
+
output: '',
|
|
1991
|
+
error: `computer_use failed: ${error.message}`,
|
|
1992
|
+
};
|
|
1551
1993
|
throw error;
|
|
1552
1994
|
}
|
|
1995
|
+
finally {
|
|
1996
|
+
callSignal = undefined;
|
|
1997
|
+
}
|
|
1553
1998
|
},
|
|
1554
1999
|
});
|
|
1555
2000
|
return Object.assign(tool, { describeUiRef });
|