Contract codes now use the agent.* values, and runtime can emit structured logs without changing results when no logger is configured. Co-authored-by: Cursor <cursoragent@cursor.com>
126 lines
4.3 KiB
JavaScript
126 lines
4.3 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
|
|
export const RESULT_V1_SCHEMA_VERSION = 1;
|
|
export const MAX_RESULT_MEMBERS = 32;
|
|
export const MAX_RESULT_MEMBER_BYTES = 2 * 1024 * 1024;
|
|
/** Soft cap for HTTP wire payloads; Go fail-closes at 8MB. */
|
|
export const MAX_RESULT_WIRE_BYTES = 6 * 1024 * 1024;
|
|
/** Tail window for execution.log patches; full log remains on shared volume. */
|
|
export const EXECUTION_LOG_PATCH_BYTES = 128 * 1024;
|
|
|
|
const RETRYABLE_CODES = new Set(["agent.timeout", "agent.canceled", "agent.transient"]);
|
|
const ALLOWED_CODES = new Set([
|
|
"agent.prompt_empty",
|
|
"agent.unauthorized",
|
|
"agent.timeout",
|
|
"agent.canceled",
|
|
"agent.transient",
|
|
"agent.missing_deliverable",
|
|
"agent.run_failed",
|
|
]);
|
|
|
|
export function typedErrorCode(payload) {
|
|
const raw = String(payload?.fallback_reason || payload?.error || "");
|
|
const t = raw.toLowerCase();
|
|
if (t.includes("prompt_empty")) return "agent.prompt_empty";
|
|
if (t.includes("unauthorized")) return "agent.unauthorized";
|
|
if (t.includes("timeout") || t.includes("timed out")) return "agent.timeout";
|
|
if (t.includes("cancel")) return "agent.canceled";
|
|
if (
|
|
t.includes("connection") ||
|
|
t.includes("terminated") ||
|
|
t.includes("econnreset") ||
|
|
t.includes("socket hang")
|
|
) {
|
|
return "agent.transient";
|
|
}
|
|
if (t.includes("missing target")) return "agent.missing_deliverable";
|
|
if (payload?.retryable) return "agent.transient";
|
|
if (!payload?.ok) return "agent.run_failed";
|
|
return "";
|
|
}
|
|
|
|
export function membersDigest(members) {
|
|
const rows = (Array.isArray(members) ? members : []).map((m) => ({
|
|
kind: m.kind,
|
|
path: m.path,
|
|
digest: m.digest,
|
|
size: m.size,
|
|
}));
|
|
return createHash("sha256").update(JSON.stringify(rows)).digest("hex");
|
|
}
|
|
|
|
/** Keep only the trailing bytes of execution.log for the HTTP patch wire. */
|
|
export function truncateExecutionLogContent(content, maxBytes = EXECUTION_LOG_PATCH_BYTES) {
|
|
const buf = Buffer.from(String(content || ""), "utf8");
|
|
if (buf.length <= maxBytes) {
|
|
return buf.toString("utf8");
|
|
}
|
|
const tail = buf.subarray(buf.length - maxBytes);
|
|
return `[truncated ${buf.length - maxBytes} bytes]\n` + tail.toString("utf8");
|
|
}
|
|
|
|
/**
|
|
* Cap patch bodies so members and patches stay aligned under wire limits.
|
|
* Oversized / overflow members are dropped from both lists (fail closed on wire size).
|
|
*/
|
|
export function capPatches(patches, opts = {}) {
|
|
const maxMember = opts.maxMemberBytes ?? MAX_RESULT_MEMBER_BYTES;
|
|
const maxTotal = opts.maxTotalBytes ?? MAX_RESULT_WIRE_BYTES;
|
|
const maxMembers = opts.maxMembers ?? MAX_RESULT_MEMBERS;
|
|
const out = [];
|
|
let total = 0;
|
|
for (const p of Array.isArray(patches) ? patches : []) {
|
|
const rel = String(p?.rel_path || "");
|
|
if (!rel || rel.includes("..") || rel.startsWith("/")) continue;
|
|
let content = String(p.content || "");
|
|
if (rel === "execution.log" || rel.endsWith("/execution.log")) {
|
|
content = truncateExecutionLogContent(content);
|
|
}
|
|
const buf = Buffer.from(content, "utf8");
|
|
if (buf.length > maxMember) continue;
|
|
if (out.length >= maxMembers) break;
|
|
if (total + buf.length > maxTotal) continue;
|
|
total += buf.length;
|
|
out.push({ rel_path: rel, content });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function estimateWireBytes(payload) {
|
|
return Buffer.byteLength(JSON.stringify(payload), "utf8");
|
|
}
|
|
|
|
export function withResultV1(payload) {
|
|
const out = { ...payload };
|
|
delete out.stderr;
|
|
const capped = capPatches(Array.isArray(out.patches) ? out.patches : []);
|
|
out.patches = capped;
|
|
const members = [];
|
|
for (const p of capped) {
|
|
const rel = String(p?.rel_path || "");
|
|
const buf = Buffer.from(String(p.content || ""), "utf8");
|
|
members.push({
|
|
kind: "file",
|
|
path: rel,
|
|
digest: createHash("sha256").update(buf).digest("hex"),
|
|
size: buf.length,
|
|
});
|
|
}
|
|
const ok = !!out.ok;
|
|
const errorCode = ok ? "" : typedErrorCode(out);
|
|
const retryable = !ok && RETRYABLE_CODES.has(errorCode) && !!out.retryable;
|
|
out.capabilities = { typed_result: true };
|
|
out.runner_protocol = "typed-v1";
|
|
out.result_v1 = {
|
|
schema_version: RESULT_V1_SCHEMA_VERSION,
|
|
run_id: String(out.run_id || "unknown").slice(0, 128),
|
|
outcome: ok ? "success" : "error",
|
|
retryable,
|
|
error_code: ALLOWED_CODES.has(errorCode) ? errorCode : ok ? "" : "agent.run_failed",
|
|
members,
|
|
content_digest: membersDigest(members),
|
|
};
|
|
return out;
|
|
}
|