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 () => {
|
test("subscribeToTrace merges terminal result without text with trace assistant content", async () => {
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
const originalSetTimeout = globalThis.window?.setTimeout;
|
const originalSetTimeout = globalThis.window?.setTimeout;
|
||||||
|
|||||||
@@ -184,9 +184,14 @@ export interface TraceSubscriptionConfig {
|
|||||||
export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise<void> {
|
export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise<void> {
|
||||||
const { traceId, initial, onActivity, onSnapshot, onComplete, onInfrastructureError, signal, inactivityTimeoutMs } = config;
|
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)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (!initial.resultUrl) {
|
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 {
|
function snapshotToTraceSnapshot(result: AgentChatResultResponse): TraceSnapshot {
|
||||||
const events = Array.isArray((result as { events?: TraceEvent[] }).events) ? ((result as { events?: TraceEvent[] }).events ?? []) : [];
|
const events = Array.isArray((result as { events?: TraceEvent[] }).events) ? ((result as { events?: TraceEvent[] }).events ?? []) : [];
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user