Files
openclaw/scripts/lib/cross-os-release-checks/agent.ts
2026-07-13 04:01:40 -07:00

149 lines
5.3 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { appendFileSync } from "node:fs";
import type { AgentOutputOptions, AgentTurnResult } from "./config.ts";
import { CROSS_OS_AGENT_TURN_OPTIONAL, CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS } from "./config.ts";
import { readLogTextTail } from "./logs.ts";
export function maybeBuildOptionalAgentTurnSkipResult(
error: unknown,
logPath: string,
options: { attempt?: number; maxAttempts?: number; optional?: boolean } = {},
) {
const attempt = options.attempt ?? 1;
const maxAttempts = options.maxAttempts ?? 2;
const optional = options.optional ?? CROSS_OS_AGENT_TURN_OPTIONAL;
if (
attempt < maxAttempts ||
!optional ||
!shouldSkipOptionalCrossOsAgentTurnError(error, logPath)
) {
return null;
}
const message = error instanceof Error ? error.message : String(error);
appendFileSync(
logPath,
`\n[release-checks] skipping optional cross-OS live agent turn after retryable failure: ${message}\n`,
);
return {
status: 0,
stdout: JSON.stringify({
status: "skipped",
reason: "cross-os live agent turn unavailable after retry",
}),
stderr: "",
};
}
export function shouldSkipOptionalCrossOsAgentTurnError(error: unknown, logPath: string) {
const message = error instanceof Error ? error.message : String(error);
if (
/model idle timeout|did not produce a response before the model idle timeout|gateway request timeout for agent|Command timed out|timed out and could not be terminated cleanly/u.test(
message,
)
) {
return true;
}
if (!/Agent output did not contain the expected OK marker/u.test(message)) {
return false;
}
const log = readLogTextTail(logPath);
return /"status"\s*:\s*"timeout"|Request timed out before a response was generated/u.test(log);
}
export function buildCrossOsReleaseAgentSessionId(label: string, attempt: number) {
return `cross-os-release-check-${label}-${randomUUID()}-${attempt}`;
}
export function buildReleaseAgentTurnArgs(sessionId: string) {
return [
"agent",
"--agent",
"main",
"--session-id",
sessionId,
"--message",
"Reply with exact ASCII text OK only.",
"--thinking",
"off",
"--timeout",
String(CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS),
"--json",
];
}
export function shouldRetryCrossOsAgentTurnError(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return /Agent output did not contain the expected OK marker|Agent turn used embedded fallback instead of gateway|model idle timeout|did not produce a response before the model idle timeout|gateway request timeout for agent|Command timed out|timed out and could not be terminated cleanly|rate limit reached|rate_limit_exceeded|HTTP 429|HTTP 503|upstream connect error|disconnect\/reset before headers|connection timeout/u.test(
message,
);
}
export function agentTurnUsedEmbeddedFallback(
result: Pick<AgentTurnResult, "stdout" | "stderr">,
options: AgentOutputOptions = {},
) {
const logText =
typeof options.logText === "string"
? options.logText
: typeof options.logPath === "string"
? readLogTextTail(options.logPath)
: "";
return /EMBEDDED FALLBACK:/u.test(`${result.stdout ?? ""}\n${result.stderr ?? ""}\n${logText}`);
}
export function agentOutputHasExpectedOkMarker(stdout: string, options: AgentOutputOptions = {}) {
const payloadTexts = parseAgentPayloadTexts(stdout);
if (payloadTexts.some((text) => text.trim() === "OK")) {
return true;
}
if (typeof options.logText === "string") {
const logTexts = parseAgentPayloadTexts(options.logText);
return logTexts.some((text) => text.trim() === "OK");
}
if (typeof options.logPath !== "string") {
return false;
}
const logTexts = parseAgentPayloadTexts(readLogTextTail(options.logPath));
return logTexts.some((text) => text.trim() === "OK");
}
function parseAgentPayloadTexts(stdout: string) {
try {
type AgentPayload = {
text?: string;
finalAssistantVisibleText?: string;
finalAssistantRawText?: string;
meta?: AgentPayload;
result?: AgentPayload;
payloads?: AgentPayload[];
};
const payload = JSON.parse(stdout) as AgentPayload;
const directTexts = [
payload?.finalAssistantVisibleText,
payload?.finalAssistantRawText,
payload?.meta?.finalAssistantVisibleText,
payload?.meta?.finalAssistantRawText,
payload?.result?.finalAssistantVisibleText,
payload?.result?.finalAssistantRawText,
payload?.result?.meta?.finalAssistantVisibleText,
payload?.result?.meta?.finalAssistantRawText,
].filter((text): text is string => typeof text === "string");
const entries = Array.isArray(payload?.payloads)
? payload.payloads
: Array.isArray(payload?.result?.payloads)
? payload.result.payloads
: [];
const payloadTexts = entries.flatMap((entry) =>
typeof entry?.text === "string" ? [entry.text] : [],
);
return [...directTexts, ...payloadTexts];
} catch {
const finalTextMatches = [
...stdout.matchAll(
/"(?:finalAssistantVisibleText|finalAssistantRawText|text)"\s*:\s*"([^"]*)"/gu,
),
].flatMap((match) => (match[1] === undefined ? [] : [match[1]]));
return finalTextMatches.length > 0 ? finalTextMatches : stdout.trim() ? [stdout] : [];
}
}