UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

115 lines (114 loc) • 4.42 kB
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 }; }