Examples
Node
The loop with built-in fetch. No dependencies.
View as MarkdownRequires Node.js 18 or newer for global fetch.
Polling
javascript
// sai.mjs
const API = process.env.SAI_API_URL ?? 'https://api.simular.ai'
const HEADERS = {
Authorization: `Bearer ${process.env.SAI_API_KEY}`,
'Content-Type': 'application/json',
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
async function api(method, path, body) {
const res = await fetch(`${API}/v1/agents${path}`, {
method,
headers: HEADERS,
body: body ? JSON.stringify(body) : undefined,
})
if (res.status === 429) {
await sleep(60_000)
return api(method, path, body)
}
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
return res.json()
}
async function run(task, machineId) {
const { sessionId } = await api('POST', '/message', {
message: task,
wait: false,
...(machineId && { machineId }),
})
let cursor
for (;;) {
const qs = new URLSearchParams({ sessionId, wait: '30' })
if (cursor) qs.set('since', cursor)
const page = await api('GET', `/events?${qs}`)
cursor = page.cursor
for (const ev of page.events) {
if (ev.type === 'data-progress') console.log(' ', ev.data.text)
}
switch (page.status) {
case 'running':
continue
case 'needs_approval': {
const a = page.approval
if (a.isLinkOnly) {
console.log('Open in a browser:', a.approvalUrl)
await sleep(15_000)
} else if (a.approvalType === 'choice') {
const selections = a.questions.map((q) => [q.options[0].value])
await api('POST', '/approve', { approvalId: a.approvalId, response: 'yes', selections })
} else {
console.log('Approving:', a.command ?? a.title)
await api('POST', '/approve', { approvalId: a.approvalId, response: 'yes' })
}
continue
}
case 'idle':
return page.text
default: {
const err = page.events.find((e) => e.type === 'error')
throw new Error(err?.errorText ?? 'task failed')
}
}
}
}
console.log(await run('Open Notepad, type hello, save it to the desktop as hello.txt'))bash
SAI_API_KEY=sapi_... node sai.mjsThe choice handler picks the first option. Replace it with your own logic. To stop a task early, api("POST", "/abort", { sessionId }).
Uploading a file
javascript
import { readFile } from 'node:fs/promises'
import { basename } from 'node:path'
async function upload(path) {
const res = await fetch(`${API}/v1/agents/upload`, {
method: 'POST',
headers: {
Authorization: HEADERS.Authorization,
'x-filename': encodeURIComponent(basename(path)),
},
body: await readFile(path),
})
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
return res.json()
}
const attachment = await upload('./report.pdf')
await api('POST', '/message', {
message: 'Summarise the attached PDF',
wait: false,
attachments: [attachment],
})Streaming
For a long-lived process, read the SSE body directly.
javascript
const res = await fetch(`${API}/v1/agents/message`, {
method: 'POST',
headers: { ...HEADERS, Accept: 'text/event-stream' },
body: JSON.stringify({ message: 'What is on the screen right now?' }),
})
const decoder = new TextDecoder()
let buffer = ''
let text = ''
for await (const chunk of res.body) {
buffer += decoder.decode(chunk, { stream: true })
let idx
while ((idx = buffer.indexOf('\n\n')) !== -1) {
const frame = buffer.slice(0, idx)
buffer = buffer.slice(idx + 2)
const data = frame.replace(/^data: /, '')
if (data === '[DONE]') break
const ev = JSON.parse(data)
if (ev.type === 'text-delta') text += ev.delta
if (ev.type === 'data-approval-request') console.log('approval:', ev.data.approvalId)
if (ev.type === 'error') throw new Error(ev.errorText)
}
}
console.log(text)Answer approvals on the streaming path with POST /approve from another request; the stream stays open while it waits.