1743 lines
74 KiB
TypeScript
1743 lines
74 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 { classifyCodeAgentDispatchFailure } from "./code-agent-dispatch-failure.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 { promoteWorkbenchTurnAdmission as persistWorkbenchTurnAdmissionPromotion, writeWorkbenchSessionAdmissionFact } from "./workbench-projection-writer.ts";
|
|
import { createWorkbenchReadModel } from "./workbench-read-model.ts";
|
|
import {
|
|
workbenchRealtimeCapabilities
|
|
} from "./workbench-realtime-capabilities.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,
|
|
buildCodeAgentSessionInputFact,
|
|
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 (isCodeAgentSubmissionUnavailableError(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,
|
|
realtimeCapabilities: workbenchRealtimeCapabilities(options.env ?? process.env),
|
|
admission: submitted?.agentRun ? {
|
|
state: submitted.admissionState ?? "promoted",
|
|
runId: submitted.agentRun.runId ?? null,
|
|
commandId: submitted.agentRun.commandId ?? null,
|
|
dispatchIntentId: submitted.agentRun.dispatchIntentId ?? null,
|
|
runnerJobId: submitted.agentRun.runnerJobId ?? null,
|
|
durableDispatch: submitted.agentRun.durableDispatch === true,
|
|
valuesRedacted: true
|
|
} : null,
|
|
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)
|
|
};
|
|
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 transactionalAdmission = true;
|
|
const pendingPromotion = results.get(traceId);
|
|
if (pendingPromotion?.admissionState === "promotion-pending" && pendingPromotion?.agentRun?.durableDispatch === true) {
|
|
const retryParams = { ...params, userBillingReservation: pendingPromotion.userBillingReservation ?? null };
|
|
try {
|
|
await promoteCodeAgentTurnAdmission({ payload: pendingPromotion, params: retryParams, options, traceId });
|
|
} catch (error) {
|
|
throw codeAgentPromotionUnavailableError(error, { payload: pendingPromotion, params: retryParams, options, traceId });
|
|
}
|
|
const promoted = annotateOwner({ ...pendingPromotion, accepted: true, status: "running", admissionState: "promoted" }, retryParams);
|
|
results.set(traceId, promoted);
|
|
return promoted;
|
|
}
|
|
|
|
const lifecycle = codeAgentTurnLifecycleFields(traceId, params);
|
|
if (transactionalAdmission) {
|
|
await persistCodeAgentAdmissionInputState({
|
|
params,
|
|
options,
|
|
traceId,
|
|
status: "admitting",
|
|
messageId: lifecycle.userMessageId
|
|
});
|
|
}
|
|
|
|
traceStore.append(traceId, {
|
|
type: "request",
|
|
status: "admitting",
|
|
label: "turn:admitting",
|
|
message: "Code Agent turn is validating billing and durable AgentRun dispatch before UI admission.",
|
|
terminal: false,
|
|
waitingFor: "billing-and-durable-dispatch",
|
|
valuesPrinted: false
|
|
});
|
|
const initial = initialAgentRunChatResult({ params, options, traceId });
|
|
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) {
|
|
if (transactionalAdmission) {
|
|
await persistCodeAgentAdmissionInputState({
|
|
params,
|
|
options,
|
|
traceId,
|
|
status: "blocked",
|
|
messageId: lifecycle.userMessageId,
|
|
error: billingPreflight.payload?.error ?? billingPreflight.blocker ?? { code: "billing_preflight_blocked" }
|
|
});
|
|
}
|
|
traceStore.append(traceId, {
|
|
type: "billing",
|
|
status: "blocked",
|
|
label: "billing:preflight:blocked-before-admission",
|
|
errorCode: billingPreflight.payload?.error?.code ?? billingPreflight.blocker?.code ?? "billing_preflight_blocked",
|
|
terminal: false,
|
|
valuesPrinted: false
|
|
});
|
|
throw codeAgentSubmissionUnavailableError(billingPreflight, { params, options, traceId });
|
|
}
|
|
|
|
const dispatchParams = {
|
|
...params,
|
|
...lifecycle,
|
|
userBillingReservation: billingPreflight?.reservation ?? null
|
|
};
|
|
const executionOptions = { ...options, ...(await codeAgentChatExecutionOptions(options, dispatchParams)) };
|
|
const dispatchStartedAt = Date.now();
|
|
let payload;
|
|
try {
|
|
payload = await submitAgentRunChatTurn({ params: dispatchParams, options: executionOptions, traceId, traceStore, results });
|
|
} catch (error) {
|
|
const failure = codeAgentDispatchFailure(error);
|
|
const rejected = codeAgentSubmissionUnavailablePayload(failure, { params: dispatchParams, options: executionOptions, traceId });
|
|
await finalizeCodeAgentBillingUsage({ payload: rejected, params: dispatchParams, options: executionOptions });
|
|
if (transactionalAdmission) {
|
|
await persistCodeAgentAdmissionInputState({
|
|
params: dispatchParams,
|
|
options: executionOptions,
|
|
traceId,
|
|
status: "failed",
|
|
messageId: lifecycle.userMessageId,
|
|
error: failure.payload?.error ?? error
|
|
});
|
|
}
|
|
void emitCodeAgentOtelSpan("agentrun_dispatch", traceId, executionOptions.env ?? process.env, {
|
|
startTimeMs: dispatchStartedAt,
|
|
status: "error",
|
|
error,
|
|
attributes: { sessionId: safeSessionId(dispatchParams.sessionId) || null, turnId: codeAgentTurnLifecycleFields(traceId, dispatchParams).turnId, ...codeAgentDispatchErrorOtelAttributes(error) }
|
|
});
|
|
throw codeAgentSubmissionUnavailableError(failure, { params: dispatchParams, options: executionOptions, traceId });
|
|
}
|
|
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 }
|
|
});
|
|
const pending = annotateOwner({
|
|
...withCodeAgentBillingReservation(payload, dispatchParams),
|
|
accepted: false,
|
|
status: "admitting",
|
|
admissionState: "promotion-pending"
|
|
}, dispatchParams);
|
|
results.set(traceId, pending);
|
|
try {
|
|
if (transactionalAdmission) {
|
|
await persistCodeAgentAdmissionInputState({
|
|
params: dispatchParams,
|
|
options: executionOptions,
|
|
traceId,
|
|
status: "admitting",
|
|
messageId: lifecycle.userMessageId,
|
|
commandId: payload.agentRun?.commandId ?? null,
|
|
agentRun: payload.agentRun ?? null
|
|
});
|
|
}
|
|
await promoteCodeAgentTurnAdmission({ payload: pending, params: dispatchParams, options: executionOptions, traceId });
|
|
} catch (error) {
|
|
throw codeAgentPromotionUnavailableError(error, { payload: pending, params: dispatchParams, options: executionOptions, traceId });
|
|
}
|
|
const promoted = annotateOwner({ ...pending, accepted: true, status: "running", admissionState: "promoted" }, dispatchParams);
|
|
results.set(traceId, promoted);
|
|
return promoted;
|
|
}
|
|
await recordCodeAgentTurnAdmission({ payload: acceptedPayload, params, options, traceId });
|
|
results.set(traceId, 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;
|
|
}
|
|
|
|
async function promoteCodeAgentTurnAdmission({ payload = {}, params = {}, options = {}, traceId } = {}) {
|
|
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
|
|
const ownerRecord = await recordCodeAgentSessionOwner({ payload, params, options, status: "admitting" });
|
|
if (!ownerRecord) {
|
|
const error = new Error("Code Agent turn promotion did not durably persist the admitting owner record.");
|
|
error.code = "workbench_admission_owner_not_persisted";
|
|
throw error;
|
|
}
|
|
const lifecycle = codeAgentTurnLifecycleFields(traceId, params);
|
|
const realtimeCapabilities = workbenchRealtimeCapabilities();
|
|
const inputFact = buildCodeAgentSessionInputFact({
|
|
params,
|
|
options,
|
|
traceId,
|
|
delivery: "queue",
|
|
status: "promoted",
|
|
messageId: lifecycle.userMessageId,
|
|
commandId: payload.agentRun?.commandId ?? null,
|
|
agentRun: payload.agentRun ?? null
|
|
});
|
|
const promotion = await persistWorkbenchTurnAdmissionPromotion({
|
|
runtimeStore: options.runtimeStore,
|
|
session: ownerRecord,
|
|
inputFact,
|
|
payload,
|
|
params,
|
|
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: payload.agentRun?.threadId ?? params.threadId ?? ownerRecord.threadId ?? null
|
|
});
|
|
const runningOwner = await recordCodeAgentSessionOwner({ payload: { ...payload, status: "running" }, params, options, status: "running" });
|
|
if (!runningOwner) {
|
|
const error = new Error("Code Agent turn promotion did not durably promote the session owner to running.");
|
|
error.code = "workbench_admission_owner_not_promoted";
|
|
error.valuesRedacted = true;
|
|
throw error;
|
|
}
|
|
traceStore.append(traceId, {
|
|
type: "request",
|
|
status: "admitted",
|
|
label: "turn:admitted",
|
|
message: "Code Agent turn was promoted only after billing and durable AgentRun dispatch admission committed.",
|
|
sessionId: safeSessionId(params.sessionId) || null,
|
|
threadId: safeOpaqueId(payload.agentRun?.threadId ?? params.threadId) || null,
|
|
runId: payload.agentRun?.runId ?? null,
|
|
commandId: payload.agentRun?.commandId ?? null,
|
|
dispatchIntentId: payload.agentRun?.dispatchIntentId ?? null,
|
|
runnerJobId: payload.agentRun?.runnerJobId ?? null,
|
|
waitingFor: "agentrun-kafka-projection",
|
|
terminal: false,
|
|
valuesPrinted: false
|
|
});
|
|
void emitCodeAgentOtelSpan("durable_admission", traceId, options.env ?? process.env, {
|
|
attributes: {
|
|
sessionId: safeSessionId(params.sessionId) || null,
|
|
threadId: safeOpaqueId(payload.agentRun?.threadId ?? params.threadId) || null,
|
|
turnId: lifecycle.turnId,
|
|
runId: payload.agentRun?.runId ?? null,
|
|
commandId: payload.agentRun?.commandId ?? null,
|
|
dispatchIntentId: payload.agentRun?.dispatchIntentId ?? null,
|
|
outboxCommitted: promotion?.outboxCommitted === true
|
|
}
|
|
});
|
|
return promotion;
|
|
}
|
|
|
|
async function persistCodeAgentAdmissionInputState({ params = {}, options = {}, traceId, status, messageId = null, commandId = null, agentRun = null, error = null } = {}) {
|
|
try {
|
|
const written = await recordCodeAgentSessionInputFact({
|
|
params,
|
|
options,
|
|
traceId,
|
|
delivery: "queue",
|
|
status,
|
|
messageId,
|
|
commandId,
|
|
agentRun,
|
|
error
|
|
});
|
|
if (written?.written !== true) {
|
|
const unavailable = new Error("Code Agent admission input state was not durably persisted.");
|
|
unavailable.code = "workbench_admission_input_not_persisted";
|
|
unavailable.valuesRedacted = true;
|
|
throw unavailable;
|
|
}
|
|
return written;
|
|
} catch (cause) {
|
|
throw codeAgentAdmissionUnavailableError(cause, { params, options, traceId });
|
|
}
|
|
}
|
|
|
|
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 isCodeAgentSubmissionUnavailableError(error) {
|
|
return Boolean(error?.payload && Number.isInteger(error?.statusCode) && (
|
|
isCodeAgentAdmissionUnavailableError(error)
|
|
|| error.code === "submission_unavailable"
|
|
|| error.code === "admission_promotion_unavailable"
|
|
));
|
|
}
|
|
|
|
function codeAgentSubmissionUnavailableError(failure = {}, context = {}) {
|
|
const payload = codeAgentSubmissionUnavailablePayload(failure, context);
|
|
return Object.assign(new Error(payload.error.message), {
|
|
code: "submission_unavailable",
|
|
statusCode: failure.statusCode ?? 503,
|
|
payload,
|
|
alreadyClassified: true
|
|
});
|
|
}
|
|
|
|
function codeAgentSubmissionUnavailablePayload(failure = {}, { params = {}, options = {}, traceId } = {}) {
|
|
const source = failure.payload ?? failure;
|
|
const error = source.error ?? {};
|
|
const blocker = source.blocker ?? null;
|
|
const message = error.message ?? blocker?.summary ?? "Code Agent submission failed before durable admission.";
|
|
const lifecycle = codeAgentTurnLifecycleFields(traceId, params);
|
|
const resolvedTraceId = safeTraceId(traceId) || null;
|
|
const failureIdentity = textValue(error.failureIdentity) || `admission:${lifecycle.assistantMessageId}`;
|
|
const admission = source.admission && typeof source.admission === "object" ? source.admission : null;
|
|
const failureFields = {
|
|
failureKind: textValue(error.failureKind ?? blocker?.failureKind ?? error.code ?? blocker?.code) || "submission-unavailable",
|
|
reason: textValue(error.reason ?? blocker?.reason) || "unknown-dispatch-failure",
|
|
recoveryAction: textValue(error.recoveryAction ?? blocker?.recoveryAction) || "inspect-agentrun-diagnostic",
|
|
failureIdentity,
|
|
primaryPresentation: "assistant-message",
|
|
traceId: resolvedTraceId,
|
|
turnId: lifecycle.turnId,
|
|
userMessageId: lifecycle.userMessageId,
|
|
assistantMessageId: lifecycle.assistantMessageId,
|
|
runId: textValue(error.runId ?? blocker?.runId ?? admission?.runId) || null,
|
|
commandId: textValue(error.commandId ?? blocker?.commandId ?? admission?.commandId) || null,
|
|
dispatchIntentId: textValue(error.dispatchIntentId ?? blocker?.dispatchIntentId ?? admission?.dispatchIntentId) || null,
|
|
admissionState: textValue(error.admissionState ?? blocker?.admissionState ?? admission?.state) || "rejected-before-durable-dispatch",
|
|
durableDispatch: admission?.durableDispatch === true,
|
|
valuesPrinted: false
|
|
};
|
|
return {
|
|
...source,
|
|
ok: false,
|
|
accepted: false,
|
|
status: "failed",
|
|
traceId: safeTraceId(traceId) || null,
|
|
...codeAgentOtelTraceFields(traceId, options.env ?? process.env),
|
|
...lifecycle,
|
|
conversationId: safeConversationId(params.conversationId) || null,
|
|
sessionId: safeSessionId(params.sessionId) || null,
|
|
threadId: safeOpaqueId(params.threadId) || null,
|
|
error: {
|
|
...error,
|
|
...failureFields,
|
|
code: error.code ?? blocker?.code ?? "submission_unavailable",
|
|
layer: error.layer ?? blocker?.layer ?? "admission",
|
|
retryable: error.retryable !== false,
|
|
message,
|
|
route: error.route ?? "/v1/agent/chat"
|
|
},
|
|
blocker: blocker ? { ...blocker, ...failureFields, summary: blocker.summary ?? message } : null,
|
|
admission: {
|
|
...(admission ?? {}),
|
|
state: failureFields.admissionState,
|
|
runId: failureFields.runId,
|
|
commandId: failureFields.commandId,
|
|
dispatchIntentId: failureFields.dispatchIntentId,
|
|
durableDispatch: failureFields.durableDispatch,
|
|
valuesPrinted: false
|
|
},
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
};
|
|
}
|
|
|
|
function codeAgentPromotionUnavailableError(error, { payload = {}, params = {}, options = {}, traceId } = {}) {
|
|
const message = error?.message ?? "Local Workbench admission promotion failed after AgentRun durable dispatch.";
|
|
const responsePayload = codeAgentSubmissionUnavailablePayload({
|
|
error: {
|
|
code: "admission_promotion_unavailable",
|
|
layer: "admission",
|
|
retryable: true,
|
|
message,
|
|
userMessage: "AgentRun 已持久接纳本次请求,但本地可见性提交尚未完成;请使用同一 traceId 重试补提交。",
|
|
admissionState: "promotion-pending",
|
|
route: "/v1/agent/chat"
|
|
},
|
|
blocker: {
|
|
code: "admission_promotion_unavailable",
|
|
layer: "admission",
|
|
retryable: true,
|
|
summary: message
|
|
},
|
|
admission: {
|
|
state: "promotion-pending",
|
|
runId: payload.agentRun?.runId ?? null,
|
|
commandId: payload.agentRun?.commandId ?? null,
|
|
dispatchIntentId: payload.agentRun?.dispatchIntentId ?? null,
|
|
durableDispatch: payload.agentRun?.durableDispatch === true,
|
|
valuesPrinted: false
|
|
}
|
|
}, { params, options, traceId });
|
|
responsePayload.admission = {
|
|
state: "promotion-pending",
|
|
runId: payload.agentRun?.runId ?? null,
|
|
commandId: payload.agentRun?.commandId ?? null,
|
|
dispatchIntentId: payload.agentRun?.dispatchIntentId ?? null,
|
|
runnerJobId: payload.agentRun?.runnerJobId ?? null,
|
|
durableDispatch: payload.agentRun?.durableDispatch === true,
|
|
valuesRedacted: true
|
|
};
|
|
return Object.assign(new Error(message), {
|
|
code: "admission_promotion_unavailable",
|
|
statusCode: 503,
|
|
payload: responsePayload,
|
|
alreadyClassified: true
|
|
});
|
|
}
|
|
|
|
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 failure = classifyCodeAgentDispatchFailure(error);
|
|
const failureFields = {
|
|
failureKind: failure.failureKind,
|
|
reason: failure.reason,
|
|
retryable: failure.retryable,
|
|
userMessage: failure.userMessage,
|
|
recoveryAction: failure.recoveryAction,
|
|
dispatchStage: failure.dispatchStage,
|
|
upstreamTraceId: failure.upstreamTraceId,
|
|
runId: failure.runId,
|
|
commandId: failure.commandId,
|
|
dispatchIntentId: failure.dispatchIntentId,
|
|
admissionState: "rejected-before-durable-dispatch",
|
|
valuesPrinted: false
|
|
};
|
|
return {
|
|
statusCode: 503,
|
|
payload: {
|
|
error: {
|
|
...failureFields,
|
|
code: failure.failureKind,
|
|
layer: "agentrun",
|
|
message: failure.message,
|
|
route: "/v1/agent/chat"
|
|
},
|
|
blocker: {
|
|
...failureFields,
|
|
code: failure.failureKind,
|
|
layer: "agentrun",
|
|
summary: failure.message
|
|
},
|
|
admission: {
|
|
state: "rejected-before-durable-dispatch",
|
|
runId: failure.runId,
|
|
commandId: failure.commandId,
|
|
dispatchIntentId: failure.dispatchIntentId,
|
|
durableDispatch: false,
|
|
valuesPrinted: false
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|