fix(v02): preserve trace for terminal chat responses (#868)
Co-authored-by: Codex Agent <codex@hwlab.local>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user