Files
pikasTech-HWLAB/internal/cloud/server-code-agent-admission-http.ts
T

1425 lines
61 KiB
TypeScript

// 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 session and turn admission.
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 { 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";
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 handleCodeAgentChatHttp(request, response, options) {
const perf = options.backendPerformance;
const body = perf ? await perf.measure("body_read", () => readBody(request, options.bodyLimitBytes)) : 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",
model: options.env?.HWLAB_CODE_AGENT_MODEL || options.env?.OPENAI_MODEL || process.env.HWLAB_CODE_AGENT_MODEL || process.env.OPENAI_MODEL || "unknown",
layer: "api",
retryable: true
})
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendJson(response, 400, {
...createCodeAgentErrorPayload({
code: "invalid_params",
message: "Code Agent chat body must be a JSON object",
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
model: options.env?.HWLAB_CODE_AGENT_MODEL || options.env?.OPENAI_MODEL || process.env.HWLAB_CODE_AGENT_MODEL || process.env.OPENAI_MODEL || "unknown",
layer: "api",
retryable: true
})
});
return;
}
params = withMdtodoExecutionPrompt(params);
const traceId = safeTraceId(getHeader(request, "x-trace-id") || params.traceId) || `trc_${randomUUID()}`;
const lifecycle = codeAgentTurnLifecycleFields(traceId, params);
const chatParams = {
...params,
traceId,
...lifecycle,
ownerUserId: options.actor?.id ?? params.ownerUserId,
ownerRole: options.actor?.role ?? params.ownerRole
};
const manualSession = perf ? await perf.measure("manual_session", () => prepareManualCodeAgentSession({ params: chatParams, options, traceId, response })) : await prepareManualCodeAgentSession({ params: chatParams, options, traceId, response });
if (manualSession?.blocked) return;
const nativeSessionChatParams = stripSyntheticConversationContext(chatParams, traceId, options);
if (codeAgentChatShortConnectionRequested(request, params, options)) {
if (perf) perf.recordPhase({ phase: "short_connection_accept", durationMs: 0, outcome: "ok" });
let submitted = null;
try {
submitted = await submitCodeAgentChatTurn({
params: nativeSessionChatParams,
options,
traceId
});
} catch (error) {
if (isCodeAgentAdmissionUnavailableError(error)) {
sendJson(response, error.statusCode ?? 503, error.payload);
return;
}
throw error;
}
const traceUrl = `/v1/agent/traces/${encodeURIComponent(traceId)}`;
const turnUrl = `/v1/agent/turns/${encodeURIComponent(traceId)}`;
const workbenchEventsUrl = `/v1/workbench/events?sessionId=${encodeURIComponent(safeSessionId(nativeSessionChatParams.sessionId) || "")}&traceId=${encodeURIComponent(traceId)}`;
const statusCode = 202;
const admittedTiming = codeAgentAdmissionTimingFields(submitted?.createdAt ?? submitted?.updatedAt);
void emitCodeAgentOtelSpan("POST /v1/agent/chat", traceId, options.env ?? process.env, {
kind: 2,
attributes: { "http.method": "POST", "http.route": "/v1/agent/chat", "http.status_code": statusCode, sessionId: safeSessionId(nativeSessionChatParams.sessionId) || null, turnId: lifecycle.turnId, ...agentChatOtelAttributes(nativeSessionChatParams) }
});
sendJson(response, 202, {
accepted: true,
status: "running",
shortConnection: true,
controlSemantics: "submit-and-project-sse",
traceId,
...codeAgentOtelTraceFields(traceId, options.env ?? process.env),
...lifecycle,
sessionId: safeSessionId(nativeSessionChatParams.sessionId) || null,
createdAt: submitted?.createdAt ?? admittedTiming.createdAt,
updatedAt: submitted?.updatedAt ?? admittedTiming.updatedAt,
timing: submitted?.timing ?? admittedTiming.timing,
startedAt: submitted?.startedAt ?? admittedTiming.startedAt,
lastEventAt: submitted?.lastEventAt ?? admittedTiming.lastEventAt,
finishedAt: submitted?.finishedAt ?? admittedTiming.finishedAt,
durationMs: submitted?.durationMs ?? admittedTiming.durationMs,
traceUrl,
turnUrl,
workbenchEventsUrl,
cancelUrl: "/v1/agent/chat/cancel",
runnerTrace: (options.traceStore ?? defaultCodeAgentTraceStore).snapshot(traceId)
});
return;
}
const admittedBase = codeAgentAdmittedFailureBasePayload({ params: nativeSessionChatParams, options, traceId });
try {
await recordCodeAgentTurnAdmission({ payload: admittedBase, params: nativeSessionChatParams, options, traceId });
} catch (error) {
if (isCodeAgentAdmissionUnavailableError(error)) {
sendJson(response, error.statusCode ?? 503, error.payload);
return;
}
throw error;
}
const billingPreflight = perf ? await perf.measure("billing_preflight", () => preflightCodeAgentBilling({ params: nativeSessionChatParams, options, traceId, response: null, sendResponse: false })) : await preflightCodeAgentBilling({ params: nativeSessionChatParams, options, traceId, response: null, sendResponse: false });
if (billingPreflight?.blocked) {
const failed = await settleAdmittedCodeAgentFailure({
base: admittedBase,
params: nativeSessionChatParams,
options,
traceId,
results: options.codeAgentChatResults,
failure: billingPreflight,
traceLabel: "billing:preflight:failed"
});
sendJson(response, billingPreflight.statusCode ?? 503, failed);
return;
}
const nativeSessionChatParamsWithBilling = { ...nativeSessionChatParams, userBillingReservation: billingPreflight?.reservation ?? null };
const payload = perf ? await perf.measure("code_agent_run", () => runCodeAgentChat(nativeSessionChatParamsWithBilling, options)) : await runCodeAgentChat(nativeSessionChatParamsWithBilling, options);
await (perf ? perf.measure("billing_finalize", () => finalizeCodeAgentBillingUsage({ payload, params: nativeSessionChatParamsWithBilling, options })) : finalizeCodeAgentBillingUsage({ payload, params: nativeSessionChatParamsWithBilling, options }));
await (perf ? perf.measure("session_owner_record", () => recordCodeAgentSessionOwner({ payload, params: nativeSessionChatParamsWithBilling, options, status: codeAgentOwnerStatusForResult(payload) })) : recordCodeAgentSessionOwner({ payload, params: nativeSessionChatParamsWithBilling, options, status: codeAgentOwnerStatusForResult(payload) }));
const responsePayload = annotateOwner(payload, nativeSessionChatParamsWithBilling);
sendJson(response, responsePayload.status === "failed" && responsePayload.error?.code === "invalid_params" ? 400 : 200, responsePayload);
}
export async function handleCodeAgentSessionsHttp(request, response, url, options) {
const match = url.pathname.match(/^\/v1\/agent\/sessions(?:\/([^/]+)(?:\/(select))?)?$/u);
const sessionId = safeSessionId(match?.[1] ? decodeURIComponent(match[1]) : "");
const action = match?.[2] ?? null;
if (!match) {
sendJson(response, 404, manualSessionErrorPayload({ code: "session_route_not_found", message: "Code Agent session route is not implemented." }));
return;
}
if (request.method === "GET" && !sessionId) {
await listManualCodeAgentSessions(request, response, url, options);
return;
}
if (request.method === "POST" && !sessionId) {
await createManualCodeAgentSession(request, response, options);
return;
}
if (request.method === "GET" && sessionId && !action) {
await getManualCodeAgentSession(response, options, sessionId);
return;
}
if (request.method === "DELETE" && sessionId && !action) {
await archiveManualCodeAgentSession(response, options, sessionId);
return;
}
if (request.method === "POST" && sessionId && action === "select") {
await selectManualCodeAgentSession(request, response, options, sessionId);
return;
}
sendJson(response, 405, manualSessionErrorPayload({ code: "session_method_not_allowed", message: "Unsupported Code Agent session method." }));
}
async function listManualCodeAgentSessions(request, response, url, options) {
const projectId = textValue(url.searchParams.get("projectId")) || DEFAULT_CODE_AGENT_PROJECT_ID;
const limit = parsePositiveInteger(url.searchParams.get("limit"), 20);
const sessions = await options.accessController?.store?.listAgentSessionsForUser?.({
ownerUserId: options.actor?.id,
actorRole: options.actor?.role,
ownerScoped: true,
projectId,
limit
}) ?? [];
sendJson(response, 200, {
ok: true,
status: "succeeded",
contractVersion: "code-agent-manual-session-v1",
projectId,
sessions: sessions.map(publicManualAgentSession),
count: sessions.length,
valuesRedacted: true,
secretMaterialStored: false
});
}
async function createManualCodeAgentSession(request, response, options) {
const body = await readJsonObjectBody(request, options.bodyLimitBytes);
if (!body.ok) {
sendJson(response, 400, manualSessionErrorPayload({ code: body.code, message: body.message, reason: body.reason }));
return;
}
const params = body.value;
const projectId = textValue(params.projectId) || DEFAULT_CODE_AGENT_PROJECT_ID;
const conversationId = safeConversationId(params.conversationId) || `cnv_${randomUUID()}`;
const sessionId = safeSessionId(params.sessionId) || `ses_${randomUUID()}`;
const providerProfile = normalizeCodeAgentProviderProfile(params.providerProfile ?? params.codeAgentProviderProfile);
const now = new Date().toISOString();
const session = await options.accessController.recordAgentSessionOwner({
ownerUserId: options.actor?.id,
ownerRole: options.actor?.role,
sessionId,
projectId,
agentId: "hwlab-code-agent",
status: "idle",
conversationId,
threadId: null,
traceId: null,
session: {
source: "manual-session-create",
providerProfile,
sessionStatus: "idle",
createdAt: now,
valuesRedacted: true,
secretMaterialStored: false
}
});
await writeWorkbenchSessionAdmissionFact({
runtimeStore: options.runtimeStore,
session,
ownerUserId: options.actor?.id,
ownerRole: options.actor?.role,
projectId,
conversationId,
threadId: null,
status: "idle",
now
});
sendJson(response, 201, {
ok: true,
status: "created",
contractVersion: "code-agent-manual-session-v1",
session: publicManualAgentSession(session),
nextCommands: manualSessionNextCommands(session),
valuesRedacted: true,
secretMaterialStored: false
});
}
async function getManualCodeAgentSession(response, options, sessionId) {
const session = await getVisibleManualAgentSession(options, sessionId);
if (!session) {
sendJson(response, 404, manualSessionErrorPayload({ code: "session_not_found", message: "Code Agent session is not visible to the current actor.", sessionId }));
return;
}
sendJson(response, 200, {
ok: true,
status: "found",
contractVersion: "code-agent-manual-session-v1",
session: publicManualAgentSession(session),
valuesRedacted: true,
secretMaterialStored: false
});
}
async function archiveManualCodeAgentSession(response, options, sessionId) {
const session = await getVisibleManualAgentSession(options, sessionId);
if (!session) {
sendJson(response, 404, manualSessionErrorPayload({ code: "session_not_found", message: "Code Agent session is not visible to the current actor.", sessionId }));
return;
}
const result = await options.accessController?.store?.archiveAgentConversation?.({
ownerUserId: options.actor?.id,
actorRole: options.actor?.role,
ownerScoped: true,
sessionId,
conversationId: session.conversationId,
projectId: session.projectId
}) ?? { count: 0 };
sendJson(response, 200, {
ok: true,
status: "archived",
contractVersion: "code-agent-manual-session-v1",
archivedCount: result.count ?? 0,
session: publicManualAgentSession({ ...session, status: "archived", endedAt: session.endedAt ?? new Date().toISOString(), updatedAt: new Date().toISOString() }),
valuesRedacted: true,
secretMaterialStored: false
});
}
async function selectManualCodeAgentSession(request, response, options, sessionId) {
const body = await readJsonObjectBody(request, options.bodyLimitBytes);
if (!body.ok) {
sendJson(response, 400, manualSessionErrorPayload({ code: body.code, message: body.message, reason: body.reason, sessionId }));
return;
}
const session = await getVisibleManualAgentSession(options, sessionId);
if (!session) {
sendJson(response, 404, manualSessionErrorPayload({ code: "session_not_found", message: "Code Agent session is not visible to the current actor.", sessionId }));
return;
}
if (!manualSessionUsable(session.status)) {
sendJson(response, 409, manualSessionErrorPayload({ code: "session_not_usable", message: `Code Agent session ${sessionId} is ${session.status}; create a new session before continuing.`, sessionId, session }));
return;
}
sendJson(response, 200, {
ok: true,
status: "selected",
contractVersion: "code-agent-manual-session-v1",
session: publicManualAgentSession(session),
nextCommands: manualSessionNextCommands(session),
valuesRedacted: true,
secretMaterialStored: false
});
}
async function prepareManualCodeAgentSession({ params, options, traceId, response }) {
if (!options.actor?.id) {
sendJson(response, 401, manualSessionErrorPayload({
code: "auth_required",
message: "Authentication is required before using an explicit HWLAB Code Agent session.",
traceId,
retryable: true
}));
return { blocked: true };
}
const sessionId = safeSessionId(params.sessionId);
if (!sessionId) {
sendJson(response, 409, manualSessionErrorPayload({
code: "session_required",
message: "Code Agent session is explicit in HWLAB v0.2; create/select a session before sending a turn.",
traceId,
retryable: true,
nextCommands: ["hwlab-cli client agent session create --provider-profile PROFILE", "hwlab-cli client agent send --session-id <SESSION_ID> --message TEXT"]
}));
return { blocked: true };
}
const session = await getVisibleManualAgentSession(options, sessionId);
if (!session) {
sendJson(response, 404, manualSessionErrorPayload({ code: "session_not_found", message: "Code Agent session is not visible to the current actor.", traceId, sessionId }));
return { blocked: true };
}
const freshContinuation = manualSessionFreshContinuationRequired(session);
if (!manualSessionUsable(session.status) && !freshContinuation) {
sendJson(response, 409, manualSessionErrorPayload({ code: "session_not_usable", message: `Code Agent session ${sessionId} is ${session.status}; create a new session before continuing.`, traceId, sessionId, session }));
return { blocked: true };
}
const requestedProjectId = textValue(params.projectId);
const sessionProjectId = textValue(session.projectId);
if (requestedProjectId && sessionProjectId && requestedProjectId !== sessionProjectId) {
sendJson(response, 409, manualSessionErrorPayload({ code: "session_project_mismatch", message: `sessionId ${sessionId} belongs to ${sessionProjectId}; requested ${requestedProjectId}.`, traceId, sessionId, session }));
return { blocked: true };
}
const storedConversationId = safeConversationId(session.conversationId);
const requestedConversationId = safeConversationId(params.conversationId);
if (requestedConversationId && storedConversationId && requestedConversationId !== storedConversationId) {
sendJson(response, 409, manualSessionErrorPayload({ code: "session_conversation_mismatch", message: `sessionId ${sessionId} is bound to ${storedConversationId}; requested ${requestedConversationId}.`, traceId, sessionId, session }));
return { blocked: true };
}
const conversationId = requestedConversationId || storedConversationId;
if (!conversationId) {
sendJson(response, 409, manualSessionErrorPayload({ code: "session_conversation_required", message: "Code Agent session has no bound conversationId; create a new session before continuing.", traceId, sessionId, session }));
return { blocked: true };
}
params.conversationId = conversationId;
params.sessionId = sessionId;
params.projectId = requestedProjectId || sessionProjectId || DEFAULT_CODE_AGENT_PROJECT_ID;
applyManualSessionProviderProfile(params, session);
if (freshContinuation) {
params.threadId = undefined;
params.forceFreshAgentRunSession = true;
params.forceFreshAgentRunSessionReason = manualSessionFreshContinuationReason(session);
} else {
params.threadId = safeOpaqueId(params.threadId) || manualSessionContinuationThreadId(session) || undefined;
}
return { session };
}
function applyManualSessionProviderProfile(params = {}, session = null) {
const requested = firstNonEmptyValue(params.providerProfile, params.codeAgentProviderProfile);
if (requested) {
const normalized = normalizeCodeAgentProviderProfile(requested);
params.providerProfile = normalized;
params.codeAgentProviderProfile = normalized;
params.providerProfileSource = "request";
return normalized;
}
const stored = manualSessionStoredProviderProfile(session);
if (!stored) return null;
params.providerProfile = stored;
params.codeAgentProviderProfile = stored;
params.providerProfileSource = "session";
return stored;
}
function manualSessionStoredProviderProfile(session = null) {
const evidence = session?.session && typeof session.session === "object" ? session.session : {};
const traceProfile = latestCompletedTraceResultProviderProfile(evidence.traceResults);
const raw = firstNonEmptyValue(
traceProfile,
evidence.agentRun?.backendProfile,
evidence.providerProfile,
evidence.codeAgentProviderProfile,
evidence.backendProfile
);
if (!raw) return null;
const normalized = normalizeCodeAgentProviderProfile(raw);
return normalized === "runtime-default" ? null : normalized;
}
function latestCompletedTraceResultProviderProfile(traceResults = null) {
if (!traceResults || typeof traceResults !== "object") return null;
const entries = Object.values(traceResults).filter((value) => value && typeof value === "object");
for (const result of entries.reverse()) {
const agentRun = result.agentRun && typeof result.agentRun === "object" ? result.agentRun : null;
if (!agentRun) continue;
const status = normalizeTurnStatus(result.status ?? agentRun.terminalStatus ?? agentRun.commandState ?? agentRun.status);
if (status && status !== "completed") continue;
const profile = firstNonEmptyValue(agentRun.backendProfile, result.providerProfile, result.codeAgentProviderProfile);
if (profile) return profile;
}
return null;
}
function manualSessionFreshContinuationRequired(session = null) {
const status = String(session?.status ?? "").trim().toLowerCase().replace(/_/gu, "-");
if (status === "thread-resume-failed") return true;
const continuation = session?.session?.continuation && typeof session.session.continuation === "object" ? session.session.continuation : null;
return continuation?.requiresFreshSession === true;
}
function manualSessionFreshContinuationReason(session = null) {
const continuation = session?.session?.continuation && typeof session.session.continuation === "object" ? session.session.continuation : null;
return firstNonEmptyValue(continuation?.reasonCode, continuation?.reason, session?.status, "stale-continuation") ?? "stale-continuation";
}
function manualSessionContinuationThreadId(session = null) {
if (manualSessionFreshContinuationRequired(session)) return null;
const evidence = session?.session && typeof session.session === "object" ? session.session : {};
return safeOpaqueId(evidence.threadId) || safeOpaqueId(evidence.agentRun?.threadId) || safeOpaqueId(session?.threadId) || null;
}
async function getVisibleManualAgentSession(options, sessionId) {
if (!safeSessionId(sessionId) || !options.actor?.id || !options.accessController?.getAgentSession) return null;
const session = await options.accessController.getAgentSession(sessionId);
if (!session) return null;
if (session.ownerUserId === options.actor.id) return session;
return null;
}
function publicManualAgentSession(session) {
if (!session || typeof session !== "object") return null;
return {
sessionId: session.id,
agentId: session.agentId ?? "hwlab-code-agent",
status: session.status ?? null,
usable: manualSessionUsable(session.status),
threadId: safeOpaqueId(session.threadId) || null,
lastTraceId: safeTraceId(session.lastTraceId) || null,
providerProfile: textValue(session.session?.providerProfile) || null,
createdAt: session.startedAt ?? null,
updatedAt: session.updatedAt ?? null,
valuesRedacted: true,
secretMaterialStored: false
};
}
function manualSessionUsable(status) {
const value = String(status ?? "").trim().toLowerCase().replace(/_/gu, "-");
return !["blocked", "expired", "stale"].includes(value);
}
function manualSessionErrorPayload({ code, message, reason = null, traceId = null, sessionId = null, session = null, retryable = true, nextCommands = undefined } = {}) {
return {
ok: false,
accepted: false,
status: "blocked",
traceId: safeTraceId(traceId) || null,
sessionId: safeSessionId(sessionId) || safeSessionId(session?.id) || null,
session: session ? publicManualAgentSession(session) : null,
error: {
code,
layer: "api",
category: "session_blocked",
retryable,
message,
reason,
userMessage: message,
route: "/v1/agent/chat"
},
blocker: {
code,
layer: "api",
retryable,
summary: message
},
nextCommands,
valuesRedacted: true,
secretMaterialStored: false
};
}
function manualSessionNextCommands(session) {
const sessionId = session?.id ?? "<SESSION_ID>";
return [
`hwlab-cli client agent send --session-id ${sessionId} --message TEXT`,
`hwlab-cli client agent session status ${sessionId}`
];
}
async function readJsonObjectBody(request, bodyLimitBytes) {
const body = await readBody(request, bodyLimitBytes);
try {
const value = body ? JSON.parse(body) : {};
if (!value || typeof value !== "object" || Array.isArray(value)) return { ok: false, code: "invalid_params", message: "body must be a JSON object" };
return { ok: true, value };
} catch (error) {
return { ok: false, code: "parse_error", message: "Invalid JSON body", reason: error.message };
}
}
async function runCodeAgentChat(params, options) {
return handleCodeAgentChat(params, await codeAgentChatExecutionOptions(options, params));
}
function stripSyntheticConversationContext(params = {}, traceId, options = {}) {
if (Array.isArray(params.conversationContext?.messages) || Array.isArray(params.messages)) {
(options.traceStore ?? defaultCodeAgentTraceStore).append(traceId, {
type: "synthetic-context",
status: "ignored",
label: "synthetic-context:ignored",
message: "Synthetic conversation context was stripped; Code Agent continuation uses only Codex stdio native thread/resume and turn/start.",
valuesPrinted: false
});
}
const { conversationContext, conversationContextTruncated, contextSource, messages, ...nativeParams } = params;
return nativeParams;
}
async function codeAgentChatExecutionOptions(options = {}, params = {}) {
const env = codeAgentProviderProfileEnv(options.env ?? process.env, params);
Object.assign(env, await codeAgentAuthEnv(options, env, params));
return {
callProvider: options.callCodeAgentProvider,
timeoutMs: options.codeAgentTimeoutMs,
hardTimeoutMs: options.codeAgentHardTimeoutMs,
env,
workspace: options.workspace,
skillsDirs: options.skillsDirs,
skillsDirsExact: options.skillsDirsExact,
skillsDiscoveryDelayMs: options.skillsDiscoveryDelayMs,
sessionRegistry: options.sessionRegistry,
m3IoSkillRequestJson: options.m3IoSkillRequestJson ?? createCodeAgentM3HwlabApiRequestJson(options),
codexStdioManager: options.codexStdioManager,
traceStore: options.traceStore,
gatewayRegistry: options.gatewayRegistry
};
}
async function codeAgentAuthEnv(options = {}, env = process.env, params = {}) {
const ownerUserId = textValue(params.ownerUserId ?? options.actor?.id ?? "");
const store = options.accessController?.store;
const key = ownerUserId && store?.findActiveDefaultApiKeyForUser
? await store.findActiveDefaultApiKeyForUser(ownerUserId)
: null;
const envRuntimeLane = runtimeLaneForEnv(env);
const runtimeNamespace = runtimeNamespaceForEnv(env);
const namespaceRuntimeLane = runtimeLaneForNamespace(runtimeNamespace);
const runtimeLane = resolveRuntimeLaneAuthority({ envRuntimeLane, runtimeNamespace, namespaceRuntimeLane });
const inClusterApiUrl = internalCodeAgentApiUrl(env, runtimeNamespace);
const inClusterWebUrl = internalCodeAgentWebUrl(env, runtimeNamespace);
const apiUrl = firstNonEmptyValue(
codeAgentAgentRunAdapterEnabled(env) ? inClusterApiUrl : null,
env.HWLAB_RUNTIME_API_URL,
sameRuntimeEndpoint(env.HWLAB_CLOUD_API_URL, runtimeNamespace, runtimeLane),
env.HWLAB_PUBLIC_ENDPOINT,
localCloudApiUrl(env)
);
return {
HWLAB_CLOUD_API_URL: apiUrl,
HWLAB_RUNTIME_API_URL: apiUrl,
HWLAB_RUNTIME_WEB_URL: firstNonEmptyValue(codeAgentAgentRunAdapterEnabled(env) ? inClusterWebUrl : null, env.HWLAB_RUNTIME_WEB_URL, apiUrl),
HWLAB_RUNTIME_NAMESPACE: runtimeNamespace,
HWLAB_RUNTIME_LANE: runtimeLane,
HWLAB_RUNTIME_ENDPOINT_SOURCE: codeAgentAgentRunAdapterEnabled(env) ? "runtime-namespace" : "runtime-env",
HWLAB_RUNTIME_ENDPOINT_LOCKED: "1",
HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME: "1",
...(options.actorApiKeySecret ? { HWLAB_API_KEY: options.actorApiKeySecret } : key?.displaySecret ? { HWLAB_API_KEY: key.displaySecret } : {})
};
}
function internalCodeAgentApiUrl(env = process.env, namespace = runtimeNamespaceForEnv(env)) {
const serviceName = firstNonEmptyValue(env.HWLAB_CODE_AGENT_INTERNAL_API_SERVICE_NAME, env.HWLAB_CLOUD_API_SERVICE_NAME, "hwlab-cloud-api");
const port = parsePositiveInteger(firstNonEmptyValue(env.HWLAB_CODE_AGENT_INTERNAL_API_PORT, env.HWLAB_CLOUD_API_PORT), 6667);
return `http://${serviceName}.${namespace}.svc.cluster.local:${port}`;
}
function internalCodeAgentWebUrl(env = process.env, namespace = runtimeNamespaceForEnv(env)) {
const serviceName = firstNonEmptyValue(env.HWLAB_CODE_AGENT_INTERNAL_WEB_SERVICE_NAME, env.HWLAB_CLOUD_WEB_SERVICE_NAME, "hwlab-cloud-web");
const port = parsePositiveInteger(firstNonEmptyValue(env.HWLAB_CODE_AGENT_INTERNAL_WEB_PORT, env.HWLAB_CLOUD_WEB_PORT), 8080);
return `http://${serviceName}.${namespace}.svc.cluster.local:${port}`;
}
function runtimeNamespaceForEnv(env = process.env) {
return firstNonEmptyValue(
env.HWLAB_RUNTIME_NAMESPACE,
env.POD_NAMESPACE,
env.HWLAB_NAMESPACE,
defaultRuntimeNamespace(env)
);
}
function runtimeLaneForEnv(env = process.env) {
const raw = firstNonEmptyValue(env.HWLAB_RUNTIME_LANE, env.HWLAB_GITOPS_LANE, env.HWLAB_GITOPS_PROFILE, env.HWLAB_ENVIRONMENT, env.HWLAB_BOOT_REF);
if (!raw) return null;
const lane = normalizeRuntimeLane(raw);
if (!lane) {
throw runtimeAuthorityError("unsupported_runtime_lane", `unsupported HWLAB runtime lane: ${String(raw).trim()}`);
}
return lane;
}
function runtimeLaneForNamespace(namespace) {
const value = String(namespace ?? "").trim().toLowerCase();
if (value === "hwlab-prod") return "prod";
if (value === "hwlab-dev") return "dev";
const versionMatch = value.match(/^hwlab-v(\d+)$/u);
if (versionMatch) return versionRuntimeLane(versionMatch[1]);
return null;
}
function sameRuntimeEndpoint(value, namespace, lane) {
const text = firstNonEmptyValue(value);
if (!text) return null;
const endpointLane = runtimeLaneForEndpoint(text);
const endpointNamespace = runtimeNamespaceForEndpoint(text);
if (namespace && endpointNamespace && endpointNamespace !== namespace) return null;
if (lane && endpointLane && endpointLane !== lane) return null;
return text;
}
function runtimeNamespaceForEndpoint(value) {
const match = String(value ?? "").match(/\.((?:hwlab-[a-z0-9-]+))\.svc\.cluster\.local(?::|\/|$)/iu);
return match?.[1] ?? null;
}
function runtimeLaneForEndpoint(value) {
const text = String(value ?? "").toLowerCase();
const namespaceLane = runtimeLaneForNamespace(runtimeNamespaceForEndpoint(text));
if (namespaceLane) return namespaceLane;
if (/hwlab-prod|:18666(?:\/|$)|:18667(?:\/|$)/u.test(text)) return "prod";
if (/hwlab-dev|:17666(?:\/|$)|:17667(?:\/|$)|:16666(?:\/|$)|:16667(?:\/|$)/u.test(text)) return "dev";
return null;
}
function normalizeRuntimeLane(value) {
const profile = String(value ?? "").trim().toLowerCase();
if (!profile) return null;
if (profile === "prod" || profile === "production") return "prod";
if (profile === "dev" || profile === "development" || profile === "local") return "dev";
const dotted = profile.match(/^v?0+\.(\d+)$/u);
if (dotted) return versionRuntimeLane(dotted[1]);
const compact = profile.match(/^v?0*(\d+)$/u);
if (compact) return versionRuntimeLane(compact[1]);
return null;
}
function versionRuntimeLane(value) {
const version = Number.parseInt(value, 10);
if (!Number.isFinite(version) || version <= 0) return null;
return `v${String(version).padStart(2, "0")}`;
}
function resolveRuntimeLaneAuthority({ envRuntimeLane, runtimeNamespace, namespaceRuntimeLane }) {
if (!namespaceRuntimeLane) {
throw runtimeAuthorityError("unsupported_runtime_namespace", `unsupported HWLAB runtime namespace: ${runtimeNamespace}`);
}
if (envRuntimeLane && envRuntimeLane !== namespaceRuntimeLane) {
throw runtimeAuthorityError(
"runtime_authority_mismatch",
`HWLAB runtime lane ${envRuntimeLane} does not match namespace ${runtimeNamespace} (${namespaceRuntimeLane})`
);
}
return namespaceRuntimeLane;
}
function runtimeAuthorityError(code, message) {
return Object.assign(new Error(message), {
code,
statusCode: 500,
valuesPrinted: false
});
}
function defaultRuntimeNamespace(env = process.env) {
const profile = runtimeLaneForEnv(env) ?? "dev";
if (profile === "prod") return "hwlab-prod";
if (profile === "dev") return "hwlab-dev";
return `hwlab-${profile}`;
}
function localCloudApiUrl(env = process.env) {
const port = parsePositiveInteger(env.HWLAB_CLOUD_API_PORT, 6667);
return `http://127.0.0.1:${port}`;
}
function codeAgentProviderProfileEnv(env = process.env, params = {}) {
const profile = normalizeCodeAgentProviderProfile(params.providerProfile ?? params.codeAgentProviderProfile ?? params.provider ?? env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE);
if (profile === "runtime-default") return env;
const overlay = { ...env };
overlay.HWLAB_CODE_AGENT_SELECTED_PROVIDER_PROFILE = profile;
overlay.HWLAB_CODE_AGENT_PROVIDER_PROFILE_LABEL = CODE_AGENT_PROVIDER_PROFILE_LABELS[profile] ?? profile;
overlay.HWLAB_CODE_AGENT_PROVIDER = "codex-stdio";
if (profile === "codex-api") {
overlay.HWLAB_CODE_AGENT_MODEL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_CODEX_API_MODEL, env.HWLAB_CODE_AGENT_LEGACY_CODEX_MODEL, DEFAULT_CODE_AGENT_CODEX_API_MODEL);
overlay.HWLAB_CODE_AGENT_OPENAI_BASE_URL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_CODEX_API_BASE_URL, env.HWLAB_CODE_AGENT_LEGACY_OPENAI_BASE_URL, DEFAULT_CODE_AGENT_CODEX_API_BASE_URL);
} else if (profile === "minimax-m3") {
overlay.HWLAB_CODE_AGENT_MODEL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_MINIMAX_M3_MODEL, DEFAULT_CODE_AGENT_MINIMAX_M3_MODEL);
} else {
const dynamicProfile = profile !== "deepseek";
overlay.HWLAB_CODE_AGENT_MODEL = dynamicProfile
? firstNonEmptyValue(env[`HWLAB_CODE_AGENT_${providerProfileEnvKey(profile)}_MODEL`], profile)
: firstNonEmptyValue(env.HWLAB_CODE_AGENT_DEEPSEEK_MODEL, DEFAULT_CODE_AGENT_DEEPSEEK_MODEL);
if (!dynamicProfile || !codeAgentAgentRunAdapterEnabled(env)) {
overlay.HWLAB_CODE_AGENT_OPENAI_BASE_URL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL, defaultDeepSeekBaseUrlForEnv(env));
}
}
overlay.NO_PROXY = mergeNoProxyEntries(env.NO_PROXY, codeAgentClusterNoProxyEntries(env));
overlay.no_proxy = mergeNoProxyEntries(env.no_proxy ?? env.NO_PROXY, codeAgentClusterNoProxyEntries(env));
return overlay;
}
function providerProfileEnvKey(profile) {
return String(profile ?? "")
.trim()
.toUpperCase()
.replace(/[^A-Z0-9]+/gu, "_")
.replace(/^_+|_+$/gu, "") || "PROFILE";
}
function defaultDeepSeekBaseUrlForEnv(env = process.env) {
const namespace = env.HWLAB_GITOPS_PROFILE === "prod" || env.HWLAB_ENVIRONMENT === "prod" ? "hwlab-prod" : "hwlab-dev";
return `http://hwlab-deepseek-proxy.${namespace}.svc.cluster.local:4000/v1/responses`;
}
function codeAgentClusterNoProxyEntries(env = process.env) {
const namespace = env.HWLAB_GITOPS_PROFILE === "prod" || env.HWLAB_ENVIRONMENT === "prod" ? "hwlab-prod" : "hwlab-dev";
return [
`${namespace}.svc.cluster.local`,
".svc",
".cluster.local",
"127.0.0.1",
"localhost",
"::1",
"10.0.0.0/8",
"10.42.0.0/16",
"10.43.0.0/16",
"hyueapi.com",
".hyueapi.com",
"hyui.com",
".hyui.com",
"api.minimaxi.com",
".minimaxi.com"
];
}
function mergeNoProxyEntries(current, additions = []) {
return uniqueStrings([...String(current ?? "").split(","), ...additions]).join(",");
}
function safeOtelConfigText(value) {
const text = firstNonEmptyValue(value);
return text === null ? null : text.slice(0, 120);
}
function safeOtelUrlHost(value) {
const text = firstNonEmptyValue(value);
if (text === null) return null;
try {
return new URL(text).host.slice(0, 120);
} catch {
return null;
}
}
function withMdtodoExecutionPrompt(params = {}) {
const execution = mdtodoExecutionContextFromParams(params);
if (!execution?.hwpodId || !execution?.hwpodWorkspaceArgs) return params;
const message = firstNonEmptyValue(params.message, params.prompt, params.text);
if (!message || /hwpodWorkspaceArgs\s*:/u.test(message)) return params;
const taskRef = firstNonEmptyValue(params.taskRef, execution.taskRef) ?? "-";
const mdtodoRootRef = execution.mdtodoRootRef ?? "docs/MDTODO";
const prefix = [
"# MDTODO HWPOD 执行上下文",
`TaskRef: ${taskRef}`,
`sourceId: ${execution.sourceId ?? "-"}`,
`hwpodId: ${execution.hwpodId}`,
`nodeId: ${execution.nodeId ?? "-"}`,
`mdtodoRootRef: ${mdtodoRootRef}`,
`hwpodWorkspaceArgs: ${execution.hwpodWorkspaceArgs}`,
"所有 hwpod/hwpod-ctl 命令都必须携带上述 hwpodWorkspaceArgs。读取 MDTODO、报告和工程文件必须通过 HWPOD workspace/node-ops 入口,不得猜测或访问普通容器本地路径,也不得创建本地 .hwlab/hwpod-spec.yaml fallback。"
].join("\n");
const nextMessage = `${prefix}\n\n${message}`;
return {
...params,
message: nextMessage,
prompt: nextMessage,
launchContext: {
...(params.launchContext && typeof params.launchContext === "object" ? params.launchContext : {}),
hwpodId: execution.hwpodId,
nodeId: execution.nodeId,
mdtodoRootRef: execution.mdtodoRootRef,
hwpodWorkspaceArgs: execution.hwpodWorkspaceArgs,
contextFingerprint: execution.contextFingerprint,
executionContext: execution,
valuesRedacted: true
}
};
}
function mdtodoExecutionContextFromParams(params = {}) {
const launchContext = params.launchContext && typeof params.launchContext === "object" && !Array.isArray(params.launchContext) ? params.launchContext : null;
const execution = launchContext?.executionContext && typeof launchContext.executionContext === "object" && !Array.isArray(launchContext.executionContext) ? launchContext.executionContext : launchContext;
if (!execution || typeof execution !== "object") return null;
return {
sourceId: safeOtelConfigText(execution.sourceId ?? launchContext?.sourceId),
sourceKind: safeOtelConfigText(execution.sourceKind ?? launchContext?.sourceKind),
projectId: safeOtelConfigText(execution.projectId ?? launchContext?.projectId ?? params.projectId),
taskRef: safeOtelText(execution.taskRef ?? launchContext?.taskRef ?? params.taskRef),
fileRef: safeOtelConfigText(execution.fileRef ?? launchContext?.fileRef),
relativePath: safeOtelText(execution.relativePath ?? launchContext?.relativePath),
taskId: safeOtelConfigText(execution.taskId ?? launchContext?.taskId),
hwpodId: safeOtelText(execution.hwpodId ?? launchContext?.hwpodId),
nodeId: safeOtelText(execution.nodeId ?? launchContext?.nodeId),
mdtodoRootRef: safeOtelText(execution.mdtodoRootRef ?? launchContext?.mdtodoRootRef),
workspaceRootHash: safeOtelText(execution.workspaceRootHash ?? launchContext?.workspaceRootHash),
hwpodWorkspaceArgs: safeOtelText(execution.hwpodWorkspaceArgs ?? launchContext?.hwpodWorkspaceArgs, 500),
contextFingerprint: safeOtelText(execution.contextFingerprint ?? launchContext?.contextFingerprint),
valuesRedacted: true
};
}
function agentChatOtelAttributes(params = {}) {
const execution = mdtodoExecutionContextFromParams(params);
return {
"agent.chat.session_id": safeSessionId(params.sessionId) || null,
"agent.chat.provider_profile": safeOtelConfigText(params.providerProfile ?? params.codeAgentProviderProfile),
"agent.chat.provider_profile_source": safeOtelConfigText(params.providerProfileSource),
"agent.chat.task_ref": safeOtelText(params.taskRef ?? execution?.taskRef),
"agent.chat.source_id": safeOtelConfigText(execution?.sourceId ?? params.sourceId),
"agent.chat.hwpod_id": safeOtelText(execution?.hwpodId),
"agent.chat.workspace_root_hash": safeOtelText(execution?.workspaceRootHash),
"agent.chat.context_fingerprint": safeOtelText(execution?.contextFingerprint)
};
}
function safeOtelText(value, max = 360) {
const text = firstNonEmptyValue(value);
return text === null ? null : text.slice(0, max);
}
async function submitCodeAgentChatTurn({ params, options, traceId }) {
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
const results = options.codeAgentChatResults ?? createCodeAgentChatResultStore();
const timestamp = new Date().toISOString();
const runtimeEnv = options.env ?? process.env;
const adapterEnabled = codeAgentAgentRunAdapterEnabled(runtimeEnv);
const acceptedPayload = {
accepted: true,
status: "running",
traceId,
...codeAgentOtelTraceFields(traceId, process.env),
...codeAgentTurnLifecycleFields(traceId, params),
conversationId: safeConversationId(params.conversationId) || null,
sessionId: safeSessionId(params.sessionId) || null,
threadId: safeOpaqueId(params.threadId) || null,
projectId: params.projectId ?? null,
...codeAgentAdmissionTimingFields(timestamp)
};
await recordCodeAgentTurnAdmission({ payload: acceptedPayload, params, options, traceId });
results.set(traceId, annotateOwner(acceptedPayload, params));
void emitCodeAgentOtelSpan("provider_decision", traceId, runtimeEnv, {
attributes: {
...agentChatOtelAttributes(params),
adapterEnabled,
adapter: safeOtelConfigText(runtimeEnv.HWLAB_CODE_AGENT_ADAPTER ?? runtimeEnv.HWLAB_CODE_AGENT_PROVIDER),
provider: safeOtelConfigText(runtimeEnv.HWLAB_CODE_AGENT_PROVIDER),
defaultProviderProfile: safeOtelConfigText(runtimeEnv.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE),
agentRunManagerHost: safeOtelUrlHost(runtimeEnv.AGENTRUN_MGR_URL ?? runtimeEnv.HWLAB_CODE_AGENT_AGENTRUN_MGR_URL),
agentRunRunnerNamespace: safeOtelConfigText(runtimeEnv.HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE ?? runtimeEnv.AGENTRUN_RUNTIME_NAMESPACE),
agentRunSourceCommitPresent: Boolean(firstNonEmptyValue(runtimeEnv.HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT))
}
});
if (adapterEnabled) {
const run = async () => {
let initial = null;
let executionOptions = options;
try {
initial = initialAgentRunChatResult({ params, options, traceId });
results.set(traceId, annotateOwner(initial, params));
const billingStartedAt = Date.now();
const billingPreflight = await preflightCodeAgentBilling({ params, options, traceId, sendResponse: false });
void emitCodeAgentOtelSpan("billing_preflight", traceId, options.env ?? process.env, {
startTimeMs: billingStartedAt,
attributes: { sessionId: safeSessionId(params.sessionId) || null, turnId: codeAgentTurnLifecycleFields(traceId, params).turnId, blocked: Boolean(billingPreflight?.blocked) },
status: billingPreflight?.blocked ? "error" : "ok",
error: billingPreflight?.blocked ? billingPreflight?.payload?.error?.message ?? billingPreflight?.blocker?.summary ?? "billing preflight blocked" : null
});
if (billingPreflight?.blocked) {
await settleAdmittedCodeAgentFailure({
base: initial,
params,
options,
traceId,
results,
failure: billingPreflight,
traceLabel: "billing:preflight:failed"
});
return;
}
const dispatchParams = { ...params, userBillingReservation: billingPreflight?.reservation ?? null };
initial = withCodeAgentBillingReservation(initial, dispatchParams);
results.set(traceId, annotateOwner(initial, dispatchParams));
executionOptions = { ...options, ...(await codeAgentChatExecutionOptions(options, dispatchParams)) };
const dispatchStartedAt = Date.now();
const payload = await submitAgentRunChatTurn({ params: dispatchParams, options: executionOptions, traceId, traceStore, results });
void emitCodeAgentOtelSpan("agentrun_dispatch", traceId, executionOptions.env ?? process.env, {
startTimeMs: dispatchStartedAt,
attributes: { sessionId: safeSessionId(dispatchParams.sessionId) || null, turnId: codeAgentTurnLifecycleFields(traceId, dispatchParams).turnId, runId: payload?.agentRun?.runId ?? null, commandId: payload?.agentRun?.commandId ?? null, providerProfile: dispatchParams.providerProfile ?? null }
});
if (isCodeAgentResultCanceled(results.get(traceId))) return;
const owned = annotateOwner(withCodeAgentBillingReservation(payload, dispatchParams), dispatchParams);
await recordCodeAgentSessionOwner({ payload: owned, params: dispatchParams, options: executionOptions, status: "running" });
results.set(traceId, owned);
} catch (error) {
if (isCodeAgentResultCanceled(results.get(traceId))) return;
const dispatchErrorAttributes = codeAgentDispatchErrorOtelAttributes(error);
void emitCodeAgentOtelSpan("agentrun_dispatch", traceId, executionOptions.env ?? process.env, {
status: "error",
error,
attributes: { sessionId: safeSessionId(params.sessionId) || null, turnId: codeAgentTurnLifecycleFields(traceId, params).turnId, ...dispatchErrorAttributes }
});
await settleAdmittedCodeAgentFailure({
base: initial ?? codeAgentAdmittedFailureBasePayload({ params, options, traceId }),
params,
options: executionOptions,
traceId,
results,
failure: codeAgentDispatchFailure(error),
traceLabel: "agentrun:dispatch:failed"
});
}
};
setImmediate(() => { run(); });
return annotateOwner(acceptedPayload, params);
}
traceStore.ensure(traceId, {
runnerKind: "codex-app-server-stdio-runner",
workspace: options.workspace ?? options.env?.HWLAB_CODE_AGENT_CODEX_WORKSPACE ?? options.env?.HWLAB_CODE_AGENT_WORKSPACE ?? null,
sandbox: options.env?.HWLAB_CODE_AGENT_CODEX_SANDBOX ?? null,
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session"
});
traceStore.append(traceId, {
type: "request",
status: "accepted",
label: "request:accepted-short-connection",
message: "Code Agent request accepted; committed progress is delivered through the Workbench projection event stream.",
waitingFor: "codex-stdio"
});
const run = async () => {
try {
const billingStartedAt = Date.now();
const billingPreflight = await preflightCodeAgentBilling({ params, options, traceId, sendResponse: false });
void emitCodeAgentOtelSpan("billing_preflight", traceId, options.env ?? process.env, {
startTimeMs: billingStartedAt,
attributes: { sessionId: safeSessionId(params.sessionId) || null, turnId: codeAgentTurnLifecycleFields(traceId, params).turnId, blocked: Boolean(billingPreflight?.blocked) },
status: billingPreflight?.blocked ? "error" : "ok",
error: billingPreflight?.blocked ? billingPreflight?.payload?.error?.message ?? billingPreflight?.blocker?.summary ?? "billing preflight blocked" : null
});
if (billingPreflight?.blocked) {
await settleAdmittedCodeAgentFailure({
base: codeAgentAdmittedFailureBasePayload({ params, options, traceId }),
params,
options,
traceId,
results,
failure: billingPreflight,
traceLabel: "billing:preflight:failed"
});
return;
}
const dispatchParams = { ...params, userBillingReservation: billingPreflight?.reservation ?? null };
const payload = await runCodeAgentChat(dispatchParams, options);
if (isCodeAgentResultCanceled(results.get(traceId))) return;
await finalizeCodeAgentBillingUsage({ payload, params: dispatchParams, options });
await recordCodeAgentSessionOwner({ payload, params: dispatchParams, options, status: codeAgentOwnerStatusForResult(payload) });
results.set(traceId, annotateOwner(payload, dispatchParams));
traceStore.append(traceId, {
type: "result",
status: payload.status === "completed" ? "completed" : "failed",
label: `result:${payload.status}`,
message: payload.status === "completed"
? "Code Agent result is committed to the Workbench projection event stream."
: payload.error?.message ?? "Code Agent completed without a successful reply.",
sessionId: payload.sessionId ?? payload.session?.sessionId,
sessionStatus: payload.session?.status,
terminal: true
});
} catch (error) {
if (isCodeAgentResultCanceled(results.get(traceId))) return;
const payload = {
status: "failed",
traceId,
conversationId: safeConversationId(params.conversationId) || null,
sessionId: safeSessionId(params.sessionId) || null,
error: {
code: "code_agent_background_failed",
layer: "api",
retryable: true,
message: error?.message ?? "Code Agent background turn failed",
userMessage: "Code Agent 后台任务失败;错误已提交到 Workbench 实时投影。"
},
updatedAt: new Date().toISOString()
};
await finalizeCodeAgentBillingUsage({ payload, params, options });
results.set(traceId, payload);
traceStore.append(traceId, {
type: "result",
status: "failed",
label: "result:failed",
errorCode: payload.error.code,
message: payload.error.message,
terminal: true
});
}
};
setImmediate(() => {
run();
});
return annotateOwner(acceptedPayload, params);
}
async function recordCodeAgentTurnAdmission({ payload = {}, params = {}, options = {}, traceId } = {}) {
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
let ownerRecord = null;
try {
ownerRecord = await recordCodeAgentSessionOwner({ payload, params, options, status: "running" });
if (!ownerRecord) {
const error = new Error("Code Agent turn admission did not durably persist Workbench session ownership.");
error.code = "workbench_admission_owner_not_persisted";
error.valuesRedacted = true;
throw error;
}
await writeWorkbenchSessionAdmissionFact({
runtimeStore: options.runtimeStore,
session: ownerRecord,
ownerUserId: options.actor?.id ?? params.ownerUserId,
ownerRole: options.actor?.role ?? params.ownerRole,
projectId: params.projectId ?? ownerRecord.projectId ?? null,
conversationId: params.conversationId ?? ownerRecord.conversationId ?? null,
threadId: params.threadId ?? ownerRecord.threadId ?? null,
status: "running"
});
await recordCodeAgentSessionInputFact({
params,
options,
traceId,
delivery: "queue",
status: "admitted",
messageId: codeAgentTurnLifecycleFields(traceId, params).userMessageId,
commandId: payload.agentRun?.commandId ?? null
});
} catch (error) {
throw codeAgentAdmissionUnavailableError(error, { params, options, traceId });
}
traceStore.append(traceId, {
type: "request",
status: "admitted",
label: "turn:admitted",
message: "Code Agent turn was durably admitted before billing or dispatch preflight.",
sessionId: safeSessionId(params.sessionId) || null,
threadId: safeOpaqueId(params.threadId) || null,
...codeAgentOtelTraceFields(traceId, options.env ?? process.env),
waitingFor: "billing-or-dispatch",
valuesPrinted: false
});
void emitCodeAgentOtelSpan("durable_admission", traceId, options.env ?? process.env, {
attributes: { sessionId: safeSessionId(params.sessionId) || null, threadId: safeOpaqueId(params.threadId) || null, turnId: codeAgentTurnLifecycleFields(traceId, params).turnId }
});
return ownerRecord;
}
function codeAgentAdmissionUnavailableError(error, { params = {}, options = {}, traceId } = {}) {
const payload = codeAgentAdmissionUnavailablePayload({ error, params, options, traceId });
return Object.assign(new Error(payload.error.message), {
code: "admission_unavailable",
statusCode: 503,
payload,
alreadyClassified: true
});
}
function isCodeAgentAdmissionUnavailableError(error) {
return error?.code === "admission_unavailable" && error?.payload;
}
function codeAgentAdmissionUnavailablePayload({ error, params = {}, options = {}, traceId } = {}) {
const message = error?.message ?? "Code Agent turn admission is unavailable; the user message was not persisted.";
return {
ok: false,
accepted: false,
status: "failed",
traceId: safeTraceId(traceId) || null,
...codeAgentOtelTraceFields(traceId, options.env ?? process.env),
...codeAgentTurnLifecycleFields(traceId, params),
conversationId: safeConversationId(params.conversationId) || null,
sessionId: safeSessionId(params.sessionId) || null,
threadId: safeOpaqueId(params.threadId) || null,
error: {
code: "admission_unavailable",
layer: "admission",
retryable: true,
message,
userMessage: "本次输入尚未持久化,请保留草稿后重试。",
route: "/v1/agent/chat"
},
blocker: {
code: "admission_unavailable",
layer: "admission",
retryable: true,
summary: message
},
valuesRedacted: true,
secretMaterialStored: false
};
}
function codeAgentDispatchFailure(error) {
const message = error?.message ?? "AgentRun dispatch failed";
return {
statusCode: 503,
payload: {
error: {
code: error?.code ?? "agentrun_dispatch_failed",
layer: "agentrun",
retryable: true,
message,
userMessage: "AgentRun 调度失败;错误已提交到 Workbench 实时投影。",
route: "/v1/agent/chat"
},
blocker: {
code: error?.code ?? "agentrun_dispatch_failed",
layer: "agentrun",
retryable: true,
summary: message
}
}
};
}
function codeAgentDispatchErrorOtelAttributes(error) {
const agentRunError = error?.agentRunError && typeof error.agentRunError === "object" ? error.agentRunError : null;
const details = agentRunError?.error?.details && typeof agentRunError.error.details === "object" ? agentRunError.error.details : null;
return {
failureKind: safeOtelText(error?.code ?? agentRunError?.failureKind, 120),
upstreamStatusCode: Number.isInteger(error?.statusCode) ? error.statusCode : null,
upstreamTraceId: safeTraceId(agentRunError?.traceId) || null,
upstreamMessage: safeOtelText(agentRunError?.message ?? error?.message, 300),
upstreamStderrTail: safeOtelText(details?.stderr, 600)
};
}
function safeOtelText(value, limit = 300) {
const text = String(value ?? "").replace(/[\u0000-\u001f\u007f]+/gu, " ").trim();
return text ? text.slice(0, limit) : null;
}
function codeAgentAdmittedFailureBasePayload({ params = {}, options = {}, traceId } = {}) {
const now = new Date().toISOString();
return {
accepted: true,
status: "running",
shortConnection: true,
traceId,
...codeAgentTurnLifecycleFields(traceId, params),
messageId: codeAgentTurnLifecycleFields(traceId, params).assistantMessageId,
projectId: params.projectId ?? null,
conversationId: safeConversationId(params.conversationId) || null,
sessionId: safeSessionId(params.sessionId) || null,
threadId: safeOpaqueId(params.threadId) || null,
...codeAgentAdmissionTimingFields(now),
provider: codeAgentAgentRunAdapterEnabled(options.env ?? process.env) ? "agentrun" : "codex-stdio",
model: options.env?.HWLAB_CODE_AGENT_MODEL ?? "unknown",
backend: codeAgentAgentRunAdapterEnabled(options.env ?? process.env) ? "agentrun-v01" : "hwlab-cloud-api/code-agent-chat",
valuesPrinted: false
};
}
function codeAgentAdmissionTimingFields(value = null) {
const admittedAt = timestampIsoOrNow(value);
const timing = {
startedAt: admittedAt,
lastEventAt: admittedAt,
finishedAt: null,
durationMs: null,
observedAt: admittedAt,
lastEventAgeMs: 0,
valuesRedacted: true
};
return {
createdAt: admittedAt,
updatedAt: admittedAt,
timing,
startedAt: admittedAt,
lastEventAt: admittedAt,
finishedAt: null,
durationMs: null
};
}
async function settleAdmittedCodeAgentFailure({ base = {}, params = {}, options = {}, traceId, results, failure = {}, traceLabel = "code-agent:failed" } = {}) {
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
const error = failure.payload?.error ?? failure.error ?? {};
const blocker = failure.payload?.blocker ?? failure.blocker ?? null;
const message = error.message ?? blocker?.summary ?? "Code Agent request failed after admission.";
traceStore.append(traceId, {
type: "result",
status: "failed",
label: traceLabel,
errorCode: error.code ?? "code_agent_failed_after_admission",
message,
terminal: true,
valuesPrinted: false
});
const now = new Date().toISOString();
const payload = annotateOwner({
...base,
accepted: true,
status: "failed",
traceId,
...codeAgentTurnLifecycleFields(traceId, base),
messageId: base.messageId ?? codeAgentTurnLifecycleFields(traceId, base).assistantMessageId,
conversationId: safeConversationId(base.conversationId ?? params.conversationId) || null,
sessionId: safeSessionId(base.sessionId ?? params.sessionId) || null,
threadId: safeOpaqueId(base.threadId ?? params.threadId) || null,
error: {
code: error.code ?? "code_agent_failed_after_admission",
layer: error.layer ?? blocker?.layer ?? "api",
retryable: error.retryable !== false,
message,
userMessage: error.userMessage ?? message,
route: error.route ?? "/v1/agent/chat"
},
blocker: blocker ? {
code: blocker.code ?? error.code ?? "code_agent_failed_after_admission",
layer: blocker.layer ?? error.layer ?? "api",
retryable: blocker.retryable !== false,
summary: blocker.summary ?? message
} : null,
finalResponse: {
text: error.userMessage ?? message,
textChars: String(error.userMessage ?? message).length,
role: "assistant",
status: "failed",
traceId,
updatedAt: now,
source: "code-agent-admitted-failure",
valuesPrinted: false
},
runnerTrace: traceStore.snapshot(traceId),
updatedAt: now,
valuesPrinted: false,
secretMaterialStored: false
}, params);
await finalizeCodeAgentBillingUsage({ payload, params, options });
payload.runnerTrace = traceStore.snapshot(traceId);
recordCodeAgentConversationFact(payload, options);
results?.set?.(traceId, payload);
await recordCodeAgentSessionOwner({ payload, params, options, status: codeAgentOwnerStatusForResult(payload) });
return payload;
}