Files
pikasTech-HWLAB/internal/cloud/code-agent-chat.mjs
T
2026-05-28 09:48:06 +08:00

1643 lines
61 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
DEV_CODE_AGENT_PROVIDER_CONTRACT,
codeAgentSecretRefPlaceholder,
inspectCodeAgentProviderEnv
} from "./code-agent-contract.mjs";
import {
CODEX_STDIO_BACKEND,
CODEX_STDIO_CAPABILITY_LEVEL,
CODEX_STDIO_IMPLEMENTATION_TYPE,
CODEX_STDIO_PROVIDER,
CODEX_STDIO_RUNNER_KIND,
CODEX_STDIO_SESSION_MODE,
createCodexStdioSessionManager,
longLivedSessionGate as codexLongLivedSessionGate
} from "./codex-stdio-session.mjs";
import {
createCodeAgentTraceRecorder,
defaultCodeAgentTraceStore
} from "./code-agent-trace-store.mjs";
import {
DEFAULT_CODE_AGENT_SESSION_IDLE_TIMEOUT_MS,
createCodeAgentSessionRegistry
} from "./code-agent-session-registry.mjs";
import {
codeAgentSessionLifecycleSummary,
decorateCodeAgentSession
} from "./code-agent-session-lifecycle.mjs";
const DEFAULT_MODEL = DEV_CODE_AGENT_PROVIDER_CONTRACT.model;
const DEFAULT_PROJECT_ID = "prj_hwlab-cloud-workbench";
const READONLY_RUNNER_PROVIDER = "codex-readonly-runner";
const READONLY_RUNNER_BACKEND = "hwlab-cloud-api/codex-readonly-runner";
const READONLY_RUNNER_MODEL = "read-only-tools";
const READONLY_RUNNER_KIND = "hwlab-readonly-runner";
const READONLY_RUNNER_SANDBOX = "read-only";
const READONLY_SESSION_MODE = "controlled-readonly-session-registry";
const READONLY_IMPLEMENTATION_TYPE = "controlled-readonly-session-registry";
const READONLY_SESSION_CAPABILITY_LEVEL = "read-only-session-tools";
const HARDWARE_INVOKE_SHELL_METHOD = "hardware.invoke.shell";
const LEGACY_CODE_AGENT_HARDWARE_PROVIDER = "hwlab-hardware-capability";
const LEGACY_CODE_AGENT_HARDWARE_CAPABILITY_BLOCKED = "blocked";
const OPENAI_FALLBACK_RUNNER_KIND = "openai-responses-fallback";
const READONLY_TOOL_OUTPUT_LIMIT = 4000;
const MAX_READONLY_SESSIONS = 200;
const CODE_AGENT_PROVIDER_SECRET_REF = codeAgentSecretRefPlaceholder().replace("secretRef:", "");
const CODEX_STDIO_SOURCE_ISSUE = "pikasTech/HWLAB#275";
const READONLY_LIMITATION_FLAGS = Object.freeze([
"not-codex-stdio",
"not-write-capable",
"process-local-session-registry"
]);
const CODEX_STDIO_LIMITATION_FLAGS = Object.freeze([
"secret-values-redacted"
]);
const CODE_AGENT_SYSTEM_PROMPT = [
"你是 HWLAB 云工作台的 Code Agent。",
"请用中文直接回答用户的工作台问题。",
"当前最小版本可以帮助用户整理任务、查看云工作台资源和说明下一步;不要声称已经执行硬件变更。",
"不要声称已通过 M3、M4 或 M5 验收。",
"不要声称 OpenAI Responses 文本 fallback 具备 Codex runner、workspace、tools 或 skills 能力。",
"不要输出 secret、token、kubeconfig 或环境变量原文。"
].join("\n");
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const defaultCodeAgentSessionRegistry = createCodeAgentSessionRegistry({
idleTimeoutMs: DEFAULT_CODE_AGENT_SESSION_IDLE_TIMEOUT_MS,
maxSessions: MAX_READONLY_SESSIONS
});
const defaultCodexStdioSessionManager = createCodexStdioSessionManager();
export async function handleCodeAgentChat(params = {}, options = {}) {
const timestamp = nowIso(options.now);
const { conversationId, sessionId, requestedSessionId } = resolveConversationSessionIds(params);
const messageId = `msg_${randomUUID()}`;
const traceId = cleanProtocolId(params.traceId, "trc") || `trc_${randomUUID()}`;
const providerPlan = resolveProviderPlan(options.env ?? process.env, options);
const sessionRegistry = resolveCodeAgentSessionRegistry(options);
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
const traceRecorder = createCodeAgentTraceRecorder({
traceStore,
traceId,
now: options.now,
runnerKind: CODEX_STDIO_RUNNER_KIND,
workspace: options.workspace,
sandbox: "workspace-write",
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE
});
const base = {
conversationId,
sessionId,
messageId,
status: "running",
createdAt: timestamp,
updatedAt: timestamp,
traceId,
provider: providerPlan.provider,
model: providerPlan.model,
backend: providerPlan.backend,
projectId: cleanProjectId(params.projectId) || DEFAULT_PROJECT_ID
};
try {
const message = normalizeUserMessage(params.message);
const runnerIntent = {
kind: "none",
toolName: "codex-stdio.session"
};
traceRecorder.append({
type: "request",
status: "accepted",
label: "request:accepted",
promptSummary: message.length > 160 ? `${message.slice(0, 157)}...` : message,
waitingFor: "codex-stdio-readiness"
});
const codexStdioAvailability = await inspectCodexStdioFeasibility(options.env ?? process.env, options);
if (codexStdioLongLivedReady(codexStdioAvailability)) {
const stdioResult = await callCodexStdioRunner({
message,
conversationId,
sessionId: requestedSessionId,
traceId,
env: options.env ?? process.env,
now: options.now,
workspace: options.workspace,
timeoutMs: options.timeoutMs,
hardTimeoutMs: options.hardTimeoutMs,
model: providerPlan.model,
codexStdioManager: options.codexStdioManager,
traceRecorder,
conversationFacts: conversationFactsForPrompt(sessionRegistry, conversationId),
externalNetworkIntent: null
});
return completedRunnerPayload({ base, runnerResult: stdioResult, messageId, now: options.now, sessionRegistry });
}
const primaryBlocker = codexStdioAvailability.blockers?.[0] ?? null;
traceRecorder.append({
type: "error",
status: "blocked",
label: "codex-stdio:blocked",
errorCode: primaryBlocker?.code ?? "codex_stdio_blocked",
message: primaryBlocker?.summary ?? "Codex stdio is not available; no local fallback will be used.",
waitingFor: "codex-stdio-readiness",
terminal: true
});
throw runnerError(primaryBlocker?.code ?? "codex_stdio_blocked", "Codex stdio long-lived session is unavailable; no controlled-readonly, skills, shell, or text fallback will answer this request.", {
provider: CODEX_STDIO_PROVIDER,
model: providerPlan.model,
backend: CODEX_STDIO_BACKEND,
workspace: codexStdioAvailability.workspace ?? options.workspace ?? repoRoot,
sandbox: codexStdioAvailability.sandbox ?? "workspace-write",
session: null,
toolCalls: [],
skills: notRequestedSkills(),
runner: codexStdioBlockedRunnerDescriptor({ workspace: codexStdioAvailability.workspace ?? options.workspace, session: null }),
runnerTrace: traceRecorder.runnerTrace({
runnerKind: CODEX_STDIO_RUNNER_KIND,
workspace: codexStdioAvailability.workspace ?? options.workspace ?? repoRoot,
sandbox: codexStdioAvailability.sandbox ?? "workspace-write",
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
limitations: ["no-fallback", "codex-stdio-required"],
outputTruncated: false
}),
capabilityLevel: "blocked",
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionReuse: null,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
runnerLimitations: ["codex-stdio-required", "no-controlled-readonly-fallback", "no-text-fallback"],
codexStdioFeasibility: codexStdioAvailability,
longLivedSessionGate: longLivedSessionGate({
provider: CODEX_STDIO_PROVIDER,
runnerKind: CODEX_STDIO_RUNNER_KIND,
session: null,
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdioFeasibility: codexStdioAvailability
}),
blockers: codexStdioAvailability.blockers?.length
? codexStdioAvailability.blockers
: [{
code: "codex_stdio_blocked",
sourceIssue: "pikasTech/HWLAB#275",
summary: "Codex stdio readiness did not pass."
}],
route: "/v1/agent/chat",
toolName: runnerIntent.toolName ?? "codex-stdio.session",
availability: await describeCodeAgentAvailability(options.env ?? process.env, options)
});
} catch (error) {
const failedAt = nowIso(options.now);
const payload = {
...base,
status: chatStatusForError(error),
updatedAt: failedAt,
error: normalizeChatError(error, {
traceId,
provider: error.provider ?? base.provider,
model: error.model ?? base.model,
backend: error.backend ?? base.backend,
runner: error.runner,
capabilityLevel: error.capabilityLevel ?? "blocked",
route: error.route,
toolName: error.toolName
})
};
if (payload.error?.blocker) payload.blocker = payload.error.blocker;
if (payload.error?.blockers) payload.blockers = payload.error.blockers;
if (error.provider) payload.provider = error.provider;
if (error.model) payload.model = error.model;
if (error.backend) payload.backend = error.backend;
if (error.workspace !== undefined) payload.workspace = error.workspace;
if (error.sandbox !== undefined) payload.sandbox = error.sandbox;
if (error.session !== undefined) payload.session = error.session;
if (error.toolCalls !== undefined) payload.toolCalls = error.toolCalls;
if (error.skills !== undefined) payload.skills = error.skills;
if (error.runner !== undefined) payload.runner = error.runner;
if (error.runnerTrace !== undefined) payload.runnerTrace = error.runnerTrace;
if (error.providerTrace !== undefined) payload.providerTrace = error.providerTrace;
if (payload.runnerTrace === undefined) {
payload.runnerTrace = traceRecorder.runnerTrace({
runnerKind: CODEX_STDIO_RUNNER_KIND,
workspace: error.workspace ?? options.workspace ?? repoRoot,
sandbox: error.sandbox ?? "workspace-write",
sessionMode: error.sessionMode ?? CODEX_STDIO_SESSION_MODE,
implementationType: error.implementationType ?? CODEX_STDIO_IMPLEMENTATION_TYPE,
limitations: error.runnerLimitations ?? ["codex-stdio-required"]
});
}
if (error.capabilityLevel !== undefined) payload.capabilityLevel = error.capabilityLevel;
if (error.sessionMode !== undefined) payload.sessionMode = error.sessionMode;
if (error.sessionReuse !== undefined) payload.sessionReuse = error.sessionReuse;
if (error.implementationType !== undefined) payload.implementationType = error.implementationType;
if (error.runnerLimitations !== undefined) payload.runnerLimitations = error.runnerLimitations;
if (error.codexStdioFeasibility !== undefined) payload.codexStdioFeasibility = error.codexStdioFeasibility;
if (error.longLivedSessionGate !== undefined) payload.longLivedSessionGate = error.longLivedSessionGate;
if (error.conversationFacts !== undefined) {
payload.conversationFacts = error.conversationFacts;
} else if (typeof sessionRegistry.getConversationFacts === "function") {
payload.conversationFacts = sessionRegistry.getConversationFacts(conversationId);
}
if (payload.conversationFacts && payload.error?.blocker) payload.error.blocker.conversationFacts = payload.conversationFacts;
if (payload.conversationFacts && payload.blocker) payload.blocker.conversationFacts = payload.conversationFacts;
if (error.availability !== undefined) {
payload.availability = error.availability;
} else if (["provider_unavailable", "provider_timeout", "codex_cli_binary_missing"].includes(error.code)) {
payload.availability = await describeCodeAgentAvailability(options.env ?? process.env, options);
} else if (["runner_unavailable", "tool_unavailable", "skills_unavailable", "codex_stdio_blocked", "codex_stdio_failed", "codex_stdio_protocol_blocked", "codex_stdio_empty_response", "codex_stdio_command_probe_failed"].includes(error.code)) {
payload.availability = await describeCodeAgentAvailability(options.env ?? process.env, options);
}
return decorateChatSessionLifecycle(payload);
}
}
function completedRunnerPayload({ base, runnerResult, messageId, now, sessionRegistry }) {
const completedAt = nowIso(now);
const blocker = structuredCompletionBlocker(runnerResult, {
traceId: base.traceId,
provider: runnerResult.provider,
backend: runnerResult.backend,
runner: runnerResult.runner,
capabilityLevel: runnerResult.capabilityLevel
});
const payload = finalizeCodeAgentChatPayload(decorateChatSessionLifecycle({
...base,
sessionId: runnerResult.session?.sessionId ?? base.sessionId,
status: "completed",
updatedAt: completedAt,
provider: runnerResult.provider,
model: runnerResult.model,
backend: runnerResult.backend,
workspace: runnerResult.workspace,
sandbox: runnerResult.sandbox,
session: runnerResult.session,
sessionMode: runnerResult.sessionMode,
sessionReuse: runnerResult.sessionReuse,
implementationType: runnerResult.implementationType,
runnerLimitations: runnerResult.runnerLimitations,
codexStdioFeasibility: runnerResult.codexStdioFeasibility,
longLivedSessionGate: runnerResult.longLivedSessionGate,
toolCalls: runnerResult.toolCalls,
skills: runnerResult.skills,
runner: runnerResult.runner,
runnerTrace: runnerResult.runnerTrace,
capabilityLevel: runnerResult.capabilityLevel,
...(runnerResult.responseType ? { responseType: runnerResult.responseType } : {}),
...(runnerResult.m3Io ? { m3Io: runnerResult.m3Io } : {}),
...(runnerResult.hardware ? { hardware: runnerResult.hardware } : {}),
...(runnerResult.operationId !== undefined ? { operationId: runnerResult.operationId } : {}),
...(runnerResult.dispatchStatus !== undefined ? { dispatchStatus: runnerResult.dispatchStatus } : {}),
...(runnerResult.exitCode !== undefined ? { exitCode: runnerResult.exitCode } : {}),
...(runnerResult.stdoutSummary !== undefined ? { stdoutSummary: runnerResult.stdoutSummary } : {}),
...(runnerResult.stderrSummary !== undefined ? { stderrSummary: runnerResult.stderrSummary } : {}),
...(runnerResult.route !== undefined ? { route: runnerResult.route } : {}),
...(runnerResult.toolName !== undefined ? { toolName: runnerResult.toolName } : {}),
reply: {
messageId,
role: "assistant",
content: runnerResult.content,
createdAt: completedAt
},
...(blocker ? { blocker, blockers: [blocker, ...(runnerResult.blockers ?? [])].filter(Boolean) } : {}),
usage: null,
providerTrace: runnerResult.providerTrace
}));
return decorateChatSessionLifecycle(attachConversationFacts(payload, sessionRegistry, { now }));
}
export function validateCodeAgentChatSchema(payload) {
const required = [
"conversationId",
"sessionId",
"messageId",
"status",
"createdAt",
"updatedAt",
"traceId",
"provider",
"model",
"backend"
];
for (const field of required) {
if (!payload || typeof payload !== "object" || !Object.hasOwn(payload, field)) {
throw new Error(`code agent chat response missing ${field}`);
}
}
if (!["running", "completed", "failed", "timeout", "error", "canceled"].includes(payload.status)) {
throw new Error(`code agent chat response has invalid status ${JSON.stringify(payload.status)}`);
}
if (Number.isNaN(Date.parse(payload.createdAt)) || Number.isNaN(Date.parse(payload.updatedAt))) {
throw new Error("code agent chat response timestamps must be RFC 3339 strings");
}
if (["failed", "timeout", "error", "canceled"].includes(payload.status) && typeof payload.error?.message !== "string") {
throw new Error("non-completed code agent chat response must include error.message");
}
if (payload.status === "completed" && (typeof payload.reply?.content !== "string" || !payload.reply.content.trim())) {
throw new Error("completed code agent chat response must include non-empty reply.content");
}
validateCodeAgentToolCallContract(payload);
}
function validateCodeAgentToolCallContract(payload) {
if (!Array.isArray(payload.toolCalls)) return;
for (const [index, toolCall] of payload.toolCalls.entries()) {
if (!toolCall || typeof toolCall !== "object") {
throw new Error(`code agent toolCalls[${index}] must be an object`);
}
}
}
function finalizeCodeAgentChatPayload(payload) {
validateCodeAgentChatSchema(payload);
return payload;
}
function attachConversationFacts(payload, sessionRegistry, { now } = {}) {
if (!sessionRegistry || typeof sessionRegistry.recordFact !== "function") return payload;
const conversationFacts = sessionRegistry.recordFact(payload.conversationId, {
kind: payload.toolCalls?.some((toolCall) => toolCall?.name === "session.context") ? "session_context" : "runner_turn",
conversationId: payload.conversationId,
sessionId: payload.sessionId,
traceId: payload.traceId,
provider: payload.provider,
backend: payload.backend,
workspace: payload.workspace,
sandbox: payload.sandbox,
session: payload.session,
sessionMode: payload.sessionMode,
sessionReuse: payload.sessionReuse,
implementationType: payload.implementationType,
runner: payload.runner,
runnerTrace: payload.runnerTrace,
capabilityLevel: payload.capabilityLevel,
toolCalls: payload.toolCalls,
skills: payload.skills
}, { now });
return {
...payload,
conversationFacts
};
}
function decorateChatSessionLifecycle(payload) {
if (!payload || typeof payload !== "object") return payload;
const decoratedSession = payload.session && typeof payload.session === "object"
? decorateCodeAgentSession(payload.session, {
reused: payload.sessionReuse?.reused ?? payload.session?.reused,
runnerTrace: payload.runnerTrace,
sessionReuse: payload.sessionReuse,
status: payload.status,
provider: payload.provider,
runner: payload.runner,
capabilityLevel: payload.capabilityLevel,
implementationType: payload.implementationType,
longLivedSessionGate: payload.longLivedSessionGate,
error: payload.error,
blocker: payload.blocker
})
: payload.session;
const runnerTrace = payload.runnerTrace && typeof payload.runnerTrace === "object"
? {
...payload.runnerTrace,
sessionLifecycleStatus: payload.runnerTrace.sessionLifecycleStatus ??
codeAgentSessionLifecycleSummary({
session: decoratedSession,
sessionStatus: payload.runnerTrace.sessionStatus,
status: payload.status,
provider: payload.provider,
runner: payload.runner,
capabilityLevel: payload.capabilityLevel,
implementationType: payload.implementationType,
error: payload.error,
blocker: payload.blocker
}).status
}
: payload.runnerTrace;
const summary = codeAgentSessionLifecycleSummary({
...payload,
session: decoratedSession,
runnerTrace
});
return {
...payload,
session: decoratedSession,
runnerTrace,
sessionStatus: decoratedSession?.status ?? payload.sessionStatus ?? runnerTrace?.sessionStatus ?? null,
sessionLifecycleStatus: summary.status,
sessionLifecycle: summary,
sessionSummary: summary,
sessionReused: summary.reused,
isNewSession: summary.newSession,
fallbackUsed: summary.fallback,
degraded: summary.degraded,
degradationReason: summary.reason,
degradationReasonCode: summary.reasonCode
};
}
export async function describeCodeAgentAvailability(env = process.env, options = {}) {
const providerPlan = resolveProviderPlan(env, options);
const providerContract = inspectCodeAgentProviderEnv(env);
const missingEnv = providerPlan.mode === "openai"
? providerContract.missingEnv
: [];
if (providerPlan.mode === "codex-cli" && !env.OPENAI_API_KEY) {
missingEnv.push("OPENAI_API_KEY");
}
const sessionRegistry = resolveCodeAgentSessionRegistry(options);
const runnerAvailability = await inspectReadOnlyRunnerAvailability(env, {
...options,
sessionRegistry
});
const codexStdio = await inspectCodexStdioFeasibility(env, options);
const codexStdioReady = codexStdioLongLivedReady(codexStdio);
const codexStdioActive = providerPlan.mode !== "openai" && codexStdioReady;
const activeRunnerAvailability = codexStdioActive
? codexStdioRunnerAvailability(codexStdio)
: codexStdioBlockedRunnerAvailability(codexStdio);
const blocked = providerPlan.mode === "openai"
? !providerContract.ready
: providerPlan.mode === "codex-stdio"
? !codexStdioReady
: missingEnv.length > 0;
const fallbackAvailability = {
provider: providerPlan.provider,
model: providerPlan.model,
backend: providerPlan.backend,
mode: providerPlan.mode,
status: blocked ? "blocked" : "available",
ready: !blocked,
capabilityLevel: blocked ? "blocked" : "text-chat-only",
blocker: blocked ? fallbackBlockerForPlan({ providerPlan, providerContract, codexStdio }) : null,
reason: blocked ? "provider_unavailable" : null,
missingEnv,
secretRefs: blocked ? providerContract.secretRefs : [],
egress: providerContract.egress,
safety: providerContract.safety
};
const longLivedGate = longLivedSessionGate({
provider: codexStdioActive ? CODEX_STDIO_PROVIDER : runnerAvailability.provider,
runnerKind: codexStdioActive ? CODEX_STDIO_RUNNER_KIND : runnerAvailability.kind,
sessionMode: codexStdioActive ? CODEX_STDIO_SESSION_MODE : READONLY_SESSION_MODE,
implementationType: codexStdioActive ? CODEX_STDIO_IMPLEMENTATION_TYPE : READONLY_IMPLEMENTATION_TYPE,
codexStdioFeasibility: codexStdio
});
const agentKind = codexStdioActive ? "codex-stdio-feasible" : "codex-stdio-blocked";
const capabilityStatus = codexStdioActive ? "codex-stdio-feasible" : "blocked";
const codeAgentReady = codexStdioActive;
const status = codexStdioActive ? "codex-stdio-feasible" : "blocked";
const capabilityLevel = codexStdioActive ? "long-lived-codex-stdio-session" : "blocked";
return {
endpoint: "POST /v1/agent/chat",
provider: providerPlan.provider,
model: providerPlan.model,
backend: providerPlan.backend,
mode: providerPlan.mode,
schema: [
"conversationId",
"sessionId",
"messageId",
"status",
"createdAt",
"updatedAt",
"traceId",
"provider",
"model",
"backend",
"runner",
"workspace",
"sandbox",
"toolCalls",
"skills",
"runnerTrace",
"capabilityLevel",
"session",
"sessionMode",
"sessionReuse",
"implementationType",
"runnerLimitations",
"codexStdioFeasibility",
"longLivedSessionGate",
"error.message",
"error.code",
"error.missingEnv",
"availability.status"
],
runner: activeRunnerAvailability,
fallback: fallbackAvailability,
codexStdio,
status,
readinessStatus: status,
agentKind,
capabilityStatus,
blocker: codeAgentReady ? null : primaryCodeAgentBlocker({ blocked, providerContract, providerPlan, codexStdio, runnerAvailability }),
reason: codeAgentReady ? null : primaryCodeAgentReason({ blocked, codexStdio, runnerAvailability }),
summary: codeAgentAvailabilitySummary({ blocked, codexStdio, runnerAvailability }),
m3IoSkill: runnerAvailability.m3IoSkill,
missingEnv,
secretRefs: blocked ? providerContract.secretRefs : [],
egress: providerContract.egress,
safety: providerContract.safety,
capabilityLevel,
workspace: activeRunnerAvailability.workspace,
sandbox: activeRunnerAvailability.sandbox,
session: activeRunnerAvailability.sessionRegistry?.recentSessions?.[0] ?? null,
sessionStatus: activeRunnerAvailability.sessionRegistry?.recentSessions?.[0]?.status ?? null,
sessionRegistry: activeRunnerAvailability.sessionRegistry ?? runnerAvailability.sessionRegistry,
sessionMode: activeRunnerAvailability.sessionMode,
implementationType: activeRunnerAvailability.implementationType,
runnerLimitations: activeRunnerAvailability.runnerLimitations,
codexStdioFeasibility: codexStdio,
longLivedSessionGate: longLivedGate,
blockers: [
...codexStdio.blockers,
...(blocked && providerPlan.mode !== "codex-stdio" ? [{
code: "openai_fallback_blocked",
sourceIssue: "pikasTech/HWLAB#143",
summary: providerPlan.mode === "openai" ? providerContract.blocker : "Provider fallback credential/config is incomplete."
}] : [])
],
blockerCodes: [
...codexStdio.blockerCodes,
...(blocked && providerPlan.mode !== "codex-stdio" ? ["openai_fallback_blocked"] : [])
],
ready: codeAgentReady,
partialReady: false
};
}
function fallbackBlockerForPlan({ providerPlan, providerContract, codexStdio }) {
if (providerPlan.mode === "openai") return providerContract.blocker;
if (providerPlan.mode === "codex-stdio") {
return codexStdio.blockers?.[0]?.summary ?? "Codex stdio long-lived session is blocked.";
}
return "凭证缺口";
}
function codexStdioLongLivedReady(codexStdio = {}) {
return Boolean(
(codexStdio.ready === true || codexStdio.canStartLongLivedCodexStdio === true) &&
codexStdio.commandProbe?.ready === true
);
}
function codexStdioRunnerAvailability(codexStdio = {}) {
const workspace = codexStdio.workspace ?? repoRoot;
const sandbox = codexStdio.sandbox ?? "workspace-write";
return {
kind: CODEX_STDIO_RUNNER_KIND,
backend: CODEX_STDIO_BACKEND,
provider: CODEX_STDIO_PROVIDER,
workspace,
sandbox,
mode: CODEX_STDIO_SESSION_MODE,
sessionMode: CODEX_STDIO_SESSION_MODE,
session: CODEX_STDIO_SESSION_MODE,
status: "available",
ready: true,
capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
longLivedSession: true,
durableSession: true,
durable: true,
codexStdio: true,
writeCapable: true,
readOnly: false,
runnerLimitations: [...CODEX_STDIO_LIMITATION_FLAGS],
codexStdioFeasibility: codexStdio,
longLivedSessionGate: longLivedSessionGate({
provider: CODEX_STDIO_PROVIDER,
runnerKind: CODEX_STDIO_RUNNER_KIND,
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdioFeasibility: codexStdio
}),
sessionRegistry: codexStdioSessionRegistrySummary(codexStdio),
safety: {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
hardwareControlViaCloudApiOnly: false,
directGatewayCallsAllowed: true,
directBoxSimuCallsAllowed: true,
directPatchPanelCallsAllowed: true,
m3m4m5AcceptanceClaimsAllowed: false
}
};
}
function codexStdioBlockedRunnerAvailability(codexStdio = {}) {
const workspace = codexStdio.workspace ?? repoRoot;
const sandbox = codexStdio.sandbox ?? "workspace-write";
return {
kind: CODEX_STDIO_RUNNER_KIND,
backend: CODEX_STDIO_BACKEND,
provider: CODEX_STDIO_PROVIDER,
workspace,
sandbox,
mode: CODEX_STDIO_SESSION_MODE,
sessionMode: CODEX_STDIO_SESSION_MODE,
session: CODEX_STDIO_SESSION_MODE,
status: "blocked",
ready: false,
capabilityLevel: "blocked",
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
longLivedSession: false,
durableSession: false,
durable: false,
codexStdio: true,
writeCapable: false,
readOnly: false,
runnerLimitations: ["codex-stdio-required", "no-controlled-readonly-fallback", "no-text-fallback"],
codexStdioFeasibility: codexStdio,
longLivedSessionGate: longLivedSessionGate({
provider: CODEX_STDIO_PROVIDER,
runnerKind: CODEX_STDIO_RUNNER_KIND,
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdioFeasibility: codexStdio
}),
sessionRegistry: codexStdioSessionRegistrySummary(codexStdio),
safety: {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
hardwareControlViaCloudApiOnly: false,
directGatewayCallsAllowed: true,
directBoxSimuCallsAllowed: true,
directPatchPanelCallsAllowed: true,
m3m4m5AcceptanceClaimsAllowed: false
}
};
}
function codexStdioSessionRegistrySummary(codexStdio = {}) {
return {
kind: "codex-stdio-session-registry",
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
status: "available",
sessionCount: codexStdio.sessionLifecycle?.activeSessions ?? codexStdio.lifecycleSupervisor?.activeSessions ?? null,
maxSessions: codexStdio.sessionLifecycle?.maxSessions ?? codexStdio.lifecycleSupervisor?.maxSessions ?? null,
idleTimeoutMs: codexStdio.sessionLifecycle?.idleTimeoutMs ?? codexStdio.lifecycleSupervisor?.idleTimeoutMs ?? null,
statuses: ["creating", "ready", "busy", "idle", "timeout", "error", "canceled", "interrupted", "expired", "failed"],
statusCounts: codexStdio.statusCounts ?? null,
sessionMode: CODEX_STDIO_SESSION_MODE,
recentSessions: Array.isArray(codexStdio.recentSessions) ? codexStdio.recentSessions : [],
longLivedCodexStdio: true,
longLivedSession: true,
durable: true,
codexStdio: true,
secretMaterialStored: false,
valuesRedacted: true
};
}
function primaryCodeAgentBlocker({ blocked, providerContract, providerPlan, codexStdio, runnerAvailability }) {
if (codexStdio.blockers?.length > 0) return codexStdio.blockers[0].summary;
if (blocked && !runnerAvailability.ready) return providerPlan.mode === "openai" ? providerContract.blocker : "凭证缺口";
if (runnerAvailability.ready) return "Codex stdio is blocked; controlled read-only runner fallback is disabled for /v1/agent/chat.";
return null;
}
function primaryCodeAgentReason({ blocked, codexStdio, runnerAvailability }) {
if (codexStdio.blockers?.length > 0) return codexStdio.blockers[0].code;
if (blocked && !runnerAvailability.ready) return "provider_unavailable";
if (runnerAvailability.ready) return "codex_stdio_blocked_fallback_disabled";
return null;
}
function codeAgentAvailabilitySummary({ blocked, codexStdio, runnerAvailability }) {
if (codexStdio.ready) {
return "Codex stdio long-lived session adapter is feasible; /v1/agent/chat sends natural-language requests to the persistent Codex session with full workspace and tool access.";
}
if (runnerAvailability.ready) return "Codex stdio long-lived session is blocked; /v1/agent/chat will not use controlled-readonly-session-registry, local skills discovery, shell/file shortcuts, or text fallback.";
return blocked
? "OpenAI fallback 和 Codex stdio long-lived session 都 blocked;不返回伪完成。"
: "Only text-chat fallback is available; it does not satisfy the Codex stdio session gate.";
}
function resolveProviderPlan(env, options = {}) {
const provider = String(env.HWLAB_CODE_AGENT_PROVIDER || "auto").trim().toLowerCase();
const model = firstNonEmpty(
options.model,
env.HWLAB_CODE_AGENT_MODEL,
env.OPENAI_MODEL,
env.CODE_QUEUE_DEFAULT_MODEL,
DEFAULT_MODEL
);
if (provider === "openai") {
return {
provider: "openai-responses",
model,
backend: "hwlab-cloud-api/openai-responses",
mode: "openai"
};
}
if (provider === "codex-cli" || provider === "codex") {
return {
provider: "codex-cli",
model,
backend: "hwlab-cloud-api/codex-cli",
mode: "codex-cli"
};
}
if (provider === "codex-stdio") {
return {
provider: CODEX_STDIO_PROVIDER,
model,
backend: CODEX_STDIO_BACKEND,
mode: "codex-stdio"
};
}
return {
provider: env.OPENAI_API_KEY ? "openai-responses" : "codex-cli",
model,
backend: env.OPENAI_API_KEY ? "hwlab-cloud-api/openai-responses" : "hwlab-cloud-api/codex-cli",
mode: env.OPENAI_API_KEY ? "openai" : "codex-cli",
auto: true
};
}
async function inspectReadOnlyRunnerAvailability(env, options = {}) {
const workspace = resolveRunnerWorkspace(env, options);
const workspaceReady = Boolean(workspace && existsSync(workspace));
const skillsDirs = resolveSkillDirs(env, options);
const skillsDirsPresent = skillsDirs.filter((dir) => existsSync(dir));
const codexStdioFeasibility = await inspectCodexStdioFeasibility(env, options);
const sessionRegistry = resolveCodeAgentSessionRegistry(options).describe();
return {
kind: READONLY_RUNNER_KIND,
backend: READONLY_RUNNER_BACKEND,
provider: READONLY_RUNNER_PROVIDER,
workspace,
sandbox: READONLY_RUNNER_SANDBOX,
mode: READONLY_SESSION_MODE,
sessionMode: READONLY_SESSION_MODE,
session: READONLY_SESSION_MODE,
status: workspaceReady ? "available" : "blocked",
ready: workspaceReady,
capabilityLevel: workspaceReady ? READONLY_SESSION_CAPABILITY_LEVEL : "blocked",
implementationType: READONLY_IMPLEMENTATION_TYPE,
longLivedSession: true,
durableSession: false,
codexStdio: false,
writeCapable: false,
runnerLimitations: [...READONLY_LIMITATION_FLAGS],
codexStdioFeasibility,
longLivedSessionGate: longLivedSessionGate({
provider: READONLY_RUNNER_PROVIDER,
runnerKind: READONLY_RUNNER_KIND,
sessionMode: READONLY_SESSION_MODE,
implementationType: READONLY_IMPLEMENTATION_TYPE,
codexStdioFeasibility
}),
sessionRegistry,
skillsDirs,
skillsDirsPresent,
safety: runnerSafetyContract()
};
}
function structuredCompletionBlocker(result, context = {}) {
if (!result || typeof result !== "object") return null;
if (result.provider === OPENAI_FALLBACK_RUNNER_KIND || result.runner?.kind === OPENAI_FALLBACK_RUNNER_KIND || result.capabilityLevel === "text-chat-only") {
return structuredBlocker({
code: "text_chat_only_fallback",
layer: "provider",
message: "OpenAI Responses fallback is text chat only and cannot satisfy Code Agent runner/session/tool capability.",
userMessage: "当前仍是文本 fallback只能回答普通问题不能当作真实 Code Agent runner/session/tool 能力",
retryable: false,
traceId: context.traceId,
provider: result.provider ?? context.provider,
backend: result.backend ?? context.backend,
runner: result.runner ?? context.runner,
capabilityLevel: result.capabilityLevel ?? context.capabilityLevel,
blockers: result.longLivedSessionGate?.blockers
});
}
return null;
}
function notRequestedSkills() {
return {
status: "not_requested",
items: [],
count: 0,
blockers: []
};
}
async function callCodexStdioRunner({ message, conversationId, sessionId, traceId, env, now, workspace, timeoutMs, hardTimeoutMs, model, codexStdioManager, traceRecorder, conversationFacts, externalNetworkIntent = null }) {
const manager = resolveCodexStdioSessionManager({ codexStdioManager });
try {
return await manager.chat({
message,
conversationId,
sessionId,
traceId,
env,
now,
workspace,
timeoutMs,
hardTimeoutMs,
model,
traceRecorder,
conversationFacts,
externalNetworkIntent
});
} catch (error) {
const availability = error.availability ?? manager.describe({ env, workspace });
const session = error.session ?? null;
const trace = error.runnerTrace ?? {
traceId,
runnerKind: CODEX_STDIO_RUNNER_KIND,
workspace: workspace ?? availability.workspace ?? repoRoot,
sandbox: availability.sandbox ?? "workspace-write",
sessionMode: CODEX_STDIO_SESSION_MODE,
events: [`blocked:${error.code ?? "codex_stdio_failed"}`],
valuesPrinted: false
};
throw runnerError(error.code ?? "codex_stdio_failed", error.message, {
provider: CODEX_STDIO_PROVIDER,
model,
backend: CODEX_STDIO_BACKEND,
workspace: workspace ?? availability.workspace ?? null,
sandbox: availability.sandbox ?? "workspace-write",
session,
toolCalls: Array.isArray(error.toolCalls) ? error.toolCalls : [],
skills: error.skills ?? notRequestedSkills(),
runner: {
kind: CODEX_STDIO_RUNNER_KIND,
provider: CODEX_STDIO_PROVIDER,
backend: CODEX_STDIO_BACKEND,
workspace: workspace ?? availability.workspace ?? repoRoot,
sandbox: availability.sandbox ?? "workspace-write",
session: CODEX_STDIO_SESSION_MODE,
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionId: session?.sessionId ?? null,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdio: true,
longLivedSession: true,
durableSession: true,
writeCapable: true,
readOnly: false,
capabilityLevel: "blocked"
},
runnerTrace: trace,
capabilityLevel: "blocked",
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionReuse: session ? sessionReuseEvidence(session) : null,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
runnerLimitations: error.runnerLimitations ?? ["codex-stdio-blocked"],
codexStdioFeasibility: availability,
longLivedSessionGate: longLivedSessionGate({
provider: CODEX_STDIO_PROVIDER,
runnerKind: CODEX_STDIO_RUNNER_KIND,
session,
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdioFeasibility: availability
}),
blockers: error.blockers ?? availability.blockers,
route: error.route ?? null,
toolName: error.toolName ?? externalNetworkIntent?.toolName ?? (error.missingTools?.length ? "codex-stdio.required-tools" : "codex-stdio.session"),
conversationId,
targetUrl: error.targetUrl,
timeoutMs: error.timeoutMs,
hardTimeoutMs: error.hardTimeoutMs,
idleMs: error.idleMs,
lastActivityAt: error.lastActivityAt,
waitingFor: error.waitingFor,
providerTrace: error.providerTrace,
stage: error.stage,
missingTools: error.missingTools,
availability: await describeCodeAgentAvailability(env, { codexStdioManager: manager, workspace })
});
}
}
function sessionReuseEvidence(session) {
return {
conversationId: session.conversationId,
sessionId: session.sessionId,
mapped: true,
reused: session.reused,
turn: session.turn,
previousTurns: Math.max(0, session.turn - 1),
workspace: session.workspace,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
idleTimeoutMs: session.idleTimeoutMs,
expiresAt: session.expiresAt,
lastTraceId: session.lastTraceId,
status: session.status
};
}
function releaseReadOnlySession(registry, session, { now, traceId, conversationId } = {}) {
return registry.release(session.sessionId, {
now,
traceId,
conversationId,
reused: session.reused,
status: "idle"
}) ?? session;
}
function resolveCodeAgentSessionRegistry(options = {}) {
return options.sessionRegistry &&
typeof options.sessionRegistry.acquire === "function" &&
typeof options.sessionRegistry.release === "function" &&
typeof options.sessionRegistry.describe === "function"
? options.sessionRegistry
: defaultCodeAgentSessionRegistry;
}
function resolveCodexStdioSessionManager(options = {}) {
return options.codexStdioManager &&
typeof options.codexStdioManager.describe === "function" &&
typeof options.codexStdioManager.chat === "function" &&
typeof options.codexStdioManager.cancel === "function" &&
typeof options.codexStdioManager.reapIdle === "function"
? options.codexStdioManager
: defaultCodexStdioSessionManager;
}
function runnerSafetyContract() {
return {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
hardwareWritesAllowed: true,
directGatewayCallsAllowed: true,
directBoxSimuCallsAllowed: true,
directPatchPanelCallsAllowed: true,
openAiHardwareFallbackAllowed: false,
m3m4m5AcceptanceClaimsAllowed: false,
outputLimitBytes: READONLY_TOOL_OUTPUT_LIMIT
};
}
async function inspectCodexStdioFeasibility(env = process.env, options = {}) {
const manager = resolveCodexStdioSessionManager(options);
const descriptor = {
env,
workspace: options.workspace,
command: options.command,
sandbox: options.sandbox
};
const feasibility = typeof manager.probe === "function"
? await manager.probe(descriptor)
: manager.describe(descriptor);
return {
...feasibility,
checked: true,
implementationRequired: "codex-stdio-or-equivalent-long-lived-runner",
currentImplementation: feasibility.ready ? CODEX_STDIO_IMPLEMENTATION_TYPE : READONLY_IMPLEMENTATION_TYPE,
sourceIssue: CODEX_STDIO_SOURCE_ISSUE
};
}
function longLivedSessionGate({
provider,
runnerKind,
session,
sessionMode,
implementationType,
codexStdioFeasibility
} = {}) {
const normalizedProvider = String(provider ?? "").trim();
const normalizedRunnerKind = String(runnerKind ?? "").trim();
const normalizedSessionMode = String(sessionMode ?? "").trim();
const normalizedImplementation = String(implementationType ?? "").trim();
return codexLongLivedSessionGate({
provider: normalizedProvider,
runnerKind: normalizedRunnerKind,
session,
sessionMode: normalizedSessionMode,
implementationType: normalizedImplementation,
codexStdioFeasibility
});
}
function codexStdioBlockedRunnerDescriptor({ workspace, session = null } = {}) {
return {
kind: CODEX_STDIO_RUNNER_KIND,
provider: CODEX_STDIO_PROVIDER,
backend: CODEX_STDIO_BACKEND,
workspace: workspace ?? repoRoot,
sandbox: "workspace-write",
session: CODEX_STDIO_SESSION_MODE,
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionId: session?.sessionId ?? null,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdio: true,
longLivedSession: false,
durableSession: false,
writeCapable: false,
readOnly: false,
capabilityLevel: "blocked",
toolPolicy: {
allowed: ["codex-app-server.thread/start", "codex-app-server.thread/resume", "codex-app-server.turn/start"],
blocked: ["controlled-readonly-session-registry", "skills.discover-local-fallback", "pwd-local-fallback", "ls-local-fallback", "rg-local-fallback", "cat-local-fallback", "openai-text-fallback"]
},
runnerLimitations: ["codex-stdio-required", "no-controlled-readonly-fallback", "no-text-fallback"],
safety: runnerSafetyContract()
};
}
function runnerError(code, message, details = {}) {
const error = new Error(message);
error.code = code;
Object.assign(error, {
provider: READONLY_RUNNER_PROVIDER,
model: READONLY_RUNNER_MODEL,
backend: READONLY_RUNNER_BACKEND,
sandbox: READONLY_RUNNER_SANDBOX,
...details
});
return error;
}
function boundToolOutput(value, maxLength = READONLY_TOOL_OUTPUT_LIMIT) {
const text = String(value ?? "");
if (text.length <= maxLength) return { text, truncated: false };
return {
text: `${text.slice(0, maxLength)}\n[output truncated at ${maxLength} bytes]`,
truncated: true
};
}
function normalizeUserMessage(value) {
if (typeof value !== "string") {
throw badRequest("message must be a string");
}
const message = value.trim();
if (!message) {
throw badRequest("message is required");
}
if (message.length > 4000) {
throw badRequest("message exceeds 4000 characters");
}
return message;
}
function chatStatusForError(error) {
const code = String(error?.code ?? "");
if (code === "codex_stdio_canceled" || code === "session_canceled") return "canceled";
if (code === "codex_stdio_timeout" || code === "provider_timeout" || code === "session_timeout") return "timeout";
if (code === "session_error") return "error";
if (/timeout|timed out|超时/iu.test(String(error?.message ?? ""))) return "timeout";
return "failed";
}
function conversationFactsForPrompt(sessionRegistry, conversationId) {
if (!sessionRegistry || typeof sessionRegistry.getConversationFacts !== "function") return null;
try {
return sessionRegistry.getConversationFacts(conversationId);
} catch {
return null;
}
}
export function createCodeAgentErrorPayload({
code = "code_agent_failed",
message = "Code Agent request failed",
reason,
traceId = "trc_unassigned",
conversationId = "cnv_unassigned",
sessionId = "cnv_unassigned",
messageId = "msg_unassigned",
provider = "unassigned",
model = "unknown",
backend = "hwlab-cloud-api/code-agent-chat",
layer,
retryable,
capabilityLevel = "blocked",
route = "/v1/agent/chat",
toolName = null,
now
} = {}) {
const timestamp = nowIso(now);
const error = new Error(message);
Object.assign(error, { code, reason, layer, retryable, provider, model, backend, capabilityLevel, route, toolName });
const normalized = normalizeChatError(error, { traceId, provider, model, backend, capabilityLevel, route, toolName });
return decorateChatSessionLifecycle({
conversationId,
sessionId,
messageId,
status: "failed",
createdAt: timestamp,
updatedAt: timestamp,
traceId,
provider,
model,
backend,
capabilityLevel,
error: normalized,
blocker: normalized.blocker,
blockers: normalized.blockers
});
}
function normalizeChatError(error, context = {}) {
const code = String(error.code || "code_agent_failed");
const taxonomy = errorTaxonomy(code, error);
const provider = error.provider ?? context.provider ?? READONLY_RUNNER_PROVIDER;
const model = error.model ?? context.model ?? READONLY_RUNNER_MODEL;
const backend = error.backend ?? context.backend ?? READONLY_RUNNER_BACKEND;
const runnerKind = error.runner?.kind ?? error.runnerKind ?? context.runner?.kind ?? null;
const traceId = error.traceId ?? context.traceId ?? null;
const capabilityLevel = error.capabilityLevel ?? context.capabilityLevel ?? "blocked";
const route = error.route ?? context.route ?? routeForError(code, error);
const toolName = error.toolName ?? context.toolName ?? toolNameForError(code, error);
const message = redactText(error.message || taxonomy.message || "Code Agent request failed");
const lastEvent = error.runnerTrace?.lastEvent ?? context.runnerTrace?.lastEvent ?? error.blocker?.lastEvent ?? null;
const elapsedMs = error.runnerTrace?.elapsedMs ?? context.runnerTrace?.elapsedMs ?? error.blocker?.elapsedMs ?? null;
const sessionId = error.session?.sessionId ?? error.sessionId ?? error.blocker?.sessionId ?? context.sessionId ?? null;
const conversationId = error.conversationId ?? error.blocker?.conversationId ?? error.session?.conversationId ?? context.conversationId ?? null;
const stage = error.stage ?? error.blocker?.stage ?? lastEvent?.stage ?? taxonomy.layer;
const blockers = normalizeBlockers(error.blockers, {
fallbackCode: code,
layer: taxonomy.layer,
message,
userMessage: taxonomy.userMessage,
retryable: taxonomy.retryable
});
const timeoutMs = numberErrorField(error.timeoutMs ?? context.timeoutMs);
const hardTimeoutMs = numberErrorField(error.hardTimeoutMs ?? context.hardTimeoutMs);
const idleMs = numberErrorField(error.idleMs ?? context.idleMs);
const lastActivityAt = safeIsoLike(error.lastActivityAt ?? context.lastActivityAt);
const waitingFor = redactText(error.waitingFor ?? context.waitingFor ?? "");
const blocker = structuredBlocker({
code,
layer: taxonomy.layer,
message,
userMessage: error.userMessage ?? taxonomy.userMessage,
retryable: error.retryable ?? taxonomy.retryable,
traceId,
provider,
backend,
runner: error.runner ?? context.runner,
capabilityLevel,
route,
toolName,
category: taxonomy.category,
blockers,
sessionId,
conversationId,
stage,
lastEvent,
elapsedMs,
timeoutMs,
hardTimeoutMs,
idleMs,
lastActivityAt,
waitingFor
});
const normalized = {
code,
layer: taxonomy.layer,
category: taxonomy.category,
blocker,
retryable: blocker.retryable,
userMessage: blocker.userMessage,
message,
traceId,
provider,
model,
backend,
runner: runnerKind,
runnerKind,
capabilityLevel,
route,
toolName,
timeoutMs,
hardTimeoutMs,
idleMs,
lastActivityAt,
waitingFor,
missingConfig: safeMissingConfig(error),
blockers
};
for (const key of [
"missingEnv",
"missingCommands",
"command",
"exitCode",
"providerStatus",
"stderrSummary",
"missingTools",
"nextEvidence",
"reason"
]) {
if (error[key] !== undefined) {
normalized[key] = sanitizeErrorField(key, error[key]);
}
}
return normalized;
}
function errorTaxonomy(code, error = {}) {
const catalog = {
invalid_params: {
layer: "api",
category: "api_error",
retryable: true,
userMessage: "请求参数不完整或格式不正确请修正后重试"
},
parse_error: {
layer: "api",
category: "api_error",
retryable: true,
userMessage: "请求 JSON 无法解析请修正后重试"
},
provider_unavailable: {
layer: error.missingEnv?.length || error.missingCommands?.length ? "provider-config" : "provider",
category: error.missingEnv?.length || error.missingCommands?.length ? "needs_config" : "provider",
retryable: !(error.missingEnv?.length || error.missingCommands?.length),
userMessage: error.missingEnv?.length || error.missingCommands?.length
? "Code Agent provider 配置缺失需要维护者补齐后才能使用"
: "Code Agent provider 暂不可用可稍后重试"
},
provider_timeout: {
layer: "provider",
category: "timeout",
retryable: true,
userMessage: "Code Agent provider 响应超时输入已保留可稍后重试"
},
codex_cli_binary_missing: {
layer: "runner",
category: "needs_config",
retryable: false,
userMessage: "Codex CLI binary 未在运行时 PATH 中找到当前不会安装临时二进制也不会冒充真实 Codex"
},
session_busy: {
layer: "session",
category: "runner_busy",
retryable: true,
userMessage: "Code Agent session 正在处理上一轮请求请稍后重试"
},
session_expired: {
layer: "session",
category: "session_blocked",
retryable: true,
userMessage: "Code Agent session 已过期可重新发送建立新的 session"
},
session_reuse_conflict: {
layer: "session",
category: "session_blocked",
retryable: true,
userMessage: "当前 conversation session 绑定不一致可新建会话后重试"
},
session_failed: {
layer: "session",
category: "session_blocked",
retryable: true,
userMessage: "Code Agent session 已失败可重新发送建立新的 session"
},
session_interrupted: {
layer: "session",
category: "session_blocked",
retryable: true,
userMessage: "Code Agent session 已中断可重新发送建立新的 session"
},
session_canceled: {
layer: "session",
category: "session_blocked",
retryable: true,
userMessage: "Code Agent session 已取消输入和 trace 已保留可重试上一条消息"
},
session_timeout: {
layer: "session",
category: "timeout",
retryable: true,
userMessage: "Code Agent session 已超时输入和 trace 已保留可重试上一条消息"
},
session_error: {
layer: "session",
category: "session_blocked",
retryable: true,
userMessage: "Code Agent session 返回错误输入和 trace 已保留可重试上一条消息"
},
runner_unavailable: {
layer: "runner",
category: "runner_blocked",
retryable: true,
userMessage: "Code Agent runner 暂不可用可稍后重试或等待工作区挂载恢复"
},
tool_unavailable: {
layer: "tool",
category: "capability_unavailable",
retryable: false,
userMessage: "当前 Code Agent 未开放该工具能力"
},
skills_unavailable: {
layer: "skill-cli",
category: "needs_config",
retryable: false,
userMessage: "Code Agent Skill 清单未挂载或不可读需要补齐运行时配置"
},
text_chat_only_fallback: {
layer: "provider",
category: "fallback",
retryable: false,
userMessage: "当前仍是文本 fallback不具备 runner/session/tool/HWLAB API 控制能力"
},
codex_stdio_blocked: {
layer: "runner",
category: "capability_unavailable",
retryable: false,
userMessage: "Codex stdio 长会话能力仍受阻不能作为完整 Code Agent"
},
codex_stdio_protocol_blocked: {
layer: "runner",
category: "capability_unavailable",
retryable: false,
userMessage: "Codex stdio 协议工具不完整不能作为完整 Code Agent"
},
codex_stdio_empty_response: {
layer: "runner",
category: "runner_blocked",
retryable: true,
userMessage: "Codex stdio 未返回有效回复可稍后重试"
},
codex_stdio_timeout: {
layer: "runner",
category: "timeout",
retryable: true,
userMessage: "Codex stdio runner 响应超时输入sessionId traceId 已保留可重试"
},
codex_stdio_idle_timeout: {
layer: "runner",
category: "timeout",
retryable: true,
userMessage: "Codex stdio runner 超过配置时间无新事件输入sessionId traceId 已保留可重试"
},
codex_stdio_hard_timeout: {
layer: "runner",
category: "timeout",
retryable: true,
userMessage: "Codex stdio runner 达到硬性总时长上限输入sessionId traceId 已保留可重试"
},
codex_stdio_canceled: {
layer: "runner",
category: "canceled",
retryable: true,
userMessage: "本次 Codex stdio 请求已取消输入sessionId traceId 已保留可重试上一条消息"
},
codex_stdio_command_probe_failed: {
layer: "runner",
category: "runner_blocked",
retryable: true,
userMessage: "Codex stdio 基础命令探针失败readiness 不会标为完整 ready输入已保留可稍后重试"
},
codex_stdio_failed: {
layer: "runner",
category: "runner_blocked",
retryable: true,
userMessage: "Codex stdio runner 执行失败 trace 显示 transport closed after partial assistant output请新建 session 重试并结合 providerTracerunnerTrace pod restart/rollout 事件判断是否为滚动中断"
},
codex_stdio_network_failed: {
layer: "runner",
category: "runner_blocked",
retryable: true,
userMessage: "Codex stdio runner 的网络操作失败输入已保留可稍后重试"
}
};
return catalog[code] ?? {
layer: "api",
category: "api_error",
retryable: true,
userMessage: "Code Agent 请求失败输入已保留可稍后重试"
};
}
function structuredBlocker({ code, layer, message, userMessage, retryable, traceId, provider, backend, runner, capabilityLevel, route, toolName, category, blockers, sessionId = null, conversationId = null, stage = null, lastEvent = null, elapsedMs = null, timeoutMs = null, hardTimeoutMs = null, idleMs = null, lastActivityAt = null, waitingFor = null }) {
return {
code,
layer,
category: category ?? layer,
retryable: Boolean(retryable),
summary: redactText(message),
userMessage,
traceId: traceId ?? null,
provider: provider ?? null,
backend: backend ?? null,
runner: runner?.kind ?? runner ?? null,
capabilityLevel: capabilityLevel ?? null,
route: route ?? null,
toolName: toolName ?? null,
sessionId,
conversationId,
stage,
lastEvent,
elapsedMs,
timeoutMs,
hardTimeoutMs,
idleMs,
lastActivityAt,
waitingFor,
blockerCodes: Array.isArray(blockers) ? blockers.map((blocker) => blocker.code).filter(Boolean) : []
};
}
function normalizeBlockers(blockers, fallback) {
const items = Array.isArray(blockers) ? blockers : [];
const normalized = items
.filter((blocker) => blocker && typeof blocker === "object")
.map((blocker) => ({
code: String(blocker.code ?? fallback.fallbackCode),
layer: blocker.layer ?? fallback.layer,
category: blocker.category ?? fallback.layer,
retryable: Boolean(blocker.retryable ?? fallback.retryable),
summary: redactText(blocker.summary ?? blocker.message ?? fallback.message),
userMessage: blocker.userMessage ?? blocker.zh ?? fallback.userMessage,
sourceIssue: blocker.sourceIssue,
route: blocker.route,
toolName: blocker.toolName,
sessionId: blocker.sessionId,
conversationId: blocker.conversationId,
stage: blocker.stage,
lastEvent: blocker.lastEvent,
elapsedMs: blocker.elapsedMs,
timeoutMs: numberErrorField(blocker.timeoutMs),
hardTimeoutMs: numberErrorField(blocker.hardTimeoutMs),
idleMs: numberErrorField(blocker.idleMs),
lastActivityAt: safeIsoLike(blocker.lastActivityAt),
waitingFor: blocker.waitingFor ? redactText(blocker.waitingFor) : null
}));
if (normalized.length > 0) return normalized;
return [{
code: fallback.fallbackCode,
layer: fallback.layer,
category: fallback.layer,
retryable: Boolean(fallback.retryable),
summary: redactText(fallback.message),
userMessage: fallback.userMessage
}];
}
function safeMissingConfig(error) {
return [
...(Array.isArray(error.missingEnv) ? error.missingEnv : []),
...(Array.isArray(error.missingCommands) ? error.missingCommands.map((command) => `command:${command}`) : []),
...(Array.isArray(error.missingTools) ? error.missingTools.map((tool) => `tool:${tool}`) : [])
].filter((item) => typeof item === "string" && /^[A-Za-z0-9_./:-]+$/u.test(item));
}
function numberErrorField(value) {
return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : null;
}
function safeIsoLike(value) {
const text = typeof value === "string" ? value.trim() : "";
if (!text || text.length > 80) return null;
return /^[0-9T:Z.+-]+$/u.test(text) ? text : null;
}
function resolveRunnerWorkspace(env = process.env, options = {}) {
const configured = firstNonEmpty(
options.workspace,
env.HWLAB_CODE_AGENT_WORKSPACE,
env.HWLAB_RUNNER_WORKSPACE,
env.WORKSPACE
);
return path.resolve(configured || repoRoot);
}
function resolveSkillDirs(env = process.env, options = {}) {
const configured = Array.isArray(options.skillsDirs)
? options.skillsDirs
: String(firstNonEmpty(env.HWLAB_CODE_AGENT_SKILLS_DIRS, env.UNIDESK_SKILLS_PATH, ""))
.split(/[,;]/u)
.flatMap((part) => part.split(path.delimiter));
const strict = options.skillsDirsExact === true || env.HWLAB_CODE_AGENT_SKILLS_STRICT === "1";
if (strict) {
return [...new Set(configured
.filter((dir) => typeof dir === "string" && dir.trim())
.map((dir) => path.resolve(dir.trim())))];
}
return [path.resolve("/app/skills")];
}
function sanitizeErrorField(key, value) {
if (key === "missingEnv" || key === "missingCommands" || key === "missingTools") {
return Array.isArray(value) ? value.filter((item) => typeof item === "string").map((item) => redactText(item)) : [];
}
if (key === "providerStatus" || key === "exitCode") return value;
return redactText(value);
}
function routeForError(code, error = {}) {
if (error.route !== undefined) return error.route;
if (String(code).startsWith("hardware_")) return HARDWARE_INVOKE_SHELL_METHOD;
return null;
}
function toolNameForError(code, error = {}) {
if (error.toolName !== undefined) return error.toolName;
if (String(code).startsWith("hardware_")) return HARDWARE_INVOKE_SHELL_METHOD;
return null;
}
function badRequest(message) {
const error = new Error(message);
error.code = "invalid_params";
return error;
}
function cleanProtocolId(value, prefix) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
if (!trimmed) return null;
if (/^[a-z][a-z0-9]*_[A-Za-z0-9._:-]+$/u.test(trimmed)) return trimmed;
return `${prefix}_${trimmed.replace(/[^A-Za-z0-9._:-]/gu, "-").slice(0, 80)}`;
}
function cleanProjectId(value) {
return cleanProtocolId(value, "prj");
}
function resolveConversationSessionIds(params = {}) {
const requestedConversationId = cleanProtocolId(params.conversationId, "cnv");
const requestedSessionId = cleanProtocolId(params.sessionId, "ses");
if (requestedConversationId && requestedSessionId) {
return {
conversationId: requestedConversationId,
sessionId: requestedSessionId,
requestedSessionId
};
}
if (requestedConversationId) {
return {
conversationId: requestedConversationId,
sessionId: requestedConversationId,
requestedSessionId: null
};
}
if (requestedSessionId) {
const conversationId = `cnv_${requestedSessionId.replace(/^[a-z][a-z0-9]*_/u, "")}`;
return {
conversationId,
sessionId: requestedSessionId,
requestedSessionId
};
}
const conversationId = `cnv_${randomUUID()}`;
return {
conversationId,
sessionId: conversationId,
requestedSessionId: null
};
}
function nowIso(now) {
return typeof now === "function" ? now() : new Date().toISOString();
}
function firstDefined(...values) {
return values.find((value) => value !== undefined && value !== null);
}
function formatM3Value(value) {
if (value === undefined || value === null) return "未产生/不可证明";
return String(value);
}
function firstNonEmpty(...values) {
for (const value of values) {
if (typeof value === "string" && value.trim()) return value.trim();
}
return "";
}
function redactText(value) {
return String(value ?? "")
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gu, "Bearer ***")
.replace(/sk-[A-Za-z0-9._-]+/gu, "sk-***")
.replace(/([A-Za-z0-9_]*TOKEN[A-Za-z0-9_]*=)[^\s]+/giu, "$1***")
.replace(/([A-Za-z0-9_]*KEY[A-Za-z0-9_]*=)[^\s]+/giu, "$1***");
}
function redactUrl(value) {
try {
const url = new URL(value);
url.username = "";
url.password = "";
return url.toString();
} catch {
return redactText(String(value ?? "")).replace(/\/\/[^/@]+@/u, "//***@");
}
}