@isomorph.ai/app-sdk 1.38.0 → 1.39.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 +5 -3
- package/dist/computer-use-sign-in.d.ts +6 -2
- package/dist/computer-use-sign-in.js +19 -3
- package/dist/computer-use.d.ts +7 -1
- package/dist/computer-use.js +22 -1
- package/dist/index.d.ts +2 -10
- package/dist/mcp-tools.d.ts +10 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -400,6 +400,7 @@ const computerUse = createComputerUseTool({ runKey: ctx.runId ?? `my-job/${ctx.s
|
|
|
400
400
|
- It starts the task with `POST /tasks`, then checks `POST /tasks/check` every 5 s until the task is `done` or `failed`.
|
|
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
|
+
- 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.`
|
|
403
404
|
- One task record per `runKey`: a retried job run sends the same key and gets the queued, running or finished task back. Only a failed task starts a new attempt.
|
|
404
405
|
- `finalUrl` is the page the browser ended on. A website's sign-in page there means a saved sign-in has expired: ask the person to sign in again.
|
|
405
406
|
|
|
@@ -407,13 +408,14 @@ Options the app passes, never the model:
|
|
|
407
408
|
|
|
408
409
|
- `files`: PDFs the task may upload, at most five and 2 MiB in all.
|
|
409
410
|
- `session`: the name of a runtime secret a sign-in was saved under (`finishSignIn`'s `saveAs`, below). The task starts signed in and can use only that sign-in's websites. When the task is `done` the refreshed session is saved back to the same secret. A missing one fails with `No saved sign-in named <name>.` Run one task per saved sign-in at a time: two tasks would both save back, and some websites sign out the older copy.
|
|
410
|
-
- `secrets`: `{ name: value }`, at most 10, names `^[A-Za-z][A-Za-z0-9_-]{0,63}$`, values up to 4096 bytes. The computer types a value into a password field by its name
|
|
411
|
+
- `secrets`: `{ name: value }`, at most 10, names `^[A-Za-z][A-Za-z0-9_-]{0,63}$`, values up to 4096 bytes. The computer types a value into a password field by its name, and only there: it refuses to type a secret into a visible field, since the model reads the page after every step. The model sees only the names. Never put a password or any other credential in the task text; pass it here and say "sign in with the `password` secret". A user ID, customer ID or email is not a secret: give it in the task text.
|
|
412
|
+
- `maxQueueMs`: how long the task may wait for a free computer before the tool cancels it (20 minutes unless set).
|
|
411
413
|
- `allowedHosts`: the websites (1 to 10 lower-case hostnames) a task with `secrets` and no `session` may use. A saved sign-in carries its own.
|
|
412
414
|
- `onDownload({ name, contentType, size, blob })`: called once for each file a `done` task downloaded, one at a time, before the tool returns (at most 10 files, 100 MiB each). Store the `blob` yourself; the CUA keeps the files for a day at most. Without it nothing is fetched. A link that does not answer 200 fails the tool. The model only sees `downloads: [{ name, size }]`.
|
|
413
415
|
|
|
414
416
|
A task with `session` or `secrets` works the page through clicks and typing only. `connectionId` is gone: pass `session`.
|
|
415
417
|
|
|
416
|
-
(1.25.0, 1.36.0, 1.37.0; data plane ADR 0078, ADR 0090, ADR 0091)
|
|
418
|
+
(1.25.0, 1.36.0, 1.37.0, 1.39.0; data plane ADR 0078, ADR 0090, ADR 0091, ADR 0092)
|
|
417
419
|
|
|
418
420
|
## Browser sign-in for computer use
|
|
419
421
|
|
|
@@ -431,7 +433,7 @@ await sendSignInInput(handle, { op: 'click', x: 640, y: 400 });
|
|
|
431
433
|
await finishSignIn(handle, { saveAs: `vendor-central/${ctx.user.id}` }); // { url }
|
|
432
434
|
```
|
|
433
435
|
|
|
434
|
-
- `startSignIn` starts a computer that may open only `allowedHosts` and returns an opaque handle for the calls that follow; the computer answers only this app's role and stops after 30 minutes.
|
|
436
|
+
- `startSignIn` starts a computer that may open only `allowedHosts` and returns an opaque handle for the calls that follow; the computer answers only this app's role and stops after 30 minutes. It opens `startUrl`, an HTTPS page on one of those websites, or the first website's front page: an account on `accounts.zoho.in` lists that host first or passes it as `startUrl`.
|
|
435
437
|
- `showSignIn` gives `{ status: 'starting' }` until the browser is up, then `{ status: 'ready', screenshot, url }`: a base64 PNG of the 1280×800 screen and the page's address.
|
|
436
438
|
- `sendSignInInput` takes `{ op: 'click', x, y }` (screen pixels), `{ op: 'typeText', value }` (up to 512 characters), `{ op: 'pressKey', key }` (such as `Enter` or `Tab`) or `{ op: 'scroll', direction: 'up' | 'down' }`.
|
|
437
439
|
- `finishSignIn` saves the session (cookies and site storage of the allowed websites, at most 60 KB) as the runtime secret `saveAs`, stops the computer and returns the page it was on. App code never holds the cookies.
|
|
@@ -22,9 +22,13 @@ export type SignInPage = {
|
|
|
22
22
|
screenshot: string;
|
|
23
23
|
url: string;
|
|
24
24
|
};
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Starts a sign-in computer limited to `allowedHosts`; the handle names it in the calls that follow. The
|
|
27
|
+
* computer opens `startUrl`, an HTTPS page on one of those websites, or the first website's front page.
|
|
28
|
+
*/
|
|
29
|
+
export declare function startSignIn({ allowedHosts, startUrl, ...transport }: {
|
|
27
30
|
allowedHosts: readonly string[];
|
|
31
|
+
startUrl?: string;
|
|
28
32
|
} & ComputerUseTransport): Promise<string>;
|
|
29
33
|
export declare function showSignIn(handle: string, transport?: ComputerUseTransport): Promise<SignInPage>;
|
|
30
34
|
export declare function sendSignInInput(handle: string, action: SignInAction, transport?: ComputerUseTransport): Promise<void>;
|
|
@@ -5,6 +5,19 @@
|
|
|
5
5
|
// jobs. The computer answers only the app role that started it.
|
|
6
6
|
import { allowedHostsOf, computerUseApi } from './computer-use-api.js';
|
|
7
7
|
import { runtimeSecretLocation, runtimeSecrets } from './secrets.js';
|
|
8
|
+
function startUrlOf(startUrl, hosts) {
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(String(startUrl));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
url = undefined;
|
|
15
|
+
}
|
|
16
|
+
if (typeof startUrl !== 'string' || !url || url.protocol !== 'https:' || url.username || url.password || !hosts.includes(url.hostname)) {
|
|
17
|
+
throw new Error('startUrl must be an HTTPS address on one of the allowedHosts.');
|
|
18
|
+
}
|
|
19
|
+
return startUrl;
|
|
20
|
+
}
|
|
8
21
|
function computerOf(handle) {
|
|
9
22
|
let computer;
|
|
10
23
|
try {
|
|
@@ -17,10 +30,13 @@ function computerOf(handle) {
|
|
|
17
30
|
throw new Error('This is not a sign-in handle from startSignIn.');
|
|
18
31
|
return { computerId: computer.computerId, endpoint: computer.endpoint };
|
|
19
32
|
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Starts a sign-in computer limited to `allowedHosts`; the handle names it in the calls that follow. The
|
|
35
|
+
* computer opens `startUrl`, an HTTPS page on one of those websites, or the first website's front page.
|
|
36
|
+
*/
|
|
37
|
+
export async function startSignIn({ allowedHosts, startUrl, ...transport }) {
|
|
22
38
|
const hosts = allowedHostsOf(allowedHosts);
|
|
23
|
-
const { computerId, endpoint } = await computerUseApi(transport).post('/computers/start', { allowedHosts: hosts });
|
|
39
|
+
const { computerId, endpoint } = await computerUseApi(transport).post('/computers/start', { allowedHosts: hosts, ...(startUrl === undefined ? {} : { startUrl: startUrlOf(startUrl, hosts) }) });
|
|
24
40
|
return Buffer.from(JSON.stringify({ computerId, endpoint })).toString('base64url');
|
|
25
41
|
}
|
|
26
42
|
export async function showSignIn(handle, transport = {}) {
|
package/dist/computer-use.d.ts
CHANGED
|
@@ -21,7 +21,8 @@ export interface ComputerUseOptions {
|
|
|
21
21
|
allowedHosts?: readonly string[];
|
|
22
22
|
/**
|
|
23
23
|
* Values the computer types into a password field by name, at most 10; the model sees only the names.
|
|
24
|
-
* Never put a credential in the task text.
|
|
24
|
+
* Never put a credential in the task text. A user ID, customer ID or email is not a secret: the
|
|
25
|
+
* computer refuses to type a secret into a visible field, so give those in the task text.
|
|
25
26
|
*/
|
|
26
27
|
secrets?: Readonly<Record<string, string>>;
|
|
27
28
|
/** The app's code gets each file the task downloaded, one at a time, when the task is done. Never seen by the model. */
|
|
@@ -34,6 +35,11 @@ export interface ComputerUseOptions {
|
|
|
34
35
|
cancelled(): Promise<boolean> | boolean;
|
|
35
36
|
env?: NodeJS.ProcessEnv;
|
|
36
37
|
fetch?: typeof fetch;
|
|
38
|
+
/**
|
|
39
|
+
* How long the task may wait for a free computer before the tool cancels it and returns `failed`;
|
|
40
|
+
* 20 minutes unless set. The wait is reported through `progress` while it lasts.
|
|
41
|
+
*/
|
|
42
|
+
maxQueueMs?: number;
|
|
37
43
|
/** The pause between checks; the tests shorten it. */
|
|
38
44
|
pollIntervalMs?: number;
|
|
39
45
|
now?: () => number;
|
package/dist/computer-use.js
CHANGED
|
@@ -9,8 +9,18 @@ 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
11
|
const HEARTBEAT_MS = 60_000;
|
|
12
|
+
const DEFAULT_MAX_QUEUE_MS = 20 * 60_000;
|
|
12
13
|
// Checks the API did not answer (a throttle, a 5xx, no connection) before the tool gives up: about a minute.
|
|
13
14
|
const MAX_MISSED_CHECKS = 12;
|
|
15
|
+
/** The progress note of a task that has not started: the wait and its reason when the CUA names one. */
|
|
16
|
+
function queueNote(answer, waitedMs) {
|
|
17
|
+
if (answer.status !== 'queued' || !answer.waiting)
|
|
18
|
+
return `Computer use task ${answer.status}.`;
|
|
19
|
+
const minutes = Math.max(1, Math.round(waitedMs / 60_000));
|
|
20
|
+
return answer.waiting.reason === 'capacity'
|
|
21
|
+
? `Computer use task queued: no computer is free yet (${minutes} min).`
|
|
22
|
+
: `Computer use task queued: ${answer.waiting.reason} (${minutes} min).`;
|
|
23
|
+
}
|
|
14
24
|
function secretsOf(secrets) {
|
|
15
25
|
const entries = secrets && typeof secrets === 'object' && !Array.isArray(secrets) ? Object.entries(secrets) : [];
|
|
16
26
|
if (entries.length < 1 || entries.length > 10)
|
|
@@ -80,9 +90,12 @@ export function createComputerUseTool(options) {
|
|
|
80
90
|
...(allowedHosts ? { allowedHosts } : {}), ...(secrets ? { secrets } : {}),
|
|
81
91
|
});
|
|
82
92
|
await options.progress(0, 'Computer use task started.');
|
|
93
|
+
const queuedAt = now();
|
|
94
|
+
const maxQueueMs = options.maxQueueMs ?? DEFAULT_MAX_QUEUE_MS;
|
|
83
95
|
let reported = now();
|
|
84
96
|
let steps = 0;
|
|
85
97
|
let cancelSent = false;
|
|
98
|
+
let queueExpired = false;
|
|
86
99
|
let missed = 0;
|
|
87
100
|
for (;;) {
|
|
88
101
|
if (!cancelSent && await options.cancelled()) {
|
|
@@ -100,6 +113,9 @@ export function createComputerUseTool(options) {
|
|
|
100
113
|
missed = 0;
|
|
101
114
|
const answer = check.body;
|
|
102
115
|
if (answer.status === 'done' || answer.status === 'failed') {
|
|
116
|
+
if (queueExpired && answer.status === 'failed') {
|
|
117
|
+
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: [] };
|
|
118
|
+
}
|
|
103
119
|
if (answer.status === 'done' && session !== undefined && answer.session)
|
|
104
120
|
await runtimeSecrets.put(session, JSON.stringify(answer.session), transport);
|
|
105
121
|
const downloads = answer.downloads ?? [];
|
|
@@ -114,9 +130,14 @@ export function createComputerUseTool(options) {
|
|
|
114
130
|
}
|
|
115
131
|
return { status: answer.status, summary: answer.summary ?? null, finalUrl: answer.final_url ?? null, downloads: downloads.map(({ name, size }) => ({ name, size })) };
|
|
116
132
|
}
|
|
133
|
+
if (answer.status === 'queued' && !cancelSent && now() - queuedAt >= maxQueueMs) {
|
|
134
|
+
await api.post('/tasks/cancel', { run_key: options.runKey });
|
|
135
|
+
cancelSent = true;
|
|
136
|
+
queueExpired = true;
|
|
137
|
+
}
|
|
117
138
|
if (answer.steps > steps || now() - reported >= HEARTBEAT_MS) {
|
|
118
139
|
steps = Math.max(steps, answer.steps);
|
|
119
|
-
await options.progress(Math.min(steps, 99), answer.last_step ? `Computer use step ${steps}: ${answer.last_step}` :
|
|
140
|
+
await options.progress(Math.min(steps, 99), answer.last_step ? `Computer use step ${steps}: ${answer.last_step}` : queueNote(answer, now() - queuedAt));
|
|
120
141
|
reported = now();
|
|
121
142
|
}
|
|
122
143
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { McpCallRequest, McpCallResult, McpToolsOptions } from './mcp-tools.js';
|
|
2
|
-
export type { McpCallRequest, McpCallResult, McpToolDescriptor, McpSelectedTool, McpToolsOptions } from './mcp-tools.js';
|
|
2
|
+
export type { McpCallRequest, McpCallResult, McpToolDescriptor, McpSelectedTool, McpToolsOptions, McpAgentTool } from './mcp-tools.js';
|
|
3
3
|
import { type IsomorphUserRole } from './user-role.js';
|
|
4
4
|
export type { IsomorphUserRole } from './user-role.js';
|
|
5
5
|
export type IsomorphErrorCategory = 'AUTH_REQUIRED' | 'FORBIDDEN' | 'NOT_FOUND' | 'CONFLICT' | 'VALIDATION_FAILED' | 'RATE_LIMITED' | 'UNAVAILABLE' | 'INTERNAL';
|
|
@@ -2059,15 +2059,7 @@ export declare function createClient(options?: ClientOptions): {
|
|
|
2059
2059
|
page: (path?: string) => Promise<void>;
|
|
2060
2060
|
};
|
|
2061
2061
|
integrations: {
|
|
2062
|
-
tools: (connection: string, options: McpToolsOptions) => Promise<Record<string, import("
|
|
2063
|
-
content?: Array<Record<string, unknown>>;
|
|
2064
|
-
structuredContent?: Record<string, unknown>;
|
|
2065
|
-
isError?: boolean;
|
|
2066
|
-
replay?: {
|
|
2067
|
-
complete: boolean;
|
|
2068
|
-
summary?: string;
|
|
2069
|
-
};
|
|
2070
|
-
}>>>;
|
|
2062
|
+
tools: (connection: string, options: McpToolsOptions) => Promise<Record<string, import("./mcp-tools.js").McpAgentTool>>;
|
|
2071
2063
|
execute: <R extends IntegrationRequest>(connection: string, request: R) => Promise<IntegrationResult<R>>;
|
|
2072
2064
|
connect: (connection: string) => Promise<IntegrationConnectResult>;
|
|
2073
2065
|
disconnect: (connection: string) => Promise<IntegrationDisconnectResult>;
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Tool } from 'ai';
|
|
2
1
|
export interface McpToolDescriptor {
|
|
3
2
|
name: string;
|
|
4
3
|
description?: string;
|
|
@@ -40,6 +39,15 @@ export interface McpToolsOptions {
|
|
|
40
39
|
tools: readonly string[];
|
|
41
40
|
runId: string;
|
|
42
41
|
}
|
|
42
|
+
/** The schema object belongs to the optional AI SDK and carries its private brand.
|
|
43
|
+
* Keep that implementation type out of ordinary clients' declaration graph. */
|
|
44
|
+
export interface McpAgentTool {
|
|
45
|
+
description?: string;
|
|
46
|
+
inputSchema: any;
|
|
47
|
+
execute(input: Record<string, unknown>, context: {
|
|
48
|
+
toolCallId: string;
|
|
49
|
+
}): Promise<McpCallResult['mcp']>;
|
|
50
|
+
}
|
|
43
51
|
type Execute = (request: McpCallRequest) => Promise<McpCallResult>;
|
|
44
|
-
export declare function createMcpTools(connection: string, options: McpToolsOptions, execute: Execute): Promise<Record<string,
|
|
52
|
+
export declare function createMcpTools(connection: string, options: McpToolsOptions, execute: Execute): Promise<Record<string, McpAgentTool>>;
|
|
45
53
|
export {};
|