@simular-ai/sai-sdk 0.0.0-stage → 0.1.0-alpha.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Simular, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,305 @@
1
- # Temporary Holding Version
1
+ # @simular-ai/sai-sdk (TypeScript)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ TypeScript SDK for the [Sai API](https://platform.simular.ai/documentation). Hand desktop work to Sai on
4
+ a computer (a Sai cloud computer, or your own PC with the Sai app) and get the answer back, or drive a
5
+ cloud computer yourself.
6
+
7
+ Status: **alpha (0.1)**. The surface may still change before 1.0. It matches the
8
+ [Python SDK](../python/README.md) method for method.
9
+
10
+ | Layer | What it is | Status |
11
+ | -------------- | --------------------------------------------- | ---------------- |
12
+ | `sai.sessions` | A conversation with Sai on one computer | available |
13
+ | `sai.control` | Drive a cloud computer directly with Simulang | research preview |
14
+ | `sai.turns` | A turn's record: typed output, evidence | available |
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @simular-ai/sai-sdk
20
+ export SAI_API_KEY=sapi_... # create one at https://platform.simular.ai
21
+ ```
22
+
23
+ Node 18.17 or later (it uses the built-in `fetch`). No runtime dependencies. ESM only.
24
+
25
+ ## Give Sai a task
26
+
27
+ ```ts
28
+ import { Sai } from '@simular-ai/sai-sdk'
29
+
30
+ const sai = new Sai()
31
+ const [pc] = await sai.machines.list()
32
+
33
+ const session = await sai.sessions.create({ machine: pc })
34
+ const result = await session.run('Open Notepad, type hello, save it to the desktop as hello.txt')
35
+
36
+ console.log(result.status) // idle | needs_approval | error
37
+ console.log(result.text) // Sai's answer
38
+ console.log(result.usage) // tokens and cost for this task
39
+ ```
40
+
41
+ `machine` can be left out when the account has one computer. More `session.run(...)` calls continue
42
+ the same conversation; `sai.sessions.create()` starts a new one with no memory of the old.
43
+
44
+ ### Watch it work
45
+
46
+ ```ts
47
+ const turn = await session.send("Find last month's invoices in Downloads and total them")
48
+ for await (const ev of turn.events()) {
49
+ if (ev.kind === 'step') console.log(' ', ev.text)
50
+ else if (ev.kind === 'approval') await turn.respond(ev.approval!, 'yes')
51
+ }
52
+ const result = await turn.wait()
53
+ ```
54
+
55
+ `ev.kind` is one of `step`, `message`, `approval`, `tool_error`, `error`, `finished`.
56
+ `turn.events({ raw: true })` adds the wire frames (reasoning, tool calls) as `other`.
57
+
58
+ ### Approvals
59
+
60
+ `run()` / `wait()` resolve with `status === 'needs_approval'` unless you pass a handler:
61
+
62
+ ```ts
63
+ const result = await session.run('Clean up the temp folder', {
64
+ onApproval: (a) => {
65
+ if (a.isLinkOnly) {
66
+ console.log('Approve in a browser:', a.url)
67
+ return null // a person answers it; keep waiting
68
+ }
69
+ if (a.type === 'choice') return ['yes', a.questions.map((q: any) => [q.options[0].value])]
70
+ return 'yes'
71
+ },
72
+ })
73
+ ```
74
+
75
+ For an unattended run with nobody to answer, `autonomous: true` runs the turn without approval
76
+ cards, as if `'task'` (trust for the rest of the task) had been answered up front. The grant ends
77
+ with the turn. Approvals that only a person can complete in a browser (`isLinkOnly`) still stop the
78
+ turn.
79
+
80
+ ```ts
81
+ await session.run("Export last month's invoices to CSV", { autonomous: true })
82
+ ```
83
+
84
+ ### Files, models, stopping
85
+
86
+ ```ts
87
+ const report = await sai.files.upload('report.pdf') // or { data: bytes, name: 'report.pdf' }
88
+ await session.run('Summarise the attached report', { attachments: [report] })
89
+
90
+ const allowed = (await sai.models.list()).filter((m) => m.allowed).map((m) => m.id)
91
+ await session.run('...', { model: 'anthropic/claude-opus-5-5' }) // sticks to the session
92
+
93
+ const turn = await session.send('...')
94
+ await turn.abort()
95
+ ```
96
+
97
+ ## Get typed results
98
+
99
+ Any message can also return structured data. Pass `outputSchema` and the turn's record comes back in
100
+ `result.record`, with `output` matching the schema:
101
+
102
+ ```ts
103
+ const session = await sai.sessions.create({ machine: pc })
104
+ const result = await session.run(
105
+ 'Open Notepad, type "hello" and save it on the Desktop as hello.txt. Return the saved file\'s path.',
106
+ {
107
+ outputSchema: {
108
+ type: 'object',
109
+ properties: { path: { type: 'string' } },
110
+ required: ['path'],
111
+ },
112
+ },
113
+ )
114
+ if (result.ok) console.log((result.record?.output as { path: string }).path)
115
+ else console.log(result.record?.reasonCode) // e.g. output_invalid, with result.record.rawAnswer
116
+ ```
117
+
118
+ More options, all optional (the SDK sends them to the API as `options`):
119
+
120
+ | Option | What it does |
121
+ | ------------------ | ---------------------------------------------------------------------------- |
122
+ | `inputs` | Fill `{{name}}` placeholders in the message; every placeholder needs a value |
123
+ | `outputSchema` | JSON Schema (at most 32 KB) the output must match |
124
+ | `successCriteria` | A guideline for a judge, or `{ id, check, returnEvidence? }` yes/no checks |
125
+ | `idempotencyKey` | The same key again returns the earlier turn and sends nothing |
126
+ | `recordTrajectory` | Keep a screenshot and the action for each step |
127
+
128
+ `inputs`, `outputSchema` or `successCriteria` make the message _structured_. A structured message
129
+ behaves like any other (same session, same computer); it only adds the typed result. It is rejected with
130
+ `ConflictError` (`code === 'session_busy'`) while the session is working, because it starts a turn of
131
+ its own. A plain message sent then joins the running turn instead.
132
+
133
+ ```ts
134
+ const result = await session.run(
135
+ 'Save "Invoice {{invoice_number}}: {{amount}} USD" on the Desktop as {{file_name}}. Return its path.',
136
+ {
137
+ inputs: { invoice_number: 'INV-1001', amount: '42.50', file_name: 'inv-1001.txt' },
138
+ outputSchema: {
139
+ type: 'object',
140
+ properties: { path: { type: 'string' } },
141
+ required: ['path'],
142
+ },
143
+ successCriteria: [
144
+ {
145
+ id: 'saved',
146
+ check: 'The file is saved and contains the invoice line',
147
+ returnEvidence: true,
148
+ },
149
+ ],
150
+ idempotencyKey: 'inv-1001', // a retry returns this turn and sends nothing
151
+ recordTrajectory: true,
152
+ },
153
+ )
154
+ const record = result.record
155
+ console.log(record?.status) // succeeded | failed
156
+ console.log(record?.evidence) // [{ id: 'saved', passed: true, score: 0.94, evidence: '...' }]
157
+ console.log(record?.usage) // tokens of the agent's model calls in this turn
158
+ ```
159
+
160
+ `result.ok` is true only when the turn succeeded. On `failed`, `record.reasonCode` says why
161
+ (`agent_error`, `output_invalid`, `criteria_not_met`, ...); the output is never a partial object.
162
+
163
+ **Reading a turn later.** Every message gets a turn id, `turn.id` (plain messages too):
164
+
165
+ ```ts
166
+ const turn = await session.send('...')
167
+ let record = await sai.turns.get(turn) // never waits; `finalizing` while the result is worked out
168
+ record = await sai.turns.wait(turn, { timeoutMs: 600_000 }) // until succeeded, failed or needs_input
169
+ for (const step of await sai.turns.trajectory(turn)) {
170
+ // needs recordTrajectory: true
171
+ console.log(step.index, step.title, step.code)
172
+ if (step.hasScreenshot)
173
+ await writeFile(`step${step.index}.jpg`, await sai.turns.screenshot(turn, step.index))
174
+ }
175
+ ```
176
+
177
+ Each takes a `Turn`, a `TurnRecord` or the id string. `record.status` is `running`, `needs_input` (an
178
+ approval is pending: answer it on `sai.sessions.resume(record.sessionId)`), `finalizing`, `succeeded` or
179
+ `failed`.
180
+
181
+ ## Drive a computer yourself
182
+
183
+ Direct control runs [Simulang](https://platform.simular.ai/documentation/concepts/direct-control)
184
+ blocks on a cloud computer. While the lease is open, Sai's own tasks on that computer wait, so always close it.
185
+
186
+ ```ts
187
+ const lease = await sai.control.open({ machine: pc })
188
+ try {
189
+ console.log((await lease.shell('hostname')).stdout)
190
+
191
+ const r = await lease.exec(`
192
+ var page = await machine.browser.newtab('https://example.com')
193
+ var snap = await page.snapshot()
194
+ snap.title
195
+ `)
196
+ console.log(r.result) // Example Domain
197
+ await writeFile(`page.${r.screenshotMimeType!.split('/')[1]}`, r.screenshot!) // jpeg or png
198
+ } finally {
199
+ await lease.close()
200
+ }
201
+ ```
202
+
203
+ With TypeScript 5.2+ and a runtime that supports explicit resource management, `await using lease = ...`
204
+ closes it for you.
205
+
206
+ `exec()` resolves with an `ExecResult`; a block that threw still resolves, so check `r.error`
207
+ (`exception`, `timeout`, `interrupted`). Use `var`, not `let`/`const`, for values the next block needs.
208
+ `lease.screenshot()` grabs the whole screen; it needs the computer's screen-capture service, and where that is
209
+ off it throws `BlockError`.
210
+
211
+ ## Learn code from turns (research preview)
212
+
213
+ `sai.learn` turns what the agent did into Simulang code you own, and repairs that code when it breaks.
214
+ `sai.judge` checks recorded turns against criteria you choose afterwards. The turns must be sent with
215
+ `recordTrajectory: true`.
216
+
217
+ ```ts
218
+ const turn = await session.send('Open {{site}} and read the title of the top story', {
219
+ inputs: { site: 'https://news.ycombinator.com' },
220
+ recordTrajectory: true,
221
+ })
222
+ await sai.turns.wait(turn)
223
+
224
+ const learned = await sai.learn.create([turn]) // about two minutes
225
+ console.log(
226
+ learned.template,
227
+ learned.parameters.map((p) => p.name),
228
+ )
229
+ console.log(learned.code) // async function main(params = { site: "..." }) { ... }
230
+
231
+ await using lease = await sai.control.open({ machine: pc })
232
+ const r = await lease.run(learned, { site: 'https://lobste.rs' }) // inputs over the defaults
233
+ console.log(r.result) // what main returned
234
+
235
+ // It broke on a new site: repair it from the run (or from feedback, or a new turn that solved it)
236
+ const fixed = await sai.learn.create([], {
237
+ initialCode: learned.code,
238
+ evaluations: [{ inputs: { site: 'https://lobste.rs' }, result: r, passed: false }],
239
+ })
240
+ console.log(fixed.changes)
241
+
242
+ const judgement = await sai.judge.create(
243
+ [turn],
244
+ [{ id: 'title', check: 'The agent reported a story title' }],
245
+ )
246
+ console.log(judgement.verdict, judgement.criteria[0]?.score)
247
+ ```
248
+
249
+ `learned.status` is `failed` when the code still has `learned.validation.errors` after one retry.
250
+ Pass a judgement as `{ ..., judgement }` in an evaluation to repair code from its evidence.
251
+
252
+ ## Run Sai for your customers
253
+
254
+ With an [Organisation](https://platform.simular.ai/documentation/organisations), each customer is a
255
+ Tenant. Your backend manages Tenants with an Organisation Key, and acts as one with a Tenant token.
256
+
257
+ ```ts
258
+ const org = new Sai({ apiKey: 'sapi_...', orgId: 'org_...' }) // an Organisation Key; or SAI_API_KEY, SAI_ORG_ID
259
+
260
+ const tenant = await org.tenants.create('Riverside Clinic', { externalId: 'clinic-4821' })
261
+ const code = await org.tenants.createEnrollment(tenant) // hand code.token to your installer
262
+ console.log(await org.tenantMachines.list(tenant))
263
+
264
+ const clinic = await org.tenants.client(tenant) // acts as the Tenant; renews its token
265
+ const session = await clinic.sessions.create()
266
+ await session.run("Open the calendar and read today's appointments")
267
+ ```
268
+
269
+ - `tenants.list({ status, externalId, limit, cursor })` resolves with one page: `page.tenants`,
270
+ `page.nextCursor`. `externalId` finds your customer's Tenant without storing `tenant.id`.
271
+ - `tenants.update(tenant, { name, metadata, status: 'disabled' | 'active' })`; `tenants.delete(tenant)`
272
+ disables it and revokes every machine.
273
+ - `tenants.token(tenant)` is the raw exchange: a one-hour `TenantToken` to pass as
274
+ `new Sai({ apiKey: token.token })`.
275
+ - `tenantMachines.get(tenant, machineId)`, `tenantMachines.revoke(tenant, machine)`.
276
+
277
+ ## Errors
278
+
279
+ Every non-2xx response throws a `SaiError` subclass with `status`, `code`, `message` and `retryAfter`:
280
+ `AuthenticationError` (401), `BadRequestError` (400), `PermissionDeniedError` (403), `NotFoundError`
281
+ (404), `ConflictError` (409), `LinkOnlyApprovalError` (`approvalUrl`), `GoneError` (410),
282
+ `SpendCapReachedError` (403, the key reached its monthly limit; `resetsAt`), `RateLimitError` (429),
283
+ `ServiceUnavailableError` (503), `ConnectionError`, `BlockError`.
284
+
285
+ A task that ran out of credit ends with `result.status === 'error'` and `result.errorCode` set to
286
+ `insufficient_credits` or `free_computer_time_exhausted`: stop retrying for today.
287
+
288
+ ## Configuration
289
+
290
+ | Option | Env | Default |
291
+ | ----------- | ------------- | ------------------------ |
292
+ | `apiKey` | `SAI_API_KEY` | required |
293
+ | `baseUrl` | `SAI_API_URL` | `https://api.simular.ai` |
294
+ | `timeoutMs` | | 60 000 per request |
295
+ | `fetch` | | global `fetch` |
296
+ | `orgId` | `SAI_ORG_ID` | for `tenants.token()` |
297
+
298
+ ## Development
299
+
300
+ ```bash
301
+ cd typescript
302
+ npm install
303
+ npm test
304
+ npm run typecheck
305
+ ```
@@ -0,0 +1,78 @@
1
+ import { Control } from './control.js';
2
+ import { Http, type Credential, type FetchLike } from './http.js';
3
+ import { Machines } from './machines.js';
4
+ import { Sessions } from './sessions.js';
5
+ import { TenantMachines, Tenants } from './tenants.js';
6
+ import { Turns } from './turns.js';
7
+ import { Judge, Learn } from './learn.js';
8
+ import { type Account, type Attachment, type Model } from './types.js';
9
+ export interface SaiOptions {
10
+ /** Defaults to `process.env.SAI_API_KEY`. */
11
+ apiKey?: string;
12
+ /** Defaults to `process.env.SAI_API_URL`, then `https://api.simular.ai`. */
13
+ baseUrl?: string;
14
+ /** Per request. Default 60 000. */
15
+ timeoutMs?: number;
16
+ versionTag?: string;
17
+ /** Replace `fetch`, e.g. in tests. */
18
+ fetch?: FetchLike;
19
+ /**
20
+ * The Organisation that `tenants.token()` and `tenants.client()` exchange for. Defaults to
21
+ * `process.env.SAI_ORG_ID`. Only an Organisation Key needs it.
22
+ */
23
+ orgId?: string;
24
+ }
25
+ /**
26
+ * The Sai API client.
27
+ *
28
+ * ```ts
29
+ * const sai = new Sai() // SAI_API_KEY, and SAI_API_URL if set
30
+ * const session = await sai.sessions.create()
31
+ * console.log((await session.run('Open Notepad and type hello')).text)
32
+ * ```
33
+ */
34
+ export declare class Sai {
35
+ readonly machines: Machines;
36
+ readonly sessions: Sessions;
37
+ readonly turns: Turns;
38
+ /** Recorded turns → Simulang code, and code repair (research preview). */
39
+ readonly learn: Learn;
40
+ /** Judge recorded turns against new criteria (research preview). */
41
+ readonly judge: Judge;
42
+ readonly control: Control;
43
+ readonly models: Models;
44
+ readonly files: Files;
45
+ readonly account: AccountResource;
46
+ /** Your customers under an Organisation. Takes an Organisation Key. */
47
+ readonly tenants: Tenants;
48
+ /** A Tenant's enrolled computers. Takes an Organisation Key. */
49
+ readonly tenantMachines: TenantMachines;
50
+ /** @param credential internal: `tenants.client()` passes a token source instead of `apiKey`. */
51
+ constructor(opts?: SaiOptions, credential?: Credential);
52
+ }
53
+ export declare class Models {
54
+ private readonly http;
55
+ constructor(http: Http);
56
+ /** Models a session can run on; `allowed` says whether this plan may use each. */
57
+ list(): Promise<Model[]>;
58
+ }
59
+ export declare class Files {
60
+ private readonly http;
61
+ constructor(http: Http);
62
+ /**
63
+ * Upload one file (max 25 MB) to attach to a message. Pass a path (Node), or
64
+ * the bytes with a name.
65
+ */
66
+ upload(file: string | {
67
+ data: Uint8Array | Blob;
68
+ name: string;
69
+ }, opts?: {
70
+ name?: string;
71
+ }): Promise<Attachment>;
72
+ }
73
+ export declare class AccountResource {
74
+ private readonly http;
75
+ constructor(http: Http);
76
+ /** Plan and spendable credit. Slow-ish (billing lookup): not for a poll loop. */
77
+ get(): Promise<Account>;
78
+ }
package/dist/client.js ADDED
@@ -0,0 +1,110 @@
1
+ import { Control } from './control.js';
2
+ import { DEFAULT_BASE_URL, Http } from './http.js';
3
+ import { Machines } from './machines.js';
4
+ import { Sessions } from './sessions.js';
5
+ import { TenantMachines, Tenants } from './tenants.js';
6
+ import { Turns } from './turns.js';
7
+ import { Judge, Learn } from './learn.js';
8
+ import { parseAccount, parseAttachment, parseModel, } from './types.js';
9
+ /**
10
+ * The Sai API client.
11
+ *
12
+ * ```ts
13
+ * const sai = new Sai() // SAI_API_KEY, and SAI_API_URL if set
14
+ * const session = await sai.sessions.create()
15
+ * console.log((await session.run('Open Notepad and type hello')).text)
16
+ * ```
17
+ */
18
+ export class Sai {
19
+ machines;
20
+ sessions;
21
+ turns;
22
+ /** Recorded turns → Simulang code, and code repair (research preview). */
23
+ learn;
24
+ /** Judge recorded turns against new criteria (research preview). */
25
+ judge;
26
+ control;
27
+ models;
28
+ files;
29
+ account;
30
+ /** Your customers under an Organisation. Takes an Organisation Key. */
31
+ tenants;
32
+ /** A Tenant's enrolled computers. Takes an Organisation Key. */
33
+ tenantMachines;
34
+ /** @param credential internal: `tenants.client()` passes a token source instead of `apiKey`. */
35
+ constructor(opts = {}, credential) {
36
+ const env = typeof process !== 'undefined' ? process.env : {};
37
+ const key = credential ?? opts.apiKey ?? env.SAI_API_KEY;
38
+ if (!key) {
39
+ throw new Error('No API key: pass apiKey or set SAI_API_KEY. Create one at https://platform.simular.ai.');
40
+ }
41
+ const http = new Http(key, opts.baseUrl ?? env.SAI_API_URL ?? DEFAULT_BASE_URL, opts.timeoutMs ?? 60_000, opts.versionTag, opts.fetch ?? ((input, init) => fetch(input, init)));
42
+ this.machines = new Machines(http);
43
+ this.sessions = new Sessions(http, this.machines);
44
+ this.turns = new Turns(http);
45
+ this.learn = new Learn(http);
46
+ this.judge = new Judge(http);
47
+ this.control = new Control(http);
48
+ this.models = new Models(http);
49
+ this.files = new Files(http);
50
+ this.account = new AccountResource(http);
51
+ this.tenants = new Tenants(http, opts.orgId ?? env.SAI_ORG_ID, (c) => new Sai(opts, c));
52
+ this.tenantMachines = new TenantMachines(http);
53
+ }
54
+ }
55
+ export class Models {
56
+ http;
57
+ constructor(http) {
58
+ this.http = http;
59
+ }
60
+ /** Models a session can run on; `allowed` says whether this plan may use each. */
61
+ async list() {
62
+ const d = await this.http.json('GET', '/v1/agents/models');
63
+ return (d.models ?? []).map(parseModel);
64
+ }
65
+ }
66
+ export class Files {
67
+ http;
68
+ constructor(http) {
69
+ this.http = http;
70
+ }
71
+ /**
72
+ * Upload one file (max 25 MB) to attach to a message. Pass a path (Node), or
73
+ * the bytes with a name.
74
+ */
75
+ async upload(file, opts = {}) {
76
+ let data;
77
+ let name;
78
+ if (typeof file === 'string') {
79
+ const { readFile } = await import('node:fs/promises');
80
+ const { basename } = await import('node:path');
81
+ data = new Uint8Array(await readFile(file));
82
+ name = opts.name ?? basename(file);
83
+ }
84
+ else {
85
+ data = file.data;
86
+ name = opts.name ?? file.name;
87
+ }
88
+ const d = await this.http.json('POST', '/v1/agents/upload', {
89
+ body: data instanceof Blob ? data : new Blob([data]),
90
+ // The server reads only a body that has a Content-Type; it derives the MIME type from the name.
91
+ headers: {
92
+ 'Content-Type': 'application/octet-stream',
93
+ 'x-filename': encodeURIComponent(name),
94
+ },
95
+ timeoutMs: 120_000,
96
+ });
97
+ return parseAttachment(d);
98
+ }
99
+ }
100
+ export class AccountResource {
101
+ http;
102
+ constructor(http) {
103
+ this.http = http;
104
+ }
105
+ /** Plan and spendable credit. Slow-ish (billing lookup): not for a poll loop. */
106
+ async get() {
107
+ return parseAccount(await this.http.json('GET', '/v1/agents/account'));
108
+ }
109
+ }
110
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Direct control: drive a cloud computer with Simulang. No agent.
3
+ *
4
+ * Maps to `/v1/machines/{id}/control`, a lease on the machine: at most one per
5
+ * machine, so the machine id is the handle. Research preview, open to every
6
+ * account; model calls the code makes (`callLLM`, `ground`, `stateSatisfies`)
7
+ * are billed as usual.
8
+ */
9
+ import type { Http } from './http.js';
10
+ import { type MachineRef } from './machines.js';
11
+ import { type ExecResult, type LearnResult } from './types.js';
12
+ export declare class Control {
13
+ private readonly http;
14
+ constructor(http: Http);
15
+ /**
16
+ * Take a cloud computer. Resolves once the lease holds it, up to ~3
17
+ * minutes when a Sai task holds it or it has to start; at once if the lease
18
+ * is already open. Close it when done (`await using`, or `try/finally`):
19
+ * Sai's tasks on that computer wait until then.
20
+ */
21
+ open(opts: {
22
+ machine: MachineRef;
23
+ }): Promise<ControlLease>;
24
+ }
25
+ export declare class ControlLease {
26
+ private readonly http;
27
+ readonly machineId: string;
28
+ readonly idleTimeoutSec: number;
29
+ private closed;
30
+ private readonly path;
31
+ constructor(http: Http, machineId: string, idleTimeoutSec: number);
32
+ [Symbol.asyncDispose](): Promise<void>;
33
+ /**
34
+ * Run one Simulang (JavaScript) block. Use `var` for anything the next block
35
+ * needs. A block that throws, times out or is interrupted still resolves:
36
+ * check `result.error`. Throws `GoneError` once the lease has ended (closed,
37
+ * idle, taken by a Sai task, or the computer stopped answering): open again.
38
+ */
39
+ exec(code: string, opts?: {
40
+ timeoutMs?: number;
41
+ }): Promise<ExecResult>;
42
+ /**
43
+ * Run code from `sai.learn.create()`: its `main` with `inputs` over the parameters' defaults
44
+ * (an argument to `main` replaces its whole default object, so missing inputs keep their
45
+ * defaults). `main`'s return value is `result.result`. A string runs as code with `inputs` as is.
46
+ */
47
+ run(learned: LearnResult | string, inputs?: Record<string, string | number | boolean>, opts?: {
48
+ timeoutMs?: number;
49
+ }): Promise<ExecResult>;
50
+ /** Run a shell command: `{ stdout, stderr, exitCode, timedOut }`. */
51
+ shell(command: string, opts?: {
52
+ cwd?: string;
53
+ timeoutMs?: number;
54
+ }): Promise<{
55
+ stdout: string;
56
+ stderr: string;
57
+ exitCode: number;
58
+ timedOut: boolean;
59
+ }>;
60
+ readFile(path: string): Promise<string>;
61
+ writeFile(path: string, text: string): Promise<void>;
62
+ /**
63
+ * The whole screen as image bytes (usually JPEG; use `exec` for the MIME type). Needs the
64
+ * computer's screen-capture service; throws `BlockError` where it is off (a page or app
65
+ * snapshot still carries an image).
66
+ */
67
+ screenshot(): Promise<Uint8Array>;
68
+ /** Stop the running block at its next step. False if nothing was running. */
69
+ interrupt(): Promise<boolean>;
70
+ /** `{ status: open|closed, closedReason?, lastExecAt, ... }`. */
71
+ get(): Promise<Record<string, unknown>>;
72
+ /** Clear the REPL and release the computer. Safe to call twice. */
73
+ close(): Promise<void>;
74
+ }