@namzu/sdk 58.1.1 → 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 (312) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/dist/account/binding.d.ts +20 -0
  3. package/dist/account/binding.d.ts.map +1 -0
  4. package/dist/account/binding.js +27 -0
  5. package/dist/account/binding.js.map +1 -0
  6. package/dist/account/index.d.ts +9 -0
  7. package/dist/account/index.d.ts.map +1 -0
  8. package/dist/account/index.js +5 -0
  9. package/dist/account/index.js.map +1 -0
  10. package/dist/account/lock.d.ts +33 -0
  11. package/dist/account/lock.d.ts.map +1 -0
  12. package/dist/account/lock.js +170 -0
  13. package/dist/account/lock.js.map +1 -0
  14. package/dist/account/registry.d.ts +21 -0
  15. package/dist/account/registry.d.ts.map +1 -0
  16. package/dist/account/registry.js +340 -0
  17. package/dist/account/registry.js.map +1 -0
  18. package/dist/account/types.d.ts +124 -0
  19. package/dist/account/types.d.ts.map +1 -0
  20. package/dist/account/types.js +35 -0
  21. package/dist/account/types.js.map +1 -0
  22. package/dist/compaction/context-window.d.ts +53 -0
  23. package/dist/compaction/context-window.d.ts.map +1 -1
  24. package/dist/compaction/context-window.js +141 -2
  25. package/dist/compaction/context-window.js.map +1 -1
  26. package/dist/execution/workflow/index.d.ts +3 -1
  27. package/dist/execution/workflow/index.d.ts.map +1 -1
  28. package/dist/execution/workflow/index.js +2 -1
  29. package/dist/execution/workflow/index.js.map +1 -1
  30. package/dist/execution/workflow/journal.d.ts +24 -4
  31. package/dist/execution/workflow/journal.d.ts.map +1 -1
  32. package/dist/execution/workflow/journal.js +131 -9
  33. package/dist/execution/workflow/journal.js.map +1 -1
  34. package/dist/execution/workflow/prelude.d.ts +2 -0
  35. package/dist/execution/workflow/prelude.d.ts.map +1 -1
  36. package/dist/execution/workflow/prelude.js +49 -8
  37. package/dist/execution/workflow/prelude.js.map +1 -1
  38. package/dist/execution/workflow/resume.d.ts +59 -0
  39. package/dist/execution/workflow/resume.d.ts.map +1 -0
  40. package/dist/execution/workflow/resume.js +273 -0
  41. package/dist/execution/workflow/resume.js.map +1 -0
  42. package/dist/execution/workflow/runtime.d.ts.map +1 -1
  43. package/dist/execution/workflow/runtime.js +112 -8
  44. package/dist/execution/workflow/runtime.js.map +1 -1
  45. package/dist/execution/workflow/types.d.ts +105 -1
  46. package/dist/execution/workflow/types.d.ts.map +1 -1
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/pals/communication/ingress-schema.d.ts +38 -38
  52. package/dist/peers/record.d.ts +98 -0
  53. package/dist/peers/record.d.ts.map +1 -1
  54. package/dist/peers/record.js +17 -0
  55. package/dist/peers/record.js.map +1 -1
  56. package/dist/peers/registry.d.ts.map +1 -1
  57. package/dist/peers/registry.js +2 -2
  58. package/dist/peers/registry.js.map +1 -1
  59. package/dist/plugin/compat.d.ts +24 -0
  60. package/dist/plugin/compat.d.ts.map +1 -0
  61. package/dist/plugin/compat.js +149 -0
  62. package/dist/plugin/compat.js.map +1 -0
  63. package/dist/plugin/loader.d.ts +2 -1
  64. package/dist/plugin/loader.d.ts.map +1 -1
  65. package/dist/plugin/loader.js +6 -0
  66. package/dist/plugin/loader.js.map +1 -1
  67. package/dist/pricing/index.d.ts.map +1 -1
  68. package/dist/pricing/index.js +2 -1
  69. package/dist/pricing/index.js.map +1 -1
  70. package/dist/process/kill-tree.d.ts +44 -0
  71. package/dist/process/kill-tree.d.ts.map +1 -1
  72. package/dist/process/kill-tree.js +102 -4
  73. package/dist/process/kill-tree.js.map +1 -1
  74. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  75. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  76. package/dist/prompt/coding-agent-doctrine.js +9 -1
  77. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  78. package/dist/provider/errors.d.ts.map +1 -1
  79. package/dist/provider/errors.js +25 -2
  80. package/dist/provider/errors.js.map +1 -1
  81. package/dist/provider/model-window-suffix.d.ts +18 -0
  82. package/dist/provider/model-window-suffix.d.ts.map +1 -0
  83. package/dist/provider/model-window-suffix.js +77 -0
  84. package/dist/provider/model-window-suffix.js.map +1 -0
  85. package/dist/provider/token-budget.d.ts.map +1 -1
  86. package/dist/provider/token-budget.js +9 -1
  87. package/dist/provider/token-budget.js.map +1 -1
  88. package/dist/public-runtime.d.ts +6 -3
  89. package/dist/public-runtime.d.ts.map +1 -1
  90. package/dist/public-runtime.js +6 -4
  91. package/dist/public-runtime.js.map +1 -1
  92. package/dist/public-types.d.ts +3 -0
  93. package/dist/public-types.d.ts.map +1 -1
  94. package/dist/registry/command/kernel-commands.d.ts +1 -4
  95. package/dist/registry/command/kernel-commands.d.ts.map +1 -1
  96. package/dist/registry/command/kernel-commands.js +0 -28
  97. package/dist/registry/command/kernel-commands.js.map +1 -1
  98. package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
  99. package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
  100. package/dist/runtime/jobs/awaited-jobs.js +11 -0
  101. package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
  102. package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
  103. package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
  104. package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
  105. package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
  106. package/dist/runtime/jobs/liveness.d.ts +67 -0
  107. package/dist/runtime/jobs/liveness.d.ts.map +1 -0
  108. package/dist/runtime/jobs/liveness.js +94 -0
  109. package/dist/runtime/jobs/liveness.js.map +1 -0
  110. package/dist/runtime/jobs/registry.d.ts +21 -0
  111. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  112. package/dist/runtime/jobs/registry.js +104 -2
  113. package/dist/runtime/jobs/registry.js.map +1 -1
  114. package/dist/runtime/jobs/wake-budget.d.ts +35 -0
  115. package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
  116. package/dist/runtime/jobs/wake-budget.js +48 -0
  117. package/dist/runtime/jobs/wake-budget.js.map +1 -0
  118. package/dist/runtime/query/detached-calls.d.ts +65 -0
  119. package/dist/runtime/query/detached-calls.d.ts.map +1 -0
  120. package/dist/runtime/query/detached-calls.js +116 -0
  121. package/dist/runtime/query/detached-calls.js.map +1 -0
  122. package/dist/runtime/query/executor.d.ts +11 -3
  123. package/dist/runtime/query/executor.d.ts.map +1 -1
  124. package/dist/runtime/query/executor.js +100 -8
  125. package/dist/runtime/query/executor.js.map +1 -1
  126. package/dist/runtime/query/index.d.ts +20 -0
  127. package/dist/runtime/query/index.d.ts.map +1 -1
  128. package/dist/runtime/query/index.js +31 -5
  129. package/dist/runtime/query/index.js.map +1 -1
  130. package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
  131. package/dist/runtime/query/iteration/outstanding-work.js +11 -3
  132. package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
  133. package/dist/runtime/query/iteration/phases/context.d.ts +7 -0
  134. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  135. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  136. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  137. package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
  138. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  139. package/dist/runtime/query/session-storage.d.ts.map +1 -1
  140. package/dist/runtime/query/session-storage.js +8 -3
  141. package/dist/runtime/query/session-storage.js.map +1 -1
  142. package/dist/runtime/query/steering.d.ts +29 -3
  143. package/dist/runtime/query/steering.d.ts.map +1 -1
  144. package/dist/runtime/query/steering.js +58 -3
  145. package/dist/runtime/query/steering.js.map +1 -1
  146. package/dist/runtime/query/tooling.d.ts +1 -0
  147. package/dist/runtime/query/tooling.d.ts.map +1 -1
  148. package/dist/runtime/query/tooling.js +1 -0
  149. package/dist/runtime/query/tooling.js.map +1 -1
  150. package/dist/runtime/system-events.d.ts +5 -0
  151. package/dist/runtime/system-events.d.ts.map +1 -1
  152. package/dist/runtime/system-events.js +7 -1
  153. package/dist/runtime/system-events.js.map +1 -1
  154. package/dist/sandbox/denial.d.ts +11 -0
  155. package/dist/sandbox/denial.d.ts.map +1 -0
  156. package/dist/sandbox/denial.js +61 -0
  157. package/dist/sandbox/denial.js.map +1 -0
  158. package/dist/sandbox/file-walk-program.d.ts.map +1 -1
  159. package/dist/sandbox/file-walk-program.js +31 -13
  160. package/dist/sandbox/file-walk-program.js.map +1 -1
  161. package/dist/sandbox/file-walk.d.ts +2 -0
  162. package/dist/sandbox/file-walk.d.ts.map +1 -1
  163. package/dist/sandbox/file-walk.js +64 -13
  164. package/dist/sandbox/file-walk.js.map +1 -1
  165. package/dist/sandbox/ignore-matcher.d.ts +30 -0
  166. package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
  167. package/dist/sandbox/ignore-matcher.js +11 -0
  168. package/dist/sandbox/ignore-matcher.js.map +1 -0
  169. package/dist/sandbox/ignore-source.d.ts +26 -0
  170. package/dist/sandbox/ignore-source.d.ts.map +1 -0
  171. package/dist/sandbox/ignore-source.js +221 -0
  172. package/dist/sandbox/ignore-source.js.map +1 -0
  173. package/dist/sandbox/provider/local.d.ts.map +1 -1
  174. package/dist/sandbox/provider/local.js +11 -1
  175. package/dist/sandbox/provider/local.js.map +1 -1
  176. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  177. package/dist/scheduler/completion-inbox.js +1 -0
  178. package/dist/scheduler/completion-inbox.js.map +1 -1
  179. package/dist/session/log-hash.d.ts.map +1 -1
  180. package/dist/session/log-hash.js +18 -1
  181. package/dist/session/log-hash.js.map +1 -1
  182. package/dist/store/session-index/sqlite.d.ts.map +1 -1
  183. package/dist/store/session-index/sqlite.js +169 -16
  184. package/dist/store/session-index/sqlite.js.map +1 -1
  185. package/dist/tools/builtins/bash.d.ts.map +1 -1
  186. package/dist/tools/builtins/bash.js +10 -4
  187. package/dist/tools/builtins/bash.js.map +1 -1
  188. package/dist/tools/builtins/computer-use.d.ts +35 -0
  189. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  190. package/dist/tools/builtins/computer-use.js +512 -67
  191. package/dist/tools/builtins/computer-use.js.map +1 -1
  192. package/dist/tools/builtins/glob.d.ts +1 -0
  193. package/dist/tools/builtins/glob.d.ts.map +1 -1
  194. package/dist/tools/builtins/glob.js +65 -28
  195. package/dist/tools/builtins/glob.js.map +1 -1
  196. package/dist/tools/builtins/grep.d.ts +2 -0
  197. package/dist/tools/builtins/grep.d.ts.map +1 -1
  198. package/dist/tools/builtins/grep.js +94 -40
  199. package/dist/tools/builtins/grep.js.map +1 -1
  200. package/dist/tools/builtins/job.d.ts +1 -1
  201. package/dist/tools/builtins/job.d.ts.map +1 -1
  202. package/dist/tools/builtins/job.js +34 -7
  203. package/dist/tools/builtins/job.js.map +1 -1
  204. package/dist/tools/builtins/search-support.d.ts +44 -0
  205. package/dist/tools/builtins/search-support.d.ts.map +1 -0
  206. package/dist/tools/builtins/search-support.js +89 -0
  207. package/dist/tools/builtins/search-support.js.map +1 -0
  208. package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
  209. package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
  210. package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
  211. package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
  212. package/dist/tools/builtins/wait-for-job.d.ts +1 -1
  213. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
  214. package/dist/tools/builtins/wait-for-job.js +13 -2
  215. package/dist/tools/builtins/wait-for-job.js.map +1 -1
  216. package/dist/tools/command-shell.d.ts.map +1 -1
  217. package/dist/tools/command-shell.js +14 -5
  218. package/dist/tools/command-shell.js.map +1 -1
  219. package/dist/types/computer-use/index.d.ts +129 -13
  220. package/dist/types/computer-use/index.d.ts.map +1 -1
  221. package/dist/types/computer-use/index.js.map +1 -1
  222. package/dist/types/errors/catalog.d.ts.map +1 -1
  223. package/dist/types/errors/catalog.js +6 -0
  224. package/dist/types/errors/catalog.js.map +1 -1
  225. package/dist/types/plugin/index.d.ts +24 -0
  226. package/dist/types/plugin/index.d.ts.map +1 -1
  227. package/dist/types/plugin/index.js +4 -0
  228. package/dist/types/plugin/index.js.map +1 -1
  229. package/dist/types/provider/errors.d.ts +7 -0
  230. package/dist/types/provider/errors.d.ts.map +1 -1
  231. package/dist/types/provider/errors.js +27 -3
  232. package/dist/types/provider/errors.js.map +1 -1
  233. package/dist/types/sandbox/index.d.ts +38 -1
  234. package/dist/types/sandbox/index.d.ts.map +1 -1
  235. package/dist/types/sandbox/index.js.map +1 -1
  236. package/dist/types/session/records.d.ts +479 -1
  237. package/dist/types/session/records.d.ts.map +1 -1
  238. package/dist/types/session/records.js +45 -1
  239. package/dist/types/session/records.js.map +1 -1
  240. package/dist/types/tool/index.d.ts +30 -0
  241. package/dist/types/tool/index.d.ts.map +1 -1
  242. package/dist/types/tool/index.js.map +1 -1
  243. package/dist/types/tool/presentation.d.ts +8 -0
  244. package/dist/types/tool/presentation.d.ts.map +1 -1
  245. package/package.json +1 -1
  246. package/src/account/binding.ts +43 -0
  247. package/src/account/index.ts +32 -0
  248. package/src/account/lock.ts +184 -0
  249. package/src/account/registry.ts +413 -0
  250. package/src/account/types.ts +137 -0
  251. package/src/compaction/context-window.ts +163 -2
  252. package/src/execution/workflow/index.ts +14 -1
  253. package/src/execution/workflow/journal.ts +161 -11
  254. package/src/execution/workflow/prelude.ts +51 -8
  255. package/src/execution/workflow/resume.ts +313 -0
  256. package/src/execution/workflow/runtime.ts +127 -9
  257. package/src/execution/workflow/types.ts +109 -1
  258. package/src/index.ts +17 -0
  259. package/src/peers/record.ts +18 -0
  260. package/src/peers/registry.ts +2 -2
  261. package/src/plugin/compat.ts +173 -0
  262. package/src/plugin/loader.ts +7 -1
  263. package/src/pricing/index.ts +2 -1
  264. package/src/process/kill-tree.ts +106 -4
  265. package/src/prompt/coding-agent-doctrine.ts +9 -1
  266. package/src/provider/errors.ts +27 -1
  267. package/src/provider/model-window-suffix.ts +84 -0
  268. package/src/provider/token-budget.ts +9 -3
  269. package/src/public-runtime.ts +38 -2
  270. package/src/public-types.ts +16 -0
  271. package/src/registry/command/kernel-commands.ts +1 -32
  272. package/src/runtime/jobs/awaited-jobs.ts +11 -0
  273. package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
  274. package/src/runtime/jobs/liveness.ts +144 -0
  275. package/src/runtime/jobs/registry.ts +114 -2
  276. package/src/runtime/jobs/wake-budget.ts +55 -0
  277. package/src/runtime/query/detached-calls.ts +135 -0
  278. package/src/runtime/query/executor.ts +102 -12
  279. package/src/runtime/query/index.ts +50 -7
  280. package/src/runtime/query/iteration/outstanding-work.ts +12 -3
  281. package/src/runtime/query/iteration/phases/context.ts +7 -0
  282. package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
  283. package/src/runtime/query/session-storage.ts +8 -3
  284. package/src/runtime/query/steering.ts +77 -6
  285. package/src/runtime/query/tooling.ts +2 -0
  286. package/src/runtime/system-events.ts +12 -1
  287. package/src/sandbox/denial.ts +79 -0
  288. package/src/sandbox/file-walk-program.ts +32 -13
  289. package/src/sandbox/file-walk.ts +75 -12
  290. package/src/sandbox/ignore-matcher.ts +46 -0
  291. package/src/sandbox/ignore-source.ts +220 -0
  292. package/src/sandbox/provider/local.ts +11 -1
  293. package/src/scheduler/completion-inbox.ts +1 -0
  294. package/src/session/log-hash.ts +21 -0
  295. package/src/store/session-index/sqlite.ts +171 -16
  296. package/src/tools/builtins/bash.ts +11 -4
  297. package/src/tools/builtins/computer-use.ts +593 -68
  298. package/src/tools/builtins/glob.ts +79 -32
  299. package/src/tools/builtins/grep.ts +107 -43
  300. package/src/tools/builtins/job.ts +42 -12
  301. package/src/tools/builtins/search-support.ts +107 -0
  302. package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
  303. package/src/tools/builtins/wait-for-job.ts +14 -2
  304. package/src/tools/command-shell.ts +13 -5
  305. package/src/types/computer-use/index.ts +157 -15
  306. package/src/types/errors/catalog.ts +6 -0
  307. package/src/types/plugin/index.ts +8 -0
  308. package/src/types/provider/errors.ts +40 -3
  309. package/src/types/sandbox/index.ts +40 -1
  310. package/src/types/session/records.ts +52 -2
  311. package/src/types/tool/index.ts +30 -0
  312. 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({ type: z.literal('mouse_move'), to: pointSchema });
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({ type: z.literal('wait'), ms: z.number().int().nonnegative() });
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({ type: z.literal('screenshot'), window_id: z.string().min(1).optional() }),
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(['screenshot', 'zoom', 'list_windows', 'ui_snapshot']);
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
- 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.');
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 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.`);
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. 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.`);
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: { type: 'integer', description: 'Horizontal pixel coordinate, from the left edge.' },
399
- y: { type: 'integer', description: 'Vertical pixel coordinate, from the top edge.' },
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: { type: 'string', enum: buttonEnum, description: 'Mouse button; left when omitted.' },
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: { type: 'integer', description: 'Positive integer scroll distance.' },
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 from the latest ui_snapshot, such as e12.',
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 = { type: 'string', description: 'The text set_value puts in the control.' };
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: { type: 'integer', description: 'Left edge of the region, in screenshot pixels.' },
499
- y: { type: 'integer', description: 'Top edge of the region, in screenshot pixels.' },
500
- width: { type: 'integer', description: 'Width of the region, in screenshot pixels.' },
501
- height: { type: 'integer', description: 'Height of the region, in screenshot pixels.' },
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
- constructor(message, unknown) {
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
- // Refs count up across snapshots, so a ref from an earlier one is never
779
- // silently a different control of the latest: it is simply not here.
780
- let uiRefs = new Map();
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 'done';
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(() => runHost({ type: 'mouse_click', at, button: item.button }, frame));
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({ type: 'scroll', at, direction: item.direction, amount: item.amount }, frame));
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 control of the latest ui_snapshot (u${uiSnapshots}); refs are valid only until the next ui_snapshot, so use the refs it showed`);
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 new StepFailure(outcome.detail ?? `${item.action} did not take effect on ${describeUiRef(item.ref)}`);
1009
- return outcome.detail ? `done (${outcome.detail})` : 'done';
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(input.window_id);
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
- const ref = actionable ? `e${uiRefCount + 1}` : undefined;
1042
- const line = `${' '.repeat(depth)}${uiElementLine(element, ref, frame)}`;
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
- uiRefCount += 1;
1052
- refs.set(ref, { hostRef: element.ref, element });
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
- uiRefs = refs;
1061
- uiSnapshotWindowId = snapshot.windowId ?? input.window_id;
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
- `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.`,
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 footer = cut || snapshot.truncated
1082
- ? [
1083
- cut
1084
- ? `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.`
1085
- : 'The host stopped reading the tree before it ended; controls further down are missing. Use a screenshot for the rest.',
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: header[0] ?? '',
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 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.',
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
- { type: 'text', text: [describeFrame(shot.frame), ...firstLookHint(shot.frame)].join('\n') },
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({ label: step.label, status: 'failed', error: failure.error });
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 note = await step.run(signal);
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', note });
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
- ? { label: record.label, status: record.status, error: record.error }
1468
- : { label: record.label, status: record.status }),
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 { success: false, output: '', error: `computer_use failed: ${error.message}` };
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 });