@isomorph.ai/app-sdk 1.39.0 → 1.41.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/README.md CHANGED
@@ -397,7 +397,7 @@ const computerUse = createComputerUseTool({ runKey: ctx.runId ?? `my-job/${ctx.s
397
397
  ```
398
398
 
399
399
  - The tool calls the task API at `ISOMORPH_COMPUTER_USE_URL` (region `ISOMORPH_COMPUTER_USE_REGION`, default `us-east-1`); Provision sets both when the cell has the API. Each request is signed with SigV4 (`execute-api`) as the runtime's own role: the Lambda environment's credentials, or on a cluster pod its ServiceAccount role through STS `AssumeRoleWithWebIdentity`.
400
- - It starts the task with `POST /tasks`, then checks `POST /tasks/check` every 5 s until the task is `done` or `failed`.
400
+ - It starts the task with `POST /tasks`, then checks `POST /tasks/check` every 5 s until the task is `done`, `failed` or `waiting_for_input`. Waiting returns immediately; end the app job rather than holding a worker while a person signs in.
401
401
  - It calls `progress(step count, note)` on every new step and at least once a minute, so the job's lease does not run out.
402
402
  - When `cancelled()` turns true it sends `POST /tasks/cancel` once and keeps checking until the task has `failed`.
403
403
  - While the task is `queued` and the CUA reports `waiting` (no computer is free), the progress note says so with the minutes waited. After `maxQueueMs` (20 minutes unless set) the tool cancels the task and returns `failed` with `No computer was free within N minutes; the task was cancelled before it started.`
@@ -417,6 +417,65 @@ A task with `session` or `secrets` works the page through clicks and typing only
417
417
 
418
418
  (1.25.0, 1.36.0, 1.37.0, 1.39.0; data plane ADR 0078, ADR 0090, ADR 0091, ADR 0092)
419
419
 
420
+ ## Help on the task's existing computer
421
+
422
+ `dist/computer-use-handoff.js` adds short server calls for a paused task. It does not start another computer and does not import `ai`. It requires `runKey` and `authorize`, a server callback checked before **every** call. The callback must check the current authenticated person's access to the app's task row. An app-role signature, client-supplied task ID or pause ID alone does not prove that person owns the task.
423
+
424
+ ```text
425
+ Job's browser tool → waiting_for_input → job ends
426
+ │
427
+ App action → authorized screen/input or private login
428
+ │
429
+ User/app explicitly requests resume → new job attaches to original runKey
430
+ ```
431
+
432
+ The app keeps the original `runKey`, task ownership and returned pause metadata in its existing task row. Neither model nor browser chooses the backend `runKey`. For example, in an action backed by an app-owned `computer_tasks` table with `id`, `owner_id` and `run_key` columns:
433
+
434
+ ```ts
435
+ import { createClient } from '@isomorph.ai/app-sdk';
436
+ import { createComputerUseHandoff } from '@isomorph.ai/app-sdk/dist/computer-use-handoff.js';
437
+
438
+ export default async function viewComputer(input: { taskId: string; pauseId: string }, ctx: { user: { id: string } }) {
439
+ const app = createClient();
440
+ const owned = async () => (await app.data.from('computer_tasks').select('run_key')
441
+ .eq('id', input.taskId).eq('owner_id', ctx.user.id).single()).data;
442
+ const task = await owned();
443
+ if (!task) throw new Error('Unknown task.');
444
+ const computer = createComputerUseHandoff({ runKey: task.run_key, authorize: async () => Boolean(await owned()) });
445
+ return computer.view(input.pauseId);
446
+ }
447
+ ```
448
+
449
+ Keep that table protected by the app's user row rules. A trusted action must still check `owner_id` against `ctx.user.id`; never use workload identity as proof of human ownership.
450
+
451
+ - `requestTakeover(reason, requiredInput?)` requests a pause; `requiredInput` is `human` (default) or `login`. Poll `check()` until `waiting_for_input` gives `taskId`, `pauseId`, `reason`, `requiredInput` and optional login result.
452
+ - `view(pauseId)` returns `{status:'pausing'}` or `{status:'ready', screenshot, url}`. Reuse the existing sign-in screenshot UI: display the base64 PNG as an image, map clicks to image pixels, and forward keyboard/scroll events through app actions. Viewing does not itself resume the agent.
453
+ - `input(pauseId, action)` sends the same `SignInAction` click/type/key/scroll commands used by `sendSignInInput`. The CUA accepts human input only while the task is paused; it blocks the helper and main agent from acting at the same time.
454
+ - `completeLogin(pauseId, {allowedHosts, username, password})`, or `completeLogin(pauseId, {otp})`, queues the separate browser-action-only login agent on this computer. **Call only from private server code, never expose this method or credentials as model tool inputs, prompts, progress, logs or job input/results.** Poll `check()` for `login.status: success | needs_input | human_help_needed`, with `input:'otp'` when another code is needed. Helper success leaves the main agent paused.
455
+ - `resume(pauseId)` explicitly resumes; it does not wait for completion. On Continue, call this from the authorized app action, then enqueue the app's continuation job. That job creates `createComputerUseTool({...jobOptions, runKey: originalRunKey, attach:true})`: it polls the same task without sending `/tasks` again. Keep the original saved-session and download options if needed. Stale/duplicate resumes fail with 409; do not silently retry with a new pause ID. If enqueue fails, retry attaching to the already resumed task, not another resume.
456
+ - `cancel()` stops the task, including while waiting.
457
+
458
+ The outer agent may use app-defined tools for takeover and opening the authorized view. Resume requires an explicit user or app decision. Keep any outer-agent conversation in the app's existing job/task storage when ending a waiting job; this SDK does not persist the outer model's messages. The CUA persists its own conversation/resume state under the same task folder as screenshots and progress in its existing result bucket, without passwords or OTPs.
459
+
460
+ A website that texts or mails a one-time code: `signInWithOtp` from `dist/computer-use-otp.js` does the password login, waits for the helper to ask for the code, reads it from the company's own phone number or mailbox through the app's integration grant, answers it once and returns when the helper succeeded or needs a person. The app process does the reading; the computer-use service never calls governance.
461
+
462
+ ```ts
463
+ import { signInWithOtp } from '@isomorph.ai/app-sdk/dist/computer-use-otp.js';
464
+ const answer = await signInWithOtp(computer, input.pauseId, { allowedHosts: ['netbanking.hdfcbank.com'], username, password }, {
465
+ execute: app.integrations.execute,
466
+ sms: { connection: 'company-phone', from: 'HDFCBK' }, // phone.sms.list on the company line
467
+ // email: { connection: 'company-gmail', provider: 'gmail', from: 'alerts@hdfcbank.net' }, // or the connected mailbox (gmail | outlook)
468
+ });
469
+ // { status: 'success' } | { status: 'human_help_needed', reason } | { status: 'ended', taskStatus }
470
+ ```
471
+
472
+ - Only messages that arrived after the login started count; the code is the 4 to 8 digits next to "OTP", "code" or "password" (`extract` overrides it). It waits `timeoutMs` (3 min) for the message, checking every `pollIntervalMs` (5 s).
473
+ - One code per pause: a website that asks for a second one gets a person (`human_help_needed`), as does a helper that gives up. Success leaves the task paused; resume it as above.
474
+ - `readOtp(source, since)` reads a code on its own, for a page that asks for one outside a login.
475
+ - The connection needs the app's grant for `phone.sms.list`, or `gmail.thread.list` + `gmail.message.read` (Outlook: `outlook.message.list` + `outlook.message.read`), and the portal account must be registered to that number or mailbox.
476
+
477
+ `getSavedLogin(accountRef, website)` still returns `{status:'unavailable'}`; governance saved-login storage is not part of the SDK. These task helpers require the matching CUA handoff release; existing new-computer sign-in helpers below remain separate.
478
+
420
479
  ## Browser sign-in for computer use
421
480
 
422
481
  A person signs in to a website once on a short-lived computer the app shows as screenshots; the app keeps the browser session as a runtime secret and passes its name to later computer use tasks as `session`. Each call is short, so these run from actions as well as jobs.
@@ -654,3 +713,61 @@ keep the workflow run ID and tool-call ID stable across retries. Direct writes
654
713
  must supply their own persisted UUID `idempotencyKey`. A tool marked read-only
655
714
  does not require a key. An incomplete replay requires reconciliation, not a new
656
715
  write. Account consent and app permissions are enforced by the platform.
716
+
717
+ ### Tools for the outer app agent
718
+
719
+ `createComputerUseHandoffTools` from `dist/computer-use-handoff-tool.js` creates
720
+ `request_computer_input` and `resume_computer_task`. Register these beside the
721
+ existing computer-use tool. Bind them to the original task's `runKey` in server
722
+ code; the model supplies only the current `pauseId`.
723
+
724
+ ```ts
725
+ import { generateText, hasToolCall, stepCountIs } from 'ai';
726
+ import { createComputerUseHandoffTools } from '@isomorph.ai/app-sdk/dist/computer-use-handoff-tool.js';
727
+
728
+ const handoffTools = createComputerUseHandoffTools({
729
+ runKey: task.run_key,
730
+ progress: ctx.progress,
731
+ cancelled: ctx.cancelled,
732
+ authorize: () => userOwnsTask(ctx.user.id, task.id),
733
+ onInputRequired: (pause, page) => showComputerInApp(task.id, pause, page),
734
+ canResume: pauseId => userPressedContinue(ctx.user.id, task.id, pauseId),
735
+ });
736
+ const result = await generateText({
737
+ model,
738
+ messages: savedMessages,
739
+ tools: { computer_use: computerUse, ...handoffTools },
740
+ stopWhen: [stepCountIs(10), hasToolCall('request_computer_input')],
741
+ });
742
+ await saveOuterMessages(task.id, [...savedMessages, ...result.response.messages]);
743
+ ```
744
+
745
+ The ownership, screen, Continue and conversation functions in this example are
746
+ app-provided functions, not SDK exports. The app must save the completed tool
747
+ result before returning its waiting response to the UI. The stop condition ends
748
+ the outer loop after the input request; creating tools alone does not stop a loop.
749
+
750
+ The request tool checks the current pause and calls `view()`. Its callback receives
751
+ the screenshot privately, so the app can show its existing computer view. The
752
+ model receives `human_input_required`, task/pause references, reason and required
753
+ input, without screenshot bytes or a computer endpoint. Screen refreshes and human
754
+ input still use authorized backend `view()` and `input()` calls.
755
+
756
+ When the human presses Continue, the app records approval for that user, task and
757
+ pause and queues its outer-agent continuation with the saved conversation and
758
+ original run key. Only this trusted user action may make `canResume` return true;
759
+ never derive approval from model arguments or a chat string. Retain that approval
760
+ for retries of the same continuation. A new pause needs new approval. The resume
761
+ tool checks approval, calls resume and follows the existing task with `attach:true`.
762
+ If an earlier attempt already resumed the task, it attaches without resuming twice.
763
+ It may return another waiting result; use the input tool again for that new pause.
764
+ Credentials and OTPs remain private backend `completeLogin()` inputs, not model tools.
765
+
766
+ `onMessage` on the computer-use tool receives allowlisted CUA progress/handoff
767
+ messages when they change; the progress callback also receives their text. Waiting
768
+ results and handoff `check()` preserve the latest valid `message`. This is latest
769
+ message delivery, not a full message history.
770
+
771
+ This SDK does not supply an app-specific screen, conversation database or job
772
+ scheduler. It supplies the tools and waiting contract; the example wires them to
773
+ an app's existing actions, task rows, screen and job runner.
@@ -0,0 +1,590 @@
1
+ import { type ComputerUseHandoffOptions, type ComputerUsePage, type ComputerUseWaiting } from './computer-use-handoff.js';
2
+ import { type ComputerUseOptions } from './computer-use.js';
3
+ export interface ComputerUseHandoffToolOptions extends ComputerUseHandoffOptions, ComputerUseOptions {
4
+ onInputRequired(input: ComputerUseWaiting, page: ComputerUsePage): Promise<void> | void;
5
+ canResume(pauseId: string): Promise<boolean> | boolean;
6
+ }
7
+ export declare function createComputerUseHandoffTools(options: ComputerUseHandoffToolOptions): {
8
+ request_computer_input: ({
9
+ deferLoading?: boolean;
10
+ title?: string;
11
+ providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
12
+ metadata?: import("@ai-sdk/provider").JSONObject;
13
+ inputSchema: import("ai").FlexibleSchema<{
14
+ pauseId: string;
15
+ }>;
16
+ contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
17
+ needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
18
+ pauseId: string;
19
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
20
+ onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
21
+ onInputDelta?: ((options: {
22
+ inputTextDelta: string;
23
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
24
+ onInputAvailable?: ((options: {
25
+ input: {
26
+ pauseId: string;
27
+ };
28
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
29
+ toModelOutput?: ((options: {
30
+ toolCallId: string;
31
+ input: {
32
+ pauseId: string;
33
+ };
34
+ output: NoInfer<{
35
+ status: "human_input_required";
36
+ taskId: string;
37
+ pauseId: string;
38
+ reason: string;
39
+ requiredInput: "login" | "otp" | "human";
40
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
41
+ login?: {
42
+ status: "success" | "needs_input" | "human_help_needed";
43
+ input?: "otp";
44
+ };
45
+ }>;
46
+ }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
47
+ } & {
48
+ outputSchema?: import("ai").FlexibleSchema<{
49
+ status: "human_input_required";
50
+ taskId: string;
51
+ pauseId: string;
52
+ reason: string;
53
+ requiredInput: "login" | "otp" | "human";
54
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
55
+ login?: {
56
+ status: "success" | "needs_input" | "human_help_needed";
57
+ input?: "otp";
58
+ };
59
+ }> | undefined;
60
+ execute: import("ai").ToolExecuteFunction<{
61
+ pauseId: string;
62
+ }, {
63
+ status: "human_input_required";
64
+ taskId: string;
65
+ pauseId: string;
66
+ reason: string;
67
+ requiredInput: "login" | "otp" | "human";
68
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
69
+ login?: {
70
+ status: "success" | "needs_input" | "human_help_needed";
71
+ input?: "otp";
72
+ };
73
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
74
+ } & {
75
+ description?: string | ((options: {
76
+ context: NoInfer<import("@ai-sdk/provider-utils").Context>;
77
+ experimental_sandbox?: import("ai").Experimental_SandboxSession;
78
+ }) => string) | undefined;
79
+ strict?: boolean;
80
+ inputExamples?: {
81
+ input: NoInfer<{
82
+ pauseId: string;
83
+ }>;
84
+ }[] | undefined;
85
+ id?: never;
86
+ isProviderExecuted?: never;
87
+ args?: never;
88
+ supportsDeferredResults?: never;
89
+ } & {
90
+ type?: undefined | "function";
91
+ } & {
92
+ execute: import("ai").ToolExecuteFunction<{
93
+ pauseId: string;
94
+ }, {
95
+ status: "human_input_required";
96
+ taskId: string;
97
+ pauseId: string;
98
+ reason: string;
99
+ requiredInput: "login" | "otp" | "human";
100
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
101
+ login?: {
102
+ status: "success" | "needs_input" | "human_help_needed";
103
+ input?: "otp";
104
+ };
105
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
106
+ }) | ({
107
+ deferLoading?: boolean;
108
+ title?: string;
109
+ providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
110
+ metadata?: import("@ai-sdk/provider").JSONObject;
111
+ inputSchema: import("ai").FlexibleSchema<{
112
+ pauseId: string;
113
+ }>;
114
+ contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
115
+ needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
116
+ pauseId: string;
117
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
118
+ onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
119
+ onInputDelta?: ((options: {
120
+ inputTextDelta: string;
121
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
122
+ onInputAvailable?: ((options: {
123
+ input: {
124
+ pauseId: string;
125
+ };
126
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
127
+ toModelOutput?: ((options: {
128
+ toolCallId: string;
129
+ input: {
130
+ pauseId: string;
131
+ };
132
+ output: NoInfer<{
133
+ status: "human_input_required";
134
+ taskId: string;
135
+ pauseId: string;
136
+ reason: string;
137
+ requiredInput: "login" | "otp" | "human";
138
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
139
+ login?: {
140
+ status: "success" | "needs_input" | "human_help_needed";
141
+ input?: "otp";
142
+ };
143
+ }>;
144
+ }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
145
+ } & {
146
+ outputSchema?: import("ai").FlexibleSchema<{
147
+ status: "human_input_required";
148
+ taskId: string;
149
+ pauseId: string;
150
+ reason: string;
151
+ requiredInput: "login" | "otp" | "human";
152
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
153
+ login?: {
154
+ status: "success" | "needs_input" | "human_help_needed";
155
+ input?: "otp";
156
+ };
157
+ }> | undefined;
158
+ execute: import("ai").ToolExecuteFunction<{
159
+ pauseId: string;
160
+ }, {
161
+ status: "human_input_required";
162
+ taskId: string;
163
+ pauseId: string;
164
+ reason: string;
165
+ requiredInput: "login" | "otp" | "human";
166
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
167
+ login?: {
168
+ status: "success" | "needs_input" | "human_help_needed";
169
+ input?: "otp";
170
+ };
171
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
172
+ } & {
173
+ description?: string | ((options: {
174
+ context: NoInfer<import("@ai-sdk/provider-utils").Context>;
175
+ experimental_sandbox?: import("ai").Experimental_SandboxSession;
176
+ }) => string) | undefined;
177
+ strict?: boolean;
178
+ inputExamples?: {
179
+ input: NoInfer<{
180
+ pauseId: string;
181
+ }>;
182
+ }[] | undefined;
183
+ id?: never;
184
+ isProviderExecuted?: never;
185
+ args?: never;
186
+ supportsDeferredResults?: never;
187
+ } & {
188
+ type: "dynamic";
189
+ } & {
190
+ execute: import("ai").ToolExecuteFunction<{
191
+ pauseId: string;
192
+ }, {
193
+ status: "human_input_required";
194
+ taskId: string;
195
+ pauseId: string;
196
+ reason: string;
197
+ requiredInput: "login" | "otp" | "human";
198
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
199
+ login?: {
200
+ status: "success" | "needs_input" | "human_help_needed";
201
+ input?: "otp";
202
+ };
203
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
204
+ }) | ({
205
+ deferLoading?: boolean;
206
+ title?: string;
207
+ providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
208
+ metadata?: import("@ai-sdk/provider").JSONObject;
209
+ inputSchema: import("ai").FlexibleSchema<{
210
+ pauseId: string;
211
+ }>;
212
+ contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
213
+ needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
214
+ pauseId: string;
215
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
216
+ onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
217
+ onInputDelta?: ((options: {
218
+ inputTextDelta: string;
219
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
220
+ onInputAvailable?: ((options: {
221
+ input: {
222
+ pauseId: string;
223
+ };
224
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
225
+ toModelOutput?: ((options: {
226
+ toolCallId: string;
227
+ input: {
228
+ pauseId: string;
229
+ };
230
+ output: NoInfer<{
231
+ status: "human_input_required";
232
+ taskId: string;
233
+ pauseId: string;
234
+ reason: string;
235
+ requiredInput: "login" | "otp" | "human";
236
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
237
+ login?: {
238
+ status: "success" | "needs_input" | "human_help_needed";
239
+ input?: "otp";
240
+ };
241
+ }>;
242
+ }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
243
+ } & {
244
+ outputSchema?: import("ai").FlexibleSchema<{
245
+ status: "human_input_required";
246
+ taskId: string;
247
+ pauseId: string;
248
+ reason: string;
249
+ requiredInput: "login" | "otp" | "human";
250
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
251
+ login?: {
252
+ status: "success" | "needs_input" | "human_help_needed";
253
+ input?: "otp";
254
+ };
255
+ }> | undefined;
256
+ execute: import("ai").ToolExecuteFunction<{
257
+ pauseId: string;
258
+ }, {
259
+ status: "human_input_required";
260
+ taskId: string;
261
+ pauseId: string;
262
+ reason: string;
263
+ requiredInput: "login" | "otp" | "human";
264
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
265
+ login?: {
266
+ status: "success" | "needs_input" | "human_help_needed";
267
+ input?: "otp";
268
+ };
269
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
270
+ } & {
271
+ type: "provider";
272
+ id: `${string}.${string}`;
273
+ args: Record<string, unknown>;
274
+ description?: never;
275
+ strict?: never;
276
+ inputExamples?: never;
277
+ } & {
278
+ isProviderExecuted: false;
279
+ supportsDeferredResults?: never;
280
+ } & {
281
+ execute: import("ai").ToolExecuteFunction<{
282
+ pauseId: string;
283
+ }, {
284
+ status: "human_input_required";
285
+ taskId: string;
286
+ pauseId: string;
287
+ reason: string;
288
+ requiredInput: "login" | "otp" | "human";
289
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
290
+ login?: {
291
+ status: "success" | "needs_input" | "human_help_needed";
292
+ input?: "otp";
293
+ };
294
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
295
+ }) | ({
296
+ deferLoading?: boolean;
297
+ title?: string;
298
+ providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
299
+ metadata?: import("@ai-sdk/provider").JSONObject;
300
+ inputSchema: import("ai").FlexibleSchema<{
301
+ pauseId: string;
302
+ }>;
303
+ contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
304
+ needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
305
+ pauseId: string;
306
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
307
+ onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
308
+ onInputDelta?: ((options: {
309
+ inputTextDelta: string;
310
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
311
+ onInputAvailable?: ((options: {
312
+ input: {
313
+ pauseId: string;
314
+ };
315
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
316
+ toModelOutput?: ((options: {
317
+ toolCallId: string;
318
+ input: {
319
+ pauseId: string;
320
+ };
321
+ output: NoInfer<{
322
+ status: "human_input_required";
323
+ taskId: string;
324
+ pauseId: string;
325
+ reason: string;
326
+ requiredInput: "login" | "otp" | "human";
327
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
328
+ login?: {
329
+ status: "success" | "needs_input" | "human_help_needed";
330
+ input?: "otp";
331
+ };
332
+ }>;
333
+ }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
334
+ } & {
335
+ outputSchema?: import("ai").FlexibleSchema<{
336
+ status: "human_input_required";
337
+ taskId: string;
338
+ pauseId: string;
339
+ reason: string;
340
+ requiredInput: "login" | "otp" | "human";
341
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
342
+ login?: {
343
+ status: "success" | "needs_input" | "human_help_needed";
344
+ input?: "otp";
345
+ };
346
+ }> | undefined;
347
+ execute: import("ai").ToolExecuteFunction<{
348
+ pauseId: string;
349
+ }, {
350
+ status: "human_input_required";
351
+ taskId: string;
352
+ pauseId: string;
353
+ reason: string;
354
+ requiredInput: "login" | "otp" | "human";
355
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
356
+ login?: {
357
+ status: "success" | "needs_input" | "human_help_needed";
358
+ input?: "otp";
359
+ };
360
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
361
+ } & {
362
+ type: "provider";
363
+ id: `${string}.${string}`;
364
+ args: Record<string, unknown>;
365
+ description?: never;
366
+ strict?: never;
367
+ inputExamples?: never;
368
+ } & {
369
+ isProviderExecuted: true;
370
+ supportsDeferredResults?: boolean;
371
+ } & {
372
+ execute: import("ai").ToolExecuteFunction<{
373
+ pauseId: string;
374
+ }, {
375
+ status: "human_input_required";
376
+ taskId: string;
377
+ pauseId: string;
378
+ reason: string;
379
+ requiredInput: "login" | "otp" | "human";
380
+ message?: import("./computer-use-handoff.js").ComputerUseMessage;
381
+ login?: {
382
+ status: "success" | "needs_input" | "human_help_needed";
383
+ input?: "otp";
384
+ };
385
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>>;
386
+ });
387
+ resume_computer_task: ({
388
+ deferLoading?: boolean;
389
+ title?: string;
390
+ providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
391
+ metadata?: import("@ai-sdk/provider").JSONObject;
392
+ inputSchema: import("ai").FlexibleSchema<{
393
+ pauseId: string;
394
+ }>;
395
+ contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
396
+ needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
397
+ pauseId: string;
398
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
399
+ onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
400
+ onInputDelta?: ((options: {
401
+ inputTextDelta: string;
402
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
403
+ onInputAvailable?: ((options: {
404
+ input: {
405
+ pauseId: string;
406
+ };
407
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
408
+ toModelOutput?: ((options: {
409
+ toolCallId: string;
410
+ input: {
411
+ pauseId: string;
412
+ };
413
+ output: NoInfer<import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>>;
414
+ }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
415
+ } & {
416
+ outputSchema?: import("ai").FlexibleSchema<import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>> | undefined;
417
+ execute: import("ai").ToolExecuteFunction<{
418
+ pauseId: string;
419
+ }, import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>, NoInfer<import("@ai-sdk/provider-utils").Context>>;
420
+ } & {
421
+ description?: string | ((options: {
422
+ context: NoInfer<import("@ai-sdk/provider-utils").Context>;
423
+ experimental_sandbox?: import("ai").Experimental_SandboxSession;
424
+ }) => string) | undefined;
425
+ strict?: boolean;
426
+ inputExamples?: {
427
+ input: NoInfer<{
428
+ pauseId: string;
429
+ }>;
430
+ }[] | undefined;
431
+ id?: never;
432
+ isProviderExecuted?: never;
433
+ args?: never;
434
+ supportsDeferredResults?: never;
435
+ } & {
436
+ type?: undefined | "function";
437
+ } & {
438
+ execute: import("ai").ToolExecuteFunction<{
439
+ pauseId: string;
440
+ }, import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>, NoInfer<import("@ai-sdk/provider-utils").Context>>;
441
+ }) | ({
442
+ deferLoading?: boolean;
443
+ title?: string;
444
+ providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
445
+ metadata?: import("@ai-sdk/provider").JSONObject;
446
+ inputSchema: import("ai").FlexibleSchema<{
447
+ pauseId: string;
448
+ }>;
449
+ contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
450
+ needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
451
+ pauseId: string;
452
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
453
+ onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
454
+ onInputDelta?: ((options: {
455
+ inputTextDelta: string;
456
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
457
+ onInputAvailable?: ((options: {
458
+ input: {
459
+ pauseId: string;
460
+ };
461
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
462
+ toModelOutput?: ((options: {
463
+ toolCallId: string;
464
+ input: {
465
+ pauseId: string;
466
+ };
467
+ output: NoInfer<import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>>;
468
+ }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
469
+ } & {
470
+ outputSchema?: import("ai").FlexibleSchema<import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>> | undefined;
471
+ execute: import("ai").ToolExecuteFunction<{
472
+ pauseId: string;
473
+ }, import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>, NoInfer<import("@ai-sdk/provider-utils").Context>>;
474
+ } & {
475
+ description?: string | ((options: {
476
+ context: NoInfer<import("@ai-sdk/provider-utils").Context>;
477
+ experimental_sandbox?: import("ai").Experimental_SandboxSession;
478
+ }) => string) | undefined;
479
+ strict?: boolean;
480
+ inputExamples?: {
481
+ input: NoInfer<{
482
+ pauseId: string;
483
+ }>;
484
+ }[] | undefined;
485
+ id?: never;
486
+ isProviderExecuted?: never;
487
+ args?: never;
488
+ supportsDeferredResults?: never;
489
+ } & {
490
+ type: "dynamic";
491
+ } & {
492
+ execute: import("ai").ToolExecuteFunction<{
493
+ pauseId: string;
494
+ }, import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>, NoInfer<import("@ai-sdk/provider-utils").Context>>;
495
+ }) | ({
496
+ deferLoading?: boolean;
497
+ title?: string;
498
+ providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
499
+ metadata?: import("@ai-sdk/provider").JSONObject;
500
+ inputSchema: import("ai").FlexibleSchema<{
501
+ pauseId: string;
502
+ }>;
503
+ contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
504
+ needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
505
+ pauseId: string;
506
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
507
+ onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
508
+ onInputDelta?: ((options: {
509
+ inputTextDelta: string;
510
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
511
+ onInputAvailable?: ((options: {
512
+ input: {
513
+ pauseId: string;
514
+ };
515
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
516
+ toModelOutput?: ((options: {
517
+ toolCallId: string;
518
+ input: {
519
+ pauseId: string;
520
+ };
521
+ output: NoInfer<import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>>;
522
+ }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
523
+ } & {
524
+ outputSchema?: import("ai").FlexibleSchema<import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>> | undefined;
525
+ execute: import("ai").ToolExecuteFunction<{
526
+ pauseId: string;
527
+ }, import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>, NoInfer<import("@ai-sdk/provider-utils").Context>>;
528
+ } & {
529
+ type: "provider";
530
+ id: `${string}.${string}`;
531
+ args: Record<string, unknown>;
532
+ description?: never;
533
+ strict?: never;
534
+ inputExamples?: never;
535
+ } & {
536
+ isProviderExecuted: false;
537
+ supportsDeferredResults?: never;
538
+ } & {
539
+ execute: import("ai").ToolExecuteFunction<{
540
+ pauseId: string;
541
+ }, import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>, NoInfer<import("@ai-sdk/provider-utils").Context>>;
542
+ }) | ({
543
+ deferLoading?: boolean;
544
+ title?: string;
545
+ providerOptions?: import("@ai-sdk/provider-utils").ProviderOptions;
546
+ metadata?: import("@ai-sdk/provider").JSONObject;
547
+ inputSchema: import("ai").FlexibleSchema<{
548
+ pauseId: string;
549
+ }>;
550
+ contextSchema?: import("ai").FlexibleSchema<import("@ai-sdk/provider-utils").Context> | undefined;
551
+ needsApproval?: boolean | import("@ai-sdk/provider-utils").ToolNeedsApprovalFunction<{
552
+ pauseId: string;
553
+ }, NoInfer<import("@ai-sdk/provider-utils").Context>> | undefined;
554
+ onInputStart?: ((options: import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
555
+ onInputDelta?: ((options: {
556
+ inputTextDelta: string;
557
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
558
+ onInputAvailable?: ((options: {
559
+ input: {
560
+ pauseId: string;
561
+ };
562
+ } & import("ai").ToolExecutionOptions<NoInfer<import("@ai-sdk/provider-utils").Context>>) => void | PromiseLike<void>) | undefined;
563
+ toModelOutput?: ((options: {
564
+ toolCallId: string;
565
+ input: {
566
+ pauseId: string;
567
+ };
568
+ output: NoInfer<import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>>;
569
+ }) => import("@ai-sdk/provider-utils").ToolResultOutput | PromiseLike<import("@ai-sdk/provider-utils").ToolResultOutput>) | undefined;
570
+ } & {
571
+ outputSchema?: import("ai").FlexibleSchema<import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>> | undefined;
572
+ execute: import("ai").ToolExecuteFunction<{
573
+ pauseId: string;
574
+ }, import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>, NoInfer<import("@ai-sdk/provider-utils").Context>>;
575
+ } & {
576
+ type: "provider";
577
+ id: `${string}.${string}`;
578
+ args: Record<string, unknown>;
579
+ description?: never;
580
+ strict?: never;
581
+ inputExamples?: never;
582
+ } & {
583
+ isProviderExecuted: true;
584
+ supportsDeferredResults?: boolean;
585
+ } & {
586
+ execute: import("ai").ToolExecuteFunction<{
587
+ pauseId: string;
588
+ }, import("./computer-use.js").ComputerUseResult | AsyncIterable<import("./computer-use.js").ComputerUseResult>, NoInfer<import("@ai-sdk/provider-utils").Context>>;
589
+ });
590
+ };
@@ -0,0 +1,45 @@
1
+ import { jsonSchema, tool } from 'ai';
2
+ import { createComputerUseHandoff } from './computer-use-handoff.js';
3
+ import { createComputerUseTool } from './computer-use.js';
4
+ export function createComputerUseHandoffTools(options) {
5
+ if (typeof options.onInputRequired !== 'function' || typeof options.canResume !== 'function') {
6
+ throw new Error('Computer use handoff tools need onInputRequired and canResume callbacks.');
7
+ }
8
+ const handoff = createComputerUseHandoff(options);
9
+ const schema = jsonSchema({
10
+ type: 'object', properties: { pauseId: { type: 'string', minLength: 1 } }, required: ['pauseId'], additionalProperties: false,
11
+ });
12
+ return {
13
+ request_computer_input: tool({
14
+ description: 'Show the paused task computer to the user and request outside input. Use the current pauseId from the computer-use result. This returns human_input_required: end this agent turn and wait for the user. Never ask for passwords or one-time codes in chat.',
15
+ inputSchema: schema,
16
+ execute: async ({ pauseId }) => {
17
+ const input = await handoff.check();
18
+ if (input.status !== 'waiting_for_input' || !('pauseId' in input) || input.pauseId !== pauseId)
19
+ throw new Error('The task is not waiting at this pause.');
20
+ const page = await handoff.view(pauseId);
21
+ await options.onInputRequired(input, page);
22
+ return { ...input, status: 'human_input_required' };
23
+ },
24
+ }),
25
+ resume_computer_task: tool({
26
+ description: 'Resume the original computer-use task only after the user has supplied input and explicitly pressed Continue. Server approval is required. Follow that same task until it finishes or needs more input; never start a new task.',
27
+ inputSchema: schema,
28
+ execute: async ({ pauseId }, execution) => {
29
+ const current = await handoff.check();
30
+ if (await options.canResume(pauseId) !== true)
31
+ throw new Error('The user has not approved continuing this pause.');
32
+ if (current.status === 'waiting_for_input') {
33
+ if (!('pauseId' in current) || current.pauseId !== pauseId)
34
+ throw new Error('The task is waiting at a different pause.');
35
+ await handoff.resume(pauseId);
36
+ }
37
+ else if (!['resume_starting', 'resume_queued', 'resuming', 'running', 'done', 'failed'].includes(current.status)) {
38
+ throw new Error('The task cannot continue in its current state.');
39
+ }
40
+ const follow = createComputerUseTool({ ...options, attach: true });
41
+ return follow.execute({ task: 'Continue the original task.' }, execution);
42
+ },
43
+ }),
44
+ };
45
+ }
@@ -0,0 +1,77 @@
1
+ import { type ComputerUseTransport } from './computer-use-api.js';
2
+ import type { SignInAction } from './computer-use-sign-in.js';
3
+ export interface ComputerUseMessage {
4
+ type: 'progress' | 'handoff';
5
+ text: string;
6
+ required_input?: 'login' | 'human';
7
+ }
8
+ export declare function computerUseMessage(value: unknown): ComputerUseMessage | undefined;
9
+ export interface ComputerUseWaiting {
10
+ status: 'waiting_for_input';
11
+ taskId: string;
12
+ pauseId: string;
13
+ reason: string;
14
+ requiredInput: 'login' | 'otp' | 'human';
15
+ message?: ComputerUseMessage;
16
+ login?: {
17
+ status: 'success' | 'needs_input' | 'human_help_needed';
18
+ input?: 'otp';
19
+ };
20
+ }
21
+ export declare function waitingInput(answer: {
22
+ taskId?: unknown;
23
+ pauseId?: unknown;
24
+ reason?: unknown;
25
+ requiredInput?: unknown;
26
+ login?: unknown;
27
+ message?: unknown;
28
+ }): ComputerUseWaiting;
29
+ export type ComputerUsePage = {
30
+ status: 'pausing';
31
+ } | {
32
+ status: 'ready';
33
+ screenshot: string;
34
+ url: string;
35
+ };
36
+ export type ComputerUseLoginInput = {
37
+ allowedHosts: readonly string[];
38
+ username: string;
39
+ password: string;
40
+ otp?: never;
41
+ } | {
42
+ otp: string;
43
+ allowedHosts?: never;
44
+ username?: never;
45
+ password?: never;
46
+ };
47
+ export interface ComputerUseHandoffOptions extends ComputerUseTransport {
48
+ runKey: string;
49
+ authorize(): Promise<boolean> | boolean;
50
+ }
51
+ export declare function createComputerUseHandoff(options: ComputerUseHandoffOptions): {
52
+ requestTakeover(reason: string, requiredInput?: "login" | "human"): Promise<{
53
+ status: string;
54
+ }>;
55
+ view(pauseId: string): Promise<ComputerUsePage>;
56
+ input(pauseId: string, action: SignInAction): Promise<{
57
+ status: "accepted";
58
+ }>;
59
+ completeLogin(pauseId: string, input: ComputerUseLoginInput): Promise<{
60
+ status: "login_queued";
61
+ }>;
62
+ check(): Promise<ComputerUseWaiting | {
63
+ message?: ComputerUseMessage | undefined;
64
+ status: string;
65
+ }>;
66
+ resume(pauseId: string): Promise<{
67
+ status: "resume_queued";
68
+ }>;
69
+ cancel(): Promise<{
70
+ status: string;
71
+ }>;
72
+ };
73
+ export declare function getSavedLogin(_accountRef: string, _website: string): Promise<{
74
+ status: 'unavailable';
75
+ }>;
76
+ /** A one-time code comes from the app's own mailbox or phone number: `signInWithOtp` in `computer-use-otp.js`. */
77
+ export { readOtp, signInWithOtp } from './computer-use-otp.js';
@@ -0,0 +1,108 @@
1
+ import { allowedHostsOf, computerUseApi, refused } from './computer-use-api.js';
2
+ export function computerUseMessage(value) {
3
+ if (!value || typeof value !== 'object')
4
+ return undefined;
5
+ const message = value;
6
+ if (!['progress', 'handoff'].includes(message.type) || typeof message.text !== 'string' || !message.text.trim() || message.text.length > 500)
7
+ return undefined;
8
+ return { type: message.type, text: message.text,
9
+ ...(message.type === 'handoff' && ['login', 'human'].includes(message.required_input ?? '') ? { required_input: message.required_input } : {}) };
10
+ }
11
+ export function waitingInput(answer) {
12
+ if (typeof answer.taskId !== 'string' || !answer.taskId || typeof answer.pauseId !== 'string' || !answer.pauseId
13
+ || !['login', 'otp', 'human'].includes(String(answer.requiredInput)))
14
+ throw new Error('The computer use API returned an incomplete pause.');
15
+ const result = {
16
+ status: 'waiting_for_input', taskId: answer.taskId, pauseId: answer.pauseId,
17
+ reason: typeof answer.reason === 'string' ? answer.reason : 'Outside input needed.',
18
+ requiredInput: answer.requiredInput,
19
+ };
20
+ const message = computerUseMessage(answer.message);
21
+ if (message)
22
+ result.message = message;
23
+ const login = answer.login;
24
+ if (login && ['success', 'needs_input', 'human_help_needed'].includes(login.status)) {
25
+ result.login = { status: login.status, ...(login.input === 'otp' ? { input: 'otp' } : {}) };
26
+ }
27
+ return result;
28
+ }
29
+ export function createComputerUseHandoff(options) {
30
+ if (typeof options.runKey !== 'string' || !options.runKey || typeof options.authorize !== 'function')
31
+ throw new Error('Computer use handoff needs runKey and an authorize check for the current user.');
32
+ const api = computerUseApi(options);
33
+ const post = async (path, body = {}, privateInput = false) => {
34
+ if (await options.authorize() !== true)
35
+ throw new Error('You cannot access this computer use task.');
36
+ let answer;
37
+ try {
38
+ answer = await api.call(path, { run_key: options.runKey, ...body });
39
+ }
40
+ catch (error) {
41
+ if (privateInput)
42
+ throw new Error('The computer use login request could not be completed.');
43
+ throw error;
44
+ }
45
+ if (answer.status !== 200)
46
+ throw refused(path, answer.status, privateInput ? undefined : answer.body);
47
+ return answer.body;
48
+ };
49
+ const paused = (pauseId) => {
50
+ if (!pauseId || typeof pauseId !== 'string')
51
+ throw new Error('Computer use handoff needs the current pauseId.');
52
+ return { pauseId };
53
+ };
54
+ return {
55
+ requestTakeover(reason, requiredInput = 'human') {
56
+ if (!reason.trim() || !['login', 'human'].includes(requiredInput))
57
+ throw new Error('Takeover needs a reason and login or human input.');
58
+ return post('/tasks/takeover', { reason, requiredInput });
59
+ },
60
+ async view(pauseId) {
61
+ const page = await post('/tasks/view', paused(pauseId));
62
+ if (page.status === 'pausing')
63
+ return { status: 'pausing' };
64
+ if (page.status !== 'ready' || typeof page.screenshot !== 'string' || typeof page.url !== 'string')
65
+ throw new Error('The computer use API returned an incomplete screen.');
66
+ return { status: 'ready', screenshot: page.screenshot, url: page.url };
67
+ },
68
+ input(pauseId, action) {
69
+ return post('/tasks/input', { ...paused(pauseId), action });
70
+ },
71
+ completeLogin(pauseId, input) {
72
+ let body;
73
+ if ('otp' in input && input.otp !== undefined) {
74
+ if (Object.keys(input).some(key => key !== 'otp') || !validSecret(input.otp))
75
+ throw new Error('Pass only a non-empty OTP of at most 4096 bytes.');
76
+ body = { otp: input.otp };
77
+ }
78
+ else {
79
+ if (!validSecret(input.username) || !validSecret(input.password))
80
+ throw new Error('Login needs a non-empty username and password of at most 4096 bytes.');
81
+ body = { allowedHosts: allowedHostsOf(input.allowedHosts), username: input.username, password: input.password };
82
+ }
83
+ return post('/tasks/login', { ...paused(pauseId), ...body }, true)
84
+ .then(() => ({ status: 'login_queued' }));
85
+ },
86
+ async check() {
87
+ const answer = await post('/tasks/check');
88
+ if (answer.status === 'waiting_for_input')
89
+ return waitingInput(answer);
90
+ const message = computerUseMessage(answer.message);
91
+ return { status: String(answer.status), ...(message ? { message } : {}) };
92
+ },
93
+ resume(pauseId) {
94
+ return post('/tasks/resume', paused(pauseId));
95
+ },
96
+ cancel() {
97
+ return post('/tasks/cancel');
98
+ },
99
+ };
100
+ }
101
+ function validSecret(value) {
102
+ return typeof value === 'string' && value.length > 0 && Buffer.byteLength(value) <= 4096;
103
+ }
104
+ export async function getSavedLogin(_accountRef, _website) {
105
+ return { status: 'unavailable' };
106
+ }
107
+ /** A one-time code comes from the app's own mailbox or phone number: `signInWithOtp` in `computer-use-otp.js`. */
108
+ export { readOtp, signInWithOtp } from './computer-use-otp.js';
@@ -0,0 +1,62 @@
1
+ import type { createComputerUseHandoff } from './computer-use-handoff.js';
2
+ type Handoff = ReturnType<typeof createComputerUseHandoff>;
3
+ /** `app.integrations.execute` from `createClient()`: the app's own grant, identity and rate limit. */
4
+ export type IntegrationExecute = (connection: string, request: {
5
+ operation: string;
6
+ resource: string;
7
+ input?: Record<string, unknown>;
8
+ }) => Promise<unknown>;
9
+ export interface OtpSource {
10
+ execute: IntegrationExecute;
11
+ /** The connected mailbox the website mails the code to. `from` is the website's sender address. */
12
+ email?: {
13
+ connection: string;
14
+ provider: 'gmail' | 'outlook';
15
+ from: string;
16
+ resource?: string;
17
+ };
18
+ /** The company phone number the website texts the code to. `from` is the sender id, such as `HDFCBK`. */
19
+ sms?: {
20
+ connection: string;
21
+ from?: string;
22
+ resource?: string;
23
+ };
24
+ /** The code in a message's text; the default takes 4 to 8 digits next to "OTP", "code" or "password", else the first such group. */
25
+ extract?: (text: string) => string | undefined;
26
+ /** How long to wait for the message, default 3 minutes; checked every `pollIntervalMs`, default 5 s. */
27
+ timeoutMs?: number;
28
+ pollIntervalMs?: number;
29
+ }
30
+ export type OtpSignIn = {
31
+ status: 'success';
32
+ }
33
+ /** The website wants a person: no code arrived in time, a second code was asked for, or the helper gave up. The task is still paused. */
34
+ | {
35
+ status: 'human_help_needed';
36
+ reason: string;
37
+ }
38
+ /** The task ended while signing in. */
39
+ | {
40
+ status: 'ended';
41
+ taskStatus: string;
42
+ };
43
+ export declare function extractOtp(text: string): string | undefined;
44
+ /** Reads the website's one-time code sent since `since`, waiting up to `timeoutMs` for it to arrive. */
45
+ export declare function readOtp(options: OtpSource, since: Date, now?: () => number): Promise<string | undefined>;
46
+ /**
47
+ * Signs in on a paused task's computer with a username and password, answers
48
+ * the website's one-time code from the mailbox or phone the app may read, and
49
+ * returns once the helper succeeded or needs a person. Call it only from
50
+ * private server code: the values never reach a model, a log or a job result.
51
+ * Success leaves the task paused; the app resumes it when it decides to.
52
+ */
53
+ export declare function signInWithOtp(handoff: Handoff, pauseId: string, login: {
54
+ allowedHosts: readonly string[];
55
+ username: string;
56
+ password: string;
57
+ }, otp: OtpSource, options?: {
58
+ pollIntervalMs?: number;
59
+ timeoutMs?: number;
60
+ now?: () => number;
61
+ }): Promise<OtpSignIn>;
62
+ export {};
@@ -0,0 +1,92 @@
1
+ const CODE_NEAR_WORD = /(?:otp|one[\s-]?time|passcode|password|code|pin)\D{0,40}?(\d{4,8})\b/i;
2
+ const CODE_ALONE = /\b(\d{4,8})\b/;
3
+ export function extractOtp(text) {
4
+ return CODE_NEAR_WORD.exec(text)?.[1] ?? CODE_ALONE.exec(text)?.[1];
5
+ }
6
+ function source(options) {
7
+ if (typeof options.execute !== 'function' || (options.email === undefined) === (options.sms === undefined))
8
+ throw new Error('An OTP source is execute plus exactly one of email or sms.');
9
+ return { kind: options.email ? 'email' : 'sms' };
10
+ }
11
+ /** The newest message from the website since `since`, as text, or undefined when none has arrived. */
12
+ async function latestMessage(options, since) {
13
+ const arrived = (at) => new Date(at).getTime() >= since.getTime();
14
+ if (options.sms) {
15
+ const { connection, from, resource = 'company-line' } = options.sms;
16
+ const answer = await options.execute(connection, { operation: 'phone.sms.list', resource, input: { since: since.toISOString(), ...(from ? { from } : {}) } });
17
+ const message = (answer.messages ?? []).filter(item => arrived(item.receivedAt)).sort((a, b) => b.receivedAt.localeCompare(a.receivedAt))[0];
18
+ return message?.body;
19
+ }
20
+ const { connection, provider, from, resource = 'inbox' } = options.email;
21
+ if (provider === 'gmail') {
22
+ const list = await options.execute(connection, { operation: 'gmail.thread.list', resource, input: { q: `from:${from} newer_than:1d`, limit: 10 } });
23
+ const thread = (list.threads ?? []).filter(item => arrived(item.date)).sort((a, b) => b.date.localeCompare(a.date))[0];
24
+ if (!thread)
25
+ return undefined;
26
+ const message = await options.execute(connection, { operation: 'gmail.message.read', resource, input: { messageId: thread.messageId } });
27
+ return `${message.subject}\n${message.text}`;
28
+ }
29
+ const list = await options.execute(connection, { operation: 'outlook.message.list', resource, input: { q: `from:${from}` } });
30
+ const summary = (list.mailMessages ?? []).filter(item => arrived(item.receivedAt) && item.from.toLowerCase().includes(from.toLowerCase())).sort((a, b) => b.receivedAt.localeCompare(a.receivedAt))[0];
31
+ if (!summary)
32
+ return undefined;
33
+ const message = await options.execute(connection, { operation: 'outlook.message.read', resource, input: { messageId: summary.id } });
34
+ return `${message.subject}\n${message.text}`;
35
+ }
36
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
37
+ /** Reads the website's one-time code sent since `since`, waiting up to `timeoutMs` for it to arrive. */
38
+ export async function readOtp(options, since, now = Date.now) {
39
+ source(options);
40
+ const extract = options.extract ?? extractOtp;
41
+ const deadline = now() + (options.timeoutMs ?? 180_000);
42
+ for (;;) {
43
+ const text = await latestMessage(options, since);
44
+ const code = text === undefined ? undefined : extract(text);
45
+ if (code)
46
+ return code;
47
+ if (now() >= deadline)
48
+ return undefined;
49
+ await sleep(options.pollIntervalMs ?? 5_000);
50
+ }
51
+ }
52
+ /**
53
+ * Signs in on a paused task's computer with a username and password, answers
54
+ * the website's one-time code from the mailbox or phone the app may read, and
55
+ * returns once the helper succeeded or needs a person. Call it only from
56
+ * private server code: the values never reach a model, a log or a job result.
57
+ * Success leaves the task paused; the app resumes it when it decides to.
58
+ */
59
+ export async function signInWithOtp(handoff, pauseId, login, otp, options = {}) {
60
+ source(otp);
61
+ const now = options.now ?? Date.now;
62
+ // A clock a minute ahead of the mail server must not hide the code.
63
+ const since = new Date(now() - 60_000);
64
+ await handoff.completeLogin(pauseId, login);
65
+ let answered = false;
66
+ const deadline = now() + (options.timeoutMs ?? 600_000);
67
+ for (;;) {
68
+ await sleep(options.pollIntervalMs ?? 5_000);
69
+ const check = await handoff.check();
70
+ if (check.status === 'done' || check.status === 'failed')
71
+ return { status: 'ended', taskStatus: check.status };
72
+ if (check.status !== 'waiting_for_input') {
73
+ if (now() >= deadline)
74
+ return { status: 'human_help_needed', reason: 'The sign-in helper did not finish in time.' };
75
+ continue;
76
+ }
77
+ const login = check.login;
78
+ if (login?.status === 'success')
79
+ return { status: 'success' };
80
+ if (login?.status === 'human_help_needed')
81
+ return { status: 'human_help_needed', reason: 'The sign-in helper could not finish on its own.' };
82
+ if (login?.status === 'needs_input' && login.input === 'otp') {
83
+ if (answered)
84
+ return { status: 'human_help_needed', reason: 'The website asked for a second code.' };
85
+ const code = await readOtp(otp, since, now);
86
+ if (!code)
87
+ return { status: 'human_help_needed', reason: `No code arrived by ${otp.email ? 'email' : 'SMS'} in time.` };
88
+ await handoff.completeLogin(pauseId, { otp: code });
89
+ answered = true;
90
+ }
91
+ }
92
+ }
@@ -1,3 +1,4 @@
1
+ import { type ComputerUseWaiting, type ComputerUseMessage } from './computer-use-handoff.js';
1
2
  export interface ComputerUseFile {
2
3
  name: string;
3
4
  contentType: "application/pdf";
@@ -29,6 +30,8 @@ export interface ComputerUseOptions {
29
30
  onDownload?(download: ComputerUseDownload): Promise<void> | void;
30
31
  /** The job's runId, or `<job name>/<scheduledAt>` for a scheduled job: one task record per job run. */
31
32
  runKey: string;
33
+ attach?: boolean;
34
+ onMessage?(message: ComputerUseMessage): Promise<void> | void;
32
35
  /** The job's progress: called on each new step and at least once a minute, which renews the job's lease. */
33
36
  progress(percent: number, note?: string): Promise<void> | void;
34
37
  /** The job's cancelled(): when true the tool cancels the task and waits until the CUA stops. */
@@ -44,7 +47,7 @@ export interface ComputerUseOptions {
44
47
  pollIntervalMs?: number;
45
48
  now?: () => number;
46
49
  }
47
- export interface ComputerUseResult {
50
+ export interface ComputerUseFinished {
48
51
  status: 'done' | 'failed';
49
52
  summary: string | null;
50
53
  /** The page the browser was on when the task ended; a sign-in page there means the saved sign-in has expired. */
@@ -55,6 +58,7 @@ export interface ComputerUseResult {
55
58
  size: number;
56
59
  }[];
57
60
  }
61
+ export type ComputerUseResult = ComputerUseFinished | ComputerUseWaiting;
58
62
  export declare function createComputerUseTool(options: ComputerUseOptions): ({
59
63
  deferLoading?: boolean;
60
64
  title?: string;
@@ -8,6 +8,7 @@ import { setTimeout as sleep } from 'node:timers/promises';
8
8
  import { jsonSchema, tool } from 'ai';
9
9
  import { allowedHostsOf, computerUseApi, refused } from './computer-use-api.js';
10
10
  import { runtimeSecretLocation, runtimeSecrets } from './secrets.js';
11
+ import { computerUseMessage, waitingInput } from './computer-use-handoff.js';
11
12
  const HEARTBEAT_MS = 60_000;
12
13
  const DEFAULT_MAX_QUEUE_MS = 20 * 60_000;
13
14
  // Checks the API did not answer (a throttle, a 5xx, no connection) before the tool gives up: about a minute.
@@ -82,14 +83,15 @@ export function createComputerUseTool(options) {
82
83
  }
83
84
  };
84
85
  return tool({
85
- description: 'Do one task in a web browser, such as filling in and submitting a form on a website. Describe the whole task in plain words, with the website address and any details it needs. Waits until the task ends and returns its status and a summary. Files the task downloads go to the app; the result lists their names.',
86
+ description: 'Do one task in a web browser, such as filling in and submitting a form on a website. Describe the whole task in plain words, with the website address and any details it needs. Returns when the task ends or pauses for outside input. When waiting_for_input, ask the app or human for help; do not start another task or resume without their explicit request. Files the task downloads go to the app; the result lists their names.',
86
87
  inputSchema: jsonSchema({ type: 'object', properties: { task: { type: 'string', minLength: 1 } }, required: ['task'], additionalProperties: false }),
87
88
  execute: async ({ task }) => {
88
- await api.post('/tasks', {
89
- run_key: options.runKey, task, ...(files?.length ? { files } : {}), ...(session === undefined ? {} : { session: await savedSession(session) }),
90
- ...(allowedHosts ? { allowedHosts } : {}), ...(secrets ? { secrets } : {}),
91
- });
92
- await options.progress(0, 'Computer use task started.');
89
+ if (!options.attach)
90
+ await api.post('/tasks', {
91
+ run_key: options.runKey, task, ...(files?.length ? { files } : {}), ...(session === undefined ? {} : { session: await savedSession(session) }),
92
+ ...(allowedHosts ? { allowedHosts } : {}), ...(secrets ? { secrets } : {}),
93
+ });
94
+ await options.progress(0, options.attach ? 'Computer use task reattached.' : 'Computer use task started.');
93
95
  const queuedAt = now();
94
96
  const maxQueueMs = options.maxQueueMs ?? DEFAULT_MAX_QUEUE_MS;
95
97
  let reported = now();
@@ -97,6 +99,7 @@ export function createComputerUseTool(options) {
97
99
  let cancelSent = false;
98
100
  let queueExpired = false;
99
101
  let missed = 0;
102
+ let lastMessage = "";
100
103
  for (;;) {
101
104
  if (!cancelSent && await options.cancelled()) {
102
105
  await api.post('/tasks/cancel', { run_key: options.runKey });
@@ -112,6 +115,15 @@ export function createComputerUseTool(options) {
112
115
  if (check?.status === 200) {
113
116
  missed = 0;
114
117
  const answer = check.body;
118
+ const message = computerUseMessage(answer.message);
119
+ if (message && JSON.stringify(message) !== lastMessage) {
120
+ await options.onMessage?.(message);
121
+ await options.progress(Math.min(steps, 99), message.text);
122
+ reported = now();
123
+ lastMessage = JSON.stringify(message);
124
+ }
125
+ if (answer.status === 'waiting_for_input' && !cancelSent)
126
+ return waitingInput(answer);
115
127
  if (answer.status === 'done' || answer.status === 'failed') {
116
128
  if (queueExpired && answer.status === 'failed') {
117
129
  return { status: 'failed', summary: `No computer was free within ${Math.round(maxQueueMs / 60_000)} minutes; the task was cancelled before it started.`, finalUrl: null, downloads: [] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/app-sdk",
3
- "version": "1.39.0",
3
+ "version": "1.41.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",