137 lines
6.2 KiB
TypeScript
137 lines
6.2 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,
|
|
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(previous.sourceRunId ?? previous.runId ?? mapping.runId);
|
|
const commandId = textValue(previous.sourceCommandId ?? previous.commandId ?? mapping.commandId);
|
|
const lastSourceSeq = Math.max(
|
|
maxNonNegativeInteger(previous.lastSourceSeq, previous.lastAgentRunSeq),
|
|
maxNonNegativeInteger(mapping.lastSeq),
|
|
maxNonNegativeInteger(eventsResponse?.traceLastSeq)
|
|
);
|
|
const sourceLatestSeq = Math.max(
|
|
maxNonNegativeInteger(previous.sourceLatestSeq, previous.upstreamLatestSeq),
|
|
maxNonNegativeInteger(eventsResponse?.maxSeq),
|
|
lastSourceSeq
|
|
);
|
|
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");
|
|
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: Math.max(maxNonNegativeInteger(previous.lastProjectedSeq), lastSourceSeq),
|
|
sourceLatestSeq,
|
|
upstreamLatestSeq: sourceLatestSeq,
|
|
projectionStatus: nextProjectionStatus,
|
|
projectionHealth: nextProjectionHealth,
|
|
resultSyncState: nextResultSyncState,
|
|
lastProjectedAt: 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: error ? previous.nextRetryAt ?? null : null,
|
|
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 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();
|
|
}
|