@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.
Files changed (260) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/compaction/context-window.d.ts +53 -0
  3. package/dist/compaction/context-window.d.ts.map +1 -1
  4. package/dist/compaction/context-window.js +141 -2
  5. package/dist/compaction/context-window.js.map +1 -1
  6. package/dist/execution/workflow/index.d.ts +3 -1
  7. package/dist/execution/workflow/index.d.ts.map +1 -1
  8. package/dist/execution/workflow/index.js +2 -1
  9. package/dist/execution/workflow/index.js.map +1 -1
  10. package/dist/execution/workflow/journal.d.ts +24 -4
  11. package/dist/execution/workflow/journal.d.ts.map +1 -1
  12. package/dist/execution/workflow/journal.js +131 -9
  13. package/dist/execution/workflow/journal.js.map +1 -1
  14. package/dist/execution/workflow/prelude.d.ts +2 -0
  15. package/dist/execution/workflow/prelude.d.ts.map +1 -1
  16. package/dist/execution/workflow/prelude.js +49 -8
  17. package/dist/execution/workflow/prelude.js.map +1 -1
  18. package/dist/execution/workflow/resume.d.ts +59 -0
  19. package/dist/execution/workflow/resume.d.ts.map +1 -0
  20. package/dist/execution/workflow/resume.js +273 -0
  21. package/dist/execution/workflow/resume.js.map +1 -0
  22. package/dist/execution/workflow/runtime.d.ts.map +1 -1
  23. package/dist/execution/workflow/runtime.js +112 -8
  24. package/dist/execution/workflow/runtime.js.map +1 -1
  25. package/dist/execution/workflow/types.d.ts +105 -1
  26. package/dist/execution/workflow/types.d.ts.map +1 -1
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/plugin/compat.d.ts +24 -0
  32. package/dist/plugin/compat.d.ts.map +1 -0
  33. package/dist/plugin/compat.js +149 -0
  34. package/dist/plugin/compat.js.map +1 -0
  35. package/dist/plugin/loader.d.ts +2 -1
  36. package/dist/plugin/loader.d.ts.map +1 -1
  37. package/dist/plugin/loader.js +6 -0
  38. package/dist/plugin/loader.js.map +1 -1
  39. package/dist/pricing/index.d.ts.map +1 -1
  40. package/dist/pricing/index.js +2 -1
  41. package/dist/pricing/index.js.map +1 -1
  42. package/dist/process/kill-tree.d.ts +44 -0
  43. package/dist/process/kill-tree.d.ts.map +1 -1
  44. package/dist/process/kill-tree.js +102 -4
  45. package/dist/process/kill-tree.js.map +1 -1
  46. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  47. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  48. package/dist/prompt/coding-agent-doctrine.js +9 -1
  49. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  50. package/dist/provider/model-window-suffix.d.ts +18 -0
  51. package/dist/provider/model-window-suffix.d.ts.map +1 -0
  52. package/dist/provider/model-window-suffix.js +77 -0
  53. package/dist/provider/model-window-suffix.js.map +1 -0
  54. package/dist/public-runtime.d.ts +5 -3
  55. package/dist/public-runtime.d.ts.map +1 -1
  56. package/dist/public-runtime.js +5 -4
  57. package/dist/public-runtime.js.map +1 -1
  58. package/dist/public-types.d.ts +1 -0
  59. package/dist/public-types.d.ts.map +1 -1
  60. package/dist/registry/command/kernel-commands.d.ts +1 -4
  61. package/dist/registry/command/kernel-commands.d.ts.map +1 -1
  62. package/dist/registry/command/kernel-commands.js +0 -28
  63. package/dist/registry/command/kernel-commands.js.map +1 -1
  64. package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
  65. package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
  66. package/dist/runtime/jobs/awaited-jobs.js +11 -0
  67. package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
  68. package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
  69. package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
  70. package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
  71. package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
  72. package/dist/runtime/jobs/liveness.d.ts +67 -0
  73. package/dist/runtime/jobs/liveness.d.ts.map +1 -0
  74. package/dist/runtime/jobs/liveness.js +94 -0
  75. package/dist/runtime/jobs/liveness.js.map +1 -0
  76. package/dist/runtime/jobs/registry.d.ts +21 -0
  77. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  78. package/dist/runtime/jobs/registry.js +104 -2
  79. package/dist/runtime/jobs/registry.js.map +1 -1
  80. package/dist/runtime/jobs/wake-budget.d.ts +35 -0
  81. package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
  82. package/dist/runtime/jobs/wake-budget.js +48 -0
  83. package/dist/runtime/jobs/wake-budget.js.map +1 -0
  84. package/dist/runtime/query/detached-calls.d.ts +65 -0
  85. package/dist/runtime/query/detached-calls.d.ts.map +1 -0
  86. package/dist/runtime/query/detached-calls.js +116 -0
  87. package/dist/runtime/query/detached-calls.js.map +1 -0
  88. package/dist/runtime/query/executor.d.ts +4 -1
  89. package/dist/runtime/query/executor.d.ts.map +1 -1
  90. package/dist/runtime/query/executor.js +90 -6
  91. package/dist/runtime/query/executor.js.map +1 -1
  92. package/dist/runtime/query/index.d.ts +20 -0
  93. package/dist/runtime/query/index.d.ts.map +1 -1
  94. package/dist/runtime/query/index.js +31 -5
  95. package/dist/runtime/query/index.js.map +1 -1
  96. package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
  97. package/dist/runtime/query/iteration/outstanding-work.js +11 -3
  98. package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
  99. package/dist/runtime/query/iteration/phases/context.d.ts +7 -0
  100. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  101. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  102. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  103. package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
  104. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  105. package/dist/runtime/query/session-storage.d.ts.map +1 -1
  106. package/dist/runtime/query/session-storage.js +8 -3
  107. package/dist/runtime/query/session-storage.js.map +1 -1
  108. package/dist/runtime/query/steering.d.ts +29 -3
  109. package/dist/runtime/query/steering.d.ts.map +1 -1
  110. package/dist/runtime/query/steering.js +58 -3
  111. package/dist/runtime/query/steering.js.map +1 -1
  112. package/dist/runtime/query/tooling.d.ts +1 -0
  113. package/dist/runtime/query/tooling.d.ts.map +1 -1
  114. package/dist/runtime/query/tooling.js +1 -0
  115. package/dist/runtime/query/tooling.js.map +1 -1
  116. package/dist/runtime/system-events.d.ts +5 -0
  117. package/dist/runtime/system-events.d.ts.map +1 -1
  118. package/dist/runtime/system-events.js +7 -1
  119. package/dist/runtime/system-events.js.map +1 -1
  120. package/dist/sandbox/denial.d.ts +11 -0
  121. package/dist/sandbox/denial.d.ts.map +1 -0
  122. package/dist/sandbox/denial.js +61 -0
  123. package/dist/sandbox/denial.js.map +1 -0
  124. package/dist/sandbox/file-walk-program.d.ts.map +1 -1
  125. package/dist/sandbox/file-walk-program.js +31 -13
  126. package/dist/sandbox/file-walk-program.js.map +1 -1
  127. package/dist/sandbox/file-walk.d.ts +2 -0
  128. package/dist/sandbox/file-walk.d.ts.map +1 -1
  129. package/dist/sandbox/file-walk.js +64 -13
  130. package/dist/sandbox/file-walk.js.map +1 -1
  131. package/dist/sandbox/ignore-matcher.d.ts +30 -0
  132. package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
  133. package/dist/sandbox/ignore-matcher.js +11 -0
  134. package/dist/sandbox/ignore-matcher.js.map +1 -0
  135. package/dist/sandbox/ignore-source.d.ts +26 -0
  136. package/dist/sandbox/ignore-source.d.ts.map +1 -0
  137. package/dist/sandbox/ignore-source.js +221 -0
  138. package/dist/sandbox/ignore-source.js.map +1 -0
  139. package/dist/sandbox/provider/local.d.ts.map +1 -1
  140. package/dist/sandbox/provider/local.js +11 -1
  141. package/dist/sandbox/provider/local.js.map +1 -1
  142. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  143. package/dist/scheduler/completion-inbox.js +1 -0
  144. package/dist/scheduler/completion-inbox.js.map +1 -1
  145. package/dist/session/log-hash.d.ts.map +1 -1
  146. package/dist/session/log-hash.js +18 -1
  147. package/dist/session/log-hash.js.map +1 -1
  148. package/dist/store/session-index/sqlite.d.ts.map +1 -1
  149. package/dist/store/session-index/sqlite.js +169 -16
  150. package/dist/store/session-index/sqlite.js.map +1 -1
  151. package/dist/tools/builtins/bash.d.ts.map +1 -1
  152. package/dist/tools/builtins/bash.js +10 -4
  153. package/dist/tools/builtins/bash.js.map +1 -1
  154. package/dist/tools/builtins/computer-use.d.ts +35 -0
  155. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  156. package/dist/tools/builtins/computer-use.js +512 -67
  157. package/dist/tools/builtins/computer-use.js.map +1 -1
  158. package/dist/tools/builtins/glob.d.ts +1 -0
  159. package/dist/tools/builtins/glob.d.ts.map +1 -1
  160. package/dist/tools/builtins/glob.js +65 -28
  161. package/dist/tools/builtins/glob.js.map +1 -1
  162. package/dist/tools/builtins/grep.d.ts +2 -0
  163. package/dist/tools/builtins/grep.d.ts.map +1 -1
  164. package/dist/tools/builtins/grep.js +94 -40
  165. package/dist/tools/builtins/grep.js.map +1 -1
  166. package/dist/tools/builtins/job.d.ts +1 -1
  167. package/dist/tools/builtins/job.d.ts.map +1 -1
  168. package/dist/tools/builtins/job.js +34 -7
  169. package/dist/tools/builtins/job.js.map +1 -1
  170. package/dist/tools/builtins/search-support.d.ts +44 -0
  171. package/dist/tools/builtins/search-support.d.ts.map +1 -0
  172. package/dist/tools/builtins/search-support.js +89 -0
  173. package/dist/tools/builtins/search-support.js.map +1 -0
  174. package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
  175. package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
  176. package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
  177. package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
  178. package/dist/tools/builtins/wait-for-job.d.ts +1 -1
  179. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
  180. package/dist/tools/builtins/wait-for-job.js +13 -2
  181. package/dist/tools/builtins/wait-for-job.js.map +1 -1
  182. package/dist/tools/command-shell.d.ts.map +1 -1
  183. package/dist/tools/command-shell.js +14 -5
  184. package/dist/tools/command-shell.js.map +1 -1
  185. package/dist/types/computer-use/index.d.ts +129 -13
  186. package/dist/types/computer-use/index.d.ts.map +1 -1
  187. package/dist/types/computer-use/index.js.map +1 -1
  188. package/dist/types/plugin/index.d.ts +24 -0
  189. package/dist/types/plugin/index.d.ts.map +1 -1
  190. package/dist/types/plugin/index.js +4 -0
  191. package/dist/types/plugin/index.js.map +1 -1
  192. package/dist/types/sandbox/index.d.ts +38 -1
  193. package/dist/types/sandbox/index.d.ts.map +1 -1
  194. package/dist/types/sandbox/index.js.map +1 -1
  195. package/dist/types/session/records.d.ts +264 -0
  196. package/dist/types/session/records.d.ts.map +1 -1
  197. package/dist/types/session/records.js +32 -1
  198. package/dist/types/session/records.js.map +1 -1
  199. package/dist/types/tool/index.d.ts +30 -0
  200. package/dist/types/tool/index.d.ts.map +1 -1
  201. package/dist/types/tool/index.js.map +1 -1
  202. package/dist/types/tool/presentation.d.ts +8 -0
  203. package/dist/types/tool/presentation.d.ts.map +1 -1
  204. package/package.json +1 -1
  205. package/src/compaction/context-window.ts +163 -2
  206. package/src/execution/workflow/index.ts +14 -1
  207. package/src/execution/workflow/journal.ts +161 -11
  208. package/src/execution/workflow/prelude.ts +51 -8
  209. package/src/execution/workflow/resume.ts +313 -0
  210. package/src/execution/workflow/runtime.ts +127 -9
  211. package/src/execution/workflow/types.ts +109 -1
  212. package/src/index.ts +17 -0
  213. package/src/plugin/compat.ts +173 -0
  214. package/src/plugin/loader.ts +7 -1
  215. package/src/pricing/index.ts +2 -1
  216. package/src/process/kill-tree.ts +106 -4
  217. package/src/prompt/coding-agent-doctrine.ts +9 -1
  218. package/src/provider/model-window-suffix.ts +84 -0
  219. package/src/public-runtime.ts +21 -2
  220. package/src/public-types.ts +1 -0
  221. package/src/registry/command/kernel-commands.ts +1 -32
  222. package/src/runtime/jobs/awaited-jobs.ts +11 -0
  223. package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
  224. package/src/runtime/jobs/liveness.ts +144 -0
  225. package/src/runtime/jobs/registry.ts +114 -2
  226. package/src/runtime/jobs/wake-budget.ts +55 -0
  227. package/src/runtime/query/detached-calls.ts +135 -0
  228. package/src/runtime/query/executor.ts +92 -10
  229. package/src/runtime/query/index.ts +50 -7
  230. package/src/runtime/query/iteration/outstanding-work.ts +12 -3
  231. package/src/runtime/query/iteration/phases/context.ts +7 -0
  232. package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
  233. package/src/runtime/query/session-storage.ts +8 -3
  234. package/src/runtime/query/steering.ts +77 -6
  235. package/src/runtime/query/tooling.ts +2 -0
  236. package/src/runtime/system-events.ts +12 -1
  237. package/src/sandbox/denial.ts +79 -0
  238. package/src/sandbox/file-walk-program.ts +32 -13
  239. package/src/sandbox/file-walk.ts +75 -12
  240. package/src/sandbox/ignore-matcher.ts +46 -0
  241. package/src/sandbox/ignore-source.ts +220 -0
  242. package/src/sandbox/provider/local.ts +11 -1
  243. package/src/scheduler/completion-inbox.ts +1 -0
  244. package/src/session/log-hash.ts +21 -0
  245. package/src/store/session-index/sqlite.ts +171 -16
  246. package/src/tools/builtins/bash.ts +11 -4
  247. package/src/tools/builtins/computer-use.ts +593 -68
  248. package/src/tools/builtins/glob.ts +79 -32
  249. package/src/tools/builtins/grep.ts +107 -43
  250. package/src/tools/builtins/job.ts +42 -12
  251. package/src/tools/builtins/search-support.ts +107 -0
  252. package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
  253. package/src/tools/builtins/wait-for-job.ts +14 -2
  254. package/src/tools/command-shell.ts +13 -5
  255. package/src/types/computer-use/index.ts +157 -15
  256. package/src/types/plugin/index.ts +8 -0
  257. package/src/types/sandbox/index.ts +40 -1
  258. package/src/types/session/records.ts +36 -2
  259. package/src/types/tool/index.ts +30 -0
  260. package/src/types/tool/presentation.ts +8 -0
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod'
2
2
  import { modelReadsImages, resolveProviderCapabilities } from '../../provider/capabilities.js'
3
3
  import type {
4
+ ActionDelivery,
5
+ ActionRetrySafety,
4
6
  ComputerUseAction,
5
7
  ComputerUseCapabilities,
6
8
  ComputerUseHost,
@@ -12,8 +14,10 @@ import type {
12
14
  UiElement,
13
15
  UiElementAction,
14
16
  UiSnapshot,
17
+ UiSnapshotOptions,
15
18
  WindowInfo,
16
19
  WindowInputAction,
20
+ WindowInputResult,
17
21
  } from '../../types/computer-use/index.js'
18
22
  import type { ToolResultBlock } from '../../types/message/index.js'
19
23
  import type { LLMProvider } from '../../types/provider/index.js'
@@ -53,9 +57,12 @@ export const COMPUTER_USE_TOOL_NAME = 'computer_use' as const
53
57
  const DEFAULT_SETTLE_MS = 500
54
58
  const DEFAULT_MAX_BATCH_ACTIONS = 20
55
59
  const DEFAULT_MAX_WAIT_MS = 10_000
60
+ const DEFAULT_WAIT_FOR_MS = 10_000
56
61
  const MAX_LISTED_WINDOWS = 50
57
62
  /** The most of a ui_snapshot the model is shown, in characters (about 4 000 tokens). */
58
63
  const MAX_UI_SNAPSHOT_CHARS = 14_000
64
+ /** The refs of earlier snapshots the tool still resolves for the host. */
65
+ const MAX_HELD_UI_REFS = 4_000
59
66
 
60
67
  /**
61
68
  * How `createComputerUseTool` sizes screenshots, paces actions and bounds a
@@ -142,7 +149,10 @@ const mouseButtonSchema = z.enum(['left', 'right', 'middle']).default('left')
142
149
  const screenshotIdSchema = z.string().min(1).optional()
143
150
 
144
151
  const cursorPositionSchema = z.object({ type: z.literal('cursor_position') })
145
- const mouseMoveSchema = z.object({ type: z.literal('mouse_move'), to: pointSchema })
152
+ const mouseMoveSchema = z.object({
153
+ type: z.literal('mouse_move'),
154
+ to: pointSchema,
155
+ })
146
156
  const mouseClickSchema = z.object({
147
157
  type: z.literal('mouse_click'),
148
158
  at: pointSchema,
@@ -177,7 +187,10 @@ const keySchema = z.object({
177
187
  at: pointSchema.optional(),
178
188
  delivery_mode: z.enum(['background', 'foreground']).optional(),
179
189
  })
180
- const waitSchema = z.object({ type: z.literal('wait'), ms: z.number().int().nonnegative() })
190
+ const waitSchema = z.object({
191
+ type: z.literal('wait'),
192
+ ms: z.number().int().nonnegative(),
193
+ })
181
194
  const listWindowsSchema = z.object({ type: z.literal('list_windows') })
182
195
  const focusWindowSchema = z.object({
183
196
  type: z.literal('focus_window'),
@@ -196,6 +209,30 @@ const UI_ACTIONS = [
196
209
  const uiSnapshotSchema = z.object({
197
210
  type: z.literal('ui_snapshot'),
198
211
  window_id: z.string().min(1).optional(),
212
+ /** A ref shown by an earlier ui_snapshot: read only that region or control. */
213
+ root: z.string().min(1).optional(),
214
+ /** With root: skip this many of the region's children (the next page of a long region). */
215
+ offset: z.number().int().min(0).max(1_000_000).optional(),
216
+ /** Only controls whose text contains this, with the elements above them. */
217
+ query: z.string().min(1).max(200).optional(),
218
+ })
219
+ const WAIT_FOR_CONDITIONS = [
220
+ 'element_appears',
221
+ 'element_gone',
222
+ 'text_appears',
223
+ 'text_gone',
224
+ 'window_appears',
225
+ 'window_closed',
226
+ ] as const
227
+ const waitForSchema = z.object({
228
+ type: z.literal('wait_for'),
229
+ until: z.enum(WAIT_FOR_CONDITIONS),
230
+ /** A control's name, a text, or a window title: matched as a case-insensitive substring. */
231
+ match: z.string().min(1),
232
+ /** Narrows element_* to controls of this role, such as Button. */
233
+ role: z.string().min(1).optional(),
234
+ window_id: z.string().min(1).optional(),
235
+ timeout_ms: z.number().int().positive().optional(),
199
236
  })
200
237
  const uiActSchema = z.object({
201
238
  type: z.literal('ui_act'),
@@ -222,7 +259,10 @@ const batchItemSchema = z.discriminatedUnion('type', [
222
259
  const withFrame = { screenshot_id: screenshotIdSchema }
223
260
 
224
261
  const actionSchema = z.discriminatedUnion('type', [
225
- z.object({ type: z.literal('screenshot'), window_id: z.string().min(1).optional() }),
262
+ z.object({
263
+ type: z.literal('screenshot'),
264
+ window_id: z.string().min(1).optional(),
265
+ }),
226
266
  z.object({ type: z.literal('zoom'), region: regionSchema, ...withFrame }),
227
267
  cursorPositionSchema.extend(withFrame),
228
268
  mouseMoveSchema.extend(withFrame),
@@ -235,6 +275,7 @@ const actionSchema = z.discriminatedUnion('type', [
235
275
  listWindowsSchema.extend(withFrame),
236
276
  focusWindowSchema.extend(withFrame),
237
277
  uiSnapshotSchema,
278
+ waitForSchema,
238
279
  uiActSchema.extend(withFrame),
239
280
  z.object({
240
281
  type: z.literal('batch'),
@@ -281,6 +322,7 @@ const ALL_ACTIONS: readonly ToolActionType[] = [
281
322
  'list_windows',
282
323
  'focus_window',
283
324
  'ui_snapshot',
325
+ 'wait_for',
284
326
  'ui_act',
285
327
  'batch',
286
328
  ]
@@ -293,6 +335,7 @@ const READ_ONLY_ACTIONS = new Set<string>([
293
335
  'wait',
294
336
  'list_windows',
295
337
  'ui_snapshot',
338
+ 'wait_for',
296
339
  ])
297
340
 
298
341
  const DESTRUCTIVE_ACTIONS = new Set<string>([
@@ -305,7 +348,13 @@ const DESTRUCTIVE_ACTIONS = new Set<string>([
305
348
  ])
306
349
 
307
350
  /** Observations that show the model what is on the screen, on their own. */
308
- const SCREEN_OBSERVATIONS = new Set<string>(['screenshot', 'zoom', 'list_windows', 'ui_snapshot'])
351
+ const SCREEN_OBSERVATIONS = new Set<string>([
352
+ 'screenshot',
353
+ 'zoom',
354
+ 'list_windows',
355
+ 'ui_snapshot',
356
+ 'wait_for',
357
+ ])
309
358
 
310
359
  /**
311
360
  * Terminal applications, by the process name a host reports in
@@ -315,10 +364,11 @@ const TERMINAL_APPS =
315
364
  /^(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
316
365
 
317
366
  /** Actions a batch cannot carry: the ones that return an image or a tree, and batch itself. */
318
- const OUTSIDE_BATCH = new Set<string>(['screenshot', 'zoom', 'ui_snapshot', 'batch'])
367
+ const OUTSIDE_BATCH = new Set<string>(['screenshot', 'zoom', 'ui_snapshot', 'wait_for', 'batch'])
319
368
 
320
369
  /** A single action's whole result when it only acted: `<label>: done`, then its screenshot. */
321
- const ACKNOWLEDGEMENT = /^[^\n]*: done(\nScreenshot s\d+ \(\d+x\d+\)\.)?$/
370
+ const ACKNOWLEDGEMENT =
371
+ /^[^\n]*: (done(, verified[^\n]*)?|sent, not confirmed)(\nScreenshot s\d+ \(\d+x\d+\)\.)?$/
322
372
 
323
373
  function isRecord(value: unknown): value is Record<string, unknown> {
324
374
  return typeof value === 'object' && value !== null && !Array.isArray(value)
@@ -371,6 +421,7 @@ function requiredCapability(type: string): keyof ComputerUseCapabilities | null
371
421
  case 'focus_window':
372
422
  return 'windows'
373
423
  case 'ui_snapshot':
424
+ case 'wait_for':
374
425
  case 'ui_act':
375
426
  return 'uiTree'
376
427
  default:
@@ -431,6 +482,7 @@ function availableActions(host: ComputerUseHost, caps: ComputerUseCapabilities):
431
482
  case 'focus_window':
432
483
  return windows
433
484
  case 'ui_snapshot':
485
+ case 'wait_for':
434
486
  case 'ui_act':
435
487
  return uiTree
436
488
  case 'batch':
@@ -486,9 +538,13 @@ function buildDescription(
486
538
  )
487
539
  }
488
540
  if (!available.includes('screenshot')) return finish(lines, caps, available)
541
+ if (available.includes('ui_snapshot'))
542
+ lines.push(
543
+ "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.",
544
+ )
489
545
  lines.push(
490
546
  '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.',
491
- 'Take a screenshot before your first click.',
547
+ 'Take a screenshot before your first click on pixels.',
492
548
  )
493
549
  if (settings.screenshotAfterActions)
494
550
  lines.push(
@@ -496,7 +552,7 @@ function buildDescription(
496
552
  )
497
553
  lines.push(
498
554
  '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.',
499
- `wait pauses for ms milliseconds (at most ${settings.maxWaitMs} per call, a batch's waits together) and then shows the screen.`,
555
+ `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.' : ''}`,
500
556
  )
501
557
  if (available.includes('batch'))
502
558
  lines.push(
@@ -516,11 +572,18 @@ function buildDescription(
516
572
  )
517
573
  if (windowCaptureAvailable(host, caps))
518
574
  lines.push(
519
- `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 uses background delivery only; if a click or drag is refused, inspect a fresh image before any other action. ${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. 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.`,
575
+ `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.`,
520
576
  )
521
577
  if (available.includes('ui_snapshot'))
522
578
  lines.push(
523
- `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. Prefer ui_act to clicking pixels when the control is in the tree: it does not depend on coordinates or on which window is in front, and a batch of ui_act steps (for example pressing several buttons) runs in one call. Refs are valid only until the next ui_snapshot; take a new one after the window changes.`,
579
+ `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.`,
580
+ '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).',
581
+ ...(available.includes('wait_for')
582
+ ? [
583
+ `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.`,
584
+ ]
585
+ : []),
586
+ '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.',
524
587
  )
525
588
  lines.push(
526
589
  windowCaptureAvailable(host, caps)
@@ -561,8 +624,14 @@ function pointModelSchema(description?: string): Record<string, unknown> {
561
624
  type: 'object',
562
625
  ...(description ? { description } : {}),
563
626
  properties: {
564
- x: { type: 'integer', description: 'Horizontal pixel coordinate, from the left edge.' },
565
- y: { type: 'integer', description: 'Vertical pixel coordinate, from the top edge.' },
627
+ x: {
628
+ type: 'integer',
629
+ description: 'Horizontal pixel coordinate, from the left edge.',
630
+ },
631
+ y: {
632
+ type: 'integer',
633
+ description: 'Vertical pixel coordinate, from the top edge.',
634
+ },
566
635
  },
567
636
  required: ['x', 'y'],
568
637
  additionalProperties: false,
@@ -582,7 +651,9 @@ const ACTION_REQUIREMENTS: Readonly<Record<ToolActionType, string>> = {
582
651
  wait: 'wait needs ms',
583
652
  list_windows: 'list_windows needs no other fields',
584
653
  focus_window: 'focus_window needs window_id',
585
- ui_snapshot: 'ui_snapshot takes an optional window_id',
654
+ ui_snapshot:
655
+ 'ui_snapshot takes an optional window_id, root (a ref to open) and query (text to find)',
656
+ wait_for: 'wait_for needs until and match; role, window_id and timeout_ms are optional',
586
657
  ui_act: 'ui_act needs ref and action, and value for set_value',
587
658
  batch: 'batch needs actions',
588
659
  }
@@ -622,13 +693,20 @@ function hostModelSchema(
622
693
  ),
623
694
  at: pointModelSchema('Point the action happens at: where to click, or where to scroll.'),
624
695
  from: pointModelSchema('Drag start point, for mouse_drag.'),
625
- button: { type: 'string', enum: buttonEnum, description: 'Mouse button; left when omitted.' },
696
+ button: {
697
+ type: 'string',
698
+ enum: buttonEnum,
699
+ description: 'Mouse button; left when omitted.',
700
+ },
626
701
  direction: {
627
702
  type: 'string',
628
703
  enum: ['up', 'down', 'left', 'right'],
629
704
  description: 'Which way to scroll.',
630
705
  },
631
- amount: { type: 'integer', description: 'Positive integer scroll distance.' },
706
+ amount: {
707
+ type: 'integer',
708
+ description: 'Positive integer scroll distance.',
709
+ },
632
710
  text: { type: 'string', description: 'Literal text to type.' },
633
711
  keys: {
634
712
  type: 'string',
@@ -653,33 +731,87 @@ function hostModelSchema(
653
731
  present.includes('focus_window') ||
654
732
  (!forItems &&
655
733
  (present.includes('ui_snapshot') ||
734
+ present.includes('wait_for') ||
656
735
  (present.includes('screenshot') && windowCaptureAvailable(host, caps))))
657
736
  )
658
737
  fields.window_id = {
659
738
  type: 'string',
660
739
  description: 'A window id from list_windows or supplied by the host.',
661
740
  }
741
+ if (!forItems && present.includes('ui_snapshot')) {
742
+ fields.root = {
743
+ type: 'string',
744
+ description:
745
+ 'For ui_snapshot: a ref of a region the last outline marked "(+N more inside)". Reads only that region; other refs stay usable.',
746
+ }
747
+ fields.offset = {
748
+ type: 'integer',
749
+ description:
750
+ 'For ui_snapshot with root: where to start in a long region, as the previous page reported ("next page: offset N").',
751
+ }
752
+ fields.query = {
753
+ type: 'string',
754
+ description:
755
+ 'For ui_snapshot: list only the controls whose text contains this (case-insensitive), with the elements above them.',
756
+ }
757
+ }
758
+ if (!forItems && present.includes('wait_for')) {
759
+ fields.until = {
760
+ type: 'string',
761
+ enum: [...WAIT_FOR_CONDITIONS],
762
+ description:
763
+ "For wait_for: what to wait for. element_* and text_* read the window's controls; window_* read the window list.",
764
+ }
765
+ fields.match = {
766
+ type: 'string',
767
+ description:
768
+ 'For wait_for: the control name, text or window title to look for (case-insensitive, part of it is enough).',
769
+ }
770
+ fields.role = {
771
+ type: 'string',
772
+ description: 'For wait_for element_*: only controls of this role, such as Button or Edit.',
773
+ }
774
+ fields.timeout_ms = {
775
+ type: 'integer',
776
+ description: `For wait_for: how long to keep looking, at most ${settings.maxWaitMs}. Default ${Math.min(10_000, settings.maxWaitMs)}.`,
777
+ }
778
+ }
662
779
  if (present.includes('ui_act')) {
663
780
  fields.ref = {
664
781
  type: 'string',
665
- description: 'A ref from the latest ui_snapshot, such as e12.',
782
+ description: 'A ref shown by a ui_snapshot, such as e12.',
666
783
  }
667
784
  fields.action = {
668
785
  type: 'string',
669
786
  enum: [...UI_ACTIONS],
670
787
  description: 'What ui_act does to the control.',
671
788
  }
672
- fields.value = { type: 'string', description: 'The text set_value puts in the control.' }
789
+ fields.value = {
790
+ type: 'string',
791
+ description: 'The text set_value puts in the control.',
792
+ }
673
793
  }
674
794
  if (!forItems && present.includes('zoom'))
675
795
  fields.region = {
676
796
  type: 'object',
677
797
  description: 'Area of the screenshot to zoom into, in its pixels.',
678
798
  properties: {
679
- x: { type: 'integer', description: 'Left edge of the region, in screenshot pixels.' },
680
- y: { type: 'integer', description: 'Top edge of the region, in screenshot pixels.' },
681
- width: { type: 'integer', description: 'Width of the region, in screenshot pixels.' },
682
- height: { type: 'integer', description: 'Height of the region, in screenshot pixels.' },
799
+ x: {
800
+ type: 'integer',
801
+ description: 'Left edge of the region, in screenshot pixels.',
802
+ },
803
+ y: {
804
+ type: 'integer',
805
+ description: 'Top edge of the region, in screenshot pixels.',
806
+ },
807
+ width: {
808
+ type: 'integer',
809
+ description: 'Width of the region, in screenshot pixels.',
810
+ },
811
+ height: {
812
+ type: 'integer',
813
+ description: 'Height of the region, in screenshot pixels.',
814
+ },
683
815
  },
684
816
  required: ['x', 'y', 'width', 'height'],
685
817
  additionalProperties: false,
@@ -752,7 +884,11 @@ function waitLabel(ms: number): string {
752
884
 
753
885
  /** What a ui_act does, as a verb phrase over the control's description. */
754
886
  function uiActLabel(
755
- input: { readonly ref: string; readonly action: UiElementAction; readonly value?: string },
887
+ input: {
888
+ readonly ref: string
889
+ readonly action: UiElementAction
890
+ readonly value?: string
891
+ },
756
892
  describe?: (ref: string) => string | undefined,
757
893
  ): string {
758
894
  const target = describe?.(input.ref) ?? input.ref
@@ -810,9 +946,13 @@ function itemLabel(
810
946
  case 'focus_window':
811
947
  return `Focus window ${input.window_id}`
812
948
  case 'ui_snapshot':
949
+ if (input.root)
950
+ return `Open region ${input.root}${input.window_id ? ` of window ${input.window_id}` : ''}`
813
951
  return input.window_id
814
952
  ? `Read the controls of window ${input.window_id}`
815
953
  : 'Read the controls of the front window'
954
+ case 'wait_for':
955
+ return `Wait until ${input.until.replace('_', ' ')} ${quotedText(input.match)}`
816
956
  case 'ui_act':
817
957
  return uiActLabel(input, describe)
818
958
  case 'batch':
@@ -906,16 +1046,29 @@ interface PlannedStep {
906
1046
  readonly item: BatchItem
907
1047
  readonly label: string
908
1048
  readonly mutating: boolean
909
- run(signal: AbortSignal | undefined): Promise<string>
1049
+ run(signal: AbortSignal | undefined): Promise<string | StepOutcome>
1050
+ }
1051
+
1052
+ /** What a step that ran says about itself, beyond "done". */
1053
+ interface StepOutcome {
1054
+ readonly note: string
1055
+ readonly delivery?: ActionDelivery
1056
+ readonly retrySafety?: ActionRetrySafety
1057
+ readonly readBack?: {
1058
+ readonly expected?: string
1059
+ readonly observed?: string
1060
+ }
910
1061
  }
911
1062
 
912
1063
  type StepRecord =
913
- | { readonly label: string; readonly status: 'done'; readonly note: string }
1064
+ | ({ readonly label: string; readonly status: 'done' } & StepOutcome)
914
1065
  | {
915
1066
  readonly label: string
916
1067
  readonly status: 'failed'
917
1068
  readonly error: string
918
1069
  readonly unknown?: ComputerUseOutcomeUnknown
1070
+ readonly delivery?: ActionDelivery
1071
+ readonly retrySafety?: ActionRetrySafety
919
1072
  }
920
1073
  | { readonly label: string; readonly status: 'not run' }
921
1074
 
@@ -923,11 +1076,95 @@ class StepFailure extends Error {
923
1076
  constructor(
924
1077
  message: string,
925
1078
  readonly unknown?: ComputerUseOutcomeUnknown,
1079
+ readonly delivery?: ActionDelivery,
1080
+ readonly retrySafety?: ActionRetrySafety,
926
1081
  ) {
927
1082
  super(message)
928
1083
  }
929
1084
  }
930
1085
 
1086
+ /**
1087
+ * A host's verdict on an action, in the words the model reads. "Done" is
1088
+ * said only for a delivery the host checked; a call that merely returned is
1089
+ * "sent, not confirmed", and one that did not reach its target fails.
1090
+ */
1091
+ function deliveryOutcome(
1092
+ verdict: {
1093
+ readonly delivery?: ActionDelivery
1094
+ readonly retrySafety?: ActionRetrySafety
1095
+ readonly detail?: string
1096
+ readonly readBack?: {
1097
+ readonly expected?: string
1098
+ readonly observed?: string
1099
+ }
1100
+ },
1101
+ extra?: string,
1102
+ ): StepOutcome {
1103
+ const detail = [verdict.detail, extra].filter((part) => part && part.length > 0).join(' ')
1104
+ const base = {
1105
+ ...(verdict.delivery ? { delivery: verdict.delivery } : {}),
1106
+ ...(verdict.retrySafety ? { retrySafety: verdict.retrySafety } : {}),
1107
+ ...(verdict.readBack ? { readBack: verdict.readBack } : {}),
1108
+ }
1109
+ switch (verdict.delivery) {
1110
+ case 'delivered_verified': {
1111
+ const observed = verdict.readBack?.observed
1112
+ const seen = observed !== undefined ? ` (reads ${quotedText(observed)})` : ''
1113
+ return {
1114
+ ...base,
1115
+ note: `done, verified${seen}${detail ? `. ${detail}` : ''}`,
1116
+ }
1117
+ }
1118
+ case 'delivered_unverified':
1119
+ // An input the window provably did not react to can be sent again; one nobody could check cannot.
1120
+ if (verdict.retrySafety === 'safe' && detail)
1121
+ return {
1122
+ ...base,
1123
+ note: `sent, no effect seen. ${detail} It is safe to send it again if it is still needed.`,
1124
+ }
1125
+ return {
1126
+ ...base,
1127
+ note: detail
1128
+ ? `sent, not confirmed. ${detail} Do not repeat it before checking.`
1129
+ : 'sent, not confirmed',
1130
+ }
1131
+ default:
1132
+ return { ...base, note: detail ? `done (${detail})` : 'done' }
1133
+ }
1134
+ }
1135
+
1136
+ /** The failure a verdict that did not take effect becomes. */
1137
+ function deliveryFailure(verdict: {
1138
+ readonly delivery?: ActionDelivery
1139
+ readonly retrySafety?: ActionRetrySafety
1140
+ readonly detail?: string
1141
+ }): StepFailure {
1142
+ const text = verdict.detail ?? 'the action did not take effect'
1143
+ const advice =
1144
+ verdict.delivery === 'not_delivered'
1145
+ ? ' Not delivered: nothing reached the target, so trying again is safe once the cause is dealt with.'
1146
+ : verdict.delivery === 'delivered_unverified'
1147
+ ? ' It may have taken effect: look at the window before repeating it.'
1148
+ : ''
1149
+ return new StepFailure(`${text}${advice}`, undefined, verdict.delivery, verdict.retrySafety)
1150
+ }
1151
+
1152
+ /** A host's window or display verdict → a step outcome, or the failure it amounts to. */
1153
+ function hostOutcome(
1154
+ verdict:
1155
+ | WindowInputResult
1156
+ | {
1157
+ readonly delivery?: ActionDelivery
1158
+ readonly retrySafety?: ActionRetrySafety
1159
+ readonly detail?: string
1160
+ }
1161
+ | undefined,
1162
+ ): StepOutcome {
1163
+ if (!verdict || verdict.delivery === undefined) return { note: 'done' }
1164
+ if (verdict.delivery === 'not_delivered') throw deliveryFailure(verdict)
1165
+ return deliveryOutcome(verdict)
1166
+ }
1167
+
931
1168
  interface Capture {
932
1169
  readonly frame: ScreenshotFrame
933
1170
  readonly image: FittedImage
@@ -937,6 +1174,8 @@ interface Capture {
937
1174
  interface UiRef {
938
1175
  readonly hostRef: string
939
1176
  readonly element: UiElement
1177
+ /** The window the snapshot that showed it was of. */
1178
+ readonly windowId: string | undefined
940
1179
  }
941
1180
 
942
1181
  /**
@@ -1060,13 +1299,19 @@ export function createComputerUseTool(
1060
1299
  const available = new Set<string>(availableActions(host, caps))
1061
1300
 
1062
1301
  // The controls of the latest ui_snapshot, by the ref the model was shown.
1063
- // Refs count up across snapshots, so a ref from an earlier one is never
1064
- // silently a different control of the latest: it is simply not here.
1065
- let uiRefs = new Map<string, UiRef>()
1302
+ // A ref names a control, not one reading of it: the same host ref keeps the
1303
+ // same number in every snapshot, and a number is never reused for another
1304
+ // control. Whether the control is still there is the host's answer.
1305
+ const uiRefs = new Map<string, UiRef>()
1306
+ const uiRefByHost = new Map<string, string>()
1066
1307
  let uiSnapshots = 0
1067
1308
  let uiRefCount = 0
1068
1309
  let uiSnapshotWindowId: string | undefined
1069
1310
  let uiSnapshotFrame: ScreenshotFrame | undefined
1311
+ // Whether the latest ui_snapshot of a window had controls to act on, and which windows were
1312
+ // already pointed at them after a first coordinate click.
1313
+ const treeUsable = new Map<string, boolean>()
1314
+ const nudgedWindows = new Set<string>()
1070
1315
  const describeUiRef = (ref: string): string | undefined => {
1071
1316
  const entry = uiRefs.get(ref)
1072
1317
  return entry ? `${uiElementText(entry.element)} (${ref})` : undefined
@@ -1086,6 +1331,11 @@ export function createComputerUseTool(
1086
1331
  return null
1087
1332
  }
1088
1333
 
1334
+ // The cancellation of the call being served. A tool runs one call at a time
1335
+ // (`concurrencySafe: false`), so one variable is enough; every host call below
1336
+ // passes it so an interrupted turn stops waiting for the desktop.
1337
+ let callSignal: AbortSignal | undefined
1338
+
1089
1339
  /** Capture the display or one explicitly named window, fit it, and number it. */
1090
1340
  const capture = async (windowId?: string): Promise<Capture> => {
1091
1341
  if (windowId !== undefined) {
@@ -1095,6 +1345,7 @@ export function createComputerUseTool(
1095
1345
  )
1096
1346
  const shot = await (host.captureWindow as NonNullable<ComputerUseHost['captureWindow']>)(
1097
1347
  windowId,
1348
+ callSignal,
1098
1349
  )
1099
1350
  const image = await fitPng(shot.data, settings.limits)
1100
1351
  if (image.sourceWidth !== shot.width || image.sourceHeight !== shot.height)
@@ -1107,7 +1358,7 @@ export function createComputerUseTool(
1107
1358
  throw new StepFailure(
1108
1359
  'this host requires window_id for screenshots; no display screenshot was taken',
1109
1360
  )
1110
- const result = await host.execute({ type: 'screenshot' })
1361
+ const result = await host.execute({ type: 'screenshot' }, callSignal)
1111
1362
  if (result.type !== 'screenshot')
1112
1363
  throw new Error(`computer_use: the host answered a screenshot with "${result.type}"`)
1113
1364
  const shot = result.result
@@ -1160,7 +1411,7 @@ export function createComputerUseTool(
1160
1411
  const runHost = async (
1161
1412
  action: ComputerUseAction | WindowInputAction,
1162
1413
  frame?: ScreenshotFrame,
1163
- ): Promise<string> => {
1414
+ ): Promise<string | StepOutcome> => {
1164
1415
  try {
1165
1416
  if (frame?.window) {
1166
1417
  if (
@@ -1173,19 +1424,20 @@ export function createComputerUseTool(
1173
1424
  )
1174
1425
  if (frames.latest() !== frame)
1175
1426
  throw new StepFailure(`window screenshot ${frame.id} is stale; capture the window again`)
1176
- await (host.executeWindow as NonNullable<ComputerUseHost['executeWindow']>)(
1427
+ const verdict = await (host.executeWindow as NonNullable<ComputerUseHost['executeWindow']>)(
1177
1428
  frame.window.captureId,
1178
1429
  action as WindowInputAction,
1430
+ callSignal,
1179
1431
  )
1180
- return 'done'
1432
+ return hostOutcome(verdict ?? undefined)
1181
1433
  }
1182
- const result = await host.execute(action)
1434
+ const result = await host.execute(action, callSignal)
1183
1435
  if (result.type === 'cursor_position') {
1184
1436
  const selected = frame ?? frames.latest()
1185
1437
  if (!selected) return `at display pixel ${pointLabel(result.point)}`
1186
1438
  return `at ${pointLabel(toImagePoint(selected, result.point))} in ${selected.id}`
1187
1439
  }
1188
- return 'done'
1440
+ return result.type === 'ok' ? hostOutcome(result) : 'done'
1189
1441
  } catch (error) {
1190
1442
  if (error instanceof StepFailure) throw error
1191
1443
  if (isOutcomeUnknown(error, action.type)) throw new StepFailure(error.message, error)
@@ -1233,7 +1485,14 @@ export function createComputerUseTool(
1233
1485
  )
1234
1486
  const frame = frameFor(frameId)
1235
1487
  const at = mapPoint(frame, item.at, 'at')
1236
- return step(() => runHost({ type: 'mouse_click', at, button: item.button }, frame))
1488
+ return step(async () => {
1489
+ const outcome = await runHost({ type: 'mouse_click', at, button: item.button }, frame)
1490
+ const nudge = coordinateNudge(frame)
1491
+ if (!nudge) return outcome
1492
+ return typeof outcome === 'string'
1493
+ ? `${outcome}. ${nudge}`
1494
+ : { ...outcome, note: `${outcome.note}. ${nudge}` }
1495
+ })
1237
1496
  }
1238
1497
  case 'mouse_drag': {
1239
1498
  if (item.delivery_mode)
@@ -1262,7 +1521,15 @@ export function createComputerUseTool(
1262
1521
  )
1263
1522
  const at = mapPoint(frame, item.at, 'at')
1264
1523
  return step(() =>
1265
- runHost({ type: 'scroll', at, direction: item.direction, amount: item.amount }, frame),
1524
+ runHost(
1525
+ {
1526
+ type: 'scroll',
1527
+ at,
1528
+ direction: item.direction,
1529
+ amount: item.amount,
1530
+ },
1531
+ frame,
1532
+ ),
1266
1533
  )
1267
1534
  }
1268
1535
  case 'type_text': {
@@ -1328,14 +1595,22 @@ export function createComputerUseTool(
1328
1595
  try {
1329
1596
  outcome = await (host.focusWindow as NonNullable<ComputerUseHost['focusWindow']>)(
1330
1597
  item.window_id,
1598
+ callSignal,
1331
1599
  )
1332
1600
  } catch (error) {
1333
1601
  throw new StepFailure(errorText(error))
1334
1602
  }
1603
+ if (!outcome.ok && outcome.delivery === 'not_delivered')
1604
+ throw deliveryFailure({
1605
+ delivery: outcome.delivery,
1606
+ retrySafety: outcome.retrySafety ?? 'safe',
1607
+ detail: outcome.detail,
1608
+ })
1335
1609
  if (!outcome.ok)
1336
1610
  throw new StepFailure(
1337
- `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'}`,
1611
+ `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}` : ''}`,
1338
1612
  )
1613
+ if (outcome.detail) return { note: `done. ${outcome.detail}` }
1339
1614
  return 'done'
1340
1615
  })
1341
1616
  case 'ui_act': {
@@ -1344,7 +1619,11 @@ export function createComputerUseTool(
1344
1619
  throw new StepFailure(
1345
1620
  uiSnapshots === 0
1346
1621
  ? `there is no ${item.ref}: no ui_snapshot has been taken yet; take one and use its refs`
1347
- : `${item.ref} is not a control of the latest ui_snapshot (u${uiSnapshots}); refs are valid only until the next ui_snapshot, so use the refs it showed`,
1622
+ : `${item.ref} is not a ref any ui_snapshot showed; use a ref from a snapshot`,
1623
+ )
1624
+ if ((entry.element.hiddenChildren ?? 0) > 0 && (entry.element.actions ?? []).length === 0)
1625
+ throw new StepFailure(
1626
+ `${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.`,
1348
1627
  )
1349
1628
  const offered = entry.element.actions
1350
1629
  if (offered && offered.length > 0 && !offered.includes(item.action))
@@ -1360,15 +1639,21 @@ export function createComputerUseTool(
1360
1639
  entry.hostRef,
1361
1640
  item.action,
1362
1641
  item.value,
1642
+ callSignal,
1363
1643
  )
1364
1644
  } catch (error) {
1365
1645
  throw new StepFailure(errorText(error))
1366
1646
  }
1367
1647
  if (!outcome.ok)
1368
- throw new StepFailure(
1369
- outcome.detail ?? `${item.action} did not take effect on ${describeUiRef(item.ref)}`,
1370
- )
1371
- return outcome.detail ? `done (${outcome.detail})` : 'done'
1648
+ throw deliveryFailure({
1649
+ ...outcome,
1650
+ detail:
1651
+ outcome.code !== undefined
1652
+ ? `${outcome.code}: ${outcome.detail ?? `${describeUiRef(item.ref)} could not be resolved`}`
1653
+ : (outcome.detail ??
1654
+ `${item.action} did not take effect on ${describeUiRef(item.ref)}`),
1655
+ })
1656
+ return deliveryOutcome(outcome)
1372
1657
  })
1373
1658
  }
1374
1659
  }
@@ -1379,16 +1664,43 @@ export function createComputerUseTool(
1379
1664
  input: Extract<ActionInput, { type: 'ui_snapshot' }>,
1380
1665
  ): Promise<ToolResult> => {
1381
1666
  let snapshot: UiSnapshot
1667
+ let rootRef: UiRef | undefined
1668
+ if (input.root !== undefined) {
1669
+ rootRef = uiRefs.get(input.root)
1670
+ if (!rootRef)
1671
+ throw new StepFailure(
1672
+ `${input.root} is not a ref any ui_snapshot showed; take a ui_snapshot of the window and open a region it marks`,
1673
+ )
1674
+ }
1675
+ const windowId = input.window_id ?? rootRef?.windowId
1676
+ const options: UiSnapshotOptions = {
1677
+ ...(rootRef ? { root: rootRef.hostRef } : {}),
1678
+ ...(rootRef && input.offset !== undefined ? { offset: input.offset } : {}),
1679
+ ...(input.query !== undefined ? { query: input.query } : {}),
1680
+ }
1382
1681
  try {
1383
1682
  snapshot = await (host.uiSnapshot as NonNullable<ComputerUseHost['uiSnapshot']>)(
1384
- input.window_id,
1683
+ windowId,
1684
+ options,
1685
+ callSignal,
1385
1686
  )
1386
1687
  } catch (error) {
1387
1688
  throw new StepFailure(errorText(error))
1388
1689
  }
1690
+ return showUiSnapshot(snapshot, windowId, [], input.root)
1691
+ }
1692
+
1693
+ /** Number a read tree's controls and show it; this snapshot's refs replace the last one's. */
1694
+ const showUiSnapshot = (
1695
+ snapshot: UiSnapshot,
1696
+ requestedWindowId: string | undefined,
1697
+ preface: readonly string[] = [],
1698
+ openedRef?: string,
1699
+ ): ToolResult => {
1389
1700
  uiSnapshots += 1
1390
1701
  const id = `u${uiSnapshots}`
1391
1702
  const refs = new Map<string, UiRef>()
1703
+ const windowId = snapshot.windowId ?? requestedWindowId
1392
1704
  const latest = frames.latest()
1393
1705
  const frame = latest?.window ? undefined : latest
1394
1706
  const lines: string[] = []
@@ -1404,8 +1716,11 @@ export function createComputerUseTool(
1404
1716
  // children still stand, one level up.
1405
1717
  const silent = !actionable && element.name.trim().length === 0 && element.value === undefined
1406
1718
  if (!silent && !cut) {
1407
- const ref = actionable ? `e${uiRefCount + 1}` : undefined
1408
- const line = `${' '.repeat(depth)}${uiElementLine(element, ref, frame)}`
1719
+ // The same host ref keeps its number; a new one takes the next.
1720
+ const known = actionable ? uiRefByHost.get(element.ref) : undefined
1721
+ const ref = actionable ? (known ?? `e${uiRefCount + 1}`) : undefined
1722
+ const hidden = element.hiddenChildren ?? 0
1723
+ const line = `${' '.repeat(depth)}${uiElementLine(element, ref, frame)}${hidden > 0 ? ` (+${hidden} more inside)` : ''}`
1409
1724
  if (chars + line.length + 1 > MAX_UI_SNAPSHOT_CHARS) {
1410
1725
  cut = true
1411
1726
  } else {
@@ -1413,21 +1728,37 @@ export function createComputerUseTool(
1413
1728
  chars += line.length + 1
1414
1729
  shown += 1
1415
1730
  if (ref) {
1416
- uiRefCount += 1
1417
- refs.set(ref, { hostRef: element.ref, element })
1731
+ if (known === undefined) {
1732
+ uiRefCount += 1
1733
+ uiRefByHost.set(element.ref, ref)
1734
+ }
1735
+ refs.set(ref, { hostRef: element.ref, element, windowId })
1418
1736
  }
1419
1737
  }
1420
1738
  }
1421
1739
  for (const child of children) visit(child, silent ? depth : depth + 1)
1422
1740
  }
1423
1741
  visit(snapshot.root, 0)
1424
- uiRefs = refs
1425
- uiSnapshotWindowId = snapshot.windowId ?? input.window_id
1742
+ // Refs shown now replace what they name; the rest stay as they were, so a
1743
+ // region opened by itself leaves the refs outside it usable.
1744
+ for (const [ref, entry] of refs) uiRefs.set(ref, entry)
1745
+ if (uiRefs.size > MAX_HELD_UI_REFS) {
1746
+ for (const ref of [...uiRefs.keys()].slice(0, uiRefs.size - MAX_HELD_UI_REFS)) {
1747
+ const gone = uiRefs.get(ref)
1748
+ uiRefs.delete(ref)
1749
+ if (gone) uiRefByHost.delete(gone.hostRef)
1750
+ }
1751
+ }
1752
+ uiSnapshotWindowId = snapshot.windowId ?? requestedWindowId
1753
+ if (uiSnapshotWindowId !== undefined) treeUsable.set(uiSnapshotWindowId, refs.size > 0)
1426
1754
  uiSnapshotFrame = frames.latest()
1427
1755
  const window =
1428
1756
  snapshot.windowId && /^[\w.:-]{1,64}$/.test(snapshot.windowId) ? snapshot.windowId : undefined
1757
+ const opened = openedRef !== undefined && /^e\d{1,6}$/.test(openedRef) ? openedRef : undefined
1758
+ 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.`
1429
1759
  const header = [
1430
- `UI snapshot ${id}${window ? ` of window ${window}` : ''}: ${shown} controls shown, ${refs.size} with a ref you can pass to ui_act. Refs are valid until the next ui_snapshot.`,
1760
+ ...preface,
1761
+ summaryLine,
1431
1762
  ...(frame && !frame.window
1432
1763
  ? [
1433
1764
  `@(x, y) is a control's centre on screenshot ${frame.id}, for a click when ui_act cannot reach it.`,
@@ -1443,14 +1774,30 @@ export function createComputerUseTool(
1443
1774
  : []),
1444
1775
  ]
1445
1776
  const body = [...about, ...lines].join('\n')
1446
- const footer =
1447
- cut || snapshot.truncated
1777
+ const drill =
1778
+ 'Open a region with ui_snapshot {root: <its ref>}, or find a control by its text with ui_snapshot {query}.'
1779
+ const footer = [
1780
+ ...(snapshot.next !== undefined && opened
1448
1781
  ? [
1449
- cut
1450
- ? `The tree was cut at ${MAX_UI_SNAPSHOT_CHARS} characters after ${shown} of ${total} controls. Read a smaller window, or use a screenshot for the rest.`
1451
- : 'The host stopped reading the tree before it ended; controls further down are missing. Use a screenshot for the rest.',
1782
+ `This is one page of a long region. Next page: ui_snapshot {root: "${opened}", offset: ${snapshot.next}}.`,
1452
1783
  ]
1453
- : []
1784
+ : []),
1785
+ ...(snapshot.outline
1786
+ ? [
1787
+ `This is an outline: regions marked "(+N more inside)" hold controls that are not listed. ${drill}`,
1788
+ ]
1789
+ : []),
1790
+ ...(cut
1791
+ ? [
1792
+ `The listing was cut at ${MAX_UI_SNAPSHOT_CHARS} characters after ${shown} of ${total} controls. Drill into the region you need: ${drill}`,
1793
+ ]
1794
+ : []),
1795
+ ...(snapshot.truncated
1796
+ ? [
1797
+ `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.`,
1798
+ ]
1799
+ : []),
1800
+ ]
1454
1801
  const text = [
1455
1802
  ...header,
1456
1803
  wrapUntrusted(
@@ -1466,7 +1813,7 @@ export function createComputerUseTool(
1466
1813
  ].join('\n')
1467
1814
  return {
1468
1815
  success: true,
1469
- output: header[0] ?? '',
1816
+ output: summaryLine,
1470
1817
  content: [{ type: 'text', text }],
1471
1818
  data: {
1472
1819
  uiSnapshot: {
@@ -1475,11 +1822,141 @@ export function createComputerUseTool(
1475
1822
  controls: shown,
1476
1823
  refs: refs.size,
1477
1824
  truncated: cut || snapshot.truncated === true,
1825
+ ...(snapshot.outline ? { outline: true } : {}),
1826
+ ...(snapshot.total !== undefined ? { total: snapshot.total } : {}),
1478
1827
  },
1479
1828
  },
1480
1829
  }
1481
1830
  }
1482
1831
 
1832
+ const WAIT_FOR_POLL_MS = 500
1833
+
1834
+ /** Whether a wait_for condition holds in what the host just showed. */
1835
+ const treeHolds = (
1836
+ input: Extract<ActionInput, { type: 'wait_for' }>,
1837
+ snapshot: UiSnapshot,
1838
+ ): boolean => {
1839
+ const needle = input.match.toLowerCase()
1840
+ const role = input.role?.toLowerCase()
1841
+ let found = false
1842
+ const visit = (element: UiElement): void => {
1843
+ if (found) return
1844
+ const name = element.name.toLowerCase()
1845
+ const value = (element.value ?? '').toLowerCase()
1846
+ if (input.until.startsWith('text_')) {
1847
+ if (name.includes(needle) || value.includes(needle)) found = true
1848
+ } else if (
1849
+ name.includes(needle) &&
1850
+ (role === undefined || element.role.toLowerCase() === role)
1851
+ ) {
1852
+ found = true
1853
+ }
1854
+ for (const child of element.children ?? []) visit(child)
1855
+ }
1856
+ let nodes = 0
1857
+ const count = (element: UiElement): void => {
1858
+ nodes++
1859
+ for (const child of element.children ?? []) count(child)
1860
+ }
1861
+ count(snapshot.root)
1862
+ visit(snapshot.root)
1863
+ if (input.until.endsWith('appears')) return found
1864
+ // "Gone" is only proof on a complete read of a tree that has content: a
1865
+ // cut-off tree or a bare root (a web view whose tree has not woken yet)
1866
+ // may simply not show the element.
1867
+ return !found && snapshot.truncated !== true && nodes > 1
1868
+ }
1869
+
1870
+ /**
1871
+ * Look until a condition holds or the time is up. It replaces a fixed
1872
+ * wait: the model says what it is waiting for and gets the screen the
1873
+ * moment that is true. A read that fails is not an answer, so it is tried
1874
+ * again; only the last failure is reported if time runs out.
1875
+ */
1876
+ const waitFor = async (
1877
+ input: Extract<ActionInput, { type: 'wait_for' }>,
1878
+ context: ToolContext | undefined,
1879
+ ): Promise<ToolResult> => {
1880
+ const windowCondition = input.until.startsWith('window_')
1881
+ if (windowCondition && !available.has('list_windows'))
1882
+ throw new StepFailure('window conditions need list_windows, which this host does not offer')
1883
+ if (!windowCondition && input.window_id === undefined && uiSnapshotWindowId === undefined)
1884
+ throw new StepFailure(
1885
+ 'wait_for on a control or text needs window_id (or a ui_snapshot of the window first)',
1886
+ )
1887
+ const timeout = Math.min(
1888
+ input.timeout_ms ?? Math.min(DEFAULT_WAIT_FOR_MS, settings.maxWaitMs),
1889
+ settings.maxWaitMs,
1890
+ )
1891
+ const signal = context?.abortSignal
1892
+ const started = Date.now()
1893
+ const needle = input.match.toLowerCase()
1894
+ const what = `${input.until.replace('_', ' ')} ${quotedText(input.match)}`
1895
+ let lastError: string | undefined
1896
+ let lastSnapshot: UiSnapshot | undefined
1897
+ for (;;) {
1898
+ if (signal?.aborted) throw new StepFailure('cancelled')
1899
+ try {
1900
+ if (windowCondition) {
1901
+ const windows = await (host.listWindows as NonNullable<ComputerUseHost['listWindows']>)(
1902
+ callSignal,
1903
+ )
1904
+ const open = windows.some((window) => window.title.toLowerCase().includes(needle))
1905
+ if (open === input.until.endsWith('appears')) {
1906
+ const seconds = ((Date.now() - started) / 1000).toFixed(1)
1907
+ return {
1908
+ success: true,
1909
+ output: `wait_for ${what}: satisfied after ${seconds} s.`,
1910
+ content: [
1911
+ {
1912
+ type: 'text',
1913
+ text: `wait_for ${what}: satisfied after ${seconds} s.`,
1914
+ },
1915
+ ],
1916
+ }
1917
+ }
1918
+ } else {
1919
+ const snapshot = await (host.uiSnapshot as NonNullable<ComputerUseHost['uiSnapshot']>)(
1920
+ input.window_id ?? uiSnapshotWindowId,
1921
+ undefined,
1922
+ callSignal,
1923
+ )
1924
+ lastSnapshot = snapshot
1925
+ if (treeHolds(input, snapshot)) {
1926
+ const seconds = ((Date.now() - started) / 1000).toFixed(1)
1927
+ return showUiSnapshot(snapshot, input.window_id, [
1928
+ `wait_for ${what}: satisfied after ${seconds} s.`,
1929
+ ])
1930
+ }
1931
+ }
1932
+ lastError = undefined
1933
+ } catch (error) {
1934
+ lastError = errorText(error)
1935
+ }
1936
+ if (Date.now() - started >= timeout) break
1937
+ await sleep(WAIT_FOR_POLL_MS, signal)
1938
+ }
1939
+ 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.`
1940
+ if (lastSnapshot && !lastError) {
1941
+ const shown = showUiSnapshot(lastSnapshot, input.window_id, [message])
1942
+ return { ...shown, success: false, error: message }
1943
+ }
1944
+ return { success: false, output: '', error: message }
1945
+ }
1946
+
1947
+ /** Said once per window, after its first coordinate click: the tree is the surer way in. */
1948
+ const coordinateNudge = (frame: ScreenshotFrame): string | undefined => {
1949
+ if (!frame.window || !available.has('ui_snapshot')) return undefined
1950
+ const id = frame.window.window.id
1951
+ if (nudgedWindows.has(id)) return undefined
1952
+ const usable = treeUsable.get(id)
1953
+ if (usable === false) return undefined
1954
+ nudgedWindows.add(id)
1955
+ return usable === true
1956
+ ? '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.'
1957
+ : '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.'
1958
+ }
1959
+
1483
1960
  /**
1484
1961
  * Keys and text go to the window in front, and a terminal there — the
1485
1962
  * one running this agent, or the user's own — turns a model's typing into
@@ -1493,7 +1970,7 @@ export function createComputerUseTool(
1493
1970
  if (!available.has('list_windows')) return
1494
1971
  let windows: readonly WindowInfo[]
1495
1972
  try {
1496
- windows = await (host.listWindows as NonNullable<ComputerUseHost['listWindows']>)()
1973
+ windows = await (host.listWindows as NonNullable<ComputerUseHost['listWindows']>)(callSignal)
1497
1974
  } catch (error) {
1498
1975
  throw new StepFailure(
1499
1976
  `the window in front could not be read, so nothing was typed: ${errorText(error)}`,
@@ -1509,7 +1986,7 @@ export function createComputerUseTool(
1509
1986
  const listWindows = async (): Promise<string> => {
1510
1987
  let windows: readonly WindowInfo[]
1511
1988
  try {
1512
- windows = await (host.listWindows as NonNullable<ComputerUseHost['listWindows']>)()
1989
+ windows = await (host.listWindows as NonNullable<ComputerUseHost['listWindows']>)(callSignal)
1513
1990
  } catch (error) {
1514
1991
  throw new StepFailure(errorText(error))
1515
1992
  }
@@ -1609,7 +2086,7 @@ export function createComputerUseTool(
1609
2086
  const firstLookHint = (frame: ScreenshotFrame): string[] =>
1610
2087
  frame.id === 's1' && available.has('ui_snapshot')
1611
2088
  ? [
1612
- 'This host can also read a window’s controls: list_windows, then ui_snapshot {window_id}, then ui_act by ref (a batch of them for several buttons) — surer than clicking pixels in an ordinary application.',
2089
+ '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.',
1613
2090
  ]
1614
2091
  : []
1615
2092
 
@@ -1619,7 +2096,10 @@ export function createComputerUseTool(
1619
2096
  ? `Screenshot ${shot.frame.id} captured (${shot.frame.imageWidth}x${shot.frame.imageHeight} of window ${visibleWindowId(shot.frame.window.window.id)}).`
1620
2097
  : `Screenshot ${shot.frame.id} captured (${shot.frame.imageWidth}x${shot.frame.imageHeight} of the ${shot.frame.display.width}x${shot.frame.display.height} display).`,
1621
2098
  content: [
1622
- { type: 'text', text: [describeFrame(shot.frame), ...firstLookHint(shot.frame)].join('\n') },
2099
+ {
2100
+ type: 'text',
2101
+ text: [describeFrame(shot.frame), ...firstLookHint(shot.frame)].join('\n'),
2102
+ },
1623
2103
  imageBlock(shot.image),
1624
2104
  ],
1625
2105
  data: { screenshot: frameData(shot.frame) },
@@ -1646,10 +2126,10 @@ export function createComputerUseTool(
1646
2126
  throw new StepFailure(`window screenshot ${frame.id} is stale; capture the window again`)
1647
2127
  fitted = await cropAndFitPng(frame.window.data, rect, settings.limits)
1648
2128
  } else if (caps.regionCapture === true && typeof host.captureRegion === 'function') {
1649
- const piece = await host.captureRegion(rect)
2129
+ const piece = await host.captureRegion(rect, callSignal)
1650
2130
  fitted = await fitPng(piece.data, settings.limits)
1651
2131
  } else {
1652
- const result = await host.execute({ type: 'screenshot' })
2132
+ const result = await host.execute({ type: 'screenshot' }, callSignal)
1653
2133
  if (result.type !== 'screenshot')
1654
2134
  throw new Error(`computer_use: the host answered a screenshot with "${result.type}"`)
1655
2135
  const full: ScreenshotResult = result.result
@@ -1769,7 +2249,11 @@ export function createComputerUseTool(
1769
2249
  for (const [index, step] of planned.entries()) {
1770
2250
  if (signal?.aborted) {
1771
2251
  failure = { index, error: 'cancelled before it started' }
1772
- records.push({ label: step.label, status: 'failed', error: failure.error })
2252
+ records.push({
2253
+ label: step.label,
2254
+ status: 'failed',
2255
+ error: failure.error,
2256
+ })
1773
2257
  break
1774
2258
  }
1775
2259
  try {
@@ -1779,7 +2263,8 @@ export function createComputerUseTool(
1779
2263
  frontChecked = true
1780
2264
  }
1781
2265
  if (step.item.type !== 'type_text') frontChecked = false
1782
- const note = await step.run(signal)
2266
+ const ran = await step.run(signal)
2267
+ const outcome: StepOutcome = typeof ran === 'string' ? { note: ran } : ran
1783
2268
  if (step.item.type === 'focus_window' && windowCaptureAvailable(host, caps))
1784
2269
  postWindowId = step.item.window_id
1785
2270
  else if (
@@ -1803,10 +2288,12 @@ export function createComputerUseTool(
1803
2288
  ['mouse_click', 'mouse_drag', 'scroll', 'type_text', 'key'].includes(step.item.type)
1804
2289
  )
1805
2290
  postWindowId = actionFrame.window.window.id
1806
- records.push({ label: step.label, status: 'done', note })
2291
+ records.push({ label: step.label, status: 'done', ...outcome })
1807
2292
  if (step.mutating) changed = true
1808
2293
  } catch (error) {
1809
2294
  const unknown = error instanceof StepFailure ? error.unknown : undefined
2295
+ const delivery = error instanceof StepFailure ? error.delivery : undefined
2296
+ const retrySafety = error instanceof StepFailure ? error.retrySafety : undefined
1810
2297
  const message = signal?.aborted ? 'cancelled' : errorText(error)
1811
2298
  failure = { index, error: message, ...(unknown ? { unknown } : {}) }
1812
2299
  records.push({
@@ -1814,7 +2301,11 @@ export function createComputerUseTool(
1814
2301
  status: 'failed',
1815
2302
  error: message,
1816
2303
  ...(unknown ? { unknown } : {}),
2304
+ ...(delivery ? { delivery } : {}),
2305
+ ...(retrySafety ? { retrySafety } : {}),
1817
2306
  })
2307
+ // A step that may have acted changed the desktop even though it failed.
2308
+ if (delivery === 'delivered_unverified') changed = true
1818
2309
  if (unknown) changed = true
1819
2310
  break
1820
2311
  }
@@ -1877,8 +2368,22 @@ export function createComputerUseTool(
1877
2368
  const data = {
1878
2369
  steps: records.map((record) =>
1879
2370
  record.status === 'failed'
1880
- ? { label: record.label, status: record.status, error: record.error }
1881
- : { label: record.label, status: record.status },
2371
+ ? {
2372
+ label: record.label,
2373
+ status: record.status,
2374
+ error: record.error,
2375
+ ...(record.delivery ? { delivery: record.delivery } : {}),
2376
+ ...(record.retrySafety ? { retrySafety: record.retrySafety } : {}),
2377
+ }
2378
+ : record.status === 'done'
2379
+ ? {
2380
+ label: record.label,
2381
+ status: record.status,
2382
+ ...(record.delivery ? { delivery: record.delivery } : {}),
2383
+ ...(record.retrySafety ? { retrySafety: record.retrySafety } : {}),
2384
+ ...(record.readBack ? { readBack: record.readBack } : {}),
2385
+ }
2386
+ : { label: record.label, status: record.status },
1882
2387
  ),
1883
2388
  ...(shot ? { screenshot: frameData(shot.frame) } : {}),
1884
2389
  ...(failure?.unknown
@@ -1947,6 +2452,18 @@ export function createComputerUseTool(
1947
2452
  async execute(input, context): Promise<ToolResult> {
1948
2453
  const denied = refusal(input.type)
1949
2454
  if (denied) return { success: false, output: '', error: denied }
2455
+ // The host keeps per-turn state (an Escape stop, refused permissions); it
2456
+ // is told which turn this is and released when the turn is cut short.
2457
+ host.beginTurn?.(context.turnId)
2458
+ callSignal = context.abortSignal
2459
+ if (host.endTurn) {
2460
+ const release = () => void Promise.resolve(host.endTurn?.()).catch(() => undefined)
2461
+ if (context.abortSignal.aborted) release()
2462
+ else
2463
+ context.abortSignal.addEventListener('abort', release, {
2464
+ once: true,
2465
+ })
2466
+ }
1950
2467
  try {
1951
2468
  switch (input.type) {
1952
2469
  case 'screenshot':
@@ -1955,6 +2472,8 @@ export function createComputerUseTool(
1955
2472
  return await zoom(input)
1956
2473
  case 'ui_snapshot':
1957
2474
  return await uiSnapshot(input)
2475
+ case 'wait_for':
2476
+ return await waitFor(input, context)
1958
2477
  case 'batch':
1959
2478
  return await runSteps(input.actions, input.screenshot_id, context, false)
1960
2479
  default: {
@@ -1964,8 +2483,14 @@ export function createComputerUseTool(
1964
2483
  }
1965
2484
  } catch (error) {
1966
2485
  if (error instanceof StepFailure)
1967
- return { success: false, output: '', error: `computer_use failed: ${error.message}` }
2486
+ return {
2487
+ success: false,
2488
+ output: '',
2489
+ error: `computer_use failed: ${error.message}`,
2490
+ }
1968
2491
  throw error
2492
+ } finally {
2493
+ callSignal = undefined
1969
2494
  }
1970
2495
  },
1971
2496
  })