Merge pull request #1771 from pikasTech/fix/1770-code-agent-retry-stop

fix: 收敛 Workbench Code Agent retry 与终态投影
This commit is contained in:
Lyon
2026-06-21 02:44:07 +08:00
committed by GitHub
5 changed files with 148 additions and 34 deletions
+3
View File
@@ -311,6 +311,9 @@ lanes:
HWLAB_OBSERVABILITY_CONFIG_REVISION: PJ2026-01060505-20260619-long-trace-timeout-mitigation
HWLAB_CODE_AGENT_TURN_STATUS_REFRESH_TIMEOUT_MS: "30000"
HWLAB_CODE_AGENT_AGENTRUN_NO_RESPONSE_TIMEOUT_MS: "600000"
HWLAB_CODE_AGENT_AGENTRUN_DISPATCH_RETRY_MAX: "5"
HWLAB_CODE_AGENT_AGENTRUN_DISPATCH_RETRY_BASE_MS: "1000"
HWLAB_CODE_AGENT_AGENTRUN_DISPATCH_RETRY_MAX_DELAY_MS: "30000"
HWLAB_CODE_AGENT_AGENTRUN_PROJECTION_SYNC_POLL_INTERVAL_MS: "1000"
HWLAB_CODE_AGENT_AGENTRUN_PROJECTION_RESUME_ENABLED: "1"
HWLAB_CODE_AGENT_AGENTRUN_PROJECTION_RESUME_INITIAL_DELAY_MS: "5000"
+100 -11
View File
@@ -288,11 +288,16 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
try {
const reusableParams = paramsWithAgentRunSessionThread(params, reusable.mapping.threadId);
const commandInput = buildAgentRunCommandInput({ params: reusableParams, traceId, backendProfile, sessionId: reusable.sessionId });
const command = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(reusable.mapping.runId)}/commands`, {
const command = await agentRunDispatchJson({ fetchImpl, managerUrl, path: `/api/v1/runs/${encodeURIComponent(reusable.mapping.runId)}/commands`,
method: "POST",
body: commandInput,
timeoutMs,
env
env,
traceStore,
traceId,
backendProfile,
runId: reusable.mapping.runId,
stage: "command-create-reused"
});
const commandId = requiredString(command?.id, "command.id");
traceStore.append(traceId, agentRunTraceEvent({
@@ -309,11 +314,17 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
let runnerJob = null;
try {
const runnerJobInput = buildAgentRunRunnerJobInput({ env, traceId, commandId, ownerApiKey, toolCapabilities, backendProfile });
runnerJob = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(reusable.mapping.runId)}/runner-jobs`, {
runnerJob = await agentRunDispatchJson({ fetchImpl, managerUrl, path: `/api/v1/runs/${encodeURIComponent(reusable.mapping.runId)}/runner-jobs`,
method: "POST",
body: runnerJobInput,
timeoutMs,
env
env,
traceStore,
traceId,
backendProfile,
runId: reusable.mapping.runId,
commandId,
stage: "runner-job-ensure-reused"
});
} catch (error) {
if (!isAgentRunCommandAlreadyClaimed(error)) throw error;
@@ -424,7 +435,7 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
}
const runInput = buildAgentRunCreateRunInput({ params: dispatchParams, env, traceId, backendProfile, sessionId, toolCapabilities });
const runCreateStartedAt = Date.now();
run = await agentRunJson(fetchImpl, managerUrl, "/api/v1/runs", { method: "POST", body: runInput, timeoutMs, env });
run = await agentRunDispatchJson({ fetchImpl, managerUrl, path: "/api/v1/runs", method: "POST", body: runInput, timeoutMs, env, traceStore, traceId, backendProfile, stage: "run-create" });
const runId = requiredString(run?.id, "run.id");
void emitCodeAgentOtelSpan("agentrun_run_create", traceId, env, { startTimeMs: runCreateStartedAt, attributes: { runId, backendProfile, managerUrl } });
traceStore.append(traceId, agentRunTraceEvent({
@@ -435,7 +446,7 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
}, backendProfile));
const commandInput = buildAgentRunCommandInput({ params: dispatchParams, traceId, backendProfile, sessionId });
const commandCreateStartedAt = Date.now();
command = await agentRunJson(fetchImpl, managerUrl, "/api/v1/runs/" + encodeURIComponent(runId) + "/commands", { method: "POST", body: commandInput, timeoutMs, env });
command = await agentRunDispatchJson({ fetchImpl, managerUrl, path: "/api/v1/runs/" + encodeURIComponent(runId) + "/commands", method: "POST", body: commandInput, timeoutMs, env, traceStore, traceId, backendProfile, runId, stage: "command-create" });
const commandId = requiredString(command?.id, "command.id");
void emitCodeAgentOtelSpan("agentrun_command_create", traceId, env, { startTimeMs: commandCreateStartedAt, attributes: { runId, commandId, backendProfile, managerUrl } });
traceStore.append(traceId, agentRunTraceEvent({
@@ -447,7 +458,7 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
const runnerJobInput = buildAgentRunRunnerJobInput({ env, traceId, commandId, ownerApiKey, toolCapabilities, backendProfile });
try {
const runnerJobStartedAt = Date.now();
runnerJob = await agentRunJson(fetchImpl, managerUrl, "/api/v1/runs/" + encodeURIComponent(runId) + "/runner-jobs", { method: "POST", body: runnerJobInput, timeoutMs, env });
runnerJob = await agentRunDispatchJson({ fetchImpl, managerUrl, path: "/api/v1/runs/" + encodeURIComponent(runId) + "/runner-jobs", method: "POST", body: runnerJobInput, timeoutMs, env, traceStore, traceId, backendProfile, runId, commandId, stage: "runner-job-create" });
void emitCodeAgentOtelSpan("agentrun_runner_job_create", traceId, env, { startTimeMs: runnerJobStartedAt, attributes: { runId, commandId, backendProfile, managerUrl, jobName: runnerJob?.jobName ?? runnerJob?.jobIdentity?.name ?? null } });
} catch (error) {
if (attempt === 0 && await shouldResetSessionAfterEviction("session-store-evicted", error?.message)) {
@@ -477,6 +488,72 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
return decorateAgentRunRunningResult({ base: initialAgentRunChatResult({ params: finalDispatchParams, options, traceId }), mapping, traceStore, traceId });
}
async function agentRunDispatchJson({ fetchImpl, managerUrl, path, method = "GET", body = undefined, timeoutMs, env = process.env, traceStore = defaultCodeAgentTraceStore, traceId, backendProfile, runId = null, commandId = null, stage = "agentrun-dispatch" } = {}) {
const policy = agentRunDispatchRetryPolicy(env);
let retryAttempt = 0;
for (;;) {
try {
return await agentRunJson(fetchImpl, managerUrl, path, { method, body, timeoutMs, env });
} catch (error) {
const failureKind = agentRunDispatchFailureKind(error);
if (!agentRunDispatchRetryable(error, failureKind) || retryAttempt >= policy.maxRetries) throw Object.assign(error, { retryAttempt, retryMax: policy.maxRetries, failureKind });
retryAttempt += 1;
const retryDelayMs = agentRunDispatchRetryDelayMs(policy, retryAttempt);
const nextRetryAt = new Date(Date.now() + retryDelayMs).toISOString();
traceStore.append(traceId, agentRunTraceEvent({
type: "provider_retry",
eventType: "backend",
status: "retrying",
label: `agentrun:dispatch-retry:${failureKind}`,
errorCode: failureKind,
failureKind,
retryable: true,
willRetry: true,
retryAttempt,
retryMax: policy.maxRetries,
retryDelayMs,
nextRetryAt,
runId,
commandId,
waitingFor: "agentrun-dispatch-retry",
message: `AgentRun dispatch transient ${failureKind}; retry ${retryAttempt}/${policy.maxRetries} after ${retryDelayMs}ms at stage ${stage}.`,
terminal: false,
valuesPrinted: false
}, backendProfile));
await sleepAgentRunDispatchRetry(retryDelayMs);
}
}
}
function agentRunDispatchRetryPolicy(env = process.env) {
return {
maxRetries: parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_DISPATCH_RETRY_MAX, 5),
baseDelayMs: parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_DISPATCH_RETRY_BASE_MS, 1000),
maxDelayMs: parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_DISPATCH_RETRY_MAX_DELAY_MS, 30000)
};
}
function agentRunDispatchRetryDelayMs(policy, retryAttempt) {
const base = Math.max(1, Number(policy.baseDelayMs ?? 1000));
const cap = Math.max(base, Number(policy.maxDelayMs ?? 30000));
return Math.min(cap, Math.trunc(base * Math.pow(2, Math.max(0, Number(retryAttempt ?? 1) - 1))));
}
function agentRunDispatchRetryable(error, failureKind) {
const status = Number(error?.statusCode ?? error?.status ?? error?.agentRunError?.statusCode ?? 0);
if ([429, 502, 503, 504].includes(status)) return true;
if (["provider-unavailable", "provider-stream-disconnected", "agentrun-timeout", "agentrun-timeout", "timeout", "fetch-failed", "network-error"].includes(failureKind)) return true;
return /ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|socket hang up|fetch failed|network|timeout/iu.test(String(error?.message ?? ""));
}
function agentRunDispatchFailureKind(error) {
return normalizedFailureKind(error?.agentRunError?.failureKind ?? error?.failureKind ?? error?.code ?? error?.name ?? (Number(error?.statusCode) ? `http-${error.statusCode}` : "agentrun-dispatch-error"));
}
function sleepAgentRunDispatchRetry(ms) {
return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
}
function isAgentRunCommandAlreadyClaimed(error) {
const statusCode = Number(error?.statusCode ?? 0);
const message = String(error?.message ?? error?.agentRunError?.message ?? "");
@@ -1703,7 +1780,7 @@ function canReuseAgentRunSessionForOwner(session, params = {}, options = {}) {
function agentRunReuseBlocker(run = {}, mapping = {}) {
const status = String(run?.terminalStatus ?? run?.status ?? mapping.runStatus ?? "").toLowerCase();
if (TERMINAL_RUN_STATUSES.has(status)) return `run_status_${status}`;
if (TERMINAL_RUN_STATUSES.has(status) && status !== "completed") return `run_status_${status}`;
if (!run?.claimedBy) return "runner_not_claimed";
if (runLeaseExpired(run.leaseExpiresAt)) return "runner_reuse_window_expired";
return null;
@@ -2383,7 +2460,7 @@ function mapAgentRunEvent(event, mapping = {}) {
return { ...base, type: "diff", eventType: "status", status: "running", label: "agentrun:diff", message: textPayload(payload, "diff") };
}
if (event.type === "error") {
if (isRetryableProviderInterruptionPayload(payload)) return agentRunProviderRetryEvent({ base, payload, status: "retrying", label: `agentrun:provider-retry:${String(payload.failureKind ?? "provider-stream-disconnected")}` });
if (isRetryableProviderInterruptionPayload(payload, { activeError: true })) return agentRunProviderRetryEvent({ base, payload: { ...payload, willRetry: true }, status: "retrying", label: `agentrun:provider-retry:${String(payload.failureKind ?? "provider-stream-disconnected")}` });
return { ...base, type: "error", eventType: "error", status: "failed", label: `agentrun:error:${String(payload.failureKind ?? "backend")}`, errorCode: payload.failureKind ?? "agentrun_error", failureKind: payload.failureKind ?? null, willRetry: payload.willRetry === true ? true : undefined, message: textPayload(payload, "AgentRun error") };
}
if (event.type === "terminal_status") {
@@ -2408,6 +2485,10 @@ function agentRunProviderRetryEvent({ base = {}, payload = {}, status = "retryin
retryable: true,
transient: true,
terminal: false,
retryAttempt: numberOrNull(payload.retryAttempt),
retryMax: numberOrNull(payload.retryMax),
retryDelayMs: numberOrNull(payload.retryDelayMs),
nextRetryAt: firstNonEmpty(payload.nextRetryAt) || null,
retryOf: payload.retryOf ?? null,
terminalStatus: payload.terminalStatus ?? null,
message: textPayload(payload, "Provider stream disconnected; AgentRun will retry."),
@@ -2415,14 +2496,22 @@ function agentRunProviderRetryEvent({ base = {}, payload = {}, status = "retryin
};
}
function isRetryableProviderInterruptionPayload(payload = {}) {
return normalizedFailureKind(payload?.failureKind ?? payload?.errorCode ?? payload?.code) === "provider-stream-disconnected" && payload?.willRetry === true;
function isRetryableProviderInterruptionPayload(payload = {}, options = {}) {
const kind = normalizedFailureKind(payload?.failureKind ?? payload?.errorCode ?? payload?.code);
if (kind === "provider-stream-disconnected") return payload?.willRetry === true;
if (kind === "provider-unavailable") return payload?.willRetry === true || (options.activeError === true && payload?.terminal !== true && payload?.final !== true);
return false;
}
function normalizedFailureKind(value) {
return String(value ?? "").trim().toLowerCase().replace(/_/gu, "-") || null;
}
function numberOrNull(value) {
const number = Number(value);
return Number.isFinite(number) ? number : null;
}
function agentRunTraceEvent(event = {}, backendSource = {}) {
const source = backendSource && typeof backendSource === "object" ? backendSource : { backendProfile: backendSource };
const backendProfile = firstNonEmpty(event.backendProfile, source.backendProfile, source.providerProfile, source.agentRun?.backendProfile);
@@ -352,7 +352,8 @@ function normalizeMessageFact(message = {}, index, { traceId, sessionId, convers
const appliesToContextTrace = role !== "user" && Boolean(traceId && messageTraceId && messageTraceId === traceId);
const messageTiming = normalizeTimingProjection(message.timing) ?? normalizeTimingProjection(message);
const contextTimingProjection = appliesToContextTrace ? normalizeTimingProjection(contextTiming) : null;
const timing = role === "user" ? messageTiming ?? emptyTimingProjection() : messageTiming ?? contextTimingProjection ?? emptyTimingProjection();
const timing = role === "user" ? messageTiming ?? emptyTimingProjection() : contextTimingProjection ?? messageTiming ?? emptyTimingProjection();
const terminalCompleted = messageTerminal && status === "completed";
return {
...message,
messageId,
@@ -376,6 +377,7 @@ function normalizeMessageFact(message = {}, index, { traceId, sessionId, convers
durationMs: timing.durationMs,
createdAt: timestampValue(message.createdAt ?? timestamp),
updatedAt: timestampValue(message.updatedAt ?? timestamp),
...(terminalCompleted ? { error: null, blocker: null, projection: null, projectionStatus: null, projectionHealth: null, staleMs: null } : {}),
valuesPrinted: false
};
}
+24 -8
View File
@@ -116,15 +116,16 @@ export function createWorkbenchTurnTimingProjection({ result = null, session = n
export function projectionDiagnostics({ traceId = null, projection = null, result = null, trace = null, refreshError = null } = {}) {
const turn = projection ?? createWorkbenchTurnProjection({ traceId, result, trace });
const source = projectionDiagnosticSource(trace);
const blocker = refreshError ?? source?.blocker ?? trace?.blocker ?? result?.blocker ?? result?.error ?? null;
const sealedCompleted = turn.terminal === true && turn.status === "completed" && Boolean(turn.finalResponse?.text);
const source = sealedCompleted ? null : projectionDiagnosticSource(trace);
const blocker = sealedCompleted ? null : refreshError ?? source?.blocker ?? trace?.blocker ?? result?.blocker ?? result?.error ?? null;
const hasProjectionInput = hasTraceProjection(trace) || Boolean(result || result?.agentRun);
const sourceStatus = normalizeProjectionStatus(source?.projectionStatus ?? trace?.projectionStatus);
const status = sourceStatus ?? (blocker ? "blocked" : turn.terminal ? "caught-up" : hasProjectionInput ? "projecting" : "unknown");
const sourceStatus = sealedCompleted ? null : normalizeProjectionStatus(source?.projectionStatus ?? trace?.projectionStatus);
const status = sealedCompleted ? "caught-up" : sourceStatus ?? (blocker ? "blocked" : turn.terminal ? "caught-up" : hasProjectionInput ? "projecting" : "unknown");
const diagnostic = blocker ? diagnosticBlocker(blocker) : null;
const projectionHealth = normalizeProjectionHealth(source?.projectionHealth ?? trace?.projectionHealth)
const projectionHealth = sealedCompleted ? "healthy" : normalizeProjectionHealth(source?.projectionHealth ?? trace?.projectionHealth)
?? projectionHealthFor({ status, turn, hasProjectionInput, blocker: diagnostic });
const staleMs = projectionStaleMs(source?.staleMs ?? trace?.staleMs, turn.updatedAt ?? trace?.updatedAt ?? result?.updatedAt);
const staleMs = sealedCompleted ? null : projectionStaleMs(source?.staleMs ?? trace?.staleMs, turn.updatedAt ?? trace?.updatedAt ?? result?.updatedAt);
return {
projectionStatus: status,
projectionHealth,
@@ -282,8 +283,18 @@ function retryableProviderInterruptionRecord(value) {
const record = objectValue(value);
if (!record) return null;
const kind = normalizeWorkbenchStatus(record.failureKind ?? record.errorCode ?? record.code ?? record.name);
if (kind === "provider-stream-disconnected" && record.willRetry === true) {
return { failureKind: kind, willRetry: true, runId: textValue(record.runId) || null, commandId: textValue(record.commandId) || null, valuesRedacted: true };
if ((kind === "provider-stream-disconnected" || kind === "provider-unavailable") && record.willRetry === true) {
return {
failureKind: kind,
willRetry: true,
retryAttempt: numberOrNull(record.retryAttempt),
retryMax: numberOrNull(record.retryMax),
retryDelayMs: numberOrNull(record.retryDelayMs),
nextRetryAt: textValue(record.nextRetryAt) || null,
runId: textValue(record.runId) || null,
commandId: textValue(record.commandId) || null,
valuesRedacted: true
};
}
for (const nested of [record.payload, record.providerTrace, record.agentRun, record.error, record.blocker, record.terminalEvidence]) {
const found = retryableProviderInterruptionRecord(nested);
@@ -406,6 +417,11 @@ function textValue(value) {
return String(value ?? "").trim();
}
function numberOrNull(value) {
const number = Number(value);
return Number.isFinite(number) ? number : null;
}
function messageAuthorityTextValue(value) {
const text = String(value ?? "").replace(/\r\n?/gu, "\n");
if (!text.trim() || text.trim() === "[object Object]") return "";
+18 -14
View File
@@ -530,8 +530,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
appendActiveMessages(user, pending);
startWorkbenchSubmitJourney({ traceId, sessionId, entry: submitEntry, backend: providerProfile.value, transport: "sse" });
chatPending.value = true;
currentRequest.value = { traceId, sessionId, threadId, status: "running" };
projectOptimisticRunningTurn({ sessionId, threadId, traceId, userText: value });
rememberRecentDraft(value);
recordActivity("submit");
const payload = { message: value, prompt: value, sessionId, threadId: providerThreadId, traceId, providerProfile: providerProfile.value, gatewayShellTimeoutMs: gatewayShellTimeoutMs.value, targetTraceId: composer.value.targetTraceId, steerTraceId };
@@ -555,16 +553,13 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const canonicalTraceId = alignOptimisticTurnMessages(traceId, response.data);
if (canonicalTraceId !== traceId) {
reduceServerState({ type: "turn.forget", traceId });
if (currentRequest.value?.traceId === traceId) {
currentRequest.value = {
...currentRequest.value,
traceId: canonicalTraceId,
sessionId: firstNonEmptyString(response.data.sessionId, currentRequest.value.sessionId),
threadId: firstNonEmptyString(response.data.threadId, currentRequest.value.threadId),
status: response.data.status ?? currentRequest.value.status
};
}
}
currentRequest.value = {
traceId: canonicalTraceId,
sessionId: firstNonEmptyString(response.data.sessionId, sessionId),
threadId: firstNonEmptyString(response.data.threadId, threadId),
status: response.data.status ?? "running"
};
applyTurnStatusSnapshot(canonicalTraceId, response.data);
void refreshSessions(sessionId);
if ((response.data as AgentChatResultResponse).terminal === true || isTerminalMessageStatus(response.data.status)) {
@@ -720,7 +715,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
updatedAt: new Date().toISOString()
} as NonNullable<ChatMessage["runnerTrace"]>;
const runnerTrace = mergeRunnerTrace(message.runnerTrace, nextTrace);
const error = normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error);
const error = isCompletedResultWithFinalResponse(result) ? null : normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error);
const projection = projectionFromResult(result) ?? runnerTrace.projection ?? message.projection ?? null;
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
rememberTraceAuthority(runnerTrace);
@@ -1043,7 +1038,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
if (!messageMatchesTraceAuthority(message, traceId, authoritySessionId, ownerSessionId)) return message;
const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result);
rememberTraceAuthority(runnerTrace);
const error = normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error);
const error = isCompletedResultWithFinalResponse(result) ? null : normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error);
const projection = projectionFromResult(result) ?? runnerTrace.projection ?? message.projection ?? null;
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
return { ...message, ...messageTimingPatch(result), runnerTrace, error: error ?? message.error ?? null, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
@@ -1082,7 +1077,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
if (!messageMatchesTraceAuthority(message, traceId, authoritySessionId, ownerSessionId)) return message;
const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result);
rememberTraceAuthority(runnerTrace);
const error = normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error);
const error = isCompletedResultWithFinalResponse(result) ? null : normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error);
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
const projection = projectionFromResult(result) ?? runnerTrace.projection ?? message.projection ?? null;
return { ...message, ...messageTimingPatch(result), title: normalizeWorkbenchMessageTitle(message.role, message.title), runnerTrace, error: error ?? message.error ?? null, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
@@ -1103,6 +1098,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const message = [...messages.value].reverse().find((item) => shouldApplyTraceToMessage(item, traceId, activeSessionId.value)) ?? null;
const sessionId = normalizeWorkbenchSessionId(firstNonEmptyString(message?.sessionId, message?.runnerTrace?.sessionId, activeSessionId.value));
const existing = turnStatusAuthority.value[traceId];
if (existing?.terminal === true && normalizedStatusText(existing.status) === "completed") return;
const status = existing?.status ?? normalizedStatusText(message?.status) ?? null;
const running = existing?.running === true || (!isTerminalMessageStatus(message?.status) && isTraceActiveStatus(message?.status));
const now = new Date().toISOString();
@@ -1739,6 +1735,14 @@ function projectedAgentMessageText(input: { status: unknown; finalText?: string
return firstNonEmptyString(input.errorText, text) ?? "";
}
function isCompletedResultWithFinalResponse(value: unknown): boolean {
const record = recordValue(value);
if (!record) return false;
const status = normalizedStatusText(record.status);
if (status !== "completed") return false;
return Boolean(finalResponseText(record.finalResponse) ?? messageText(record.assistantText) ?? messageText(record.text) ?? messageText(record.summary));
}
function isFailureTerminalStatus(value: unknown): boolean {
return ["failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed"].includes(String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"));
}