@isomorph.ai/app-sdk 2.0.0 → 2.1.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/dist/computer-use-handoff.d.ts +9 -0
- package/dist/computer-use-handoff.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +18 -4
- package/package.json +1 -1
|
@@ -83,6 +83,14 @@ export type ComputerUseLoginInput = {
|
|
|
83
83
|
username?: never;
|
|
84
84
|
password?: never;
|
|
85
85
|
};
|
|
86
|
+
export interface ComputerUseLiveConnection {
|
|
87
|
+
endpoint: string;
|
|
88
|
+
headers: {
|
|
89
|
+
'X-aws-proxy-auth': string;
|
|
90
|
+
'X-aws-proxy-port': '8080';
|
|
91
|
+
};
|
|
92
|
+
viewerPath: string;
|
|
93
|
+
}
|
|
86
94
|
export interface ComputerUseHandoffOptions extends ComputerUseTransport {
|
|
87
95
|
runKey: string;
|
|
88
96
|
authorize(): Promise<boolean> | boolean;
|
|
@@ -90,6 +98,7 @@ export interface ComputerUseHandoffOptions extends ComputerUseTransport {
|
|
|
90
98
|
export declare function createComputerUseHandoff(options: ComputerUseHandoffOptions): {
|
|
91
99
|
requestTakeover(reason: string, requiredInput?: "login" | "human"): Promise<ComputerUsePause>;
|
|
92
100
|
view(pauseId: string): Promise<ComputerUsePage>;
|
|
101
|
+
live(pauseId: string): Promise<ComputerUseLiveConnection>;
|
|
93
102
|
input(pauseId: string, action: ComputerUseInputAction): Promise<ComputerUseInputResult>;
|
|
94
103
|
completeLogin(pauseId: string, input: ComputerUseLoginInput): Promise<{
|
|
95
104
|
status: "login_queued";
|
|
@@ -73,6 +73,9 @@ export function createComputerUseHandoff(options) {
|
|
|
73
73
|
throw new Error('The computer use API returned an incomplete screen.');
|
|
74
74
|
return { status: 'ready', screenshot: page.screenshot, url: page.url };
|
|
75
75
|
},
|
|
76
|
+
live(pauseId) {
|
|
77
|
+
return post('/tasks/live', paused(pauseId));
|
|
78
|
+
},
|
|
76
79
|
input(pauseId, action) {
|
|
77
80
|
return post('/tasks/input', { ...paused(pauseId), action });
|
|
78
81
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -2017,6 +2017,9 @@ export declare function createClient(options?: ClientOptions): {
|
|
|
2017
2017
|
rpc: <T = unknown>(name: string, args?: Record<string, unknown>) => Promise<T>;
|
|
2018
2018
|
watch: <T = Record<string, unknown>>(build: () => QueryBuilder<T, any>, onResult: (result: WatchResult) => void, options?: WatchOptions) => (() => void);
|
|
2019
2019
|
};
|
|
2020
|
+
computerView: {
|
|
2021
|
+
url(action: string, taskId: string): string;
|
|
2022
|
+
};
|
|
2020
2023
|
files: {
|
|
2021
2024
|
createUpload: (input: {
|
|
2022
2025
|
path: string;
|
package/dist/index.js
CHANGED
|
@@ -773,6 +773,13 @@ export function createClient(options = {}) {
|
|
|
773
773
|
clearTimer(timer); };
|
|
774
774
|
},
|
|
775
775
|
},
|
|
776
|
+
computerView: {
|
|
777
|
+
url(action, taskId) {
|
|
778
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,127}$/.test(action) || !/^[A-Za-z0-9_-]{1,128}$/.test(taskId))
|
|
779
|
+
throw new IsomorphError('VALIDATION_FAILED', 'Computer view needs an action and task ID.');
|
|
780
|
+
return `${transport.baseUrl}/_harbour/computer-view/${encodeURIComponent(action)}/${encodeURIComponent(taskId)}/`;
|
|
781
|
+
},
|
|
782
|
+
},
|
|
776
783
|
files: {
|
|
777
784
|
createUpload: (input) => transport.json('/_harbour/files/uploads', { method: 'POST', body: JSON.stringify(input) }),
|
|
778
785
|
upload,
|
package/dist/runtime.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare const GET_RUN_ACTION = "isomorph-jobs-get";
|
|
|
12
12
|
export declare const CANCEL_RUN_ACTION = "isomorph-jobs-cancel";
|
|
13
13
|
export declare const LIST_RUNS_ACTION = "isomorph-jobs-list";
|
|
14
14
|
export interface ActionContext {
|
|
15
|
+
readonly computerView?: boolean;
|
|
15
16
|
signal: AbortSignal;
|
|
16
17
|
user: IsomorphUser;
|
|
17
18
|
tenant: string;
|
|
@@ -100,6 +101,8 @@ export interface RuntimeOptions {
|
|
|
100
101
|
gatewayIdentity?: 'sts-session';
|
|
101
102
|
/** The SQS queue an enqueue sends its run to (ISOMORPH_JOBS_QUEUE_URL); the queue loop is off when set. */
|
|
102
103
|
queueUrl?: string;
|
|
104
|
+
/** How long a refused AI binding is remembered before the next handler asks the gateway again. */
|
|
105
|
+
aiRetryMs?: number;
|
|
103
106
|
}
|
|
104
107
|
export declare class Runtime {
|
|
105
108
|
private readonly options;
|
|
@@ -109,6 +112,7 @@ export declare class Runtime {
|
|
|
109
112
|
private readonly timers;
|
|
110
113
|
private stopped;
|
|
111
114
|
private aiBinding;
|
|
115
|
+
private aiRefusedUntil;
|
|
112
116
|
private queueWorkers;
|
|
113
117
|
private queueWake;
|
|
114
118
|
private claimTail;
|
|
@@ -151,6 +155,14 @@ export declare class Runtime {
|
|
|
151
155
|
private awaitDataPath;
|
|
152
156
|
stop(): Promise<void>;
|
|
153
157
|
private every;
|
|
158
|
+
/**
|
|
159
|
+
* Puts this version's AI binding in the environment before a handler runs;
|
|
160
|
+
* app code reads OPENAI_BASE_URL and OPENAI_API_KEY itself, so the fetch
|
|
161
|
+
* cannot wait for the first AI call. A binding the gateway refuses (a
|
|
162
|
+
* capability the company has no route for, a version it has not finished)
|
|
163
|
+
* does not stop the handler: the refusal is logged and remembered for
|
|
164
|
+
* aiRetryMs, and only the handler's AI calls fail (ADR 0096).
|
|
165
|
+
*/
|
|
154
166
|
private prepareAI;
|
|
155
167
|
private refreshToken;
|
|
156
168
|
private scheduleTick;
|
package/dist/runtime.js
CHANGED
|
@@ -87,6 +87,7 @@ const MAX_BODY_BYTES = 1 << 20;
|
|
|
87
87
|
const LEASE_MS = 5 * 60_000;
|
|
88
88
|
const QUEUE_WORKERS = 2;
|
|
89
89
|
const QUEUE_POLL_MS = 2_000;
|
|
90
|
+
const AI_RETRY_MS = 30_000;
|
|
90
91
|
const TOKEN_REFRESH_MS = 60_000;
|
|
91
92
|
// A gateway session lasts an hour. An action runs at most 300 s, so a request
|
|
92
93
|
// takes a new one in the session's last 10 minutes; an Event Function
|
|
@@ -318,6 +319,7 @@ export class Runtime {
|
|
|
318
319
|
timers = [];
|
|
319
320
|
stopped = false;
|
|
320
321
|
aiBinding;
|
|
322
|
+
aiRefusedUntil = 0;
|
|
321
323
|
queueWorkers = 0;
|
|
322
324
|
queueWake = 0;
|
|
323
325
|
claimTail = Promise.resolve();
|
|
@@ -510,9 +512,17 @@ export class Runtime {
|
|
|
510
512
|
};
|
|
511
513
|
this.timers.push(setTimeout(loop, ms));
|
|
512
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* Puts this version's AI binding in the environment before a handler runs;
|
|
517
|
+
* app code reads OPENAI_BASE_URL and OPENAI_API_KEY itself, so the fetch
|
|
518
|
+
* cannot wait for the first AI call. A binding the gateway refuses (a
|
|
519
|
+
* capability the company has no route for, a version it has not finished)
|
|
520
|
+
* does not stop the handler: the refusal is logged and remembered for
|
|
521
|
+
* aiRetryMs, and only the handler's AI calls fail (ADR 0096).
|
|
522
|
+
*/
|
|
513
523
|
async prepareAI() {
|
|
514
524
|
const versionId = process.env.ISOMORPH_AI_VERSION_ID;
|
|
515
|
-
if (!versionId)
|
|
525
|
+
if (!versionId || Date.now() < this.aiRefusedUntil)
|
|
516
526
|
return;
|
|
517
527
|
this.aiBinding ??= (async () => {
|
|
518
528
|
const token = process.env.ISOMORPH_WORKLOAD_TOKEN;
|
|
@@ -520,7 +530,7 @@ export class Runtime {
|
|
|
520
530
|
throw new Error('AI workload identity is unavailable.');
|
|
521
531
|
const response = await fetch(`${this.options.gatewayUrl}/_harbour/internal/ai/connection`, { method: 'POST', headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' }, body: JSON.stringify({ versionId }), signal: AbortSignal.timeout(15_000) });
|
|
522
532
|
if (!response.ok)
|
|
523
|
-
throw new Error(
|
|
533
|
+
throw new Error(`the gateway answered ${response.status} ${(await response.text()).slice(0, 200)}`);
|
|
524
534
|
const result = await response.json();
|
|
525
535
|
const binding = result.data;
|
|
526
536
|
if (!binding?.key || !binding.gatewayBaseUrl || new URL(binding.gatewayBaseUrl).protocol !== 'https:')
|
|
@@ -533,7 +543,11 @@ export class Runtime {
|
|
|
533
543
|
process.env.ISOMORPH_AI_IMAGE_MODEL_ALIAS = binding.modelAliases.image;
|
|
534
544
|
if (binding.modelAliases?.document)
|
|
535
545
|
process.env.ISOMORPH_AI_DOCUMENT_MODEL_ALIAS = binding.modelAliases.document;
|
|
536
|
-
})().catch(error => {
|
|
546
|
+
})().catch((error) => {
|
|
547
|
+
this.aiBinding = undefined;
|
|
548
|
+
this.aiRefusedUntil = Date.now() + (this.options.aiRetryMs ?? AI_RETRY_MS);
|
|
549
|
+
this.log(`AI is not available to this app version (${versionId}): ${error instanceof Error ? error.message : String(error)}; its AI calls fail until the binding is`);
|
|
550
|
+
});
|
|
537
551
|
await this.aiBinding;
|
|
538
552
|
}
|
|
539
553
|
async refreshToken(minValidityMs = SESSION_MIN_VALIDITY_MS) {
|
|
@@ -701,7 +715,7 @@ export class Runtime {
|
|
|
701
715
|
response.writeHead(200, { 'content-type': PROGRESS_MEDIA_TYPE, ...(requestId ? { 'x-request-id': requestId } : {}) });
|
|
702
716
|
response.write(line);
|
|
703
717
|
};
|
|
704
|
-
const context = { signal: controller.signal, user, tenant, app, requestId: requestId ?? '', progress, transaction: (fn) => this.transaction(name, fn) };
|
|
718
|
+
const context = { computerView: header(request, 'X-Harbour-Computer-View') === '1', signal: controller.signal, user, tenant, app, requestId: requestId ?? '', progress, transaction: (fn) => this.transaction(name, fn) };
|
|
705
719
|
const raw = await readBody(request);
|
|
706
720
|
if (raw === undefined)
|
|
707
721
|
return refuse(response, 413, 'VALIDATION_FAILED', 'the request body is larger than 1 MiB', requestId);
|