Merge pull request #1779 from pikasTech/fix/1770-turn-timing-authority
fix: keep workbench turn timing monotonic
This commit is contained in:
@@ -233,7 +233,8 @@ function traceForDisplay(message: ChatMessage): ChatMessage["runnerTrace"] {
|
|||||||
|
|
||||||
function messageDurationMeta(message: ChatMessage): { text: string; label: string } | null {
|
function messageDurationMeta(message: ChatMessage): { text: string; label: string } | null {
|
||||||
if (message.role !== "agent") return null;
|
if (message.role !== "agent") return null;
|
||||||
const durationMs = isRunningMessage(message) ? durationSince(message.timing?.startedAt) : finiteDurationMs(message.timing?.durationMs);
|
const timing = messageTimingForDisplay(message);
|
||||||
|
const durationMs = isRunningMessage(message) ? durationSince(timing?.startedAt) : finiteDurationMs(timing?.durationMs);
|
||||||
if (durationMs === null) return null;
|
if (durationMs === null) return null;
|
||||||
const value = formatDuration(durationMs);
|
const value = formatDuration(durationMs);
|
||||||
return { text: `耗时 ${value}`, label: `总耗时:${value}` };
|
return { text: `耗时 ${value}`, label: `总耗时:${value}` };
|
||||||
@@ -241,12 +242,17 @@ function messageDurationMeta(message: ChatMessage): { text: string; label: strin
|
|||||||
|
|
||||||
function messageActivityMeta(message: ChatMessage): { text: string; label: string } | null {
|
function messageActivityMeta(message: ChatMessage): { text: string; label: string } | null {
|
||||||
if (!isRunningMessage(message)) return null;
|
if (!isRunningMessage(message)) return null;
|
||||||
const ageMs = durationSince(message.timing?.lastEventAt);
|
const timing = messageTimingForDisplay(message);
|
||||||
|
const ageMs = durationSince(timing?.lastEventAt);
|
||||||
if (ageMs === null) return null;
|
if (ageMs === null) return null;
|
||||||
const value = formatDuration(ageMs) + "前";
|
const value = formatDuration(ageMs) + "前";
|
||||||
return { text: `最近 ${value}`, label: `最近事件:${value}` };
|
return { text: `最近 ${value}`, label: `最近事件:${value}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function messageTimingForDisplay(message: ChatMessage): ChatMessage["timing"] {
|
||||||
|
return message.runnerTrace?.timing ?? message.timing ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
function durationSince(timestamp: unknown): number | null {
|
function durationSince(timestamp: unknown): number | null {
|
||||||
const startedAtMs = timestampMs(timestamp);
|
const startedAtMs = timestampMs(timestamp);
|
||||||
return startedAtMs === null ? null : Math.max(0, nowMs.value - startedAtMs);
|
return startedAtMs === null ? null : Math.max(0, nowMs.value - startedAtMs);
|
||||||
|
|||||||
@@ -118,7 +118,128 @@ export function mergeRunnerTrace(previous: ChatMessage["runnerTrace"], next: Non
|
|||||||
: keepPreviousEvents && previousAuthoritative && next.eventsCompacted === true ? previousEvents
|
: keepPreviousEvents && previousAuthoritative && next.eventsCompacted === true ? previousEvents
|
||||||
: keepPreviousEvents ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents.length > previousEvents.length ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents;
|
: keepPreviousEvents ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents.length > previousEvents.length ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents;
|
||||||
const eventCount = keepPreviousEvents ? previous.eventCount ?? events.length : next.eventCount ?? previous.eventCount ?? events.length;
|
const eventCount = keepPreviousEvents ? previous.eventCount ?? events.length : next.eventCount ?? previous.eventCount ?? events.length;
|
||||||
return { ...previous, ...next, events, eventCount, updatedAt: next.updatedAt ?? previous.updatedAt ?? new Date().toISOString() };
|
const timing = mergeTraceTimingProjection(previous, next, { preserveVisibleTerminalDuration: traceTimingWasRunning(previous) && traceTimingIsTerminal(next) });
|
||||||
|
return {
|
||||||
|
...previous,
|
||||||
|
...next,
|
||||||
|
events,
|
||||||
|
eventCount,
|
||||||
|
timing,
|
||||||
|
startedAt: timing?.startedAt ?? next.startedAt ?? previous.startedAt ?? null,
|
||||||
|
lastEventAt: timing?.lastEventAt ?? next.lastEventAt ?? previous.lastEventAt ?? null,
|
||||||
|
finishedAt: timing?.finishedAt ?? next.finishedAt ?? previous.finishedAt ?? null,
|
||||||
|
durationMs: timing?.durationMs ?? next.durationMs ?? previous.durationMs ?? null,
|
||||||
|
updatedAt: next.updatedAt ?? previous.updatedAt ?? new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeTraceTimingProjection(previous: unknown, next: unknown, options: { preserveVisibleTerminalDuration?: boolean } = {}): TraceSnapshot["timing"] | null {
|
||||||
|
const previousTiming = traceTimingCandidate(previous);
|
||||||
|
const nextTiming = traceTimingCandidate(next);
|
||||||
|
if (!previousTiming && !nextTiming) return null;
|
||||||
|
if (!previousTiming) return nextTiming;
|
||||||
|
if (!nextTiming) return previousTiming;
|
||||||
|
const startedAt = earliestTraceTimestamp(previousTiming.startedAt, nextTiming.startedAt);
|
||||||
|
const lastEventAt = latestTraceTimestamp(previousTiming.lastEventAt, nextTiming.lastEventAt);
|
||||||
|
const finishedAt = latestTraceTimestamp(previousTiming.finishedAt, nextTiming.finishedAt);
|
||||||
|
const elapsedDurationMs = traceElapsedMs(startedAt, finishedAt ?? lastEventAt);
|
||||||
|
const visibleTerminalDurationMs = options.preserveVisibleTerminalDuration === true ? traceElapsedMs(startedAt, new Date().toISOString()) : null;
|
||||||
|
const durationMs = maxTraceDuration(previousTiming.durationMs, nextTiming.durationMs, elapsedDurationMs, visibleTerminalDurationMs);
|
||||||
|
return {
|
||||||
|
...previousTiming,
|
||||||
|
...nextTiming,
|
||||||
|
startedAt,
|
||||||
|
lastEventAt,
|
||||||
|
finishedAt,
|
||||||
|
durationMs,
|
||||||
|
observedAt: latestTraceTimestamp(previousTiming.observedAt, nextTiming.observedAt),
|
||||||
|
lastEventAgeMs: null,
|
||||||
|
valuesRedacted: previousTiming.valuesRedacted !== false && nextTiming.valuesRedacted !== false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function traceTimingCandidate(value: unknown): TraceSnapshot["timing"] | null {
|
||||||
|
const record = traceRecord(value);
|
||||||
|
const timing = traceRecord(record?.timing);
|
||||||
|
if (!record && !timing) return null;
|
||||||
|
const startedAt = firstTraceTimestamp(timing?.startedAt, record?.startedAt);
|
||||||
|
const lastEventAt = firstTraceTimestamp(timing?.lastEventAt, record?.lastEventAt);
|
||||||
|
const finishedAt = firstTraceTimestamp(timing?.finishedAt, record?.finishedAt);
|
||||||
|
const observedAt = firstTraceTimestamp(timing?.observedAt, record?.observedAt);
|
||||||
|
const durationMs = maxTraceDuration(timing?.durationMs, record?.durationMs);
|
||||||
|
const lastEventAgeMs = finiteTraceDuration(timing?.lastEventAgeMs ?? record?.lastEventAgeMs);
|
||||||
|
if (!startedAt && !lastEventAt && !finishedAt && !observedAt && durationMs === null && lastEventAgeMs === null) return null;
|
||||||
|
return { ...(timing ?? {}), startedAt, lastEventAt, finishedAt, observedAt, durationMs, lastEventAgeMs, valuesRedacted: timing?.valuesRedacted !== false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function traceRecord(value: unknown): Record<string, unknown> | null {
|
||||||
|
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstTraceTimestamp(...values: unknown[]): string | null {
|
||||||
|
for (const value of values) {
|
||||||
|
if (typeof value !== "string") continue;
|
||||||
|
const text = value.trim();
|
||||||
|
if (!text) continue;
|
||||||
|
if (Number.isFinite(Date.parse(text))) return text;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function earliestTraceTimestamp(...values: unknown[]): string | null {
|
||||||
|
return selectTraceTimestamp("earliest", values);
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestTraceTimestamp(...values: unknown[]): string | null {
|
||||||
|
return selectTraceTimestamp("latest", values);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTraceTimestamp(mode: "earliest" | "latest", values: unknown[]): string | null {
|
||||||
|
let selected: { value: string; ms: number } | null = null;
|
||||||
|
for (const value of values) {
|
||||||
|
const timestamp = firstTraceTimestamp(value);
|
||||||
|
if (!timestamp) continue;
|
||||||
|
const ms = Date.parse(timestamp);
|
||||||
|
if (!selected || (mode === "earliest" ? ms < selected.ms : ms > selected.ms)) selected = { value: timestamp, ms };
|
||||||
|
}
|
||||||
|
return selected?.value ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function traceElapsedMs(startedAt: unknown, endedAt: unknown): number | null {
|
||||||
|
const start = startedAt == null ? NaN : Date.parse(String(startedAt));
|
||||||
|
const end = endedAt == null ? NaN : Date.parse(String(endedAt));
|
||||||
|
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
|
||||||
|
return Math.trunc(end - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maxTraceDuration(...values: unknown[]): number | null {
|
||||||
|
let max: number | null = null;
|
||||||
|
for (const value of values) {
|
||||||
|
const duration = finiteTraceDuration(value);
|
||||||
|
if (duration === null) continue;
|
||||||
|
if (max === null || duration > max) max = duration;
|
||||||
|
}
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finiteTraceDuration(value: unknown): number | null {
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) && number >= 0 ? Math.trunc(number) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function traceTimingWasRunning(value: unknown): boolean {
|
||||||
|
const record = traceRecord(value);
|
||||||
|
if (!record) return false;
|
||||||
|
const status = typeof record.status === "string" ? record.status.trim().toLowerCase() : "";
|
||||||
|
if (["completed", "failed", "cancelled", "canceled", "error"].includes(status)) return false;
|
||||||
|
const timing = traceTimingCandidate(value);
|
||||||
|
return !timing?.finishedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function traceTimingIsTerminal(value: unknown): boolean {
|
||||||
|
const record = traceRecord(value);
|
||||||
|
const status = typeof record?.status === "string" ? record.status.trim().toLowerCase() : "";
|
||||||
|
return ["completed", "failed", "cancelled", "canceled", "error"].includes(status) || traceTimingCandidate(value)?.finishedAt != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeTraceEvents(previousEvents: TraceEvent[], nextEvents: TraceEvent[]): TraceEvent[] {
|
function mergeTraceEvents(previousEvents: TraceEvent[], nextEvents: TraceEvent[]): TraceEvent[] {
|
||||||
@@ -298,22 +419,24 @@ function mergeTraceSnapshots(previous: TraceSnapshot | null, next: TraceSnapshot
|
|||||||
const previousEvents = Array.isArray(previous.events) ? previous.events : [];
|
const previousEvents = Array.isArray(previous.events) ? previous.events : [];
|
||||||
const nextEvents = Array.isArray(next.events) ? next.events : [];
|
const nextEvents = Array.isArray(next.events) ? next.events : [];
|
||||||
const events = mergeTraceEvents(previousEvents, nextEvents);
|
const events = mergeTraceEvents(previousEvents, nextEvents);
|
||||||
|
const timing = mergeTraceTimingProjection(previous, next, { preserveVisibleTerminalDuration: traceTimingWasRunning(previous) && traceTimingIsTerminal(next) });
|
||||||
return {
|
return {
|
||||||
...previous,
|
...previous,
|
||||||
...next,
|
...next,
|
||||||
events,
|
events,
|
||||||
eventCount: next.eventCount ?? previous.eventCount ?? events.length,
|
eventCount: next.eventCount ?? previous.eventCount ?? events.length,
|
||||||
timing: next.timing ?? previous.timing ?? null,
|
timing,
|
||||||
startedAt: next.startedAt ?? next.timing?.startedAt ?? previous.startedAt ?? previous.timing?.startedAt ?? null,
|
startedAt: timing?.startedAt ?? next.startedAt ?? next.timing?.startedAt ?? previous.startedAt ?? previous.timing?.startedAt ?? null,
|
||||||
lastEventAt: next.lastEventAt ?? next.timing?.lastEventAt ?? previous.lastEventAt ?? previous.timing?.lastEventAt ?? null,
|
lastEventAt: timing?.lastEventAt ?? next.lastEventAt ?? next.timing?.lastEventAt ?? previous.lastEventAt ?? previous.timing?.lastEventAt ?? null,
|
||||||
finishedAt: next.finishedAt ?? next.timing?.finishedAt ?? previous.finishedAt ?? previous.timing?.finishedAt ?? null,
|
finishedAt: timing?.finishedAt ?? next.finishedAt ?? next.timing?.finishedAt ?? previous.finishedAt ?? previous.timing?.finishedAt ?? null,
|
||||||
durationMs: next.durationMs ?? next.timing?.durationMs ?? previous.durationMs ?? previous.timing?.durationMs ?? null,
|
durationMs: timing?.durationMs ?? next.durationMs ?? next.timing?.durationMs ?? previous.durationMs ?? previous.timing?.durationMs ?? null,
|
||||||
lastEventLabel: next.lastEventLabel ?? previous.lastEventLabel ?? undefined,
|
lastEventLabel: next.lastEventLabel ?? previous.lastEventLabel ?? undefined,
|
||||||
updatedAt: next.updatedAt ?? previous.updatedAt ?? new Date().toISOString()
|
updatedAt: next.updatedAt ?? previous.updatedAt ?? new Date().toISOString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function traceSnapshotWithTurnStatus(trace: TraceSnapshot, turn: TraceSnapshot): TraceSnapshot {
|
function traceSnapshotWithTurnStatus(trace: TraceSnapshot, turn: TraceSnapshot): TraceSnapshot {
|
||||||
|
const timing = mergeTraceTimingProjection(trace, turn, { preserveVisibleTerminalDuration: traceTimingWasRunning(trace) && traceTimingIsTerminal(turn) });
|
||||||
return {
|
return {
|
||||||
...trace,
|
...trace,
|
||||||
traceId: trace.traceId ?? turn.traceId,
|
traceId: trace.traceId ?? turn.traceId,
|
||||||
@@ -325,11 +448,11 @@ function traceSnapshotWithTurnStatus(trace: TraceSnapshot, turn: TraceSnapshot):
|
|||||||
terminalEvidence: trace.terminalEvidence ?? turn.terminalEvidence,
|
terminalEvidence: trace.terminalEvidence ?? turn.terminalEvidence,
|
||||||
traceSummary: trace.traceSummary ?? turn.traceSummary,
|
traceSummary: trace.traceSummary ?? turn.traceSummary,
|
||||||
error: trace.error ?? turn.error,
|
error: trace.error ?? turn.error,
|
||||||
timing: turn.timing ?? trace.timing ?? null,
|
timing,
|
||||||
startedAt: turn.startedAt ?? turn.timing?.startedAt ?? trace.startedAt ?? trace.timing?.startedAt ?? null,
|
startedAt: timing?.startedAt ?? turn.startedAt ?? turn.timing?.startedAt ?? trace.startedAt ?? trace.timing?.startedAt ?? null,
|
||||||
lastEventAt: turn.lastEventAt ?? turn.timing?.lastEventAt ?? trace.lastEventAt ?? trace.timing?.lastEventAt ?? null,
|
lastEventAt: timing?.lastEventAt ?? turn.lastEventAt ?? turn.timing?.lastEventAt ?? trace.lastEventAt ?? trace.timing?.lastEventAt ?? null,
|
||||||
finishedAt: turn.finishedAt ?? turn.timing?.finishedAt ?? trace.finishedAt ?? trace.timing?.finishedAt ?? null,
|
finishedAt: timing?.finishedAt ?? turn.finishedAt ?? turn.timing?.finishedAt ?? trace.finishedAt ?? trace.timing?.finishedAt ?? null,
|
||||||
durationMs: turn.durationMs ?? turn.timing?.durationMs ?? trace.durationMs ?? trace.timing?.durationMs ?? null,
|
durationMs: timing?.durationMs ?? turn.durationMs ?? turn.timing?.durationMs ?? trace.durationMs ?? trace.timing?.durationMs ?? null,
|
||||||
projection: trace.projection ?? turn.projection ?? null,
|
projection: trace.projection ?? turn.projection ?? null,
|
||||||
projectionStatus: trace.projectionStatus ?? turn.projectionStatus ?? null,
|
projectionStatus: trace.projectionStatus ?? turn.projectionStatus ?? null,
|
||||||
projectionHealth: trace.projectionHealth ?? turn.projectionHealth ?? null,
|
projectionHealth: trace.projectionHealth ?? turn.projectionHealth ?? null,
|
||||||
|
|||||||
Reference in New Issue
Block a user