// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010403 API契约 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010205 HWLAB接入 draft-2026-06-25-p0-session-warm-runner-contract; PJ2026-0102 Agent编排 draft-2026-06-17-r0; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0. // Responsibility: Code Agent HTTP steer and trace resources. import { createHash, randomUUID } from "node:crypto"; import { M3_IO_CONTROL_ROUTE, M3_STATUS_ROUTE, describeM3StatusLive, handleM3IoControl } from "./m3-io-control.ts"; import { agentRunSessionEvidence, cancelAgentRunChatTurn, codeAgentAgentRunAdapterEnabled, initialAgentRunChatResult, loadPersistedAgentRunResult, steerAgentRunChatTurn, submitAgentRunChatTurn } from "./code-agent-agentrun-adapter.ts"; import { createCodeAgentErrorPayload, handleCodeAgentChat } from "./code-agent-chat.ts"; import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts"; import { codeAgentSessionLifecycleSummary } from "./code-agent-session-lifecycle.ts"; import { messageAuthorityTextValue } from "./code-agent-agentrun-prompt.ts"; import { writeWorkbenchProjectionSession, writeWorkbenchSessionAdmissionFact } from "./workbench-projection-writer.ts"; import { createWorkbenchReadModel } from "./workbench-read-model.ts"; import { codeAgentOtelTraceFields, emitCodeAgentOtelSpan } from "./otel-trace.ts"; import { firstHeaderValue, getHeader, parsePositiveInteger, positiveInteger, readBody, safeConversationId, safeOpaqueId, safeSessionId, safeTraceId, sendJson, truthyFlag } from "./server-http-utils.ts"; import { normalizeCodeAgentProviderProfile, firstNonEmptyValue, recordCodeAgentSessionInputFact, stableCodeAgentInputId, codeAgentInputDelivery, codeAgentInputStatus, codeAgentResultTimingFields, firstTimestampIso, firstLatestTimestampIso, codeAgentChatShortConnectionRequested, preflightCodeAgentBilling, recordCodeAgentBillingUsage, finalizeCodeAgentBillingUsage, releaseCodeAgentBillingReservation, withCodeAgentBillingReservation, codeAgentBillingEnabled, codeAgentBillingMetadata, sanitizeBillingReservation, sanitizeBillingRecord, sanitizeBillingRelease, codeAgentUsedTokens, normalizeTurnStatus, recordCodeAgentConversationFact, recordCodeAgentSessionOwner, codeAgentOwnerStatusForResult, textValue, timestampIsoOrNow, timestampIsoOrNull, codeAgentSessionOwnerEvidence, codeAgentContinuationEvidence, codeAgentFreshContinuationFailure, normalizeCodeAgentFailureKind, codeAgentFailureRequiresFreshContinuation, codeAgentProviderProfileEvidence, codeAgentTraceResultEvidence, codeAgentPromptMetadata, codeAgentPromptMetadataFromText, codeAgentPromptMetadataFromParts, codeAgentPromptFields, codeAgentPromptId, clippedPromptText, sha256Text, compactCodeAgentObject, codeAgentBillingReservationEvidence, codeAgentConversationMessagesEvidence, boundedConversationMessageText, conversationText, conversationTextRaw, codeAgentMessageTraceSuffix, codeAgentTurnLifecycleFields, codeAgentLifecycleMessageId, safeTurnId, safeMessageId, codeAgentConversationAgentMessageStatus, codeAgentPayloadHasSealedFinalResponse, codeAgentConversationRunnerTraceEvidence, codeAgentFinalResponseEvidence, codeAgentFinalResponseText, codeAgentTraceSummaryEvidence, annotateOwner, canAccessOwnedResult, isCodeAgentResultCanceled, codeAgentCancelScopeMismatch, cancelExpectedValues, cancelScopeFieldMismatch, cancelBlockedPayload, isTraceCommandTerminalStatus, traceSnapshotWithTerminalEvidence, traceSnapshotWithAttachedTerminalEvidence, agentRunTerminalTraceEvidence, agentRunTerminalFinalResponse, traceRetentionSummary, numberOrNull, createCodeAgentChatResultStore, compactCodeAgentChatResultPayload, terminalEvidencePayload, compactRunnerTraceForResult, traceEventLabel, resultTraceEventLimit, createCodeAgentM3HwlabApiRequestJson, recordCodeAgentProjectionMetrics, recordCodeAgentTurnReadMetric, turnReadDegradedReason, responseBodyBytes, statusClassLabel, sourceLatestSeqFromResult, sourceLatestAtFromResult, terminalSourceAtFromResult, projectionReason, nowMs, uniqueStrings } from "./server-code-agent-http-support.ts"; import { codeAgentCompatProjectionPayload, codeAgentRequestScopedOptions, measureCodeAgentHttpPhase, readCodeAgentCompatProjection, traceProjectMismatchSnapshot } from "./server-code-agent-turn-http.ts"; const DEFAULT_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT = 120; const DEFAULT_CODE_AGENT_TRACE_PAGE_LIMIT = 100; const MAX_CODE_AGENT_TRACE_PAGE_LIMIT = 100; const DEFAULT_CODE_AGENT_TURN_STATUS_REFRESH_TIMEOUT_MS = 2500; const DEFAULT_CODE_AGENT_PROJECT_ID = "prj_hwpod_workbench"; const DEFAULT_CODE_AGENT_PROVIDER_PROFILE = "deepseek"; const CODE_AGENT_TERMINAL_STATUSES = new Set(["completed", "failed", "blocked", "timeout", "cancelled", "canceled"]); const CODE_AGENT_PROVIDER_PROFILE_ALIASES = Object.freeze({ codex: "codex-api" }); const CODE_AGENT_PROVIDER_PROFILE_ID_PATTERN = /^[a-z0-9][a-z0-9.-]{0,63}$/u; const CODE_AGENT_PROVIDER_PROFILE_LABELS = Object.freeze({ "deepseek": "DeepSeek", "codex-api": "Codex API", "gpt.pika": "gpt.pika", "minimax-m3": "MiniMax-M3 via AgentRun", "runtime-default": "运行默认" }); const DEFAULT_CODE_AGENT_CODEX_API_MODEL = "gpt-5.5"; const DEFAULT_CODE_AGENT_CODEX_API_BASE_URL = "http://127.0.0.1:49280/responses"; const DEFAULT_CODE_AGENT_DEEPSEEK_MODEL = "deepseek-chat"; const DEFAULT_CODE_AGENT_MINIMAX_M3_MODEL = "MiniMax-M3"; export async function handleCodeAgentSteerHttp(request, response, options) { const body = await readBody(request, options.bodyLimitBytes); let params = {}; try { params = body ? JSON.parse(body) : {}; } catch (error) { sendJson(response, 400, { ...createCodeAgentErrorPayload({ code: "parse_error", message: "Invalid JSON body", reason: error.message, traceId: getHeader(request, "x-trace-id") || "trc_unassigned", layer: "api", retryable: true, route: "/v1/agent/chat/steer" }) }); return; } if (!params || typeof params !== "object" || Array.isArray(params)) { sendJson(response, 400, { ...createCodeAgentErrorPayload({ code: "invalid_params", message: "Code Agent steer body must be a JSON object", traceId: getHeader(request, "x-trace-id") || "trc_unassigned", layer: "api", retryable: true, route: "/v1/agent/chat/steer" }) }); return; } const traceId = safeTraceId(getHeader(request, "x-trace-id") || params.traceId || params.targetTraceId); const message = firstNonEmptyValue(params.message, params.prompt, params.text); if (!traceId || !message) { sendJson(response, 400, { ...createCodeAgentErrorPayload({ code: !traceId ? "steer_trace_missing" : "steer_message_missing", message: !traceId ? "traceId is required to steer the current Code Agent turn." : "message is required to steer the current Code Agent turn.", traceId: traceId || "trc_unassigned", layer: "api", retryable: true, route: "/v1/agent/chat/steer" }) }); return; } const traceStore = options.traceStore ?? defaultCodeAgentTraceStore; const durableResult = await loadPersistedAgentRunResult(traceId, options); const currentResult = durableResult ?? options.codeAgentChatResults?.get?.(traceId); if (!canAccessOwnedResult(currentResult, options.actor)) { sendJson(response, 403, { ok: false, status: "forbidden", traceId, error: { code: "code_agent_trace_forbidden", message: "Code Agent trace is not visible to the current actor" } }); return; } if (!currentResult?.agentRun?.runId || !currentResult?.agentRun?.commandId) { sendJson(response, 404, { ok: false, status: "not_found", traceId, error: { code: "steer_trace_not_found", message: "No AgentRun-backed Code Agent turn was found for the requested traceId." }, route: "/v1/agent/chat/steer", runnerTrace: traceStore.snapshot(traceId) }); return; } if (isTraceCommandTerminalStatus(currentResult.status)) { sendJson(response, 409, { ok: false, accepted: false, status: "blocked", traceId, route: "/v1/agent/chat/steer", sessionId: safeSessionId(currentResult.sessionId ?? params.sessionId) || null, threadId: safeOpaqueId(currentResult.threadId ?? params.threadId) || null, agentRun: currentResult.agentRun, runnerTrace: traceStore.snapshot(traceId), error: { code: "steer_trace_terminal", layer: "api", category: "not_in_flight", retryable: false, message: `Trace ${traceId} is already terminal and cannot accept a steer command.`, traceId, route: "/v1/agent/chat/steer", toolName: "agentrun.command.steer" }, valuesPrinted: false }); return; } try { await recordCodeAgentSessionInputFact({ params: { ...params, sessionId: currentResult.sessionId ?? currentResult.session?.sessionId ?? params.sessionId, ownerUserId: options.actor?.id, ownerRole: options.actor?.role }, options, traceId, delivery: "steer", status: "promoted", commandId: currentResult.agentRun.commandId }); const payload = await steerAgentRunChatTurn({ traceId, currentResult, params: { ...params, message, ownerUserId: options.actor?.id ?? params.ownerUserId, ownerRole: options.actor?.role ?? params.ownerRole }, options, traceStore }); sendJson(response, 202, payload); } catch (error) { traceStore.append(traceId, { type: "backend", status: "failed", label: "agentrun:steer:failed", errorCode: error?.code ?? "agentrun_steer_failed", message: error?.message ?? "AgentRun steer command failed.", terminal: false, valuesPrinted: false }); sendJson(response, error?.statusCode === 404 ? 404 : 409, { ok: false, accepted: false, status: "failed", traceId, route: "/v1/agent/chat/steer", sessionId: safeSessionId(currentResult.sessionId ?? params.sessionId) || null, threadId: safeOpaqueId(currentResult.threadId ?? params.threadId) || null, agentRun: currentResult.agentRun, runnerTrace: traceStore.snapshot(traceId), error: { code: error?.code ?? "agentrun_steer_failed", layer: "agentrun", category: "steer_failed", retryable: true, message: error?.message ?? "AgentRun steer command failed.", traceId, route: "/v1/agent/chat/steer", toolName: "agentrun.command.steer" } }); } } export async function handleCodeAgentTraceHttp(request, response, url, options) { const startedAt = nowMs(); const parts = url.pathname.split("/").filter(Boolean); const traceId = decodeURIComponent(parts[3] ?? ""); const streamSegment = parts[4]; const pageOptions = codeAgentTracePageOptions(url); if (!/^trc_[A-Za-z0-9_.:-]+$/u.test(traceId)) { sendJson(response, 400, { error: { code: "invalid_trace_id", message: "traceId must start with trc_ and contain only safe identifier characters" } }); return; } const requestOptions = codeAgentRequestScopedOptions(options); const projectMismatch = await measureCodeAgentHttpPhase(requestOptions, "trace_project_check", () => traceProjectMismatchSnapshot(traceId, url, requestOptions, "trace")); if (projectMismatch) { sendJson(response, projectMismatch.statusCode, projectMismatch.body); return; } const context = await measureCodeAgentHttpPhase(requestOptions, "trace_projection_read", () => readCodeAgentCompatProjection(traceId, requestOptions)); if (context.statusCode) { sendJson(response, context.statusCode, context.body); return; } if (streamSegment === "stream") { sendJson(response, 410, { ok: false, status: "removed", traceId, error: { code: "trace_store_sse_removed", message: "Trace-store SSE was removed; use the committed Workbench projection event stream.", workbenchEventsUrl: `/v1/workbench/events?traceId=${encodeURIComponent(traceId)}` }, valuesRedacted: true }); return; } const body = await measureCodeAgentHttpPhase(requestOptions, "trace_paginate", () => { return paginateTraceSnapshot(context.trace, pageOptions); }); const statusCode = context.found ? 200 : 404; void emitCodeAgentOtelSpan("trace_events_read", traceId, requestOptions.env ?? process.env, { startTimeMs: startedAt, attributes: { "http.method": "GET", "http.route": "/v1/agent/traces/:traceId", "http.status_code": statusCode, found: Boolean(context.found), returnedEvents: Array.isArray(body?.events) ? body.events.length : 0, sinceSeq: pageOptions.sinceSeq, limit: pageOptions.limit, fromSeq: body?.range?.fromSeq ?? null, toSeq: body?.range?.toSeq ?? null, totalEvents: body?.range?.total ?? body?.eventCount ?? null, hasMore: Boolean(body?.hasMore), fullTraceLoaded: Boolean(body?.fullTraceLoaded) } }); sendJson(response, statusCode, codeAgentCompatProjectionPayload(body, context)); } function codeAgentTracePageOptions(url) { const sinceSeq = nonNegativeInteger(url.searchParams.get("sinceSeq") ?? url.searchParams.get("since"), 0); const requestedLimit = parsePositiveInteger(url.searchParams.get("limit"), DEFAULT_CODE_AGENT_TRACE_PAGE_LIMIT); return { sinceSeq, limit: Math.min(requestedLimit, MAX_CODE_AGENT_TRACE_PAGE_LIMIT) }; } function paginateTraceSnapshot(snapshot, { sinceSeq, limit }) { const sourceEvents = Array.isArray(snapshot?.events) ? snapshot.events : []; const indexedEvents = sourceEvents.map((event, index) => ({ event, seq: traceEventSequence(event, index) })); const firstIndex = indexedEvents.findIndex((item) => item.seq > sinceSeq); const startIndex = firstIndex >= 0 ? firstIndex : indexedEvents.length; const pageItems = indexedEvents.slice(startIndex, startIndex + limit); const events = pageItems.map((item) => item.event); const fromSeq = pageItems.length > 0 ? pageItems[0].seq : null; const toSeq = pageItems.length > 0 ? pageItems[pageItems.length - 1].seq : null; const hasMore = startIndex + pageItems.length < indexedEvents.length; const totalEvents = snapshot?.eventCount ?? indexedEvents.length; return { ...snapshot, events, eventCount: totalEvents, range: { sinceSeq, fromSeq, toSeq, limit, returned: events.length, total: totalEvents }, truncated: hasMore, hasMore, nextSinceSeq: pageItems.length > 0 ? toSeq : sinceSeq, fullTraceLoaded: !hasMore }; } function traceEventSequence(event, index) { const seq = Number(event?.seq); return Number.isFinite(seq) && seq > 0 ? Math.trunc(seq) : index + 1; } function nonNegativeInteger(value, fallback) { const parsed = Number.parseInt(value ?? "", 10); return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback; }