fix(workbench): expose code agent timing metadata
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
sendJson
|
||||
} from "./server-http-utils.ts";
|
||||
import { createWorkbenchReadModel } from "./workbench-read-model.ts";
|
||||
import { createWorkbenchTurnProjection, durableTraceStatus, RUNNING_STATUSES, TERMINAL_STATUSES } from "./workbench-turn-projection.ts";
|
||||
import { createWorkbenchTurnProjection, createWorkbenchTurnTimingProjection, durableTraceStatus, RUNNING_STATUSES, TERMINAL_STATUSES } from "./workbench-turn-projection.ts";
|
||||
|
||||
const DEFAULT_PAGE_LIMIT = 50;
|
||||
const DEFAULT_SESSION_LIST_LIMIT = 20;
|
||||
@@ -436,8 +436,10 @@ function factSessionSummary(session, facts = {}) {
|
||||
const traceId = factLastTraceId(session) ?? factLatestTraceIdForSession(facts, sessionId);
|
||||
const projection = traceId ? factProjectionForTrace(facts, traceId) : null;
|
||||
const turn = traceId ? factTurnForTrace(facts, traceId) : null;
|
||||
const checkpoint = traceId ? factCheckpointForTrace(facts, traceId) : null;
|
||||
const messages = factMessagesForSession(session, facts);
|
||||
const status = normalizeStatus(turn?.status ?? session?.status);
|
||||
const timing = factTimingProjection({ turn, checkpoint, session, status });
|
||||
return {
|
||||
sessionId,
|
||||
threadId: safeOpaqueId(session?.threadId) ?? (textValue(session?.threadId) || null),
|
||||
@@ -467,6 +469,11 @@ function factSessionSummary(session, facts = {}) {
|
||||
projectionHealth: projection?.projectionHealth ?? null,
|
||||
staleMs: projection?.staleMs ?? null,
|
||||
blocker: projection?.blocker ?? null,
|
||||
timing,
|
||||
startedAt: timing.startedAt,
|
||||
lastEventAt: timing.lastEventAt,
|
||||
finishedAt: timing.finishedAt,
|
||||
durationMs: timing.durationMs,
|
||||
updatedAt: factUpdatedAt(turn)
|
||||
} : null,
|
||||
valuesRedacted: true
|
||||
@@ -503,30 +510,45 @@ function factMessagesForSession(session, facts = {}) {
|
||||
return factArray(facts.messages)
|
||||
.filter((message) => message.sessionId === sessionId)
|
||||
.sort(compareFactMessagesAsc)
|
||||
.map((message) => factMessageDto(message, partsByMessageId.get(message.messageId) ?? []))
|
||||
.map((message) => factMessageDto(message, partsByMessageId.get(message.messageId) ?? [], facts))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function factMessageDto(message, parts = []) {
|
||||
function factMessageDto(message, parts = [], facts = {}) {
|
||||
const messageId = safeMessageId(message?.messageId) || textValue(message?.messageId);
|
||||
if (!messageId) return null;
|
||||
const traceId = safeTraceId(message?.traceId) ?? null;
|
||||
const role = textValue(message?.role) || "agent";
|
||||
const status = normalizeStatus(message?.status);
|
||||
const timing = isAssistantLikeRole(role)
|
||||
? factTimingProjection({
|
||||
message,
|
||||
turn: traceId ? factTurnForTrace(facts, traceId, message?.turnId) : null,
|
||||
checkpoint: traceId ? factCheckpointForTrace(facts, traceId) : null,
|
||||
status
|
||||
})
|
||||
: factTimingProjection({ message, status });
|
||||
const text = projectionText(message?.text, message?.content, message?.message, message?.finalResponse);
|
||||
const normalizedParts = parts.length > 0
|
||||
? [...parts].sort(compareFactPartsAsc).map((part) => factPartDto(part, messageId, traceId)).filter(Boolean)
|
||||
: text ? [partFact({ type: "text", text, status: message?.status }, 0, messageId, traceId)] : [];
|
||||
return {
|
||||
messageId,
|
||||
role: textValue(message?.role) || "agent",
|
||||
role,
|
||||
sessionId: (safeSessionId(message?.sessionId) ?? textValue(message?.sessionId)) || null,
|
||||
traceId,
|
||||
turnId: safeTurnId(message?.turnId) || traceId,
|
||||
status: normalizeStatus(message?.status),
|
||||
status,
|
||||
parts: normalizedParts,
|
||||
text: text || "",
|
||||
textPreview: text ? text.slice(0, 240) : null,
|
||||
createdAt: textValue(message?.createdAt) || null,
|
||||
updatedAt: factUpdatedAt(message),
|
||||
timing,
|
||||
startedAt: timing.startedAt,
|
||||
lastEventAt: timing.lastEventAt,
|
||||
finishedAt: timing.finishedAt,
|
||||
durationMs: timing.durationMs,
|
||||
projectionStatus: null,
|
||||
projectionHealth: null,
|
||||
valuesRedacted: message?.valuesRedacted !== false
|
||||
@@ -569,6 +591,7 @@ function factTurnSnapshot({ turn = null, session = null, facts = {}, traceId, tu
|
||||
const checkpoint = safeTrace ? factCheckpointForTrace(facts, safeTrace) : null;
|
||||
const finalText = projectionText(turn?.finalResponse, turn?.assistantText, turn?.text, assistantMessage?.text);
|
||||
const trace = factTraceSnapshot(facts, safeTrace);
|
||||
const timing = factTimingProjection({ turn, checkpoint, trace, session, status });
|
||||
return {
|
||||
turnId: resolvedTurnId,
|
||||
traceId: safeTrace,
|
||||
@@ -581,6 +604,11 @@ function factTurnSnapshot({ turn = null, session = null, facts = {}, traceId, tu
|
||||
assistantMessageId: assistantMessage?.messageId ?? turn?.messageId ?? null,
|
||||
assistantText: finalText ?? null,
|
||||
finalResponse: turn?.finalResponse ?? (finalText ? { text: finalText, status, traceId: safeTrace, valuesPrinted: false } : null),
|
||||
timing,
|
||||
startedAt: timing.startedAt,
|
||||
lastEventAt: timing.lastEventAt,
|
||||
finishedAt: timing.finishedAt,
|
||||
durationMs: timing.durationMs,
|
||||
agentRun: checkpoint ? {
|
||||
runId: textValue(checkpoint.runId) || null,
|
||||
commandId: textValue(checkpoint.commandId) || null,
|
||||
@@ -592,6 +620,11 @@ function factTurnSnapshot({ turn = null, session = null, facts = {}, traceId, tu
|
||||
traceId: safeTrace,
|
||||
status: trace.status,
|
||||
eventCount: trace.eventCount,
|
||||
timing: trace.timing,
|
||||
startedAt: trace.startedAt,
|
||||
lastEventAt: trace.lastEventAt,
|
||||
finishedAt: trace.finishedAt,
|
||||
durationMs: trace.durationMs,
|
||||
updatedAt: trace.updatedAt
|
||||
},
|
||||
urls: {
|
||||
@@ -610,13 +643,20 @@ function factTraceSnapshot(facts = {}, traceId) {
|
||||
.filter(Boolean);
|
||||
const status = durableTraceStatus(events);
|
||||
const lastEvent = events.at(-1) ?? null;
|
||||
const checkpoint = factCheckpointForTrace(facts, safeTrace);
|
||||
const timing = factTimingProjection({ trace: { traceId: safeTrace, status, events, lastEvent, updatedAt: lastEvent?.updatedAt ?? lastEvent?.createdAt ?? checkpoint?.updatedAt ?? null }, checkpoint, status });
|
||||
return {
|
||||
traceId: safeTrace,
|
||||
status,
|
||||
eventCount: events.length,
|
||||
events,
|
||||
lastEvent,
|
||||
updatedAt: lastEvent?.updatedAt ?? lastEvent?.createdAt ?? factCheckpointForTrace(facts, safeTrace)?.updatedAt ?? null,
|
||||
timing,
|
||||
startedAt: timing.startedAt,
|
||||
lastEventAt: timing.lastEventAt,
|
||||
finishedAt: timing.finishedAt,
|
||||
durationMs: timing.durationMs,
|
||||
updatedAt: lastEvent?.updatedAt ?? lastEvent?.createdAt ?? checkpoint?.updatedAt ?? null,
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
};
|
||||
@@ -659,6 +699,7 @@ function factProjectionForTrace(facts = {}, traceId) {
|
||||
const projectionStatus = normalizeProjectionStatus(checkpoint.projectionStatus);
|
||||
const projectionHealth = normalizeProjectionHealth(checkpoint.projectionHealth, projectionStatus);
|
||||
const diagnostic = objectValue(checkpoint.diagnostic);
|
||||
const timing = factTimingProjection({ checkpoint });
|
||||
return {
|
||||
projectionStatus,
|
||||
projectionHealth,
|
||||
@@ -667,11 +708,56 @@ function factProjectionForTrace(facts = {}, traceId) {
|
||||
sourceCommandId: textValue(checkpoint.commandId ?? checkpoint.sourceCommandId) || null,
|
||||
staleMs: null,
|
||||
blocker: diagnostic.blocker ?? checkpoint.blocker ?? null,
|
||||
timing,
|
||||
startedAt: timing.startedAt,
|
||||
lastEventAt: timing.lastEventAt,
|
||||
finishedAt: timing.finishedAt,
|
||||
durationMs: timing.durationMs,
|
||||
updatedAt: factUpdatedAt(checkpoint),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function factTimingProjection({ message = null, turn = null, checkpoint = null, trace = null, session = null, status = null } = {}) {
|
||||
const records = [message, turn, checkpoint, trace, session].filter(Boolean);
|
||||
const directTiming = records.map((record) => objectValue(record?.timing)).find(Boolean) ?? null;
|
||||
const direct = {
|
||||
status,
|
||||
timing: directTiming,
|
||||
startedAt: firstFactTimestamp(records, "startedAt"),
|
||||
lastEventAt: firstFactTimestamp(records, "lastEventAt"),
|
||||
finishedAt: firstFactTimestamp(records, "finishedAt"),
|
||||
durationMs: firstFactDuration(records),
|
||||
updatedAt: firstFactTimestamp(records, "updatedAt")
|
||||
};
|
||||
return createWorkbenchTurnTimingProjection({ result: direct, trace, session, status });
|
||||
}
|
||||
|
||||
function firstFactTimestamp(records, key) {
|
||||
for (const record of records) {
|
||||
const nested = objectValue(record?.timing);
|
||||
const timestamp = timestampIso(record?.[key] ?? nested?.[key]);
|
||||
if (timestamp) return timestamp;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function firstFactDuration(records) {
|
||||
for (const record of records) {
|
||||
const nested = objectValue(record?.timing);
|
||||
for (const value of [record?.durationMs, nested?.durationMs]) {
|
||||
const number = Number(value);
|
||||
if (Number.isFinite(number) && number >= 0) return Math.trunc(number);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function timestampIso(value) {
|
||||
const ms = Date.parse(String(value ?? ""));
|
||||
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
||||
}
|
||||
|
||||
function factCheckpointForTrace(facts = {}, traceId) {
|
||||
const safeTrace = safeTraceId(traceId);
|
||||
if (!safeTrace) return null;
|
||||
@@ -950,6 +1036,11 @@ async function handleWorkbenchTraceEventPage(response, url, options, actor, rawT
|
||||
sourceCommandId: responseProjection.sourceCommandId,
|
||||
staleMs: responseProjection.staleMs,
|
||||
blocker: responseProjection.blocker,
|
||||
timing: responseProjection.timing,
|
||||
startedAt: responseProjection.startedAt,
|
||||
lastEventAt: responseProjection.lastEventAt,
|
||||
finishedAt: responseProjection.finishedAt,
|
||||
durationMs: responseProjection.durationMs,
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user