fix: record terminal turn effects once

This commit is contained in:
lyon
2026-06-18 13:17:28 +08:00
parent 9db2116d23
commit a69ff3cc99
2 changed files with 126 additions and 6 deletions
+92
View File
@@ -2396,6 +2396,98 @@ test("cloud api turn status skips AgentRun refresh for complete terminal evidenc
}
});
test("cloud api turn status records terminal side effects once (#1422)", async () => {
const ownerCalls = [];
const factCalls = [];
const traceId = "trc_issue1422_terminal_effects_once";
const runId = "run_issue1422_terminal_effects_once";
const commandId = "cmd_issue1422_terminal_effects_once";
const finalText = "终态副作用只需要收敛一次。";
const codeAgentChatResults = new Map([[traceId, {
accepted: true,
status: "completed",
shortConnection: true,
traceId,
conversationId: "cnv_issue1422_terminal_effects_once",
sessionId: "ses_issue1422_terminal_effects_once",
threadId: "thread-issue1422-terminal-effects-once",
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:1",
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",
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
HWLAB_ENVIRONMENT: "v02",
HWLAB_GITOPS_PROFILE: "v02"
},
sessionRegistry: {
recordFact(conversationId, fact) {
factCalls.push({ conversationId, fact });
return { ok: true };
}
},
accessController: {
required: false,
async authenticate() {
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
},
async recordAgentSessionOwner(input) {
ownerCalls.push(input);
return { ok: true, sessionId: input.sessionId };
}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
for (let i = 0; i < 2; i += 1) {
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.finalResponse.text, finalText);
}
assert.equal(ownerCalls.length, 1);
assert.equal(factCalls.length, 1);
assert.equal(codeAgentChatResults.get(traceId).turnStatusTerminalEffects.recorded, true);
} finally {
await new Promise((resolve, reject) => {
server.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();
+34 -6
View File
@@ -1222,9 +1222,7 @@ async function resolveCodeAgentTurnStatusSnapshot(traceId, options) {
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 });
recordCodeAgentConversationFact(result, options);
await recordCodeAgentSessionOwner({ payload: result, params: result, options, status: codeAgentOwnerStatusForResult(result) });
await recordCodeAgentTerminalTurnStatusEffects({ payload: result, params: result, options });
}
} catch (error) {
resultPollError = error;
@@ -1265,9 +1263,7 @@ async function resolveCodeAgentTurnStatusSnapshot(traceId, options) {
agentRunResult = synced.result ?? agentRunResult;
}
if (isTraceCommandTerminalStatus(agentRunResult?.status)) {
await finalizeCodeAgentBillingUsage({ payload: agentRunResult, params: agentRunResult, options });
recordCodeAgentConversationFact(agentRunResult, options);
await recordCodeAgentSessionOwner({ payload: agentRunResult, params: agentRunResult, options, status: codeAgentOwnerStatusForResult(agentRunResult), preserveLastTraceId: true });
await recordCodeAgentTerminalTurnStatusEffects({ payload: agentRunResult, params: agentRunResult, options, preserveLastTraceId: true });
}
} catch (error) {
refreshError = error;
@@ -1781,6 +1777,38 @@ function recordCodeAgentConversationFact(payload = {}, options = {}) {
}
}
async function recordCodeAgentTerminalTurnStatusEffects({ payload = {}, params = {}, options = {}, preserveLastTraceId = false } = {}) {
if (!payload || typeof payload !== "object" || !isTraceCommandTerminalStatus(payload.status)) return null;
if (payload.turnStatusTerminalEffects?.recorded === true) return payload.turnStatusTerminalEffects;
const billing = await finalizeCodeAgentBillingUsage({ payload, params, options });
recordCodeAgentConversationFact(payload, options);
const owner = await recordCodeAgentSessionOwner({ payload, params, options, status: codeAgentOwnerStatusForResult(payload), preserveLastTraceId });
const billingSettled = codeAgentTerminalBillingSettled({ payload, params, options, billing });
const ownerRequired = Boolean(options.actor?.id && options.accessController?.recordAgentSessionOwner);
const ownerSettled = !ownerRequired || Boolean(owner);
const effects = {
recorded: billingSettled && ownerSettled,
billingSettled,
ownerSettled,
preserveLastTraceId: Boolean(preserveLastTraceId),
recordedAt: new Date().toISOString(),
valuesPrinted: false
};
if (effects.recorded) {
payload.turnStatusTerminalEffects = effects;
const traceId = safeTraceId(payload.traceId ?? params.traceId);
if (traceId) options.codeAgentChatResults?.set?.(traceId, payload);
}
return effects;
}
function codeAgentTerminalBillingSettled({ payload = {}, params = {}, options = {}, billing = null } = {}) {
const reservation = params.userBillingReservation ?? payload.userBillingReservation;
const reservationId = typeof reservation?.reservationId === "string" ? reservation.reservationId : "";
if (!reservationId || !options.userBillingClient?.configured) return true;
return billing?.recorded === true || billing?.released === true || payload.billing?.recorded === true || payload.billing?.released === true;
}
export async function handleCodeAgentSteerHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};