@sema-agent/core
Version:
Stateless, task-oriented AI agent core
115 lines (114 loc) • 4.42 kB
JavaScript
import { isRemoteExecutionEnv, RemoteExecutionError } from "./remote-env.js";
import { readMember, thrownIs } from "./thrown-value.js";
const DEFAULT_MAX_OUTPUT_BYTES = 64 * 1024;
function clampBytes(s, maxBytes) {
const total = Buffer.byteLength(s, "utf8");
if (total <= maxBytes)
return { text: s, bytes: total, truncated: false };
const buf = Buffer.from(s, "utf8");
let end = maxBytes;
while (end > 0 && (buf[end] & 0xc0) === 0x80)
end--;
return { text: buf.toString("utf8", 0, end), bytes: end, truncated: true };
}
async function runStreamStep(env, step, options, maxOutputBytes) {
const label = step.label ?? step.command;
const out = [];
const errOut = [];
let outBytes = 0;
let errBytes = 0;
let truncated = false;
let exitCode = null;
const append = (bucket, used, chunk) => {
if (used >= maxOutputBytes) {
truncated = true;
return used;
}
const { text, bytes, truncated: cut } = clampBytes(chunk, maxOutputBytes - used);
if (cut)
truncated = true;
bucket.push(text);
return used + bytes;
};
try {
for await (const chunk of env.execStream(step.command, {
cwd: step.cwd,
env: step.env,
timeout: step.timeoutSec,
signal: options.signal,
readTimeoutMs: options.readTimeoutMs,
maxOutputBytes,
})) {
if (chunk.type === "stdout")
outBytes = append(out, outBytes, chunk.data);
else if (chunk.type === "stderr")
errBytes = append(errOut, errBytes, chunk.data);
else
exitCode = chunk.exitCode;
}
}
catch (e) {
const remoteCode = thrownIs(e, RemoteExecutionError) ? readMember(e, "code") : undefined;
const code = typeof remoteCode === "string" ? remoteCode : readMember(e, "name") === "AbortError" ? "aborted" : "unknown";
return { label, command: step.command, exitCode: null, stdout: out.join(""), stderr: errOut.join(""), ok: false, errorCode: code, truncated };
}
return { label, command: step.command, exitCode, stdout: out.join(""), stderr: errOut.join(""), ok: exitCode === 0, truncated };
}
async function runBufferedStep(env, step, options, maxOutputBytes) {
const label = step.label ?? step.command;
const res = await env.exec(step.command, {
cwd: step.cwd,
env: step.env,
timeout: step.timeoutSec,
abortSignal: options.signal,
});
if (!res.ok) {
const po = clampBytes(res.error.partialStdout ?? "", maxOutputBytes);
const pe = clampBytes(res.error.partialStderr ?? "", maxOutputBytes);
return {
label,
command: step.command,
exitCode: null,
stdout: po.text,
stderr: pe.text,
ok: false,
errorCode: res.error.code,
truncated: po.truncated || pe.truncated,
};
}
const so = clampBytes(res.value.stdout, maxOutputBytes);
const se = clampBytes(res.value.stderr, maxOutputBytes);
return {
label,
command: step.command,
exitCode: res.value.exitCode,
stdout: so.text,
stderr: se.text,
ok: res.value.exitCode === 0,
truncated: so.truncated || se.truncated,
};
}
function assertRemote(env) {
if (!isRemoteExecutionEnv(env))
throw new Error("not a remote env");
return env;
}
export async function runExecGate(env, steps, options = {}) {
const stopOnFailure = options.stopOnFailure ?? true;
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
const remote = isRemoteExecutionEnv(env);
const results = [];
for (const step of steps) {
if (options.signal?.aborted) {
results.push({ label: step.label ?? step.command, command: step.command, exitCode: null, stdout: "", stderr: "", ok: false, errorCode: "aborted" });
break;
}
const r = remote
? await runStreamStep(assertRemote(env), step, options, maxOutputBytes)
: await runBufferedStep(env, step, options, maxOutputBytes);
results.push(r);
if (!r.ok && stopOnFailure)
break;
}
return { passed: results.length > 0 && results.every((s) => s.ok), steps: results };
}