fix(workbench): keep running agent timing visible (#1980)

Co-authored-by: root <root@lyon.remote>
This commit is contained in:
Lyon
2026-06-23 19:23:06 +08:00
committed by GitHub
parent 64bee6e784
commit bf030c3125
3 changed files with 143 additions and 10 deletions
+28 -2
View File
@@ -233,8 +233,7 @@ export function initialAgentRunChatResult({ params = {}, options = {}, traceId }
sessionId: safeSessionId(params.sessionId) || agentRunSessionId(traceId),
threadId: safeOpaqueId(params.threadId) || null,
messageId: `msg_${safeTraceId(traceId)?.slice(4) || randomUUID()}`,
createdAt: timestamp,
updatedAt: timestamp,
...agentRunAdmissionTimingFields(timestamp),
provider: providerForBackendProfile(backendProfile),
model: modelForBackendProfile(backendProfile, env),
backend: backendForBackendProfile(backendProfile),
@@ -258,6 +257,28 @@ export function initialAgentRunChatResult({ params = {}, options = {}, traceId }
};
}
function agentRunAdmissionTimingFields(value) {
const admittedAt = timestampIsoOrNow(value);
const timing = {
startedAt: admittedAt,
lastEventAt: admittedAt,
finishedAt: null,
durationMs: null,
observedAt: admittedAt,
lastEventAgeMs: 0,
valuesRedacted: true
};
return {
createdAt: admittedAt,
updatedAt: admittedAt,
timing,
startedAt: admittedAt,
lastEventAt: admittedAt,
finishedAt: null,
durationMs: null
};
}
export async function submitAgentRunChatTurn({ params = {}, options = {}, traceId, traceStore = defaultCodeAgentTraceStore, results }) {
const env = options.env ?? process.env;
const managerUrl = resolveAgentRunManagerUrl(env);
@@ -3222,6 +3243,11 @@ function nowIso(now) {
return typeof now === "function" ? now() : new Date().toISOString();
}
function timestampIsoOrNow(value = null) {
const parsed = Date.parse(String(value ?? ""));
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : new Date().toISOString();
}
function redactUrl(value) {
try {
const url = new URL(value);
+101 -6
View File
@@ -113,8 +113,9 @@ export async function handleCodeAgentChatHttp(request, response, options) {
if (codeAgentChatShortConnectionRequested(request, params, options)) {
if (perf) perf.recordPhase({ phase: "short_connection_accept", durationMs: 0, outcome: "ok" });
let submitted = null;
try {
await submitCodeAgentChatTurn({
submitted = await submitCodeAgentChatTurn({
params: nativeSessionChatParams,
options,
traceId
@@ -129,6 +130,7 @@ export async function handleCodeAgentChatHttp(request, response, options) {
const traceUrl = `/v1/agent/traces/${encodeURIComponent(traceId)}`;
const turnUrl = `/v1/agent/turns/${encodeURIComponent(traceId)}`;
const statusCode = 202;
const admittedTiming = codeAgentAdmissionTimingFields(submitted?.createdAt ?? submitted?.updatedAt);
void emitCodeAgentOtelSpan("POST /v1/agent/chat", traceId, options.env ?? process.env, {
kind: 2,
attributes: { "http.method": "POST", "http.route": "/v1/agent/chat", "http.status_code": statusCode, sessionId: safeSessionId(nativeSessionChatParams.sessionId) || null, turnId: lifecycle.turnId }
@@ -142,6 +144,13 @@ export async function handleCodeAgentChatHttp(request, response, options) {
...codeAgentOtelTraceFields(traceId, options.env ?? process.env),
...lifecycle,
sessionId: safeSessionId(nativeSessionChatParams.sessionId) || null,
createdAt: submitted?.createdAt ?? admittedTiming.createdAt,
updatedAt: submitted?.updatedAt ?? admittedTiming.updatedAt,
timing: submitted?.timing ?? admittedTiming.timing,
startedAt: submitted?.startedAt ?? admittedTiming.startedAt,
lastEventAt: submitted?.lastEventAt ?? admittedTiming.lastEventAt,
finishedAt: submitted?.finishedAt ?? admittedTiming.finishedAt,
durationMs: submitted?.durationMs ?? admittedTiming.durationMs,
traceUrl,
resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`,
turnUrl,
@@ -779,6 +788,7 @@ function firstNonEmptyValue(...values) {
async function submitCodeAgentChatTurn({ params, options, traceId }) {
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
const results = options.codeAgentChatResults ?? createCodeAgentChatResultStore();
const timestamp = new Date().toISOString();
const acceptedPayload = {
accepted: true,
status: "running",
@@ -789,7 +799,7 @@ async function submitCodeAgentChatTurn({ params, options, traceId }) {
sessionId: safeSessionId(params.sessionId) || null,
threadId: safeOpaqueId(params.threadId) || null,
projectId: params.projectId ?? null,
updatedAt: new Date().toISOString()
...codeAgentAdmissionTimingFields(timestamp)
};
await recordCodeAgentTurnAdmission({ payload: acceptedPayload, params, options, traceId });
results.set(traceId, annotateOwner(acceptedPayload, params));
@@ -858,7 +868,7 @@ async function submitCodeAgentChatTurn({ params, options, traceId }) {
}
};
setImmediate(() => { run(); });
return;
return annotateOwner(acceptedPayload, params);
}
traceStore.ensure(traceId, {
runnerKind: "codex-app-server-stdio-runner",
@@ -945,6 +955,7 @@ async function submitCodeAgentChatTurn({ params, options, traceId }) {
setImmediate(() => {
run();
});
return annotateOwner(acceptedPayload, params);
}
async function recordCodeAgentTurnAdmission({ payload = {}, params = {}, options = {}, traceId } = {}) {
@@ -1070,8 +1081,7 @@ function codeAgentAdmittedFailureBasePayload({ params = {}, options = {}, traceI
conversationId: safeConversationId(params.conversationId) || null,
sessionId: safeSessionId(params.sessionId) || null,
threadId: safeOpaqueId(params.threadId) || null,
createdAt: now,
updatedAt: now,
...codeAgentAdmissionTimingFields(now),
provider: codeAgentAgentRunAdapterEnabled(options.env ?? process.env) ? "agentrun" : "codex-stdio",
model: options.env?.HWLAB_CODE_AGENT_MODEL ?? "unknown",
backend: codeAgentAgentRunAdapterEnabled(options.env ?? process.env) ? "agentrun-v01" : "hwlab-cloud-api/code-agent-chat",
@@ -1079,6 +1089,79 @@ function codeAgentAdmittedFailureBasePayload({ params = {}, options = {}, traceI
};
}
function codeAgentAdmissionTimingFields(value = null) {
const admittedAt = timestampIsoOrNow(value);
const timing = {
startedAt: admittedAt,
lastEventAt: admittedAt,
finishedAt: null,
durationMs: null,
observedAt: admittedAt,
lastEventAgeMs: 0,
valuesRedacted: true
};
return {
createdAt: admittedAt,
updatedAt: admittedAt,
timing,
startedAt: admittedAt,
lastEventAt: admittedAt,
finishedAt: null,
durationMs: null
};
}
function codeAgentResultTimingFields(...sources) {
const records = sources.filter((source) => source && typeof source === "object");
const timingSource = records.map((source) => source.timing).find((timing) => timing && typeof timing === "object") ?? {};
const startedAt = firstTimestampIso(timingSource.startedAt, ...records.map((source) => source.startedAt), ...records.map((source) => source.createdAt));
const lastEventAt = firstLatestTimestampIso(timingSource.lastEventAt, ...records.map((source) => source.lastEventAt), ...records.map((source) => source.updatedAt), ...records.map((source) => source.createdAt));
const finishedAt = firstLatestTimestampIso(timingSource.finishedAt, ...records.map((source) => source.finishedAt), ...records.map((source) => source.completedAt));
const durationMs = numberOrNull(timingSource.durationMs ?? records.map((source) => source.durationMs).find((value) => numberOrNull(value) !== null));
const observedAt = timestampIsoOrNull(timingSource.observedAt);
const lastEventAgeMs = numberOrNull(timingSource.lastEventAgeMs);
if (!startedAt && !lastEventAt && !finishedAt && durationMs === null && lastEventAgeMs === null) return {};
const timing = {
...(timingSource && typeof timingSource === "object" ? timingSource : {}),
startedAt: startedAt ?? null,
lastEventAt: lastEventAt ?? null,
finishedAt: finishedAt ?? null,
durationMs,
observedAt: observedAt ?? null,
lastEventAgeMs,
valuesRedacted: timingSource.valuesRedacted !== false
};
return {
timing,
startedAt: timing.startedAt,
lastEventAt: timing.lastEventAt,
finishedAt: timing.finishedAt,
durationMs: timing.durationMs
};
}
function firstTimestampIso(...values) {
for (const value of values) {
const timestamp = timestampIsoOrNull(value);
if (timestamp) return timestamp;
}
return null;
}
function firstLatestTimestampIso(...values) {
let latest = null;
let latestMs = Number.NEGATIVE_INFINITY;
for (const value of values) {
const timestamp = timestampIsoOrNull(value);
if (!timestamp) continue;
const ms = Date.parse(timestamp);
if (!Number.isFinite(ms) || ms < latestMs) continue;
latest = timestamp;
latestMs = ms;
}
return latest;
}
async function settleAdmittedCodeAgentFailure({ base = {}, params = {}, options = {}, traceId, results, failure = {}, traceLabel = "code-agent:failed" } = {}) {
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
const error = failure.payload?.error ?? failure.error ?? {};
@@ -2225,6 +2308,7 @@ function codeAgentTurnStatusPayload({ traceId, result, snapshot, resultPollError
const running = isTurnRunningStatus(status);
const terminal = isTurnTerminalStatus(status);
const runnerTrace = snapshotObject && snapshotObject.status !== "missing" ? snapshotObject : resultObject?.runnerTrace ?? null;
const timingFields = codeAgentResultTimingFields(resultObject, snapshotObject, runnerTrace);
return {
ok: found,
action: "code-agent.turn.status",
@@ -2237,7 +2321,8 @@ function codeAgentTurnStatusPayload({ traceId, result, snapshot, resultPollError
conversationId: safeConversationId(resultObject?.conversationId ?? snapshotObject?.conversationId) || null,
sessionId: safeSessionId(resultObject?.sessionId ?? resultObject?.session?.sessionId ?? snapshotObject?.sessionId) || null,
threadId: safeOpaqueId(resultObject?.threadId ?? resultObject?.session?.threadId ?? snapshotObject?.threadId) || null,
updatedAt: textValue(resultObject?.updatedAt ?? resultObject?.agentRun?.updatedAt ?? snapshotObject?.updatedAt) || null,
updatedAt: textValue(resultObject?.updatedAt ?? resultObject?.agentRun?.updatedAt ?? snapshotObject?.updatedAt ?? timingFields.lastEventAt ?? timingFields.startedAt) || null,
...timingFields,
lastEventLabel: textValue(snapshotObject?.lastEventLabel ?? runnerTrace?.lastEventLabel ?? lastEvent?.label ?? lastEvent?.type) || null,
waitingFor: textValue(snapshotObject?.waitingFor ?? runnerTrace?.waitingFor) || null,
resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`,
@@ -2970,6 +3055,16 @@ function textValue(value) {
return String(value ?? "").trim();
}
function timestampIsoOrNow(value = null) {
const parsed = Date.parse(String(value ?? ""));
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : new Date().toISOString();
}
function timestampIsoOrNull(value = null) {
const parsed = Date.parse(String(value ?? ""));
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
}
function codeAgentSessionOwnerEvidence(payload = {}, params = {}) {
const traceId = payload.traceId ?? params.traceId ?? null;
const finalResponse = codeAgentFinalResponseEvidence(payload, traceId);
+14 -2
View File
@@ -767,8 +767,9 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
const sessionId = composer.value.sessionId;
const threadId = composer.value.threadId;
const providerThreadId = providerThreadIdForRequest(threadId);
const user = makeMessage("user", value, "sent", { traceId: steerTraceId ?? traceId, sessionId, threadId, title: steerMode ? "用户引导" : "用户" });
const pending = makeMessage("agent", "", "running", { traceId, sessionId, threadId, title: "Code Agent", retryInput: value, traceAutoLifecycle: "running" });
const submittedAt = new Date().toISOString();
const user = makeMessage("user", value, "sent", { traceId: steerTraceId ?? traceId, sessionId, threadId, title: steerMode ? "用户引导" : "用户", createdAt: submittedAt, updatedAt: submittedAt });
const pending = makeMessage("agent", "", "running", { traceId, sessionId, threadId, title: "Code Agent", retryInput: value, traceAutoLifecycle: "running", createdAt: submittedAt, updatedAt: submittedAt, ...optimisticRunningTimingPatch(submittedAt) });
appendActiveMessages(user, pending);
startWorkbenchSubmitJourney({ traceId, sessionId, entry: submitEntry, backend: providerProfile.value, transport: "sse" });
chatPending.value = true;
@@ -1456,6 +1457,17 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
return { ...patch, timing, startedAt: timing.startedAt ?? null, lastEventAt: timing.lastEventAt ?? null, finishedAt: null, durationMs: null };
}
function optimisticRunningTimingPatch(startedAt: string): Partial<ChatMessage> {
const timing = {
startedAt,
lastEventAt: startedAt,
finishedAt: null,
durationMs: null,
valuesRedacted: true,
} as WorkbenchTurnTimingProjection;
return { timing, startedAt, lastEventAt: startedAt, finishedAt: null, durationMs: null };
}
async function clearActiveTrace(traceId: string, reason: string): Promise<void> {
if (currentRequest.value?.traceId === traceId) currentRequest.value = null;
}