fix(v02): preserve trace for terminal chat responses (#868)

Co-authored-by: Codex Agent <codex@hwlab.local>
This commit is contained in:
Lyon
2026-06-04 22:53:31 +08:00
committed by GitHub
parent 47582b2a27
commit 99d0604861
2 changed files with 64 additions and 2 deletions
@@ -195,6 +195,47 @@ test("subscribeToTrace preserves trace when terminal result with text arrives be
}
});
test("subscribeToTrace preserves trace when initial response is already terminal", async () => {
const originalFetch = globalThis.fetch;
const events: TraceEvent[] = [
{ seq: 1, label: "agentrun:request:accepted", status: "accepted" },
{ seq: 2, label: "agentrun:result:failed", type: "result", status: "failed", message: "provider failed" }
];
const fetches: string[] = [];
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
fetches.push(url);
if (url.includes("/trace/")) {
return jsonResponse({ status: "failed", traceId: "trc_initial_terminal", events, eventCount: events.length });
}
return jsonResponse({});
}) as typeof fetch;
try {
const snapshots: TraceSnapshot[] = [];
const completed = await new Promise<AgentChatResultResponse>((resolve, reject) => {
void subscribeToTrace({
traceId: "trc_initial_terminal",
initial: { status: "failed", traceId: "trc_initial_terminal", assistantText: "provider failed" },
onActivity: () => undefined,
onSnapshot: (snapshot) => snapshots.push(snapshot),
onComplete: resolve,
onInfrastructureError: (error) => reject(new Error(error)),
signal: new AbortController().signal,
inactivityTimeoutMs: 1000
}).catch(reject);
});
assert.equal(completed.assistantText, "provider failed");
assert.equal(completed.runnerTrace?.events?.length, 2);
assert.equal(completed.runnerTrace?.eventCount, 2);
assert.equal(completed.traceEvents?.length, 2);
assert.equal(snapshots.length, 1);
assert.deepEqual(fetches, ["/v1/agent/chat/trace/trc_initial_terminal?projectId=prj_device_pod_workbench"]);
} finally {
globalThis.fetch = originalFetch;
}
});
test("subscribeToTrace merges terminal result without text with trace assistant content", async () => {
const originalFetch = globalThis.fetch;
const originalSetTimeout = globalThis.window?.setTimeout;
+23 -2
View File
@@ -184,9 +184,14 @@ export interface TraceSubscriptionConfig {
export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise<void> {
const { traceId, initial, onActivity, onSnapshot, onComplete, onInfrastructureError, signal, inactivityTimeoutMs } = config;
// If the initial response is already terminal, fire onComplete and return.
// If the initial response is already terminal, still fetch the trace once.
// Provider fast-fail responses can include final text immediately while the
// trace endpoint already has the event list needed for refresh persistence.
if (isTerminalStatus(initial.status)) {
onComplete(initial as AgentChatResultResponse);
const traceSnapshot = await fetchTerminalTraceSnapshot(traceId, traceId, inactivityTimeoutMs, signal);
if (signal.aborted) return;
if (traceSnapshot) onSnapshot(traceSnapshot);
onComplete(mergeTraceResults(initial as AgentChatResultResponse, traceSnapshot));
return;
}
if (!initial.resultUrl) {
@@ -308,6 +313,22 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
}
}
async function fetchTerminalTraceSnapshot(traceId: string, traceUrlOrId: string, inactivityTimeoutMs: number, signal: AbortSignal): Promise<TraceSnapshot | null> {
if (signal.aborted) return null;
const url = traceUrlOrId.startsWith("/") ? traceUrlOrId : `/v1/agent/chat/trace/${encodeURIComponent(traceUrlOrId)}?projectId=prj_device_pod_workbench`;
const tracePolled = await api.getAgentChatResult(url, inactivityTimeoutMs, null);
if (signal.aborted || !tracePolled.ok || !tracePolled.data) return null;
const events = Array.isArray(tracePolled.data.events) ? tracePolled.data.events : [];
const last = events.at(-1);
return snapshotToTraceSnapshot({
...tracePolled.data,
traceId,
events,
eventCount: Number(tracePolled.data.eventCount ?? events.length) || 0,
lastEventLabel: String(last?.label ?? last?.type ?? tracePolled.data.lastEventLabel ?? "")
} as AgentChatResultResponse);
}
function snapshotToTraceSnapshot(result: AgentChatResultResponse): TraceSnapshot {
const events = Array.isArray((result as { events?: TraceEvent[] }).events) ? ((result as { events?: TraceEvent[] }).events ?? []) : [];
return {