126 lines
3.8 KiB
JavaScript
126 lines
3.8 KiB
JavaScript
/**
|
|
* Success / retry classification for a PI RPC session.
|
|
* Write() arguments in traces are never treated as deliverables.
|
|
*/
|
|
|
|
export function newSettleState() {
|
|
return {
|
|
retryFailed: false,
|
|
providerTransient: false,
|
|
lastErr: "",
|
|
succeeded: false,
|
|
};
|
|
}
|
|
|
|
function messageField(ev, key) {
|
|
const msg = ev && ev.message;
|
|
if (!msg || typeof msg !== "object") {
|
|
return "";
|
|
}
|
|
const v = msg[key];
|
|
return typeof v === "string" ? v : "";
|
|
}
|
|
|
|
export function isTransientText(s) {
|
|
const t = String(s || "").toLowerCase();
|
|
return t.includes("connection error") ||
|
|
t.includes("terminated") ||
|
|
t.includes("auto_retry") ||
|
|
t.includes("econnreset") ||
|
|
t.includes("socket hang up");
|
|
}
|
|
|
|
export function isPermanentReason(s) {
|
|
const t = String(s || "").toLowerCase();
|
|
return t.includes("prompt_empty") ||
|
|
t.includes("unauthorized") ||
|
|
t.includes("治理");
|
|
}
|
|
|
|
export function applyAgentEvent(state, ev) {
|
|
if (!state || !ev || typeof ev !== "object") {
|
|
return state;
|
|
}
|
|
const t = ev.type || "";
|
|
const stop = messageField(ev, "stopReason");
|
|
const msgErr = messageField(ev, "errorMessage") ||
|
|
(typeof ev.errorMessage === "string" ? ev.errorMessage : "") ||
|
|
(typeof ev.finalError === "string" ? ev.finalError : "") ||
|
|
(ev.error && typeof ev.error.message === "string" ? ev.error.message : "");
|
|
|
|
if (msgErr && isTransientText(msgErr)) {
|
|
state.providerTransient = true;
|
|
state.lastErr = msgErr;
|
|
}
|
|
if (stop === "error") {
|
|
state.providerTransient = true;
|
|
if (msgErr) {
|
|
state.lastErr = msgErr;
|
|
}
|
|
}
|
|
if (t === "auto_retry_start") {
|
|
state.providerTransient = true;
|
|
if (ev.errorMessage) {
|
|
state.lastErr = String(ev.errorMessage);
|
|
}
|
|
}
|
|
if (t === "auto_retry_end") {
|
|
if (ev.success === false) {
|
|
state.retryFailed = true;
|
|
state.providerTransient = true;
|
|
state.lastErr = ev.finalError || ev.errorMessage || state.lastErr || "auto_retry_failed";
|
|
} else if (ev.success === true) {
|
|
// SDK in-process retry recovered. Drop this-round transient so a later
|
|
// agent_settled can succeed; later events may set a new error again.
|
|
state.retryFailed = false;
|
|
state.providerTransient = false;
|
|
state.lastErr = "";
|
|
} else {
|
|
state.retryFailed = false;
|
|
}
|
|
}
|
|
if (t === "agent_error") {
|
|
const err = (ev.error && ev.error.message) || ev.message || "agent_error";
|
|
const text = typeof err === "string" ? err : "agent_error";
|
|
state.lastErr = text;
|
|
if (isTransientText(text)) {
|
|
state.providerTransient = true;
|
|
}
|
|
}
|
|
if (t === "agent_settled") {
|
|
// PI emits agent_settled even after exhausted SDK retries.
|
|
state.succeeded = !state.retryFailed && !state.providerTransient;
|
|
}
|
|
return state;
|
|
}
|
|
|
|
export function classifyRunOutcome({
|
|
succeeded = false,
|
|
retryFailed = false,
|
|
providerTransient = false,
|
|
lastErr = "",
|
|
missing = [],
|
|
} = {}) {
|
|
const miss = Array.isArray(missing) ? missing.filter(Boolean) : [];
|
|
if (isPermanentReason(lastErr)) {
|
|
return { ok: false, retryable: false, fallback_reason: lastErr || "permanent_failure" };
|
|
}
|
|
if (!succeeded) {
|
|
const reason = lastErr || "agent_not_settled";
|
|
const retryable = (retryFailed || providerTransient || isTransientText(reason)) &&
|
|
!isPermanentReason(reason);
|
|
return { ok: false, retryable, fallback_reason: reason };
|
|
}
|
|
if (miss.length > 0) {
|
|
// Clean settle without writing the target is still retryable once at the
|
|
// product layer (Go AssessStepFailure → settled-no-deliverable). Runner
|
|
// signals retryable; Go remains the final adjudicator and bound.
|
|
return {
|
|
ok: false,
|
|
retryable: true,
|
|
fallback_reason: `missing target deliverable ${miss.join(",")}`,
|
|
};
|
|
}
|
|
return { ok: true, retryable: false, fallback_reason: "" };
|
|
}
|