Files
pikasTech-HWLAB/internal/cloud/workbench-projection-cursor.ts
T
2026-07-02 17:31:07 +08:00

206 lines
9.5 KiB
TypeScript

/*
* SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p1-agentrun-incremental-cursor; PJ2026-010205 HWLAB接入 draft-2026-06-17-r0.
* Responsibility: Pure cursor planning for AgentRun events projection; never derives cursor by scanning trace events or result payloads.
*/
const DEFAULT_AGENTRUN_EVENTS_PAGE_LIMIT = 500;
const MAX_AGENTRUN_EVENTS_PAGE_LIMIT = 1000;
export function buildAgentRunProjectionEventsFetchPlan({ projectionState = null, agentRun = null, pageLimit = DEFAULT_AGENTRUN_EVENTS_PAGE_LIMIT } = {}) {
const state = normalizeProjectionState(projectionState);
const mapping = normalizeAgentRunMapping(agentRun);
const runId = textValue(state?.sourceRunId ?? state?.runId ?? mapping.runId);
const commandId = textValue(state?.sourceCommandId ?? state?.commandId ?? mapping.commandId);
const stateSeq = maxNonNegativeInteger(state?.lastSourceSeq, state?.lastAgentRunSeq);
const mappingSeq = maxNonNegativeInteger(mapping.lastSeq);
return {
runId,
commandId,
afterSeq: Math.max(stateSeq, mappingSeq),
limit: boundedPageLimit(pageLimit),
cursorSource: state ? "projection-state" : "agent-run-result",
valuesPrinted: false
};
}
export function buildWorkbenchProjectionStateUpdate({
currentState = null,
result = null,
agentRun = null,
eventsResponse = null,
resultSyncState = null,
projectionStatus = null,
projectionHealth = null,
polling = null,
retryParams = null,
error = null,
now = () => new Date().toISOString()
} = {}) {
const previous = normalizeProjectionState(currentState) ?? {};
const mapping = normalizeAgentRunMapping(agentRun ?? result?.agentRun);
const timestamp = timestampValue(now);
const traceId = textValue(previous.traceId ?? result?.traceId ?? mapping.traceId);
const runId = textValue(mapping.runId) || textValue(previous.sourceRunId ?? previous.runId);
const commandId = textValue(mapping.commandId) || textValue(previous.sourceCommandId ?? previous.commandId);
const previousRunId = textValue(previous.sourceRunId ?? previous.runId);
const previousCommandId = textValue(previous.sourceCommandId ?? previous.commandId);
const sourceChanged = Boolean((previousRunId && runId && previousRunId !== runId) || (previousCommandId && commandId && previousCommandId !== commandId));
const previousCursorSeq = sourceChanged ? 0 : maxNonNegativeInteger(previous.lastSourceSeq, previous.lastAgentRunSeq);
const lastSourceSeq = Math.max(
previousCursorSeq,
maxNonNegativeInteger(mapping.lastSeq),
maxNonNegativeInteger(eventsResponse?.traceLastSeq)
);
const sourceLatestSeq = Math.max(
sourceChanged ? 0 : maxNonNegativeInteger(previous.sourceLatestSeq, previous.upstreamLatestSeq),
maxNonNegativeInteger(eventsResponse?.maxSeq),
lastSourceSeq
);
const nextRetryParams = normalizeProjectionRetryParams(retryParams) ?? normalizeProjectionRetryParams(previous.retryParams);
const nextPolling = normalizeProjectionPolling(polling);
const nextResultSyncState = textValue(resultSyncState ?? previous.resultSyncState) || "not_started";
const nextProjectionStatus = textValue(projectionStatus) || inferProjectionStatus({ lastSourceSeq, sourceLatestSeq, resultSyncState: nextResultSyncState, previous });
const nextProjectionHealth = textValue(projectionHealth) || (error ? "degraded" : "healthy");
const nextRetryAt = nextPolling?.nextRetryAt ?? (error ? previous.nextRetryAt ?? null : null);
return {
...previous,
traceId,
sessionId: textValue(previous.sessionId ?? result?.sessionId ?? mapping.sessionId) || null,
conversationId: textValue(previous.conversationId ?? result?.conversationId ?? mapping.conversationId) || null,
threadId: textValue(previous.threadId ?? result?.threadId ?? mapping.threadId) || null,
ownerUserId: textValue(previous.ownerUserId ?? result?.ownerUserId) || null,
ownerRole: textValue(previous.ownerRole ?? result?.ownerRole) || null,
managerUrl: textValue(previous.managerUrl ?? mapping.managerUrl) || null,
backendProfile: textValue(previous.backendProfile ?? mapping.backendProfile) || null,
providerId: textValue(previous.providerId ?? mapping.providerId) || null,
sourceRunId: runId,
sourceCommandId: commandId,
runId,
commandId,
lastSourceSeq,
lastAgentRunSeq: lastSourceSeq,
lastProjectedSeq: sourceChanged ? lastSourceSeq : Math.max(maxNonNegativeInteger(previous.lastProjectedSeq), lastSourceSeq),
sourceLatestSeq,
upstreamLatestSeq: sourceLatestSeq,
projectionStatus: nextProjectionStatus,
projectionHealth: nextProjectionHealth,
resultSyncState: nextResultSyncState,
lastProjectedAt: sourceChanged || lastSourceSeq > maxNonNegativeInteger(previous.lastSourceSeq, previous.lastAgentRunSeq) ? timestamp : previous.lastProjectedAt ?? timestamp,
lastResultSyncAt: nextResultSyncState === "synced" ? timestamp : previous.lastResultSyncAt ?? null,
lastErrorCode: error ? textValue(error.code ?? error.errorCode ?? "projection_sync_failed") : null,
lastErrorMessage: error ? textValue(error.message ?? "Projection sync failed.") : null,
failureCount: error ? maxNonNegativeInteger(previous.failureCount) + 1 : 0,
nextRetryAt,
pollCount: nextPolling?.pollCount ?? maxNonNegativeInteger(previous.pollCount),
noProgressPollCount: nextPolling?.noProgressPollCount ?? (error ? maxNonNegativeInteger(previous.noProgressPollCount) : 0),
backoffMs: nextPolling?.backoffMs ?? 0,
backoffReason: nextPolling?.backoffReason ?? null,
rootCause: nextPolling?.rootCause ?? null,
terminalConsistencyGap: nextPolling?.terminalConsistencyGap ?? null,
retryParams: nextRetryParams,
createdAt: previous.createdAt ?? timestamp,
updatedAt: timestamp,
valuesPrinted: false
};
}
export function agentRunResultSyncDeferred({ forceResultSync = false, options = {}, env = process.env } = {}) {
if (forceResultSync) return false;
if (options.deferAgentRunResultSync === true) return true;
const configured = String(env?.HWLAB_CODE_AGENT_AGENTRUN_RESULT_SYNC_IN_RUNNING ?? "").trim().toLowerCase();
return ["0", "false", "off", "no"].includes(configured);
}
function normalizeProjectionState(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value;
}
function normalizeAgentRunMapping(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}
function normalizeProjectionRetryParams(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const message = textValue(value.message ?? value.prompt ?? value.text);
if (!message) return null;
const result = {
message,
prompt: message,
text: message,
sessionId: textValue(value.sessionId) || null,
conversationId: textValue(value.conversationId) || null,
threadId: textValue(value.threadId) || null,
projectId: textValue(value.projectId) || null,
ownerUserId: textValue(value.ownerUserId) || null,
ownerRole: textValue(value.ownerRole) || null,
providerProfile: textValue(value.providerProfile ?? value.codeAgentProviderProfile ?? value.backendProfile) || null,
codeAgentProviderProfile: textValue(value.codeAgentProviderProfile ?? value.providerProfile ?? value.backendProfile) || null,
backendProfile: textValue(value.backendProfile ?? value.providerProfile ?? value.codeAgentProviderProfile) || null,
providerId: textValue(value.providerId) || null,
valuesPrinted: false
};
for (const [key, item] of Object.entries(result)) {
if (item === null || item === "") delete result[key];
}
return result;
}
function normalizeProjectionPolling(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const result = {
pollCount: maxNonNegativeInteger(value.pollCount),
noProgressPollCount: maxNonNegativeInteger(value.noProgressPollCount),
backoffMs: maxNonNegativeInteger(value.backoffMs),
backoffReason: clippedText(value.backoffReason, 80),
rootCause: clippedText(value.rootCause, 120),
terminalConsistencyGap: maxNonNegativeInteger(value.terminalConsistencyGap),
nextRetryAt: timestampText(value.nextRetryAt)
};
return result;
}
function inferProjectionStatus({ lastSourceSeq, sourceLatestSeq, resultSyncState, previous }) {
if (["pending", "timed_out", "failed"].includes(resultSyncState)) return "terminal";
if (sourceLatestSeq > lastSourceSeq) return "projecting";
return textValue(previous?.projectionStatus) || "projecting";
}
function boundedPageLimit(value) {
const parsed = Number.parseInt(String(value ?? ""), 10);
if (!Number.isInteger(parsed) || parsed <= 0) return DEFAULT_AGENTRUN_EVENTS_PAGE_LIMIT;
return Math.min(parsed, MAX_AGENTRUN_EVENTS_PAGE_LIMIT);
}
function maxNonNegativeInteger(...values) {
let max = 0;
for (const value of values) {
const parsed = Number.parseInt(String(value ?? ""), 10);
if (Number.isInteger(parsed) && parsed >= 0 && parsed > max) max = parsed;
}
return max;
}
function timestampValue(now) {
const value = typeof now === "function" ? now() : now;
const parsed = Date.parse(String(value ?? ""));
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : new Date().toISOString();
}
function textValue(value) {
return String(value ?? "").trim();
}
function clippedText(value, limit) {
const text = textValue(value);
if (!text) return null;
return text.length > limit ? text.slice(0, limit) : text;
}
function timestampText(value) {
const text = textValue(value);
if (!text) return null;
const parsed = Date.parse(text);
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
}