@agent-compose/sdk 0.8.1 → 0.8.2
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/agent/agent-context.d.ts +1 -1
- package/dist/client.d.ts +29 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.js +267 -85
- package/dist/runtimes/_cli-agent.d.ts +25 -0
- package/dist/runtimes/codex.buildcommand.test.d.ts +9 -0
- package/dist/runtimes/openai-desktop.js +259 -85
- package/dist/sandbox/sizes.d.ts +120 -30
- package/dist/sandbox.d.ts +1 -1
- package/dist/types/api-conversations.d.ts +167 -0
- package/dist/types/api-factory.d.ts +59 -7
- package/dist/types/api-runs.d.ts +2 -1
- package/dist/types/workflow-metadata.d.ts +6 -5
- package/package.json +1 -1
- package/src/agent/agent-context.ts +28 -17
- package/src/client.ts +56 -20
- package/src/index.ts +11 -6
- package/src/runtimes/_cli-agent.ts +293 -56
- package/src/runtimes/codex.ts +8 -2
- package/src/sandbox/providers/e2b.ts +8 -4
- package/src/sandbox/sizes.ts +127 -44
- package/src/sandbox.ts +8 -0
- package/src/types/api-conversations.ts +155 -0
- package/src/types/api-factory.ts +57 -7
- package/src/types/api-runs.ts +2 -1
- package/src/types/workflow-metadata.ts +6 -5
|
@@ -1756,6 +1756,13 @@ class AgentComposeClient {
|
|
|
1756
1756
|
const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/preview/${port}`, { method: "DELETE" });
|
|
1757
1757
|
return body.closed;
|
|
1758
1758
|
}
|
|
1759
|
+
holdBackgroundWork(conversationId, input) {
|
|
1760
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/background-work`, { method: "POST", body: { ...input?.minutes !== undefined ? { minutes: input.minutes } : {} } });
|
|
1761
|
+
}
|
|
1762
|
+
async releaseBackgroundWork(conversationId) {
|
|
1763
|
+
const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/background-work`, { method: "DELETE" });
|
|
1764
|
+
return body.released;
|
|
1765
|
+
}
|
|
1759
1766
|
forkSession(conversationId, input) {
|
|
1760
1767
|
return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/fork`, { method: "POST", body: input?.title ? { title: input.title } : {} });
|
|
1761
1768
|
}
|
|
@@ -2005,7 +2012,7 @@ class AgentComposeClient {
|
|
|
2005
2012
|
async createRepoLink(input, opts) {
|
|
2006
2013
|
const slug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
2007
2014
|
const body = await this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}/repo-links`, { method: "POST", body: input });
|
|
2008
|
-
return body.
|
|
2015
|
+
return body.links;
|
|
2009
2016
|
}
|
|
2010
2017
|
deleteRepoLink(linkId, opts) {
|
|
2011
2018
|
const slug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
@@ -2029,9 +2036,17 @@ class AgentComposeClient {
|
|
|
2029
2036
|
createApiKey(input) {
|
|
2030
2037
|
return this.fetch("/api-keys", { method: "POST", body: input });
|
|
2031
2038
|
}
|
|
2032
|
-
async listApiKeys() {
|
|
2033
|
-
const
|
|
2034
|
-
|
|
2039
|
+
async listApiKeys(opts) {
|
|
2040
|
+
const qs = new URLSearchParams;
|
|
2041
|
+
if (opts?.limit !== undefined)
|
|
2042
|
+
qs.set("limit", String(opts.limit));
|
|
2043
|
+
if (opts?.status !== undefined)
|
|
2044
|
+
qs.set("status", opts.status);
|
|
2045
|
+
if (opts?.cursor !== undefined)
|
|
2046
|
+
qs.set("cursor", opts.cursor);
|
|
2047
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
2048
|
+
const body = await this.fetch(`/api-keys${suffix}`);
|
|
2049
|
+
return { data: body.data, hasMore: body.has_more, nextCursor: body.next_cursor ?? null };
|
|
2035
2050
|
}
|
|
2036
2051
|
getUsage(from, to) {
|
|
2037
2052
|
const qs = `?from=${encodeURIComponent(from.toISOString())}&to=${encodeURIComponent(to.toISOString())}`;
|
|
@@ -3783,6 +3798,29 @@ function wrapJsonlCommand(args) {
|
|
|
3783
3798
|
function now4() {
|
|
3784
3799
|
return new Date().toISOString();
|
|
3785
3800
|
}
|
|
3801
|
+
var TAIL_REATTACH_DELAY_MS = 2000;
|
|
3802
|
+
var LAUNCH_EXEC_TIMEOUT_MS = 30000;
|
|
3803
|
+
var LAUNCH_PID_RECOVERY_ATTEMPTS = 3;
|
|
3804
|
+
var LAUNCH_PID_RECOVERY_DELAY_MS = 1000;
|
|
3805
|
+
var INSTALL_PROBE_TIMEOUT_MS = 30000;
|
|
3806
|
+
function parsePid(raw) {
|
|
3807
|
+
const pid = Number.parseInt(raw.trim().split(`
|
|
3808
|
+
`).pop() ?? "", 10);
|
|
3809
|
+
return Number.isInteger(pid) && pid > 0 ? pid : Number.NaN;
|
|
3810
|
+
}
|
|
3811
|
+
async function recoverLaunchPid(read, pidPath) {
|
|
3812
|
+
for (let attempt = 0;attempt < LAUNCH_PID_RECOVERY_ATTEMPTS; attempt++) {
|
|
3813
|
+
try {
|
|
3814
|
+
const pid = parsePid(await read(pidPath));
|
|
3815
|
+
if (!Number.isNaN(pid))
|
|
3816
|
+
return pid;
|
|
3817
|
+
} catch {}
|
|
3818
|
+
if (attempt < LAUNCH_PID_RECOVERY_ATTEMPTS - 1) {
|
|
3819
|
+
await new Promise((r) => setTimeout(r, LAUNCH_PID_RECOVERY_DELAY_MS));
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
return Number.NaN;
|
|
3823
|
+
}
|
|
3786
3824
|
function shellQuote(value) {
|
|
3787
3825
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
3788
3826
|
}
|
|
@@ -3868,7 +3906,7 @@ class CliAgentRunner {
|
|
|
3868
3906
|
async ensureInstalled() {
|
|
3869
3907
|
if (this.installed)
|
|
3870
3908
|
return;
|
|
3871
|
-
const probe = await this.sandbox.commands.run(`command -v ${this.spec.bin}
|
|
3909
|
+
const probe = await this.sandbox.commands.run(`command -v ${this.spec.bin}`, { timeoutMs: INSTALL_PROBE_TIMEOUT_MS });
|
|
3872
3910
|
if (probe.exitCode === 0) {
|
|
3873
3911
|
this.installed = true;
|
|
3874
3912
|
return;
|
|
@@ -4033,51 +4071,153 @@ class CliAgentRunner {
|
|
|
4033
4071
|
keepBytes: JSONL_GUARD_KEEP_BYTES
|
|
4034
4072
|
});
|
|
4035
4073
|
const lines = new AsyncQueue;
|
|
4036
|
-
let
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
buf = buf.slice(nl + 1);
|
|
4044
|
-
if (line)
|
|
4045
|
-
lines.push(line);
|
|
4046
|
-
}
|
|
4047
|
-
};
|
|
4048
|
-
const runOpts = {
|
|
4049
|
-
...this.options.cwd ? { cwd: this.options.cwd } : {},
|
|
4050
|
-
onStdout,
|
|
4051
|
-
timeoutMs: 0
|
|
4052
|
-
};
|
|
4053
|
-
let exited = false;
|
|
4054
|
-
const settle = (res) => {
|
|
4055
|
-
exited = true;
|
|
4056
|
-
const tail = buf.trim();
|
|
4057
|
-
if (tail)
|
|
4058
|
-
lines.push(tail);
|
|
4059
|
-
lines.close();
|
|
4060
|
-
return res;
|
|
4061
|
-
};
|
|
4062
|
-
const fail = (err) => {
|
|
4063
|
-
exited = true;
|
|
4064
|
-
lines.close();
|
|
4065
|
-
throw err;
|
|
4066
|
-
};
|
|
4067
|
-
let kill;
|
|
4068
|
-
let runPromise;
|
|
4074
|
+
let exitCode = null;
|
|
4075
|
+
let exitStderr = "";
|
|
4076
|
+
let transportError = null;
|
|
4077
|
+
let consumerStopped = false;
|
|
4078
|
+
let reap = () => {};
|
|
4079
|
+
let transport;
|
|
4080
|
+
const durableRead = this.sandbox.files.read?.bind(this.sandbox.files);
|
|
4069
4081
|
if (this.sandbox.commands.runBackground) {
|
|
4070
|
-
const
|
|
4071
|
-
|
|
4072
|
-
|
|
4082
|
+
const outPath = `${promptPath}.out`;
|
|
4083
|
+
const errPath = `${promptPath}.err`;
|
|
4084
|
+
const detachedScript = `{ ${guardedCmd}; } >> ${shellQuote(outPath)} 2>> ${shellQuote(errPath)} </dev/null; ` + `printf '\\n%s %s\\n' ${shellQuote(sentinel)} "$?" >> ${shellQuote(outPath)}`;
|
|
4085
|
+
const pidPath = `${promptPath}.pid`;
|
|
4086
|
+
const launchCmd = `: > ${shellQuote(outPath)}; : > ${shellQuote(errPath)}; ` + `if command -v setsid >/dev/null 2>&1; then setsid sh -c ${shellQuote(detachedScript)} >/dev/null 2>&1 </dev/null & ` + `else sh -c ${shellQuote(detachedScript)} >/dev/null 2>&1 </dev/null & fi; ` + `echo "$!" > ${shellQuote(pidPath)}; echo "$!"`;
|
|
4087
|
+
let pid = Number.NaN;
|
|
4088
|
+
let launched = null;
|
|
4089
|
+
let launchErr = null;
|
|
4090
|
+
try {
|
|
4091
|
+
launched = await this.sandbox.commands.run(launchCmd, {
|
|
4092
|
+
...this.options.cwd ? { cwd: this.options.cwd } : {},
|
|
4093
|
+
timeoutMs: LAUNCH_EXEC_TIMEOUT_MS
|
|
4094
|
+
});
|
|
4095
|
+
} catch (err) {
|
|
4096
|
+
launchErr = err;
|
|
4097
|
+
}
|
|
4098
|
+
if (launched) {
|
|
4099
|
+
pid = parsePid(launched.stdout ?? "");
|
|
4100
|
+
if (launched.exitCode !== 0 || Number.isNaN(pid)) {
|
|
4101
|
+
throw new Error(`failed to launch ${this.spec.kind}: ` + `exit ${launched.exitCode}${launched.stderr ? `: ${launched.stderr.slice(-500)}` : ""}`);
|
|
4102
|
+
}
|
|
4103
|
+
} else {
|
|
4104
|
+
pid = durableRead ? await recoverLaunchPid(durableRead, pidPath) : Number.NaN;
|
|
4105
|
+
if (Number.isNaN(pid))
|
|
4106
|
+
throw launchErr;
|
|
4107
|
+
console.warn(`[cli-agent] ${this.spec.kind} launch exec stream died but the detached runner is up ` + `(pid ${pid} via ${pidPath}) — continuing on the durable transport: ${formatError(launchErr)}`);
|
|
4108
|
+
}
|
|
4109
|
+
let offset = 0;
|
|
4110
|
+
let currentTail = null;
|
|
4111
|
+
const consumeLine = (raw) => {
|
|
4112
|
+
offset += Buffer.byteLength(raw, "utf8") + 1;
|
|
4113
|
+
const line = raw.trim();
|
|
4114
|
+
if (!line)
|
|
4115
|
+
return;
|
|
4116
|
+
if (line.startsWith(sentinel)) {
|
|
4117
|
+
const code = Number(line.slice(sentinel.length).trim());
|
|
4118
|
+
exitCode = Number.isFinite(code) ? code : 0;
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
lines.push(line);
|
|
4122
|
+
};
|
|
4123
|
+
reap = () => {
|
|
4124
|
+
this.sandbox.commands.run(`kill -TERM -- -${pid} 2>/dev/null; kill -TERM ${pid} 2>/dev/null; true`, { timeoutMs: 1e4 }).catch(() => {});
|
|
4125
|
+
currentTail?.kill().catch(() => {});
|
|
4126
|
+
};
|
|
4127
|
+
transport = (async () => {
|
|
4128
|
+
while (exitCode === null && !consumerStopped && !opts.signal?.aborted) {
|
|
4129
|
+
let buf = "";
|
|
4130
|
+
let handle = null;
|
|
4131
|
+
const onStdout = (data) => {
|
|
4132
|
+
buf += data;
|
|
4133
|
+
let nl;
|
|
4134
|
+
while ((nl = buf.indexOf(`
|
|
4135
|
+
`)) >= 0) {
|
|
4136
|
+
consumeLine(buf.slice(0, nl));
|
|
4137
|
+
buf = buf.slice(nl + 1);
|
|
4138
|
+
if (exitCode !== null) {
|
|
4139
|
+
handle?.kill().catch(() => {});
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
};
|
|
4144
|
+
try {
|
|
4145
|
+
handle = await this.sandbox.commands.runBackground(`tail -c +${offset + 1} -f ${shellQuote(outPath)}`, { timeoutMs: 0, onStdout });
|
|
4146
|
+
} catch {}
|
|
4147
|
+
if (handle) {
|
|
4148
|
+
if (exitCode !== null)
|
|
4149
|
+
handle.kill().catch(() => {});
|
|
4150
|
+
currentTail = handle;
|
|
4151
|
+
await handle.wait().catch(() => {
|
|
4152
|
+
return;
|
|
4153
|
+
});
|
|
4154
|
+
currentTail = null;
|
|
4155
|
+
}
|
|
4156
|
+
if (exitCode !== null || consumerStopped || opts.signal?.aborted)
|
|
4157
|
+
break;
|
|
4158
|
+
if (durableRead) {
|
|
4159
|
+
try {
|
|
4160
|
+
const bytes = Buffer.from(await durableRead(outPath), "utf8");
|
|
4161
|
+
let rest = bytes.subarray(offset).toString("utf8");
|
|
4162
|
+
let nl;
|
|
4163
|
+
while (exitCode === null && (nl = rest.indexOf(`
|
|
4164
|
+
`)) >= 0) {
|
|
4165
|
+
consumeLine(rest.slice(0, nl));
|
|
4166
|
+
rest = rest.slice(nl + 1);
|
|
4167
|
+
}
|
|
4168
|
+
} catch {}
|
|
4169
|
+
}
|
|
4170
|
+
if (exitCode !== null)
|
|
4171
|
+
break;
|
|
4172
|
+
let alive = null;
|
|
4173
|
+
try {
|
|
4174
|
+
const probe = await this.sandbox.commands.run(`kill -0 ${pid} 2>/dev/null`, { timeoutMs: 1e4 });
|
|
4175
|
+
alive = probe.exitCode === 0;
|
|
4176
|
+
} catch {
|
|
4177
|
+
alive = null;
|
|
4178
|
+
}
|
|
4179
|
+
if (alive === false) {
|
|
4180
|
+
exitCode = JSONL_GUARD_NO_SENTINEL_EXIT;
|
|
4181
|
+
break;
|
|
4182
|
+
}
|
|
4183
|
+
console.warn(`[cli-agent] ${this.spec.kind} turn stream lost with the runner alive — re-tailing ${outPath} from byte ${offset}`);
|
|
4184
|
+
await new Promise((r) => setTimeout(r, TAIL_REATTACH_DELAY_MS));
|
|
4185
|
+
}
|
|
4186
|
+
if (exitCode !== null && exitCode !== 0 && durableRead) {
|
|
4187
|
+
try {
|
|
4188
|
+
exitStderr = (await durableRead(errPath)).slice(-2000);
|
|
4189
|
+
} catch {}
|
|
4190
|
+
}
|
|
4191
|
+
})().finally(() => lines.close());
|
|
4073
4192
|
} else {
|
|
4074
|
-
|
|
4193
|
+
let buf = "";
|
|
4194
|
+
const onStdout = (data) => {
|
|
4195
|
+
buf += data;
|
|
4196
|
+
let nl;
|
|
4197
|
+
while ((nl = buf.indexOf(`
|
|
4198
|
+
`)) >= 0) {
|
|
4199
|
+
const line = buf.slice(0, nl).trim();
|
|
4200
|
+
buf = buf.slice(nl + 1);
|
|
4201
|
+
if (line)
|
|
4202
|
+
lines.push(line);
|
|
4203
|
+
}
|
|
4204
|
+
};
|
|
4205
|
+
const runOpts = {
|
|
4206
|
+
...this.options.cwd ? { cwd: this.options.cwd } : {},
|
|
4207
|
+
onStdout,
|
|
4208
|
+
timeoutMs: 0
|
|
4209
|
+
};
|
|
4210
|
+
transport = this.sandbox.commands.run(guardedCmd, runOpts).then((res) => {
|
|
4211
|
+
const tail = buf.trim();
|
|
4212
|
+
if (tail)
|
|
4213
|
+
lines.push(tail);
|
|
4214
|
+
exitCode = res.exitCode;
|
|
4215
|
+
exitStderr = (res.stderr ?? "").slice(-2000);
|
|
4216
|
+
}, (err) => {
|
|
4217
|
+
transportError = err;
|
|
4218
|
+
}).finally(() => lines.close());
|
|
4075
4219
|
}
|
|
4076
|
-
|
|
4077
|
-
const reap = () => {
|
|
4078
|
-
if (!exited)
|
|
4079
|
-
kill?.().catch(() => {});
|
|
4080
|
-
};
|
|
4220
|
+
transport.catch(() => {});
|
|
4081
4221
|
if (opts.signal) {
|
|
4082
4222
|
if (opts.signal.aborted)
|
|
4083
4223
|
reap();
|
|
@@ -4101,18 +4241,28 @@ class CliAgentRunner {
|
|
|
4101
4241
|
yield msg;
|
|
4102
4242
|
}
|
|
4103
4243
|
}
|
|
4104
|
-
|
|
4105
|
-
if (
|
|
4106
|
-
|
|
4107
|
-
|
|
4244
|
+
await transport;
|
|
4245
|
+
if (transportError)
|
|
4246
|
+
throw transportError;
|
|
4247
|
+
if (exitCode === null) {
|
|
4248
|
+
if (!sawError) {
|
|
4249
|
+
yield { type: "error", text: `${this.spec.kind} aborted — the in-sandbox runner was terminated`, timestamp: now4() };
|
|
4250
|
+
}
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
4253
|
+
if (exitCode !== 0 && !sawError) {
|
|
4254
|
+
const detail = exitCode === JSONL_GUARD_NO_SENTINEL_EXIT ? " (the runner died without reporting an exit code)" : "";
|
|
4255
|
+
yield { type: "error", text: `${this.spec.kind} exited with code ${exitCode}${detail}${exitStderr ? `: ${exitStderr}` : ""}`, timestamp: now4() };
|
|
4108
4256
|
return;
|
|
4109
4257
|
}
|
|
4110
4258
|
if (!sawError)
|
|
4111
4259
|
yield { type: "done", sessionId: sessionId ?? "", timestamp: now4() };
|
|
4112
4260
|
} finally {
|
|
4113
4261
|
opts.signal?.removeEventListener("abort", reap);
|
|
4114
|
-
|
|
4115
|
-
|
|
4262
|
+
consumerStopped = true;
|
|
4263
|
+
if (exitCode === null)
|
|
4264
|
+
reap();
|
|
4265
|
+
this.sandbox.commands.run(`rm -f ${shellQuote(promptPath)} ${shellQuote(guardPath)} ${shellQuote(`${promptPath}.out`)} ${shellQuote(`${promptPath}.err`)} ${shellQuote(`${promptPath}.pid`)}`, { timeoutMs: 1e4 }).catch(() => {});
|
|
4116
4266
|
}
|
|
4117
4267
|
} catch (err) {
|
|
4118
4268
|
yield { type: "error", text: formatError(err), timestamp: now4() };
|
|
@@ -4153,11 +4303,11 @@ var codexSpec = {
|
|
|
4153
4303
|
"--skip-git-repo-check",
|
|
4154
4304
|
"--dangerously-bypass-approvals-and-sandbox",
|
|
4155
4305
|
...model ? ["-m", shellQuote(model)] : [],
|
|
4156
|
-
...effort ? ["-c", `model_reasoning_effort=${effort === "max" ? "xhigh" : effort}`] : []
|
|
4157
|
-
...cwd ? ["-C", shellQuote(cwd)] : []
|
|
4306
|
+
...effort ? ["-c", `model_reasoning_effort=${effort === "max" ? "xhigh" : effort}`] : []
|
|
4158
4307
|
].join(" ");
|
|
4308
|
+
const cd = cwd ? `cd ${shellQuote(cwd)} && ` : "";
|
|
4159
4309
|
const exec = sessionId ? `codex exec resume ${shellQuote(sessionId)} ${flags}` : `codex exec ${flags}`;
|
|
4160
|
-
return `${exec} - < ${shellQuote(promptPath)}`;
|
|
4310
|
+
return `${cd}${exec} - < ${shellQuote(promptPath)}`;
|
|
4161
4311
|
},
|
|
4162
4312
|
extractSessionId: (p) => p.type === "thread.started" && typeof p.thread_id === "string" ? p.thread_id : undefined,
|
|
4163
4313
|
mapEvent: (p) => {
|
|
@@ -4552,23 +4702,36 @@ function toE2bNetwork(policy) {
|
|
|
4552
4702
|
}
|
|
4553
4703
|
|
|
4554
4704
|
// src/sandbox/sizes.ts
|
|
4555
|
-
var
|
|
4556
|
-
"
|
|
4557
|
-
"
|
|
4558
|
-
"
|
|
4559
|
-
"
|
|
4705
|
+
var SANDBOX_MACHINES = {
|
|
4706
|
+
"1vcpu-2gb": { vcpus: 1, memoryMB: 2048 },
|
|
4707
|
+
"2vcpu-4gb": { vcpus: 2, memoryMB: 4096 },
|
|
4708
|
+
"4vcpu-8gb": { vcpus: 4, memoryMB: 8192 },
|
|
4709
|
+
"8vcpu-8gb": { vcpus: 8, memoryMB: 8192 },
|
|
4710
|
+
"8vcpu-16gb": { vcpus: 8, memoryMB: 16384 },
|
|
4711
|
+
"32vcpu-64gb": { vcpus: 32, memoryMB: 65536 }
|
|
4560
4712
|
};
|
|
4713
|
+
var SANDBOX_SIZES = Object.keys(SANDBOX_MACHINES);
|
|
4714
|
+
var SANDBOX_VCPUS = Object.fromEntries(SANDBOX_SIZES.map((s) => [s, SANDBOX_MACHINES[s].vcpus]));
|
|
4561
4715
|
var DEFAULT_SANDBOX_SIZE = "2vcpu-4gb";
|
|
4562
|
-
var
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
];
|
|
4716
|
+
var SESSION_DEFAULT_SANDBOX_SIZE = "4vcpu-8gb";
|
|
4717
|
+
var E2B_MAX_VCPUS = 8;
|
|
4718
|
+
var E2B_MAX_MEMORY_MB = 8192;
|
|
4719
|
+
var E2B_TEMPLATE_SIZES = SANDBOX_SIZES.filter((s) => SANDBOX_MACHINES[s].vcpus <= E2B_MAX_VCPUS && SANDBOX_MACHINES[s].memoryMB <= E2B_MAX_MEMORY_MB);
|
|
4566
4720
|
function isE2bSupportedSize(size) {
|
|
4567
4721
|
return E2B_TEMPLATE_SIZES.includes(size);
|
|
4568
4722
|
}
|
|
4723
|
+
var VERCEL_MEMORY_MB_PER_VCPU = 2048;
|
|
4724
|
+
function isVercelSupportedSize(size) {
|
|
4725
|
+
const m = SANDBOX_MACHINES[size];
|
|
4726
|
+
return m.memoryMB === m.vcpus * VERCEL_MEMORY_MB_PER_VCPU;
|
|
4727
|
+
}
|
|
4569
4728
|
function e2bMachineSpec(size) {
|
|
4570
|
-
const
|
|
4571
|
-
return { cpuCount, memoryMB:
|
|
4729
|
+
const m = SANDBOX_MACHINES[size];
|
|
4730
|
+
return { cpuCount: m.vcpus, memoryMB: m.memoryMB };
|
|
4731
|
+
}
|
|
4732
|
+
function sandboxSizeLabel(size) {
|
|
4733
|
+
const m = SANDBOX_MACHINES[size];
|
|
4734
|
+
return `${m.vcpus} vCPU · ${Math.round(m.memoryMB / 1024)} GB`;
|
|
4572
4735
|
}
|
|
4573
4736
|
function e2bBaseTemplate(size) {
|
|
4574
4737
|
return `agent-compose-base-${size}`;
|
|
@@ -4785,7 +4948,7 @@ function makeE2bSandboxProvider(sb) {
|
|
|
4785
4948
|
return { resumeHandle: sb.sandboxId };
|
|
4786
4949
|
},
|
|
4787
4950
|
async extendLifetime(ms) {
|
|
4788
|
-
await sb.setTimeout(ms);
|
|
4951
|
+
await sb.setTimeout(Math.min(ms, e2bMaxSandboxMs()));
|
|
4789
4952
|
},
|
|
4790
4953
|
async updateNetworkPolicy(policy) {
|
|
4791
4954
|
await sb.updateNetwork(toE2bNetwork(policy));
|
|
@@ -6439,38 +6602,49 @@ there is nothing to type, paste, or screenshot a credential from.
|
|
|
6439
6602
|
|
|
6440
6603
|
## Recording a demo — the desktop, captured to a video the human can play
|
|
6441
6604
|
|
|
6442
|
-
"Record a demo of you using X" is a normal ask, and this machine does it
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6605
|
+
"Record a demo of you using X" is a normal ask, and this machine does it.
|
|
6606
|
+
(For a LIVE view no recording is needed — the session header's **Desktop**
|
|
6607
|
+
button already streams this display to any teammate watching; a recording is
|
|
6608
|
+
the durable, replayable artifact. Both modes exist; say so when it matters.)
|
|
6609
|
+
|
|
6610
|
+
**Use \`ac-record\` — the platform recorder is already on PATH** (cloud
|
|
6611
|
+
sessions; \`command -v ac-record\` to confirm on older machines):
|
|
6448
6612
|
|
|
6449
|
-
|
|
6613
|
+
ac-record start # begins capturing the desktop (display :0)
|
|
6614
|
+
# ... drive the app with xdotool, screenshotting as you go ...
|
|
6615
|
+
ac-record stop # finishes + saves to recordings/ in your workspace
|
|
6616
|
+
ac-record status # one JSON line: {"recording":true,...}
|
|
6450
6617
|
|
|
6451
|
-
|
|
6618
|
+
It records the whole display (with desktop audio when the machine has a
|
|
6619
|
+
PulseAudio monitor), enforces sane caps (5 min / 200 MB — start a fresh
|
|
6620
|
+
recording per scene rather than one long take), keeps the file playable even
|
|
6621
|
+
if the machine dies mid-take, and \`stop\` prints the saved path — the file
|
|
6622
|
+
lands ON THE DRIVE in \`recordings/\`, visible in Files and playable in the
|
|
6623
|
+
dashboard. A human watching the Desktop pane sees the recording indicator
|
|
6624
|
+
while you record.
|
|
6452
6625
|
|
|
6453
|
-
|
|
6626
|
+
If \`ac-record\` is missing (older machine), record by hand.
|
|
6627
|
+
**ffmpeg IS pre-installed** on platform images (\`command -v ffmpeg\`; only
|
|
6628
|
+
if absent: \`sudo apt-get update -q && sudo apt-get install -y -q ffmpeg\`):
|
|
6454
6629
|
|
|
6455
6630
|
DISPLAY=:0 ffmpeg -f x11grab \\
|
|
6456
6631
|
-video_size "$(DISPLAY=:0 xdotool getdisplaygeometry | tr ' ' x)" \\
|
|
6457
6632
|
-framerate 10 -i :0 -c:v libvpx -b:v 1M -deadline realtime -cpu-used 8 \\
|
|
6458
6633
|
demo.webm &
|
|
6459
6634
|
FFMPEG_PID=$!
|
|
6460
|
-
# ... drive the app with xdotool
|
|
6635
|
+
# ... drive the app with xdotool ...
|
|
6461
6636
|
kill -INT "$FFMPEG_PID" && wait "$FFMPEG_PID"
|
|
6462
6637
|
|
|
6463
|
-
The gotchas, each one earned:
|
|
6638
|
+
The hand-rolled gotchas, each one earned:
|
|
6464
6639
|
- **Stop with SIGINT (\`kill -INT\`), never SIGKILL** — ffmpeg finalizes the
|
|
6465
6640
|
file on SIGINT; a hard kill truncates the encode mid-write.
|
|
6466
|
-
- **Record WebM (matroska-family), not MP4** — mp4 writes its moov atom
|
|
6467
|
-
END, so a killed or crashed encode leaves an UNPLAYABLE file; webm
|
|
6468
|
-
playable up to the last written frame and plays natively in the
|
|
6469
|
-
|
|
6470
|
-
afterwards if you truly need it, never record straight to it.
|
|
6641
|
+
- **Record WebM (matroska-family), not plain MP4** — mp4 writes its moov atom
|
|
6642
|
+
at the END, so a killed or crashed encode leaves an UNPLAYABLE file; webm
|
|
6643
|
+
stays playable up to the last written frame and plays natively in the
|
|
6644
|
+
browser. (\`ac-record\` sidesteps this with fragmented mp4.)
|
|
6471
6645
|
- **\`-video_size\` must match the real screen** — x11grab does not default to
|
|
6472
6646
|
it; read the geometry from \`xdotool getdisplaygeometry\` as above.
|
|
6473
|
-
- **10–
|
|
6647
|
+
- **10–15 fps is right for a screen demo** — small files, legible UI motion;
|
|
6474
6648
|
this is not video production.
|
|
6475
6649
|
- **Write to the drive, not /tmp** — the recording must land in your working
|
|
6476
6650
|
directory to persist and show up in Files; a file in /tmp dies with the
|
package/dist/sandbox/sizes.d.ts
CHANGED
|
@@ -1,19 +1,78 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sandbox machine sizes
|
|
2
|
+
* Sandbox machine sizes — THE single source of the size vocabulary.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Everything that names a size (the server's `SANDBOX_DEFAULT_SIZE` env enum,
|
|
5
|
+
* the register/invoke zod schemas, the run + session row types, the CLI's
|
|
6
|
+
* `--size` flag, the dashboard pickers via `GET /v1/sandbox-sizes`) derives
|
|
7
|
+
* from `SANDBOX_SIZES` / `SandboxSize` here. Adding a size is a ONE-LINE edit
|
|
8
|
+
* to `SANDBOX_MACHINES`: the type, the enums, the E2B build matrix and the
|
|
9
|
+
* pickers all follow. Do NOT re-declare the union inline anywhere.
|
|
10
|
+
*
|
|
11
|
+
* A size is a coarse hardware knob that maps to provider machine specs:
|
|
12
|
+
* Vercel honours it natively via `resources.vcpus`; E2B sizing is BAKED INTO
|
|
13
|
+
* THE TEMPLATE (e2b 2.30.5 has no create-time cpu/mem knob — `NewSandbox`
|
|
14
|
+
* carries only `templateID`), so on E2B a size resolves to a pre-built
|
|
15
|
+
* per-size template.
|
|
7
16
|
*/
|
|
8
|
-
/**
|
|
9
|
-
* than abstract t-shirt sizes
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
/** Every sandbox hardware SKU, with its real machine spec. Named for the
|
|
18
|
+
* machine (vCPU + RAM) rather than abstract t-shirt sizes, so a size can
|
|
19
|
+
* never quietly mean something different than it says.
|
|
20
|
+
*
|
|
21
|
+
* RAM is 2048 MB/vCPU everywhere EXCEPT `8vcpu-8gb`, which exists because
|
|
22
|
+
* E2B caps a sandbox at 8 vCPU / 8192 MB (e2b.dev/docs/billing: Hobby and
|
|
23
|
+
* Pro both "8 vCPU / 8 GB", raised only by arrangement): 8 vCPU at the 2 GB
|
|
24
|
+
* rule would need 16 GiB and cannot be built. `8vcpu-8gb` is the CPU ceiling
|
|
25
|
+
* at the memory ceiling — the only way to get 8 cores on E2B today, and a
|
|
26
|
+
* spec already proven bakeable by the devbox (`E2B_DEVBOX_SPEC`).
|
|
27
|
+
*
|
|
28
|
+
* Adding an entry here automatically: widens `SandboxSize`, widens every
|
|
29
|
+
* derived enum, and — if it fits under the E2B caps — adds it to
|
|
30
|
+
* `E2B_TEMPLATE_SIZES`, which is what `infra/e2b-template/build.ts` and the
|
|
31
|
+
* `sandbox-images` CI job loop over. Two templates get baked per size, so
|
|
32
|
+
* the matrix is not free; see that workflow's header. */
|
|
33
|
+
export declare const SANDBOX_MACHINES: {
|
|
34
|
+
/** 1 vCPU / 2 GiB — the cheap floor. Plenty for a terminal session or a
|
|
35
|
+
* shell-shaped agent; tight for a big `bun install` or a browser. */
|
|
36
|
+
readonly "1vcpu-2gb": {
|
|
37
|
+
readonly vcpus: 1;
|
|
38
|
+
readonly memoryMB: 2048;
|
|
39
|
+
};
|
|
40
|
+
/** 2 vCPU / 4 GiB — the default (Vercel's own default machine too). */
|
|
41
|
+
readonly "2vcpu-4gb": {
|
|
42
|
+
readonly vcpus: 2;
|
|
43
|
+
readonly memoryMB: 4096;
|
|
44
|
+
};
|
|
45
|
+
/** 4 vCPU / 8 GiB — comfortable for builds and multi-tool agent turns. */
|
|
46
|
+
readonly "4vcpu-8gb": {
|
|
47
|
+
readonly vcpus: 4;
|
|
48
|
+
readonly memoryMB: 8192;
|
|
49
|
+
};
|
|
50
|
+
/** 8 vCPU / 8 GiB — E2B's per-sandbox CEILING (cores maxed at the memory
|
|
51
|
+
* cap). NOT expressible on Vercel, whose RAM follows vCPUs at 2 GB each. */
|
|
52
|
+
readonly "8vcpu-8gb": {
|
|
53
|
+
readonly vcpus: 8;
|
|
54
|
+
readonly memoryMB: 8192;
|
|
55
|
+
};
|
|
56
|
+
/** 8 vCPU / 16 GiB — Vercel only; exceeds E2B's 8 GiB memory cap. */
|
|
57
|
+
readonly "8vcpu-16gb": {
|
|
58
|
+
readonly vcpus: 8;
|
|
59
|
+
readonly memoryMB: 16384;
|
|
60
|
+
};
|
|
61
|
+
/** 32 vCPU / 64 GiB — Vercel Enterprise only; far past every E2B cap. */
|
|
62
|
+
readonly "32vcpu-64gb": {
|
|
63
|
+
readonly vcpus: 32;
|
|
64
|
+
readonly memoryMB: 65536;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/** Sandbox hardware SKU. Derived from `SANDBOX_MACHINES` — never re-spelled
|
|
68
|
+
* as an inline union. */
|
|
69
|
+
export type SandboxSize = keyof typeof SANDBOX_MACHINES;
|
|
70
|
+
/** The vocabulary as an ordered, smallest-first array — the shape zod
|
|
71
|
+
* (`z.enum`), the CLI's `--size` validation, and the wire catalogue want.
|
|
72
|
+
* Ordering is the pickers' display order, so keep it ascending. */
|
|
73
|
+
export declare const SANDBOX_SIZES: readonly [SandboxSize, ...SandboxSize[]];
|
|
74
|
+
/** SKU → Vercel vCPU count (Vercel's RAM follows automatically at 2048
|
|
75
|
+
* MB/vCPU — which is why `isVercelSupportedSize` exists). */
|
|
17
76
|
export declare const SANDBOX_VCPUS: Record<SandboxSize, number>;
|
|
18
77
|
/** SDK fallback size when neither the caller nor the deployment specifies one.
|
|
19
78
|
* Deliberately conservative — the OPERATIONAL default is the server's
|
|
@@ -21,30 +80,61 @@ export declare const SANDBOX_VCPUS: Record<SandboxSize, number>;
|
|
|
21
80
|
* small matters because Vercel rate-limits creation by vCPUs-per-window
|
|
22
81
|
* (`api-sandboxes-vcpus-creation`); a large default 429s bursty/simultaneous
|
|
23
82
|
* creates. Workloads that need more RAM/CPU declare `resources.size` on the
|
|
24
|
-
* workflow rather than inflating the default for everyone.
|
|
83
|
+
* workflow rather than inflating the default for everyone.
|
|
84
|
+
*
|
|
85
|
+
* NOT `1vcpu-2gb`: the floor is an opt-IN for cheap sessions, not a quiet
|
|
86
|
+
* downgrade of every existing run's machine. */
|
|
25
87
|
export declare const DEFAULT_SANDBOX_SIZE: SandboxSize;
|
|
26
|
-
/**
|
|
27
|
-
* (
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
88
|
+
/** SESSION default — deliberately one size up from the run default
|
|
89
|
+
* (2026-08-13): a session's sandbox carries the full desktop toolbelt
|
|
90
|
+
* (VS Code + Chromium + dockerd) plus the KasmVNC encoder at the 60fps
|
|
91
|
+
* cap, and that stack swap-thrashes on 4 GiB while the encoder starves on
|
|
92
|
+
* 2 shared vCPUs. Workflow runs keep DEFAULT_SANDBOX_SIZE — no desktop,
|
|
93
|
+
* no toolbelt weight. Sessions bill active time only (parked = storage),
|
|
94
|
+
* so the delta applies to active hours, not the fleet. */
|
|
95
|
+
export declare const SESSION_DEFAULT_SANDBOX_SIZE: SandboxSize;
|
|
96
|
+
/** E2B's per-sandbox ceiling on the plans we run (e2b.dev/docs/billing —
|
|
97
|
+
* Hobby: "8 vCPU / 8 GB"; Pro: the same, "8+" only by arrangement with
|
|
98
|
+
* support). Recorded live too: `Template.build` 400s with "Memory can't be
|
|
99
|
+
* higher than 8192 MiB" past the memory cap.
|
|
100
|
+
*
|
|
101
|
+
* These two numbers are the ONLY knob for which sizes get an E2B template —
|
|
102
|
+
* raise them after E2B raises the account limit and the build matrix (and
|
|
103
|
+
* therefore the session picker) widens on its own. */
|
|
104
|
+
export declare const E2B_MAX_VCPUS = 8;
|
|
105
|
+
export declare const E2B_MAX_MEMORY_MB = 8192;
|
|
106
|
+
/** The E2B sizes we pre-build a template for — DERIVED from the caps, not
|
|
107
|
+
* hand-listed, so a new `SANDBOX_MACHINES` entry can never be offered
|
|
108
|
+
* without a template or omitted despite fitting. E2B sizing is
|
|
109
|
+
* template-baked (no per-create cpu/mem knob), so honouring `resources.size`
|
|
110
|
+
* on E2B means ONE pre-built template per size; `infra/e2b-template/build.ts`
|
|
111
|
+
* loops exactly this list. The register / invoke / session-spawn / resize
|
|
112
|
+
* guards all reject an unsupported E2B size before it can reach a create. */
|
|
34
113
|
export declare const E2B_TEMPLATE_SIZES: readonly SandboxSize[];
|
|
35
|
-
/** Is `size` one E2B can be built/booted at?
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* exactly one place. */
|
|
114
|
+
/** Is `size` one E2B can be built/booted at? False for the sizes past E2B's
|
|
115
|
+
* 8 vCPU / 8 GiB ceiling (`8vcpu-16gb`, `32vcpu-64gb`) — those run on Vercel.
|
|
116
|
+
* The "no E2B equivalent" decision lives in exactly one place: the caps. */
|
|
39
117
|
export declare function isE2bSupportedSize(size: SandboxSize): boolean;
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
118
|
+
/** Vercel's fixed memory-per-vCPU ratio. Vercel takes `resources.vcpus` and
|
|
119
|
+
* allocates RAM itself at this rate — there is no independent memory knob. */
|
|
120
|
+
export declare const VERCEL_MEMORY_MB_PER_VCPU = 2048;
|
|
121
|
+
/** Is `size` expressible on Vercel? Only when its RAM matches what Vercel
|
|
122
|
+
* would allocate for that vCPU count — otherwise asking for it would hand
|
|
123
|
+
* the caller a machine that does not match the name (`8vcpu-8gb` would come
|
|
124
|
+
* back with 16 GiB). Vercel's own ceiling (32 vCPU, Enterprise) is a plan
|
|
125
|
+
* matter, not a shape matter, so it is not encoded here. */
|
|
126
|
+
export declare function isVercelSupportedSize(size: SandboxSize): boolean;
|
|
127
|
+
/** Machine spec for a SandboxSize, in the shape `Template.build` wants. Used
|
|
128
|
+
* by `infra/e2b-template/build.ts` to stamp the per-size base + agent-env
|
|
129
|
+
* templates. Reads the explicit table rather than deriving RAM from vCPUs —
|
|
130
|
+
* `8vcpu-8gb` is deliberately off the 2048 MB/vCPU line. */
|
|
44
131
|
export declare function e2bMachineSpec(size: SandboxSize): {
|
|
45
132
|
cpuCount: number;
|
|
46
133
|
memoryMB: number;
|
|
47
134
|
};
|
|
135
|
+
/** Human label for a size — "2 vCPU · 4 GB". The wire catalogue carries it so
|
|
136
|
+
* the dashboard never has to parse the id back into numbers. */
|
|
137
|
+
export declare function sandboxSizeLabel(size: SandboxSize): string;
|
|
48
138
|
/** Stable E2B template ALIAS for the platform base at a given size
|
|
49
139
|
* (`agent-compose-base-<size>`). Aliases — not snapshot ids — so the refs are
|
|
50
140
|
* multi-account-clean: the same string resolves in any E2B account that built
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type { SandboxProvider, DesktopSandboxProvider, SandboxCommandRunOptions,
|
|
|
14
14
|
export type { SandboxNetworkHeaderTransform, SandboxNetworkAllowRule, SandboxNetworkSubnetPolicy, SandboxNetworkPolicy, } from "./sandbox/network-policy.js";
|
|
15
15
|
export { DOT_SEGMENT_PATH_RE2, toVercelNetworkPolicy, toE2bNetwork } from "./sandbox/network-policy.js";
|
|
16
16
|
export type { SandboxSize } from "./sandbox/sizes.js";
|
|
17
|
-
export { SANDBOX_VCPUS, DEFAULT_SANDBOX_SIZE, E2B_TEMPLATE_SIZES, isE2bSupportedSize, e2bMachineSpec, e2bBaseTemplate, e2bAgentEnvTemplate, isPlatformE2bTemplateAlias, } from "./sandbox/sizes.js";
|
|
17
|
+
export { SANDBOX_SIZES, SANDBOX_MACHINES, SANDBOX_VCPUS, DEFAULT_SANDBOX_SIZE, SESSION_DEFAULT_SANDBOX_SIZE, E2B_TEMPLATE_SIZES, E2B_MAX_VCPUS, E2B_MAX_MEMORY_MB, VERCEL_MEMORY_MB_PER_VCPU, isE2bSupportedSize, isVercelSupportedSize, sandboxSizeLabel, e2bMachineSpec, e2bBaseTemplate, e2bAgentEnvTemplate, isPlatformE2bTemplateAlias, } from "./sandbox/sizes.js";
|
|
18
18
|
export { E2B_DEVBOX_TEMPLATE, E2B_DEVBOX_SPEC, E2B_DEVBOX_RECIPE_VERSION, e2bDevboxTemplateRef, } from "./sandbox/devbox.js";
|
|
19
19
|
export { AGENT_COMPOSE_TAG } from "./sandbox/provider-def.js";
|
|
20
20
|
export type { SandboxCreateOpts, OwnedSandbox } from "./sandbox/provider-def.js";
|