@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.
- package/CHANGELOG.md +124 -0
- package/dist/account/binding.d.ts +20 -0
- package/dist/account/binding.d.ts.map +1 -0
- package/dist/account/binding.js +27 -0
- package/dist/account/binding.js.map +1 -0
- package/dist/account/index.d.ts +9 -0
- package/dist/account/index.d.ts.map +1 -0
- package/dist/account/index.js +5 -0
- package/dist/account/index.js.map +1 -0
- package/dist/account/lock.d.ts +33 -0
- package/dist/account/lock.d.ts.map +1 -0
- package/dist/account/lock.js +170 -0
- package/dist/account/lock.js.map +1 -0
- package/dist/account/registry.d.ts +21 -0
- package/dist/account/registry.d.ts.map +1 -0
- package/dist/account/registry.js +340 -0
- package/dist/account/registry.js.map +1 -0
- package/dist/account/types.d.ts +124 -0
- package/dist/account/types.d.ts.map +1 -0
- package/dist/account/types.js +35 -0
- package/dist/account/types.js.map +1 -0
- package/dist/compaction/context-window.d.ts +53 -0
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +141 -2
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/execution/workflow/index.d.ts +3 -1
- package/dist/execution/workflow/index.d.ts.map +1 -1
- package/dist/execution/workflow/index.js +2 -1
- package/dist/execution/workflow/index.js.map +1 -1
- package/dist/execution/workflow/journal.d.ts +24 -4
- package/dist/execution/workflow/journal.d.ts.map +1 -1
- package/dist/execution/workflow/journal.js +131 -9
- package/dist/execution/workflow/journal.js.map +1 -1
- package/dist/execution/workflow/prelude.d.ts +2 -0
- package/dist/execution/workflow/prelude.d.ts.map +1 -1
- package/dist/execution/workflow/prelude.js +49 -8
- package/dist/execution/workflow/prelude.js.map +1 -1
- package/dist/execution/workflow/resume.d.ts +59 -0
- package/dist/execution/workflow/resume.d.ts.map +1 -0
- package/dist/execution/workflow/resume.js +273 -0
- package/dist/execution/workflow/resume.js.map +1 -0
- package/dist/execution/workflow/runtime.d.ts.map +1 -1
- package/dist/execution/workflow/runtime.js +112 -8
- package/dist/execution/workflow/runtime.js.map +1 -1
- package/dist/execution/workflow/types.d.ts +105 -1
- package/dist/execution/workflow/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/pals/communication/ingress-schema.d.ts +38 -38
- package/dist/peers/record.d.ts +98 -0
- package/dist/peers/record.d.ts.map +1 -1
- package/dist/peers/record.js +17 -0
- package/dist/peers/record.js.map +1 -1
- package/dist/peers/registry.d.ts.map +1 -1
- package/dist/peers/registry.js +2 -2
- package/dist/peers/registry.js.map +1 -1
- package/dist/plugin/compat.d.ts +24 -0
- package/dist/plugin/compat.d.ts.map +1 -0
- package/dist/plugin/compat.js +149 -0
- package/dist/plugin/compat.js.map +1 -0
- package/dist/plugin/loader.d.ts +2 -1
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +6 -0
- package/dist/plugin/loader.js.map +1 -1
- package/dist/pricing/index.d.ts.map +1 -1
- package/dist/pricing/index.js +2 -1
- package/dist/pricing/index.js.map +1 -1
- package/dist/process/kill-tree.d.ts +44 -0
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +102 -4
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +9 -1
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +25 -2
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/model-window-suffix.d.ts +18 -0
- package/dist/provider/model-window-suffix.d.ts.map +1 -0
- package/dist/provider/model-window-suffix.js +77 -0
- package/dist/provider/model-window-suffix.js.map +1 -0
- package/dist/provider/token-budget.d.ts.map +1 -1
- package/dist/provider/token-budget.js +9 -1
- package/dist/provider/token-budget.js.map +1 -1
- package/dist/public-runtime.d.ts +6 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +6 -4
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +3 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.d.ts +1 -4
- package/dist/registry/command/kernel-commands.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.js +0 -28
- package/dist/registry/command/kernel-commands.js.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
- package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.js +11 -0
- package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
- package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
- package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
- package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
- package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
- package/dist/runtime/jobs/liveness.d.ts +67 -0
- package/dist/runtime/jobs/liveness.d.ts.map +1 -0
- package/dist/runtime/jobs/liveness.js +94 -0
- package/dist/runtime/jobs/liveness.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +21 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +104 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/jobs/wake-budget.d.ts +35 -0
- package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
- package/dist/runtime/jobs/wake-budget.js +48 -0
- package/dist/runtime/jobs/wake-budget.js.map +1 -0
- package/dist/runtime/query/detached-calls.d.ts +65 -0
- package/dist/runtime/query/detached-calls.d.ts.map +1 -0
- package/dist/runtime/query/detached-calls.js +116 -0
- package/dist/runtime/query/detached-calls.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +11 -3
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +100 -8
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +20 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +31 -5
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.js +11 -3
- package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +7 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/session-storage.d.ts.map +1 -1
- package/dist/runtime/query/session-storage.js +8 -3
- package/dist/runtime/query/session-storage.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +29 -3
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +58 -3
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/runtime/system-events.d.ts +5 -0
- package/dist/runtime/system-events.d.ts.map +1 -1
- package/dist/runtime/system-events.js +7 -1
- package/dist/runtime/system-events.js.map +1 -1
- package/dist/sandbox/denial.d.ts +11 -0
- package/dist/sandbox/denial.d.ts.map +1 -0
- package/dist/sandbox/denial.js +61 -0
- package/dist/sandbox/denial.js.map +1 -0
- package/dist/sandbox/file-walk-program.d.ts.map +1 -1
- package/dist/sandbox/file-walk-program.js +31 -13
- package/dist/sandbox/file-walk-program.js.map +1 -1
- package/dist/sandbox/file-walk.d.ts +2 -0
- package/dist/sandbox/file-walk.d.ts.map +1 -1
- package/dist/sandbox/file-walk.js +64 -13
- package/dist/sandbox/file-walk.js.map +1 -1
- package/dist/sandbox/ignore-matcher.d.ts +30 -0
- package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
- package/dist/sandbox/ignore-matcher.js +11 -0
- package/dist/sandbox/ignore-matcher.js.map +1 -0
- package/dist/sandbox/ignore-source.d.ts +26 -0
- package/dist/sandbox/ignore-source.d.ts.map +1 -0
- package/dist/sandbox/ignore-source.js +221 -0
- package/dist/sandbox/ignore-source.js.map +1 -0
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +11 -1
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +1 -0
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/session/log-hash.d.ts.map +1 -1
- package/dist/session/log-hash.js +18 -1
- package/dist/session/log-hash.js.map +1 -1
- package/dist/store/session-index/sqlite.d.ts.map +1 -1
- package/dist/store/session-index/sqlite.js +169 -16
- package/dist/store/session-index/sqlite.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +10 -4
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +35 -0
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +512 -67
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/glob.d.ts +1 -0
- package/dist/tools/builtins/glob.d.ts.map +1 -1
- package/dist/tools/builtins/glob.js +65 -28
- package/dist/tools/builtins/glob.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +2 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +94 -40
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/job.d.ts +1 -1
- package/dist/tools/builtins/job.d.ts.map +1 -1
- package/dist/tools/builtins/job.js +34 -7
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/search-support.d.ts +44 -0
- package/dist/tools/builtins/search-support.d.ts.map +1 -0
- package/dist/tools/builtins/search-support.js +89 -0
- package/dist/tools/builtins/search-support.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job.js +13 -2
- package/dist/tools/builtins/wait-for-job.js.map +1 -1
- package/dist/tools/command-shell.d.ts.map +1 -1
- package/dist/tools/command-shell.js +14 -5
- package/dist/tools/command-shell.js.map +1 -1
- package/dist/types/computer-use/index.d.ts +129 -13
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.js.map +1 -1
- package/dist/types/errors/catalog.d.ts.map +1 -1
- package/dist/types/errors/catalog.js +6 -0
- package/dist/types/errors/catalog.js.map +1 -1
- package/dist/types/plugin/index.d.ts +24 -0
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +4 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/errors.d.ts +7 -0
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +27 -3
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +38 -1
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/session/records.d.ts +479 -1
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +45 -1
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/index.d.ts +30 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +8 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/account/binding.ts +43 -0
- package/src/account/index.ts +32 -0
- package/src/account/lock.ts +184 -0
- package/src/account/registry.ts +413 -0
- package/src/account/types.ts +137 -0
- package/src/compaction/context-window.ts +163 -2
- package/src/execution/workflow/index.ts +14 -1
- package/src/execution/workflow/journal.ts +161 -11
- package/src/execution/workflow/prelude.ts +51 -8
- package/src/execution/workflow/resume.ts +313 -0
- package/src/execution/workflow/runtime.ts +127 -9
- package/src/execution/workflow/types.ts +109 -1
- package/src/index.ts +17 -0
- package/src/peers/record.ts +18 -0
- package/src/peers/registry.ts +2 -2
- package/src/plugin/compat.ts +173 -0
- package/src/plugin/loader.ts +7 -1
- package/src/pricing/index.ts +2 -1
- package/src/process/kill-tree.ts +106 -4
- package/src/prompt/coding-agent-doctrine.ts +9 -1
- package/src/provider/errors.ts +27 -1
- package/src/provider/model-window-suffix.ts +84 -0
- package/src/provider/token-budget.ts +9 -3
- package/src/public-runtime.ts +38 -2
- package/src/public-types.ts +16 -0
- package/src/registry/command/kernel-commands.ts +1 -32
- package/src/runtime/jobs/awaited-jobs.ts +11 -0
- package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
- package/src/runtime/jobs/liveness.ts +144 -0
- package/src/runtime/jobs/registry.ts +114 -2
- package/src/runtime/jobs/wake-budget.ts +55 -0
- package/src/runtime/query/detached-calls.ts +135 -0
- package/src/runtime/query/executor.ts +102 -12
- package/src/runtime/query/index.ts +50 -7
- package/src/runtime/query/iteration/outstanding-work.ts +12 -3
- package/src/runtime/query/iteration/phases/context.ts +7 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
- package/src/runtime/query/session-storage.ts +8 -3
- package/src/runtime/query/steering.ts +77 -6
- package/src/runtime/query/tooling.ts +2 -0
- package/src/runtime/system-events.ts +12 -1
- package/src/sandbox/denial.ts +79 -0
- package/src/sandbox/file-walk-program.ts +32 -13
- package/src/sandbox/file-walk.ts +75 -12
- package/src/sandbox/ignore-matcher.ts +46 -0
- package/src/sandbox/ignore-source.ts +220 -0
- package/src/sandbox/provider/local.ts +11 -1
- package/src/scheduler/completion-inbox.ts +1 -0
- package/src/session/log-hash.ts +21 -0
- package/src/store/session-index/sqlite.ts +171 -16
- package/src/tools/builtins/bash.ts +11 -4
- package/src/tools/builtins/computer-use.ts +593 -68
- package/src/tools/builtins/glob.ts +79 -32
- package/src/tools/builtins/grep.ts +107 -43
- package/src/tools/builtins/job.ts +42 -12
- package/src/tools/builtins/search-support.ts +107 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
- package/src/tools/builtins/wait-for-job.ts +14 -2
- package/src/tools/command-shell.ts +13 -5
- package/src/types/computer-use/index.ts +157 -15
- package/src/types/errors/catalog.ts +6 -0
- package/src/types/plugin/index.ts +8 -0
- package/src/types/provider/errors.ts +40 -3
- package/src/types/sandbox/index.ts +40 -1
- package/src/types/session/records.ts +52 -2
- package/src/types/tool/index.ts +30 -0
- 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({
|
|
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({
|
|
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({
|
|
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>([
|
|
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 =
|
|
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
|
|
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.
|
|
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: {
|
|
565
|
-
|
|
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:
|
|
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: {
|
|
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: {
|
|
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
|
|
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 = {
|
|
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: {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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: {
|
|
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'
|
|
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
|
-
//
|
|
1064
|
-
//
|
|
1065
|
-
|
|
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
|
|
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(() =>
|
|
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(
|
|
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
|
|
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
|
|
1369
|
-
outcome
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1408
|
-
const
|
|
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
|
-
|
|
1417
|
-
|
|
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
|
-
|
|
1425
|
-
|
|
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
|
-
|
|
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
|
|
1447
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
{
|
|
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({
|
|
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
|
|
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',
|
|
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
|
-
? {
|
|
1881
|
-
|
|
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 {
|
|
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
|
})
|