Files
WeiCHEN 4830b8ba6d fix(squad): retry settled steps that never wrote the deliverable
Classify clean settle + missing target as transient (settled-no-deliverable)
and re-invoke once with a corrective write-tool prompt, instead of permanent
MISSING_TARGET failure.
2026-09-23 19:18:05 +08:00

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: "" };
}