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),
|
sessionId: safeSessionId(params.sessionId) || agentRunSessionId(traceId),
|
||||||
threadId: safeOpaqueId(params.threadId) || null,
|
threadId: safeOpaqueId(params.threadId) || null,
|
||||||
messageId: `msg_${safeTraceId(traceId)?.slice(4) || randomUUID()}`,
|
messageId: `msg_${safeTraceId(traceId)?.slice(4) || randomUUID()}`,
|
||||||
createdAt: timestamp,
|
...agentRunAdmissionTimingFields(timestamp),
|
||||||
updatedAt: timestamp,
|
|
||||||
provider: providerForBackendProfile(backendProfile),
|
provider: providerForBackendProfile(backendProfile),
|
||||||
model: modelForBackendProfile(backendProfile, env),
|
model: modelForBackendProfile(backendProfile, env),
|
||||||
backend: backendForBackendProfile(backendProfile),
|
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 }) {
|
export async function submitAgentRunChatTurn({ params = {}, options = {}, traceId, traceStore = defaultCodeAgentTraceStore, results }) {
|
||||||
const env = options.env ?? process.env;
|
const env = options.env ?? process.env;
|
||||||
const managerUrl = resolveAgentRunManagerUrl(env);
|
const managerUrl = resolveAgentRunManagerUrl(env);
|
||||||
@@ -3222,6 +3243,11 @@ function nowIso(now) {
|
|||||||
return typeof now === "function" ? now() : new Date().toISOString();
|
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) {
|
function redactUrl(value) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(value);
|
const url = new URL(value);
|
||||||
|
|||||||
@@ -113,8 +113,9 @@ export async function handleCodeAgentChatHttp(request, response, options) {
|
|||||||
|
|
||||||
if (codeAgentChatShortConnectionRequested(request, params, options)) {
|
if (codeAgentChatShortConnectionRequested(request, params, options)) {
|
||||||
if (perf) perf.recordPhase({ phase: "short_connection_accept", durationMs: 0, outcome: "ok" });
|
if (perf) perf.recordPhase({ phase: "short_connection_accept", durationMs: 0, outcome: "ok" });
|
||||||
|
let submitted = null;
|
||||||
try {
|
try {
|
||||||
await submitCodeAgentChatTurn({
|
submitted = await submitCodeAgentChatTurn({
|
||||||
params: nativeSessionChatParams,
|
params: nativeSessionChatParams,
|
||||||
options,
|
options,
|
||||||
traceId
|
traceId
|
||||||
@@ -129,6 +130,7 @@ export async function handleCodeAgentChatHttp(request, response, options) {
|
|||||||
const traceUrl = `/v1/agent/traces/${encodeURIComponent(traceId)}`;
|
const traceUrl = `/v1/agent/traces/${encodeURIComponent(traceId)}`;
|
||||||
const turnUrl = `/v1/agent/turns/${encodeURIComponent(traceId)}`;
|
const turnUrl = `/v1/agent/turns/${encodeURIComponent(traceId)}`;
|
||||||
const statusCode = 202;
|
const statusCode = 202;
|
||||||
|
const admittedTiming = codeAgentAdmissionTimingFields(submitted?.createdAt ?? submitted?.updatedAt);
|
||||||
void emitCodeAgentOtelSpan("POST /v1/agent/chat", traceId, options.env ?? process.env, {
|
void emitCodeAgentOtelSpan("POST /v1/agent/chat", traceId, options.env ?? process.env, {
|
||||||
kind: 2,
|
kind: 2,
|
||||||
attributes: { "http.method": "POST", "http.route": "/v1/agent/chat", "http.status_code": statusCode, sessionId: safeSessionId(nativeSessionChatParams.sessionId) || null, turnId: lifecycle.turnId }
|
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),
|
...codeAgentOtelTraceFields(traceId, options.env ?? process.env),
|
||||||
...lifecycle,
|
...lifecycle,
|
||||||
sessionId: safeSessionId(nativeSessionChatParams.sessionId) || null,
|
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,
|
traceUrl,
|
||||||
resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`,
|
resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`,
|
||||||
turnUrl,
|
turnUrl,
|
||||||
@@ -779,6 +788,7 @@ function firstNonEmptyValue(...values) {
|
|||||||
async function submitCodeAgentChatTurn({ params, options, traceId }) {
|
async function submitCodeAgentChatTurn({ params, options, traceId }) {
|
||||||
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
|
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
|
||||||
const results = options.codeAgentChatResults ?? createCodeAgentChatResultStore();
|
const results = options.codeAgentChatResults ?? createCodeAgentChatResultStore();
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
const acceptedPayload = {
|
const acceptedPayload = {
|
||||||
accepted: true,
|
accepted: true,
|
||||||
status: "running",
|
status: "running",
|
||||||
@@ -789,7 +799,7 @@ async function submitCodeAgentChatTurn({ params, options, traceId }) {
|
|||||||
sessionId: safeSessionId(params.sessionId) || null,
|
sessionId: safeSessionId(params.sessionId) || null,
|
||||||
threadId: safeOpaqueId(params.threadId) || null,
|
threadId: safeOpaqueId(params.threadId) || null,
|
||||||
projectId: params.projectId ?? null,
|
projectId: params.projectId ?? null,
|
||||||
updatedAt: new Date().toISOString()
|
...codeAgentAdmissionTimingFields(timestamp)
|
||||||
};
|
};
|
||||||
await recordCodeAgentTurnAdmission({ payload: acceptedPayload, params, options, traceId });
|
await recordCodeAgentTurnAdmission({ payload: acceptedPayload, params, options, traceId });
|
||||||
results.set(traceId, annotateOwner(acceptedPayload, params));
|
results.set(traceId, annotateOwner(acceptedPayload, params));
|
||||||
@@ -858,7 +868,7 @@ async function submitCodeAgentChatTurn({ params, options, traceId }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
setImmediate(() => { run(); });
|
setImmediate(() => { run(); });
|
||||||
return;
|
return annotateOwner(acceptedPayload, params);
|
||||||
}
|
}
|
||||||
traceStore.ensure(traceId, {
|
traceStore.ensure(traceId, {
|
||||||
runnerKind: "codex-app-server-stdio-runner",
|
runnerKind: "codex-app-server-stdio-runner",
|
||||||
@@ -945,6 +955,7 @@ async function submitCodeAgentChatTurn({ params, options, traceId }) {
|
|||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
run();
|
run();
|
||||||
});
|
});
|
||||||
|
return annotateOwner(acceptedPayload, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function recordCodeAgentTurnAdmission({ payload = {}, params = {}, options = {}, traceId } = {}) {
|
async function recordCodeAgentTurnAdmission({ payload = {}, params = {}, options = {}, traceId } = {}) {
|
||||||
@@ -1070,8 +1081,7 @@ function codeAgentAdmittedFailureBasePayload({ params = {}, options = {}, traceI
|
|||||||
conversationId: safeConversationId(params.conversationId) || null,
|
conversationId: safeConversationId(params.conversationId) || null,
|
||||||
sessionId: safeSessionId(params.sessionId) || null,
|
sessionId: safeSessionId(params.sessionId) || null,
|
||||||
threadId: safeOpaqueId(params.threadId) || null,
|
threadId: safeOpaqueId(params.threadId) || null,
|
||||||
createdAt: now,
|
...codeAgentAdmissionTimingFields(now),
|
||||||
updatedAt: now,
|
|
||||||
provider: codeAgentAgentRunAdapterEnabled(options.env ?? process.env) ? "agentrun" : "codex-stdio",
|
provider: codeAgentAgentRunAdapterEnabled(options.env ?? process.env) ? "agentrun" : "codex-stdio",
|
||||||
model: options.env?.HWLAB_CODE_AGENT_MODEL ?? "unknown",
|
model: options.env?.HWLAB_CODE_AGENT_MODEL ?? "unknown",
|
||||||
backend: codeAgentAgentRunAdapterEnabled(options.env ?? process.env) ? "agentrun-v01" : "hwlab-cloud-api/code-agent-chat",
|
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" } = {}) {
|
async function settleAdmittedCodeAgentFailure({ base = {}, params = {}, options = {}, traceId, results, failure = {}, traceLabel = "code-agent:failed" } = {}) {
|
||||||
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
|
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
|
||||||
const error = failure.payload?.error ?? failure.error ?? {};
|
const error = failure.payload?.error ?? failure.error ?? {};
|
||||||
@@ -2225,6 +2308,7 @@ function codeAgentTurnStatusPayload({ traceId, result, snapshot, resultPollError
|
|||||||
const running = isTurnRunningStatus(status);
|
const running = isTurnRunningStatus(status);
|
||||||
const terminal = isTurnTerminalStatus(status);
|
const terminal = isTurnTerminalStatus(status);
|
||||||
const runnerTrace = snapshotObject && snapshotObject.status !== "missing" ? snapshotObject : resultObject?.runnerTrace ?? null;
|
const runnerTrace = snapshotObject && snapshotObject.status !== "missing" ? snapshotObject : resultObject?.runnerTrace ?? null;
|
||||||
|
const timingFields = codeAgentResultTimingFields(resultObject, snapshotObject, runnerTrace);
|
||||||
return {
|
return {
|
||||||
ok: found,
|
ok: found,
|
||||||
action: "code-agent.turn.status",
|
action: "code-agent.turn.status",
|
||||||
@@ -2237,7 +2321,8 @@ function codeAgentTurnStatusPayload({ traceId, result, snapshot, resultPollError
|
|||||||
conversationId: safeConversationId(resultObject?.conversationId ?? snapshotObject?.conversationId) || null,
|
conversationId: safeConversationId(resultObject?.conversationId ?? snapshotObject?.conversationId) || null,
|
||||||
sessionId: safeSessionId(resultObject?.sessionId ?? resultObject?.session?.sessionId ?? snapshotObject?.sessionId) || null,
|
sessionId: safeSessionId(resultObject?.sessionId ?? resultObject?.session?.sessionId ?? snapshotObject?.sessionId) || null,
|
||||||
threadId: safeOpaqueId(resultObject?.threadId ?? resultObject?.session?.threadId ?? snapshotObject?.threadId) || 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,
|
lastEventLabel: textValue(snapshotObject?.lastEventLabel ?? runnerTrace?.lastEventLabel ?? lastEvent?.label ?? lastEvent?.type) || null,
|
||||||
waitingFor: textValue(snapshotObject?.waitingFor ?? runnerTrace?.waitingFor) || null,
|
waitingFor: textValue(snapshotObject?.waitingFor ?? runnerTrace?.waitingFor) || null,
|
||||||
resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`,
|
resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`,
|
||||||
@@ -2970,6 +3055,16 @@ function textValue(value) {
|
|||||||
return String(value ?? "").trim();
|
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 = {}) {
|
function codeAgentSessionOwnerEvidence(payload = {}, params = {}) {
|
||||||
const traceId = payload.traceId ?? params.traceId ?? null;
|
const traceId = payload.traceId ?? params.traceId ?? null;
|
||||||
const finalResponse = codeAgentFinalResponseEvidence(payload, traceId);
|
const finalResponse = codeAgentFinalResponseEvidence(payload, traceId);
|
||||||
|
|||||||
@@ -767,8 +767,9 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
|
|||||||
const sessionId = composer.value.sessionId;
|
const sessionId = composer.value.sessionId;
|
||||||
const threadId = composer.value.threadId;
|
const threadId = composer.value.threadId;
|
||||||
const providerThreadId = providerThreadIdForRequest(threadId);
|
const providerThreadId = providerThreadIdForRequest(threadId);
|
||||||
const user = makeMessage("user", value, "sent", { traceId: steerTraceId ?? traceId, sessionId, threadId, title: steerMode ? "用户引导" : "用户" });
|
const submittedAt = new Date().toISOString();
|
||||||
const pending = makeMessage("agent", "", "running", { traceId, sessionId, threadId, title: "Code Agent", retryInput: value, traceAutoLifecycle: "running" });
|
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);
|
appendActiveMessages(user, pending);
|
||||||
startWorkbenchSubmitJourney({ traceId, sessionId, entry: submitEntry, backend: providerProfile.value, transport: "sse" });
|
startWorkbenchSubmitJourney({ traceId, sessionId, entry: submitEntry, backend: providerProfile.value, transport: "sse" });
|
||||||
chatPending.value = true;
|
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 };
|
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> {
|
async function clearActiveTrace(traceId: string, reason: string): Promise<void> {
|
||||||
if (currentRequest.value?.traceId === traceId) currentRequest.value = null;
|
if (currentRequest.value?.traceId === traceId) currentRequest.value = null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user