fix: skip completed AgentRun turn refresh

This commit is contained in:
lyon
2026-06-18 13:03:21 +08:00
parent 01e41c077a
commit e765f23af9
3 changed files with 127 additions and 2 deletions
+32 -1
View File
@@ -440,6 +440,10 @@ function isAgentRunCommandAlreadyClaimed(error) {
export async function syncAgentRunChatResult({ traceId, currentResult = null, options = {}, traceStore = defaultCodeAgentTraceStore, appendResultEvent = true, refreshEvents = true, forceResultSync = false }) {
const initial = currentResult ?? await loadPersistedAgentRunResult(traceId, options);
const mapped = initial ? await resolveAgentRunTraceCommandMapping({ traceId, mapped: initial, options }) : initial;
if (!forceResultSync && options.skipAgentRunTerminalRefreshIfComplete === true && agentRunTerminalResultRefreshSatisfied(mapped, traceId)) {
const runnerTrace = mapped.runnerTrace ?? traceStore.snapshot(traceId);
return { result: mapped, runnerTrace, found: true, eventsRefreshed: false, resultSynced: false, terminalRefreshSkipped: true };
}
if (!mapped?.agentRun?.runId || !mapped?.agentRun?.commandId) return { result: currentResult, runnerTrace: traceStore.snapshot(traceId), found: Boolean(currentResult), eventsRefreshed: false, resultSynced: false };
const env = options.env ?? process.env;
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
@@ -491,6 +495,33 @@ function agentRunCommandResultSyncRequired(mapped = {}, eventsResponse = null) {
return Boolean(agentRunTerminalStatusFromEvents(eventsResponse?.events));
}
function agentRunTerminalResultRefreshSatisfied(mapped = {}, traceId = null) {
const safeId = safeTraceId(traceId ?? mapped?.traceId);
const agentRun = mapped?.agentRun && typeof mapped.agentRun === "object" ? mapped.agentRun : null;
const traceSummary = mapped?.traceSummary && typeof mapped.traceSummary === "object" ? mapped.traceSummary : null;
const finalResponse = mapped?.finalResponse && typeof mapped.finalResponse === "object" ? mapped.finalResponse : null;
const providerTrace = agentRun?.providerTrace && typeof agentRun.providerTrace === "object"
? agentRun.providerTrace
: mapped?.providerTrace && typeof mapped.providerTrace === "object"
? mapped.providerTrace
: null;
const commandId = text(agentRun?.commandId);
const runId = text(agentRun?.runId);
if (!safeId || !runId || !commandId) return false;
if (!isAgentRunTerminalStatus(mapped?.status ?? agentRun?.terminalStatus ?? agentRun?.commandState ?? agentRun?.status)) return false;
if (safeTraceId(mapped?.traceId ?? agentRun?.traceId) !== safeId) return false;
if (safeTraceId(providerTrace?.traceId) !== safeId) return false;
const finalTraceId = safeTraceId(finalResponse?.traceId ?? safeId);
if (finalTraceId && finalTraceId !== safeId) return false;
if (!String(finalResponse?.text ?? "").trim()) return false;
if (traceSummary?.source !== "agentrun-command-result") return false;
if (safeTraceId(traceSummary.traceId ?? safeId) !== safeId) return false;
const summaryAgentRun = traceSummary.agentRun && typeof traceSummary.agentRun === "object" ? traceSummary.agentRun : null;
if (text(summaryAgentRun?.runId) !== runId) return false;
if (text(summaryAgentRun?.commandId) !== commandId) return false;
return true;
}
function agentRunTerminalStatusFromEvents(events = []) {
if (!Array.isArray(events)) return null;
for (const event of [...events].reverse()) {
@@ -529,7 +560,7 @@ async function resolveAgentRunTraceCommandMapping({ traceId, mapped, options = {
const traceSummaryCommandId = text(mapped?.traceSummary?.agentRun?.commandId);
const commandId = text(agentRun.commandId);
const traceFieldsMatch = agentRunTraceId === safeId && providerTraceId === safeId && (!traceSummaryCommandId || traceSummaryCommandId === commandId);
if (mapped.status === "running" && traceFieldsMatch) return mapped;
if (traceFieldsMatch && (mapped.status === "running" || (options.skipAgentRunTerminalRefreshIfComplete === true && agentRunTerminalResultRefreshSatisfied(mapped, safeId)))) return mapped;
const env = options.env ?? process.env;
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
const managerUrl = resolveAgentRunManagerUrl(env, agentRun.managerUrl);
+93
View File
@@ -2303,6 +2303,99 @@ test("cloud api turn status reuses AgentRun result-sync trace refresh (#1422)",
}
});
test("cloud api turn status skips AgentRun refresh for complete terminal evidence (#1422)", async () => {
const calls = [];
const traceId = "trc_issue1422_terminal_fast_path";
const runId = "run_issue1422_terminal_fast_path";
const commandId = "cmd_issue1422_terminal_fast_path";
const finalText = "终态结果已经由 AgentRun command result 投影完成。";
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
calls.push({ method: request.method, path: url.pathname, search: url.search });
response.writeHead(500, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected refresh ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const codeAgentChatResults = new Map([[traceId, {
accepted: true,
status: "completed",
shortConnection: true,
traceId,
conversationId: "cnv_issue1422_terminal_fast_path",
sessionId: "ses_issue1422_terminal_fast_path",
threadId: "thread-issue1422-terminal-fast-path",
ownerUserId: TEST_AGENT_ACTOR.id,
ownerRole: TEST_AGENT_ACTOR.role,
finalResponse: { text: finalText, textChars: finalText.length, role: "assistant", status: "completed", traceId, valuesPrinted: false },
traceSummary: {
traceId,
source: "agentrun-command-result",
sourceEventCount: 3,
terminalStatus: "completed",
finalAssistantRow: { role: "assistant", status: "completed", textChars: finalText.length, textPreview: finalText, messageId: `msg_${traceId.slice(4)}`, valuesPrinted: false },
agentRun: { runId, commandId, lastSeq: 3, valuesPrinted: false },
valuesPrinted: false
},
agentRun: {
adapter: "agentrun-v01",
managerUrl: `http://127.0.0.1:${agentRunPort}`,
runId,
commandId,
traceId,
lastSeq: 3,
status: "completed",
commandState: "completed",
terminalStatus: "completed",
providerTrace: { traceId, runId, commandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
},
providerTrace: { traceId, runId, commandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
}]]);
const server = createCloudApiServer({
traceStore: createCodeAgentTraceStore(),
codeAgentChatResults,
env: {
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
HWLAB_ENVIRONMENT: "v02",
HWLAB_GITOPS_PROFILE: "v02"
},
accessController: {
required: false,
async authenticate() {
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/turns/${traceId}`, {
headers: { cookie: "hwlab_session=test-stub-session" }
});
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.status, "completed");
assert.equal(body.terminal, true);
assert.equal(body.finalResponse.text, finalText);
assert.equal(body.traceSummary.agentRun.commandId, commandId);
assert.deepEqual(calls, []);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await new Promise((resolve, reject) => {
agentRunServer.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api repairs historical same-session AgentRun trace after lastTraceId advances (#955)", async () => {
const calls = [];
const ownerSessions = new Map();
+2 -1
View File
@@ -1219,7 +1219,7 @@ async function resolveCodeAgentTurnStatusSnapshot(traceId, options) {
try {
const synced = await syncAgentRunChatResult({ traceId, currentResult: result, options: refreshOptions, traceStore });
result = synced.result ?? result;
turnRefreshSatisfiedByResultSync = synced.eventsRefreshed === true || synced.resultSynced === true;
turnRefreshSatisfiedByResultSync = synced.eventsRefreshed === true || synced.resultSynced === true || synced.terminalRefreshSkipped === true;
if (result && !canAccessOwnedResult(result, options.actor)) return forbiddenTurnSnapshot(traceId);
if (result && isTraceCommandTerminalStatus(result.status)) {
await finalizeCodeAgentBillingUsage({ payload: result, params: result, options });
@@ -1286,6 +1286,7 @@ function codeAgentTurnStatusRefreshOptions(options = {}) {
const timeoutMs = Math.max(1, Math.min(configuredBudgetMs, upstreamTimeoutMs));
return {
...options,
skipAgentRunTerminalRefreshIfComplete: true,
env: {
...env,
HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS: String(timeoutMs),