// SPEC: PJ2026-010403 API契约 draft-2026-06-17-r0; PJ2026-010401 Web工作台 draft-2026-06-17-r0. // Responsibility: Code Agent HTTP client for send, steer, turn, trace, session and cancel APIs. import { fetchJson, type ApiRequestOptions } from "./client"; import type { AgentChatResponse, AgentChatResultResponse, ApiResult } from "@/types"; export interface AgentTraceRequestOptions { sinceSeq?: number | null; limit?: number | null; } function workbenchTracePath(traceId: string, options: AgentTraceRequestOptions = {}): string { const params = new URLSearchParams(); if (Number.isFinite(options.sinceSeq)) params.set("sinceSeq", String(Math.max(0, Math.trunc(Number(options.sinceSeq))))); if (Number.isFinite(options.limit)) params.set("limit", String(Math.max(1, Math.trunc(Number(options.limit))))); const query = params.toString(); return `/v1/workbench/traces/${encodeURIComponent(traceId)}/events${query ? `?${query}` : ""}`; } export const agentAPI = { sendAgentMessage: (payload: Record, timeoutMs: number, activityRef?: ApiRequestOptions["activityRef"]): Promise> => fetchJson("/v1/agent/chat", { method: "POST", body: JSON.stringify(payload), timeoutMs, timeoutName: "Code Agent", activityRef }), steerAgentMessage: (payload: Record, timeoutMs: number, activityRef?: ApiRequestOptions["activityRef"]): Promise> => fetchJson("/v1/agent/chat/steer", { method: "POST", body: JSON.stringify(payload), timeoutMs, timeoutName: "Code Agent steer", activityRef }), getAgentTurn: async (traceId: string, timeoutMs = 8000, activityRef?: ApiRequestOptions["activityRef"]): Promise> => normalizeWorkbenchTurnResult(await fetchJson>(`/v1/workbench/turns/${encodeURIComponent(traceId)}`, { timeoutMs, timeoutName: "Code Agent turn", activityRef }), traceId), getAgentTrace: async (traceId: string, timeoutMs = 8000, activityRef?: ApiRequestOptions["activityRef"], options?: AgentTraceRequestOptions): Promise> => normalizeWorkbenchTraceResult(await fetchJson>(workbenchTracePath(traceId, options), { timeoutMs, timeoutName: "Code Agent trace", activityRef }), traceId), createAgentSession: (payload: Record = {}): Promise }>> => fetchJson("/v1/agent/sessions", { method: "POST", body: JSON.stringify(payload), timeoutMs: 30000, timeoutName: "Code Agent session create" }), getAgentSession: (sessionId: string): Promise }>> => fetchJson(`/v1/agent/sessions/${encodeURIComponent(sessionId)}`, { timeoutMs: 8000, timeoutName: "Code Agent session" }), deleteAgentSession: (sessionId: string): Promise; archivedCount?: number }>> => fetchJson(`/v1/agent/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE", timeoutMs: 30000, timeoutName: "Code Agent session delete" }), cancelAgentMessage: (payload: Record): Promise> => fetchJson("/v1/agent/chat/cancel", { method: "POST", body: JSON.stringify(payload), timeoutMs: 30000, timeoutName: "Code Agent cancel" }) }; function normalizeWorkbenchTurnResult(result: ApiResult>, traceId: string): ApiResult { if (!result.ok || !result.data) return result as ApiResult; const turn = recordValue(result.data.turn) ?? result.data; const trace = recordValue(turn.trace); const status = canonicalWorkbenchStatus(textValue(turn.status) ?? textValue(result.data.status) ?? "unknown", textValue(trace?.status, result.data.traceStatus)); const terminal = turn.terminal === true || isTerminalStatus(status); return { ...result, data: { ...turn, traceId: textValue(turn.traceId) ?? traceId, status, running: !terminal && (turn.running === true || isActiveStatus(status)), terminal, sessionId: textValue(turn.sessionId) ?? undefined, threadId: textValue(turn.threadId) ?? undefined, agentRun: recordValue(turn.agentRun) ?? undefined, updatedAt: textValue(turn.updatedAt) ?? textValue(result.data.observedAt) ?? undefined } as AgentChatResultResponse }; } function canonicalWorkbenchStatus(primary: string | null, traceStatus: string | null): string { const primaryStatus = normalizeStatus(primary); const trace = normalizeStatus(traceStatus); if (trace && isTerminalStatus(trace) && (!primaryStatus || isActiveStatus(primaryStatus) || !isTerminalStatus(primaryStatus))) return trace; return primaryStatus ?? trace ?? "unknown"; } function normalizeStatus(value: unknown): string | null { const text = textValue(value); if (!text) return null; const normalized = text.toLowerCase().replace(/_/gu, "-"); return normalized === "cancelled" ? "canceled" : normalized; } function isActiveStatus(value: unknown): boolean { return ["accepted", "active", "busy", "creating", "pending", "processing", "running"].includes(normalizeStatus(value) ?? ""); } function isTerminalStatus(value: unknown): boolean { return ["blocked", "canceled", "completed", "failed", "stale", "thread-resume-failed", "timeout"].includes(normalizeStatus(value) ?? ""); } function normalizeWorkbenchTraceResult(result: ApiResult>, traceId: string): ApiResult { if (!result.ok || !result.data) return result as ApiResult; const events = Array.isArray(result.data.events) ? result.data.events : []; const nextSeq = Number(result.data.nextSeq ?? result.data.nextSinceSeq); return { ...result, data: { ...result.data, traceId: textValue(result.data.traceId) ?? traceId, status: textValue(result.data.traceStatus, result.data.status) ?? "unknown", traceStatus: textValue(result.data.traceStatus, result.data.status) ?? undefined, events, traceEvents: events, eventCount: Number.isFinite(Number(result.data.eventCount)) ? Number(result.data.eventCount) : events.length, hasMore: result.data.hasMore === true, fullTraceLoaded: result.data.hasMore !== true, nextSinceSeq: Number.isFinite(nextSeq) ? Math.trunc(nextSeq) : null, range: recordValue(result.data.range) ?? undefined, runnerTrace: { traceId: textValue(result.data.traceId) ?? traceId, status: textValue(result.data.traceStatus, result.data.status) ?? undefined, events, eventCount: Number.isFinite(Number(result.data.eventCount)) ? Number(result.data.eventCount) : events.length, hasMore: result.data.hasMore === true, fullTraceLoaded: result.data.hasMore !== true, nextSinceSeq: Number.isFinite(nextSeq) ? Math.trunc(nextSeq) : null, range: recordValue(result.data.range) ?? undefined, eventSource: "trace-api" } } as AgentChatResultResponse }; } function recordValue(value: unknown): Record | null { return value && typeof value === "object" ? value as Record : null; } function textValue(...values: unknown[]): string | null { for (const value of values) { if (typeof value !== "string") continue; const text = value.trim(); if (text) return text; } return null; }