feat: persist workbench session inputs (#2175)

This commit is contained in:
Lyon
2026-06-26 05:10:24 +08:00
committed by GitHub
parent 2e9e4bd663
commit b0f36a3789
6 changed files with 294 additions and 8 deletions
+95 -1
View File
@@ -1,4 +1,4 @@
// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010403 API契约 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010205 HWLAB接入 draft-2026-06-17-r0; PJ2026-0102 Agent编排 draft-2026-06-17-r0; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010403 API契约 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010205 HWLAB接入 draft-2026-06-25-p0-session-warm-runner-contract; PJ2026-0102 Agent编排 draft-2026-06-17-r0; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
// Responsibility: Code Agent HTTP turn admission, lifecycle compatibility wrappers, and Workbench projection writer/finalizer integration.
import { createHash, randomUUID } from "node:crypto";
@@ -969,6 +969,15 @@ async function recordCodeAgentTurnAdmission({ payload = {}, params = {}, options
error.valuesRedacted = true;
throw error;
}
await recordCodeAgentSessionInputFact({
params,
options,
traceId,
delivery: "queue",
status: "admitted",
messageId: codeAgentTurnLifecycleFields(traceId, params).userMessageId,
commandId: payload.agentRun?.commandId ?? null
});
} catch (error) {
throw codeAgentAdmissionUnavailableError(error, { params, traceId });
}
@@ -989,6 +998,64 @@ async function recordCodeAgentTurnAdmission({ payload = {}, params = {}, options
return ownerRecord;
}
async function recordCodeAgentSessionInputFact({ params = {}, options = {}, traceId, delivery = "queue", status = "admitted", messageId = null, commandId = null, error = null } = {}) {
const runtimeStore = options.runtimeStore ?? null;
if (typeof runtimeStore?.writeWorkbenchFacts !== "function") return null;
const resolvedTraceId = safeTraceId(traceId ?? params.traceId) || null;
const lifecycle = codeAgentTurnLifecycleFields(resolvedTraceId, params);
const sessionId = safeSessionId(params.sessionId ?? params.session?.sessionId) || null;
if (!sessionId) return null;
const resolvedMessageId = safeMessageId(messageId ?? params.messageId ?? lifecycle.userMessageId) || lifecycle.userMessageId;
const resolvedCommandId = textValue(commandId ?? params.commandId ?? params.agentRun?.commandId) || null;
const normalizedDelivery = codeAgentInputDelivery(delivery);
const normalizedStatus = codeAgentInputStatus(status);
const now = new Date().toISOString();
const sourceEventId = `${sessionId}:${lifecycle.turnId}:${normalizedDelivery}:${resolvedTraceId ?? resolvedCommandId ?? resolvedMessageId}`;
const inputId = stableCodeAgentInputId({ sessionId, turnId: lifecycle.turnId, traceId: resolvedTraceId, messageId: resolvedMessageId, commandId: resolvedCommandId, delivery: normalizedDelivery, sourceEventId });
const fact = {
inputId,
sessionId,
turnId: lifecycle.turnId,
traceId: resolvedTraceId,
messageId: resolvedMessageId,
commandId: resolvedCommandId,
delivery: normalizedDelivery,
status: normalizedStatus,
errorCode: textValue(error?.code ?? error?.error?.code) || null,
sourceEventId,
promptHash: params.message || params.prompt || params.text ? sha256Text(params.message ?? params.prompt ?? params.text).slice(0, 16) : null,
textBytes: params.message || params.prompt || params.text ? Buffer.byteLength(String(params.message ?? params.prompt ?? params.text), "utf8") : 0,
valuesRedacted: true,
secretMaterialStored: false,
createdAt: now,
updatedAt: now
};
return runtimeStore.writeWorkbenchFacts({ facts: { inputs: [fact] } }, {
sessionId,
traceId: resolvedTraceId,
turnId: lifecycle.turnId,
messageId: resolvedMessageId,
ownerUserId: options.actor?.id ?? params.ownerUserId,
projectId: params.projectId ?? DEFAULT_CODE_AGENT_PROJECT_ID,
valuesPrinted: false
});
}
function stableCodeAgentInputId(value) {
const payload = Object.fromEntries(Object.entries(value ?? {}).filter(([, item]) => item !== undefined && item !== null && item !== "").sort(([left], [right]) => left.localeCompare(right)));
return `wsi_${sha256Text(JSON.stringify(payload)).slice(0, 32)}`;
}
function codeAgentInputDelivery(value) {
const text = textValue(value).toLowerCase();
return ["queue", "steer", "cancel"].includes(text) ? text : "queue";
}
function codeAgentInputStatus(value) {
const text = textValue(value).toLowerCase().replace(/-/gu, "_");
return ["admitted", "promoted", "blocked", "failed", "canceled", "completed"].includes(text) ? text : "admitted";
}
function codeAgentAdmissionUnavailableError(error, { params = {}, traceId } = {}) {
const payload = codeAgentAdmissionUnavailablePayload({ error, params, traceId });
return Object.assign(new Error(payload.error.message), {
@@ -2614,6 +2681,20 @@ export async function handleCodeAgentCancelHttp(request, response, options) {
}));
return;
}
await recordCodeAgentSessionInputFact({
params: {
...params,
sessionId: currentResult.sessionId ?? currentResult.session?.sessionId ?? params.sessionId,
messageId: codeAgentTurnLifecycleFields(traceId, currentResult).userMessageId,
ownerUserId: options.actor?.id,
ownerRole: options.actor?.role
},
options,
traceId,
delivery: "cancel",
status: "promoted",
commandId: currentResult.agentRun.commandId
});
const payload = await cancelAgentRunChatTurn({ traceId, currentResult, options, traceStore });
if (payload) {
if (payload.alreadyTerminal === true || isTraceCommandTerminalStatus(payload.status)) {
@@ -3056,6 +3137,19 @@ export async function handleCodeAgentSteerHttp(request, response, options) {
}
try {
await recordCodeAgentSessionInputFact({
params: {
...params,
sessionId: currentResult.sessionId ?? currentResult.session?.sessionId ?? params.sessionId,
ownerUserId: options.actor?.id,
ownerRole: options.actor?.role
},
options,
traceId,
delivery: "steer",
status: "promoted",
commandId: currentResult.agentRun.commandId
});
const payload = await steerAgentRunChatTurn({
traceId,
currentResult,