@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.
@@ -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('This app version has no active AI connection yet.');
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 => { this.aiBinding = undefined; throw 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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/app-sdk",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",