Merge pull request #884 from pikasTech/fix/v02-agentrun-health-cleanup

fix(v02): 收敛 AgentRun Code Agent 健康合同
This commit is contained in:
Lyon
2026-06-05 02:07:18 +08:00
committed by GitHub
4 changed files with 359 additions and 5 deletions
@@ -31,6 +31,9 @@ const DEFAULT_PROVIDER_ID = "G14";
const DEFAULT_REPO_URL = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
const DEFAULT_RUNNER_NAMESPACE = "agentrun-v01";
const DEFAULT_TIMEOUT_MS = 1_200_000;
const AGENTRUN_RUNNER_KIND = "agentrun-v01-shared-runner";
const AGENTRUN_SESSION_MODE = "agentrun-v01-durable-session";
const AGENTRUN_IMPLEMENTATION_TYPE = "agentrun-v01-shared-execution-infra";
const AGENTRUN_BACKENDS = Object.freeze(["codex", "deepseek", "minimax-m3"]);
const THREAD_CONTINUITY_POLICY = "hwlab-agentrun-v01-reuse-runner-thread";
const SESSION_POLICY_RUN_LOCAL = "hwlab-agentrun-v01-session-runner-reuse";
@@ -52,6 +55,160 @@ export function codeAgentAgentRunAdapterEnabled(env = process.env) {
return value === ADAPTER_ID || value === "agentrun" || value === "agentrun-v0.1";
}
export function describeAgentRunAdapterAvailability(env = process.env, options = {}) {
const params = options.params ?? {};
const backendProfile = resolveAgentRunBackendProfile(env, params);
const provider = providerForBackendProfile(backendProfile);
const backend = `${ADAPTER_ID}/${backendProfile}`;
const model = modelForBackendProfile(backendProfile, env);
const blockers = [];
let managerUrl = null;
let repoUrl = null;
try {
managerUrl = resolveAgentRunManagerUrl(env);
} catch (error) {
blockers.push(agentRunAvailabilityBlocker(error, "manager-url"));
}
try {
repoUrl = resolveAgentRunRepoUrl(env);
} catch (error) {
blockers.push(agentRunAvailabilityBlocker(error, "repo-url"));
}
const providerId = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID, env.AGENTRUN_PROVIDER_ID, DEFAULT_PROVIDER_ID);
const runnerNamespace = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE, env.AGENTRUN_RUNTIME_NAMESPACE, DEFAULT_RUNNER_NAMESPACE);
const ready = blockers.length === 0 && Boolean(providerId && runnerNamespace && backendProfile && managerUrl && repoUrl);
const managerHost = hostForUrl(managerUrl);
const repoHost = hostForUrl(repoUrl);
const status = ready ? "agentrun-ready" : "blocked";
const capabilityLevel = ready ? CODEX_STDIO_CAPABILITY_LEVEL : "blocked";
return {
endpoint: "POST /v1/agent/chat",
adapter: ADAPTER_ID,
provider,
model,
backend,
infrastructureBackend: backend,
mode: ADAPTER_ID,
schema: [
"conversationId",
"sessionId",
"messageId",
"status",
"createdAt",
"updatedAt",
"traceId",
"provider",
"model",
"backend",
"infrastructureBackend",
"agentRun",
"runner",
"runnerTrace",
"session",
"sessionMode",
"sessionReuse",
"threadContinuityPolicy",
"error.message",
"error.code"
],
runner: {
kind: AGENTRUN_RUNNER_KIND,
adapter: ADAPTER_ID,
backend,
provider,
model,
namespace: runnerNamespace,
managerHost,
repoHost,
mode: ADAPTER_ID,
sessionMode: AGENTRUN_SESSION_MODE,
implementationType: AGENTRUN_IMPLEMENTATION_TYPE,
status: ready ? "available" : "blocked",
ready,
capabilityLevel,
longLivedSession: ready,
durableSession: ready,
durable: ready,
codexStdio: false,
delegatedToAgentRun: true,
writeCapable: ready,
readOnly: false,
runnerLimitations: ready ? [] : ["agentrun-adapter-config-blocked"],
secretMaterialRead: false,
valuesRedacted: true
},
agentRun: {
adapter: ADAPTER_ID,
backendProfile,
providerId,
runnerNamespace,
managerUrl,
managerHost,
repoUrl,
repoHost,
managerConfigured: Boolean(managerUrl),
repoConfigured: Boolean(repoUrl),
internalServiceDns: managerHost === "agentrun-mgr.agentrun-v01.svc.cluster.local",
ready,
valuesPrinted: false
},
status,
readinessStatus: status,
agentKind: "agentrun-v01-adapter",
capabilityStatus: status,
blocker: ready ? null : blockers[0]?.summary ?? "AgentRun adapter runtime configuration is blocked.",
reason: ready ? null : blockers[0]?.code ?? "agentrun_adapter_blocked",
summary: ready
? "HWLAB Code Agent delegates execution to AgentRun v0.1 shared infrastructure; hwlab-cloud-api keeps session/trace/API ownership and no longer manages a repo-owned runner control plane."
: "HWLAB Code Agent is configured for AgentRun v0.1, but the adapter runtime configuration is incomplete or invalid.",
missingEnv: [],
secretRefs: [],
egress: {
ready,
mode: "agentrun-managed-provider-egress",
directPublicOpenAi: false,
secretMaterialRead: false,
valuesRedacted: true
},
safety: {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
providerCredentialsDelegatedToAgentRun: true,
repoOwnedRunnerControlPlane: false,
valuesRedacted: true
},
capabilityLevel,
workspace: repoUrl,
sandbox: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_SANDBOX, env.HWLAB_CODE_AGENT_CODEX_SANDBOX, "danger-full-access"),
session: null,
sessionStatus: null,
sessionMode: AGENTRUN_SESSION_MODE,
implementationType: AGENTRUN_IMPLEMENTATION_TYPE,
runnerLimitations: ready ? [] : ["agentrun-adapter-config-blocked"],
longLivedSessionGate: {
status: ready ? "pass" : "blocked",
provider,
runnerKind: AGENTRUN_RUNNER_KIND,
sessionMode: AGENTRUN_SESSION_MODE,
implementationType: AGENTRUN_IMPLEMENTATION_TYPE,
adapter: ADAPTER_ID,
codexStdio: false,
delegatedToAgentRun: true,
blockers,
secretMaterialRead: false,
valuesRedacted: true
},
blockers,
blockerCodes: blockers.map((blocker) => blocker.code).filter(Boolean),
ready,
partialReady: false
};
}
export function initialAgentRunChatResult({ params = {}, options = {}, traceId }) {
const env = options.env ?? process.env;
const timestamp = nowIso(options.now);
@@ -1540,6 +1697,34 @@ function modelForBackendProfile(profile, env = process.env) {
return firstNonEmpty(env.HWLAB_CODE_AGENT_DEEPSEEK_MODEL, "deepseek-chat");
}
function providerForBackendProfile(profile) {
if (profile === "codex") return "codex-api";
if (profile === "minimax-m3") return "minimax-m3";
return "deepseek";
}
function agentRunAvailabilityBlocker(error, scope) {
return {
code: error?.code ?? `agentrun_${String(scope).replace(/[^a-z0-9]+/giu, "_")}_blocked`,
type: "agent_blocker",
scope: `agentrun-${scope}`,
status: "open",
sourceIssue: "pikasTech/HWLAB#879",
summary: error?.message ?? `AgentRun ${scope} is not configured correctly.` ,
secretMaterialRead: false,
valuesRedacted: true
};
}
function hostForUrl(value) {
if (!value) return null;
try {
return new URL(value).hostname.toLowerCase();
} catch {
return null;
}
}
function waitingForForPhase(phase) {
if (phase.includes("runner-job")) return "agentrun-runner";
if (phase.includes("resource-bundle")) return "agentrun-resource-bundle";
+8
View File
@@ -31,6 +31,10 @@ import {
codeAgentSessionLifecycleSummary,
decorateCodeAgentSession
} from "./code-agent-session-lifecycle.ts";
import {
codeAgentAgentRunAdapterEnabled,
describeAgentRunAdapterAvailability
} from "./code-agent-agentrun-adapter.ts";
const DEFAULT_MODEL = DEV_CODE_AGENT_PROVIDER_CONTRACT.model;
const DEFAULT_PROJECT_ID = "prj_hwlab-cloud-workbench";
const READONLY_RUNNER_PROVIDER = "codex-readonly-runner";
@@ -459,6 +463,10 @@ function decorateChatSessionLifecycle(payload) {
}
export async function describeCodeAgentAvailability(env = process.env, options = {}) {
if (codeAgentAgentRunAdapterEnabled(env)) {
return describeAgentRunAdapterAvailability(env, options);
}
const providerPlan = resolveProviderPlan(env, options);
const providerContract = inspectCodeAgentProviderEnv(env);
const missingEnv = providerPlan.mode === "openai"
+89 -5
View File
@@ -78,6 +78,9 @@ function isDbReady(db) {
}
function isCodeAgentReady(codeAgent) {
if (isAgentRunCodeAgent(codeAgent)) {
return Boolean(codeAgent?.ready === true && codeAgent?.agentRun?.managerConfigured !== false && codeAgent?.agentRun?.ready !== false);
}
return Boolean(
codeAgent?.ready === true &&
codeAgent?.longLivedSessionGate?.status === "pass" &&
@@ -116,18 +119,21 @@ function dbBlocker(db) {
function codeAgentBlocker(codeAgent) {
const primaryBlocker = Array.isArray(codeAgent?.blockers) ? codeAgent.blockers[0] : null;
const agentRun = isAgentRunCodeAgent(codeAgent);
return {
code: codeAgent?.reason ?? primaryBlocker?.code ?? "code_agent_long_lived_session_blocked",
type: "agent_blocker",
scope: "code-agent-long-lived-session",
scope: agentRun ? "code-agent-agentrun-adapter" : "code-agent-long-lived-session",
status: "open",
sourceIssue: primaryBlocker?.sourceIssue ?? "pikasTech/HWLAB#275",
sourceIssue: primaryBlocker?.sourceIssue ?? (agentRun ? "pikasTech/HWLAB#879" : "pikasTech/HWLAB#275"),
summary: codeAgent?.summary ?? "Code Agent provider readiness is not established",
evidence: {
adapter: codeAgent?.adapter ?? codeAgent?.agentRun?.adapter ?? null,
provider: codeAgent?.provider ?? "unknown",
backend: codeAgent?.backend ?? "unknown",
agentKind: codeAgent?.agentKind ?? "unknown",
capabilityStatus: codeAgent?.capabilityStatus ?? "unknown",
agentRun: codeAgent?.agentRun ?? null,
longLivedSessionGate: codeAgent?.longLivedSessionGate ?? null,
codexStdioFeasibility: codeAgent?.codexStdioFeasibility ?? null,
partialReady: codeAgent?.partialReady === true,
@@ -205,6 +211,27 @@ function buildRuntimeDurabilityReadiness({ db, runtime, runtimeReady, runtimeBlo
}
function buildProviderReadiness(codeAgent = {}) {
if (isAgentRunCodeAgent(codeAgent)) {
const ready = codeAgent?.ready === true && codeAgent?.agentRun?.ready !== false;
return {
status: ready ? "ready" : "blocked",
ready,
configured: ready,
adapter: codeAgent.adapter ?? codeAgent.agentRun?.adapter ?? "agentrun-v01",
provider: codeAgent?.provider ?? "unknown",
model: codeAgent?.model ?? "unknown",
backend: codeAgent?.backend ?? "unknown",
mode: codeAgent?.mode ?? "agentrun-v01",
backendProfile: codeAgent?.agentRun?.backendProfile ?? null,
evidence: "agentrun_runtime_adapter_config",
egressReady: codeAgent?.egress?.ready !== false,
missingEnv: [],
blocker: ready ? null : firstCode(codeAgent?.reason, codeAgent?.blockers?.[0]?.code, "agentrun_adapter_blocked"),
secretMaterialRead: false,
valuesRedacted: true
};
}
const fallback = codeAgent?.fallback ?? {};
const fallbackReady = fallback.ready === true || fallback.status === "available";
const tokenBoundary = codeAgent?.codexStdioFeasibility?.tokenBoundary ?? codeAgent?.codexStdio?.tokenBoundary ?? null;
@@ -230,6 +257,32 @@ function buildProviderReadiness(codeAgent = {}) {
}
function buildSessionRunnerReadiness(codeAgent = {}) {
if (isAgentRunCodeAgent(codeAgent)) {
const runner = codeAgent?.runner ?? {};
const ready = codeAgent?.ready === true && runner.ready !== false;
return {
status: ready ? "agentrun_ready" : "blocked",
ready,
kind: runner.kind ?? "agentrun-v01-shared-runner",
adapter: codeAgent.adapter ?? codeAgent.agentRun?.adapter ?? "agentrun-v01",
provider: codeAgent?.provider ?? runner.provider ?? "unknown",
backend: codeAgent?.backend ?? runner.backend ?? "unknown",
mode: runner.sessionMode ?? runner.mode ?? codeAgent?.sessionMode ?? "agentrun-v01-durable-session",
capabilityLevel: runner.capabilityLevel ?? codeAgent?.capabilityLevel ?? "unknown",
longLivedSession: runner.longLivedSession !== false,
durableSession: runner.durableSession !== false,
codexStdio: false,
delegatedToAgentRun: true,
writeCapable: runner.writeCapable !== false,
readOnly: false,
namespace: runner.namespace ?? codeAgent?.agentRun?.runnerNamespace ?? null,
managerHost: runner.managerHost ?? codeAgent?.agentRun?.managerHost ?? null,
blocker: ready ? null : firstCode(runner.blocker, codeAgent?.reason, codeAgent?.blockers?.[0]?.code, "agentrun_session_runner_blocked"),
runnerLimitations: Array.isArray(codeAgent?.runnerLimitations) ? [...codeAgent.runnerLimitations] : [],
secretMaterialRead: false
};
}
const stdio = codeAgent?.codexStdioFeasibility ?? codeAgent?.codexStdio ?? {};
const stdioCommandReady = stdio.commandProbe?.ready === true;
const stdioReady = (stdio.ready === true || stdio.canStartLongLivedCodexStdio === true) && stdioCommandReady;
@@ -263,6 +316,29 @@ function buildSessionRunnerReadiness(codeAgent = {}) {
}
function buildCodexStdioReadiness(codeAgent = {}) {
if (isAgentRunCodeAgent(codeAgent)) {
return {
status: "not_applicable",
ready: false,
feasible: false,
adapter: codeAgent.adapter ?? codeAgent.agentRun?.adapter ?? "agentrun-v01",
delegatedToAgentRun: true,
blocker: null,
blockerCodes: [],
sourceIssue: "pikasTech/HWLAB#879",
commandPresent: false,
commandExecutable: false,
nativeDependencyPresent: false,
commandProbeReady: false,
commandProbe: null,
supervisorConfigured: false,
workspaceReady: false,
tokenBoundaryPresent: false,
egressReady: true,
secretMaterialRead: false
};
}
const stdio = codeAgent?.codexStdioFeasibility ?? codeAgent?.codexStdio ?? {};
const commandReady = stdio.commandProbe?.ready === true;
const ready = (stdio.ready === true || stdio.canStartLongLivedCodexStdio === true) && commandReady;
@@ -297,24 +373,27 @@ function buildCodexStdioReadiness(codeAgent = {}) {
}
function buildCodeAgentStructuredReadiness({ provider, dbDurable, sessionRunner, codexStdio, codeAgent } = {}) {
const agentRun = isAgentRunCodeAgent(codeAgent);
const currentBlockers = [
provider?.ready === false ? provider.blocker ?? "provider_config_blocked" : null,
dbDurable?.ready === false ? dbDurable.blocker ?? "runtime_durable_adapter_not_ready" : null,
sessionRunner?.ready === false ? sessionRunner.blocker ?? "session_runner_blocked" : null,
codexStdio?.ready === false ? codexStdio.blocker ?? "codex_stdio_feasibility_blocked" : null
!agentRun && codexStdio?.ready === false ? codexStdio.blocker ?? "codex_stdio_feasibility_blocked" : null
].filter(Boolean);
const ready = provider?.ready === true &&
dbDurable?.ready === true &&
sessionRunner?.ready === true &&
codexStdio?.ready === true &&
(agentRun || codexStdio?.ready === true) &&
codeAgent?.ready === true;
return {
status: ready ? "ready" : currentBlockers.length > 0 ? "blocked" : "degraded",
ready,
adapter: codeAgent?.adapter ?? codeAgent?.agentRun?.adapter ?? null,
agentRunReady: agentRun ? codeAgent?.agentRun?.ready === true : null,
providerReady: provider?.ready === true,
durableDbReady: dbDurable?.ready === true,
sessionRunnerReady: sessionRunner?.ready === true,
codexStdioFeasible: codexStdio?.ready === true,
codexStdioFeasible: agentRun ? false : codexStdio?.ready === true,
currentBlocker: currentBlockers[0] ?? null,
currentBlockers,
provider: {
@@ -349,6 +428,7 @@ function buildCodeAgentStructuredReadiness({ provider, dbDurable, sessionRunner,
status: codexStdio?.status ?? "unknown",
ready: codexStdio?.ready === true,
feasible: codexStdio?.feasible === true,
delegatedToAgentRun: codexStdio?.delegatedToAgentRun === true,
blocker: codexStdio?.blocker ?? null,
blockerCodes: Array.isArray(codexStdio?.blockerCodes) ? [...codexStdio.blockerCodes] : []
},
@@ -357,6 +437,10 @@ function buildCodeAgentStructuredReadiness({ provider, dbDurable, sessionRunner,
};
}
function isAgentRunCodeAgent(codeAgent = {}) {
return codeAgent?.adapter === "agentrun-v01" || codeAgent?.agentRun?.adapter === "agentrun-v01" || codeAgent?.mode === "agentrun-v01";
}
function providerBlockerCode(codeAgent = {}, fallback = {}) {
if (Array.isArray(codeAgent?.missingEnv) && codeAgent.missingEnv.length > 0) return "provider_config_blocked";
if (fallback?.egress?.directPublicOpenAi === true || codeAgent?.egress?.directPublicOpenAi === true) return "provider_egress_blocked";
+77
View File
@@ -718,6 +718,83 @@ test("cloud api health reports provider, durable DB, and codex stdio ready when
}
});
test("cloud api health reports AgentRun adapter readiness without repo-owned codex stdio control plane", async () => {
const server = createCloudApiServer({
env: {
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080",
HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE: "agentrun-v01",
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "G14",
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
HWLAB_CODE_AGENT_DEEPSEEK_MODEL: "deepseek-chat",
HWLAB_BOOT_READ_URL: "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git",
HWLAB_CLOUD_DB_URL: "postgres://hwlab_test:password@db.internal.local:5432/hwlab",
HWLAB_CLOUD_DB_SSL_MODE: "disable"
},
dbProbe: {
probe: async ({ endpointSource, timeoutMs }) => ({
attempted: true,
networkAttempted: true,
endpointSource,
probeType: "tcp-connect",
endpointRedacted: true,
valueRedacted: true,
timeoutMs,
durationMs: 1,
result: "connected",
classification: "tcp_connected",
errorCode: null,
missingEnv: []
})
},
runtimeStore: {
async readiness() {
return durableReadyRuntime();
}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.status, "ok");
assert.equal(payload.ready, true);
assert.equal(payload.codeAgent.adapter, "agentrun-v01");
assert.equal(payload.codeAgent.status, "agentrun-ready");
assert.equal(payload.codeAgent.provider, "deepseek");
assert.equal(payload.codeAgent.backend, "agentrun-v01/deepseek");
assert.equal(payload.codeAgent.agentRun.managerHost, "agentrun-mgr.agentrun-v01.svc.cluster.local");
assert.equal(payload.codeAgent.agentRun.runnerNamespace, "agentrun-v01");
assert.equal(payload.codeAgent.runner.kind, "agentrun-v01-shared-runner");
assert.equal(payload.codeAgent.runner.delegatedToAgentRun, true);
assert.equal(payload.codeAgent.runner.codexStdio, false);
assert.equal(payload.readiness.components.codeAgent, "ready");
assert.equal(payload.readiness.provider.status, "ready");
assert.equal(payload.readiness.sessionRunner.status, "agentrun_ready");
assert.equal(payload.readiness.sessionRunner.delegatedToAgentRun, true);
assert.equal(payload.readiness.codexStdio.status, "not_applicable");
assert.equal(payload.readiness.codeAgent.ready, true);
assert.equal(payload.readiness.codeAgent.adapter, "agentrun-v01");
assert.equal(payload.readiness.codeAgent.agentRunReady, true);
assert.equal(payload.readiness.codeAgent.codexStdioFeasible, false);
assert.deepEqual(payload.readiness.codeAgent.currentBlockers, []);
const serialized = JSON.stringify(payload);
assert.equal(serialized.includes("repo-owned-codex"), false);
assert.equal(serialized.includes("codex-app-server-stdio"), false);
assert.equal(serialized.includes("CODEX_HOME"), false);
assert.equal(serialized.includes("/codex-home"), false);
assert.equal(serialized.includes("test-openai-key-material"), false);
assert.equal(serialized.includes("password"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api health blocks full Code Agent readiness when Codex stdio command probe fails", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-health-codex-stdio-probe-fail-"));
const codexHome = path.join(workspace, "codex-home");