fix(workbench): keep running agent timing visible (#1980)
Co-authored-by: root <root@lyon.remote>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user