fix: expose blocked AgentRun cancel disposition (#2092)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p1-agentrun-incremental-cursor; draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010205 HWLAB接入 draft-2026-06-17-r0; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
|
||||
// Responsibility: AgentRun v0.1 adapter, incremental Workbench projection cursor sync, and upstream timing projection for Cloud API observability.
|
||||
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
||||
import { decorateCodeAgentSession } from "./code-agent-session-lifecycle.ts";
|
||||
@@ -11,30 +11,55 @@ import {
|
||||
safeOpaqueId,
|
||||
safeSessionId,
|
||||
safeTraceId,
|
||||
text,
|
||||
truthyFlag
|
||||
text
|
||||
} from "./server-http-utils.ts";
|
||||
import { backendPerformanceRouteTemplate, currentBackendPerformanceContext } from "./backend-performance.ts";
|
||||
import { currentBackendPerformanceContext } from "./backend-performance.ts";
|
||||
import {
|
||||
DEFAULT_AGENTRUN_MGR_URL,
|
||||
agentRunJson,
|
||||
isAgentRunManagerInternalServiceHost,
|
||||
resolveAgentRunManagerUrl
|
||||
} from "./code-agent-agentrun-runtime.ts";
|
||||
import {
|
||||
agentRunAlreadyTerminalCancelPayload,
|
||||
agentRunCancelBlockedPayload,
|
||||
agentRunCancelTerminalStatus
|
||||
} from "./code-agent-agentrun-cancel.ts";
|
||||
import {
|
||||
DEFAULT_AGENTRUN_REPO_URL,
|
||||
agentRunAvailabilityBlocker,
|
||||
agentRunProviderIdOrNull,
|
||||
agentRunTraceMeta,
|
||||
backendForBackendProfile,
|
||||
modelForBackendProfile,
|
||||
providerForBackendProfile,
|
||||
requireAgentRunSourceCommit,
|
||||
resolveAgentRunBackendProfile,
|
||||
resolveAgentRunProviderId,
|
||||
resolveAgentRunRepoUrl
|
||||
} from "./code-agent-agentrun-profile.ts";
|
||||
import {
|
||||
agentRunPromptEventFields,
|
||||
agentRunPromptFields,
|
||||
agentRunPromptMetadataFromParams,
|
||||
agentRunPromptMetadataFromSession,
|
||||
agentRunPromptMetadataFromText,
|
||||
messageAuthorityTextValue
|
||||
} from "./code-agent-agentrun-prompt.ts";
|
||||
import { agentRunResultSyncDeferred, buildAgentRunProjectionEventsFetchPlan, buildWorkbenchProjectionStateUpdate } from "./workbench-projection-cursor.ts";
|
||||
import { codeAgentOtelTraceFields, codeAgentOtelTraceContext, emitCodeAgentOtelSpan } from "./otel-trace.ts";
|
||||
|
||||
const ADAPTER_ID = "agentrun-v01";
|
||||
const DEFAULT_AGENTRUN_MGR_URL = "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080";
|
||||
const AGENTRUN_MANAGER_HOST_PATTERN = /^agentrun-mgr\.[a-z0-9]([-a-z0-9]*[a-z0-9])?\.svc\.cluster\.local$/u;
|
||||
const DEFAULT_TENANT_ID = "hwlab";
|
||||
const DEFAULT_PROJECT_ID = "pikasTech/HWLAB";
|
||||
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_BACKEND_PREFIX = ADAPTER_ID;
|
||||
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_CAPABILITY_LEVEL = "agentrun-v01-shared-code-agent-session";
|
||||
const AGENTRUN_PROVIDER_TRACE_PROTOCOL = "agentrun-v01-jsonrpc";
|
||||
const AGENTRUN_PROVIDER_TRACE_WIRE_API = "agentrun-v01-command-result";
|
||||
const AGENTRUN_BACKEND_ALIASES = Object.freeze({ "codex-api": "codex", codex: "codex" });
|
||||
const AGENTRUN_BACKEND_PROFILE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/u;
|
||||
const THREAD_CONTINUITY_POLICY = "hwlab-agentrun-v01-reuse-runner-thread";
|
||||
const SESSION_POLICY_RUN_LOCAL = "hwlab-agentrun-v01-session-runner-reuse";
|
||||
const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "blocked", "cancelled", "canceled"]);
|
||||
@@ -932,7 +957,7 @@ export async function refreshAgentRunTrace({ traceId, result = null, options = {
|
||||
}
|
||||
|
||||
export async function cancelAgentRunChatTurn({ traceId, currentResult = null, options = {}, traceStore = defaultCodeAgentTraceStore }) {
|
||||
const mapped = currentResult ?? await loadPersistedAgentRunResult(traceId, options);
|
||||
let mapped = currentResult ?? await loadPersistedAgentRunResult(traceId, options);
|
||||
if (!mapped?.agentRun?.commandId) return null;
|
||||
const env = options.env ?? process.env;
|
||||
const timeoutMs = parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, 20_000);
|
||||
@@ -947,12 +972,25 @@ export async function cancelAgentRunChatTurn({ traceId, currentResult = null, op
|
||||
};
|
||||
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
||||
const managerUrl = resolveAgentRunManagerUrl(env, mapped.agentRun.managerUrl);
|
||||
await agentRunJson(fetchImpl, managerUrl, `/api/v1/commands/${encodeURIComponent(mapped.agentRun.commandId)}/cancel`, {
|
||||
method: "POST",
|
||||
body: { reason: "hwlab-user-cancel", traceId },
|
||||
timeoutMs: cancelTimeoutMs,
|
||||
env
|
||||
});
|
||||
try {
|
||||
const synced = await syncAgentRunChatResult({ traceId, currentResult: mapped, options: cancelOptions, traceStore, forceResultSync: true });
|
||||
mapped = synced?.result ?? synced ?? mapped;
|
||||
} catch {
|
||||
// Cancel must still expose the command cancel disposition; result refresh failures are reported by the normal trace/result paths.
|
||||
}
|
||||
const terminalStatus = agentRunCancelTerminalStatus(mapped);
|
||||
if (terminalStatus) return agentRunAlreadyTerminalCancelPayload({ traceId, payload: mapped, terminalStatus, options: cancelOptions, traceStore });
|
||||
const cancelPath = `/api/v1/commands/${encodeURIComponent(mapped.agentRun.commandId)}/cancel`;
|
||||
try {
|
||||
await agentRunJson(fetchImpl, managerUrl, cancelPath, {
|
||||
method: "POST",
|
||||
body: { reason: "hwlab-user-cancel", traceId },
|
||||
timeoutMs: cancelTimeoutMs,
|
||||
env
|
||||
});
|
||||
} catch (error) {
|
||||
return agentRunCancelBlockedPayload({ traceId, payload: mapped, error, cancelPath, options: cancelOptions, traceStore });
|
||||
}
|
||||
traceStore.append(traceId, agentRunTraceEvent({
|
||||
type: "cancel",
|
||||
status: "canceled",
|
||||
@@ -989,45 +1027,6 @@ export async function cancelAgentRunChatTurn({ traceId, currentResult = null, op
|
||||
return payload;
|
||||
}
|
||||
|
||||
function agentRunCancelTerminalStatus(payload = null) {
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
for (const value of [payload.agentRun?.terminalStatus, payload.agentRun?.commandState, payload.agentRun?.commandStatus]) {
|
||||
const status = normalizeAgentRunStatus(value);
|
||||
if (isAgentRunTerminalStatus(status)) return status;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function agentRunAlreadyTerminalCancelPayload({ traceId, payload, terminalStatus, options = {}, traceStore = defaultCodeAgentTraceStore } = {}) {
|
||||
const normalizedTerminalStatus = normalizeAgentRunStatus(terminalStatus);
|
||||
const agentRun = payload?.agentRun && typeof payload.agentRun === "object" ? payload.agentRun : {};
|
||||
traceStore.append(traceId, agentRunTraceEvent({
|
||||
type: "cancel",
|
||||
status: normalizedTerminalStatus || "completed",
|
||||
label: "agentrun:cancel:already_terminal",
|
||||
message: "AgentRun command is already terminal; HWLAB did not forward cancel or overwrite the terminal Workbench projection.",
|
||||
runId: agentRun.runId,
|
||||
commandId: agentRun.commandId,
|
||||
terminal: true,
|
||||
valuesPrinted: false
|
||||
}, agentRun));
|
||||
const payloadStatus = normalizeAgentRunStatus(payload?.status);
|
||||
const status = isAgentRunTerminalStatus(payloadStatus) ? payloadStatus : normalizedTerminalStatus || payloadStatus || "completed";
|
||||
const result = {
|
||||
...payload,
|
||||
status,
|
||||
canceled: false,
|
||||
alreadyTerminal: true,
|
||||
cancelStatus: "already_terminal",
|
||||
updatedAt: nowIso(options.now),
|
||||
runnerTrace: traceStore.snapshot(traceId),
|
||||
agentRun: { ...agentRun, terminalStatus: agentRun.terminalStatus ?? normalizedTerminalStatus, valuesPrinted: false },
|
||||
valuesPrinted: false
|
||||
};
|
||||
options.codeAgentChatResults?.set?.(traceId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function steerAgentRunChatTurn({ traceId, currentResult = null, params = {}, options = {}, traceStore = defaultCodeAgentTraceStore }) {
|
||||
const mapped = currentResult ?? await loadPersistedAgentRunResult(traceId, options);
|
||||
if (!safeTraceId(traceId) || !mapped?.agentRun?.runId || !mapped?.agentRun?.commandId) return null;
|
||||
@@ -1197,7 +1196,7 @@ export function agentRunSessionEvidence(payload = {}) {
|
||||
namespace: payload.agentRun.namespace ?? null,
|
||||
backendProfile: payload.agentRun.backendProfile ?? null,
|
||||
managerUrl: payload.agentRun.managerUrl ?? DEFAULT_AGENTRUN_MGR_URL,
|
||||
repoUrl: payload.agentRun.repoUrl ?? DEFAULT_REPO_URL,
|
||||
repoUrl: payload.agentRun.repoUrl ?? DEFAULT_AGENTRUN_REPO_URL,
|
||||
status: payload.agentRun.status ?? null,
|
||||
runStatus: payload.agentRun.runStatus ?? null,
|
||||
commandState: payload.agentRun.commandState ?? null,
|
||||
@@ -1386,147 +1385,6 @@ function rejectRemovedResourceWorkspaceFiles(value) {
|
||||
throw adapterError("legacy_workspace_files_removed", "workspaceFiles/resourceWorkspaceFiles are removed; use AgentRun ResourceBundleRef kind=gitbundle with bundles[]");
|
||||
}
|
||||
|
||||
function agentRunPromptMetadataFromParams(params = {}, traceId = null) {
|
||||
return agentRunPromptMetadataFromText(firstNonEmpty(params.message, params.prompt, params.text), traceId, "hwlab-code-agent-request", { fullText: true });
|
||||
}
|
||||
|
||||
function agentRunPromptMetadataFromSession(session = null, traceId = null) {
|
||||
const snapshot = session?.session && typeof session.session === "object" ? session.session : null;
|
||||
const fromTraceResult = agentRunPromptMetadataFromPayload(snapshot?.traceResults?.[traceId], traceId);
|
||||
if (fromTraceResult) return fromTraceResult;
|
||||
const messageText = agentRunPromptTextFromMessages(snapshot?.messages, traceId);
|
||||
if (messageText) return agentRunPromptMetadataFromText(messageText, traceId, "agent-session-owner-message", { fullText: true });
|
||||
const preview = firstNonEmpty(snapshot?.firstUserMessagePreview);
|
||||
return preview ? agentRunPromptMetadataFromText(preview, traceId, "agent-session-first-user-preview", { fullText: false }) : null;
|
||||
}
|
||||
|
||||
function agentRunPromptMetadataFromPayload(payload = null, traceId = null) {
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
const prompt = payload.prompt && typeof payload.prompt === "object" ? payload.prompt : null;
|
||||
const textValue = firstNonEmpty(prompt?.text, prompt?.summary, prompt?.preview, payload.promptSummary, payload.promptPreview);
|
||||
const fromText = agentRunPromptMetadataFromText(textValue, traceId, prompt?.source ?? payload.promptSource ?? "agentrun-prompt-metadata", { fullText: Boolean(prompt?.text) });
|
||||
return agentRunPromptMetadataFromParts({
|
||||
traceId,
|
||||
source: prompt?.source ?? payload.promptSource ?? fromText?.prompt?.source ?? "agentrun-prompt-metadata",
|
||||
promptId: firstNonEmpty(payload.promptId, prompt?.id, fromText?.promptId),
|
||||
promptSha256: firstNonEmpty(payload.promptSha256, prompt?.sha256, fromText?.promptSha256),
|
||||
promptSummary: firstNonEmpty(payload.promptSummary, prompt?.summary, fromText?.promptSummary),
|
||||
promptPreview: firstNonEmpty(payload.promptPreview, prompt?.preview, fromText?.promptPreview),
|
||||
textChars: Number.isInteger(prompt?.textChars) ? prompt.textChars : fromText?.prompt?.textChars
|
||||
});
|
||||
}
|
||||
|
||||
function agentRunPromptTextFromMessages(messages = [], traceId = null) {
|
||||
const list = Array.isArray(messages) ? messages : [];
|
||||
const resolvedTraceId = safeTraceId(traceId);
|
||||
const exact = list.find((message) => String(message?.role ?? "").toLowerCase() === "user" && (!resolvedTraceId || safeTraceId(message?.traceId) === resolvedTraceId));
|
||||
const fallback = list.find((message) => String(message?.role ?? "").toLowerCase() === "user");
|
||||
return firstNonEmpty(exact?.text, exact?.content, fallback?.text, fallback?.content);
|
||||
}
|
||||
|
||||
function agentRunPromptMetadataFromText(value, traceId = null, source = "hwlab-code-agent-request", options = {}) {
|
||||
const promptText = promptTextValue(value);
|
||||
if (!promptText) return null;
|
||||
return agentRunPromptMetadataFromParts({
|
||||
traceId,
|
||||
source,
|
||||
promptSha256: options.fullText === true ? sha256Text(promptText) : undefined,
|
||||
promptSummary: promptText,
|
||||
promptPreview: promptText,
|
||||
textChars: options.fullText === true ? promptText.length : undefined
|
||||
});
|
||||
}
|
||||
|
||||
function agentRunPromptMetadataFromParts({ traceId = null, source = "hwlab-code-agent-request", promptId = null, promptSha256 = null, promptSummary = null, promptPreview = null, textChars = undefined } = {}) {
|
||||
const digest = firstNonEmpty(promptSha256);
|
||||
const summary = clippedPromptText(firstNonEmpty(promptSummary, promptPreview), 240);
|
||||
const preview = clippedPromptText(firstNonEmpty(promptPreview, promptSummary), 500);
|
||||
const explicitId = firstNonEmpty(promptId);
|
||||
if (!explicitId && !digest && !summary && !preview) return null;
|
||||
const id = explicitId || agentRunPromptId(traceId, digest);
|
||||
const prompt = compactObject({
|
||||
id,
|
||||
sha256: digest || undefined,
|
||||
summary,
|
||||
preview,
|
||||
textChars: Number.isInteger(textChars) ? textChars : undefined,
|
||||
source,
|
||||
valuesPrinted: false
|
||||
});
|
||||
return compactObject({
|
||||
promptId: id,
|
||||
promptSha256: digest || undefined,
|
||||
promptSummary: summary,
|
||||
promptPreview: preview,
|
||||
prompt,
|
||||
valuesPrinted: false
|
||||
}) ?? null;
|
||||
}
|
||||
|
||||
function agentRunPromptFields(metadata = null) {
|
||||
if (!metadata) return {};
|
||||
return compactObject({
|
||||
promptId: metadata.promptId,
|
||||
promptSha256: metadata.promptSha256,
|
||||
promptSummary: metadata.promptSummary,
|
||||
promptPreview: metadata.promptPreview,
|
||||
prompt: metadata.prompt
|
||||
}) ?? {};
|
||||
}
|
||||
|
||||
function agentRunPromptEventFields(metadata = null) {
|
||||
if (!metadata) return {};
|
||||
return compactObject({
|
||||
promptId: metadata.promptId,
|
||||
promptSha256: metadata.promptSha256,
|
||||
promptSummary: metadata.promptSummary,
|
||||
promptPreview: metadata.promptPreview
|
||||
}) ?? {};
|
||||
}
|
||||
|
||||
function agentRunPromptId(traceId = null, digest = null) {
|
||||
const traceSuffix = safeTraceId(traceId)?.slice(4).replace(/[^A-Za-z0-9_.:-]/gu, "_");
|
||||
const digestSuffix = firstNonEmpty(digest)?.slice(0, 16);
|
||||
const suffix = firstNonEmpty(traceSuffix, digestSuffix);
|
||||
return suffix ? `prm_${suffix}` : undefined;
|
||||
}
|
||||
|
||||
function clippedPromptText(value, limit) {
|
||||
const promptText = promptTextValue(value);
|
||||
if (!promptText) return undefined;
|
||||
return promptText.length > limit ? `${promptText.slice(0, limit)}...` : promptText;
|
||||
}
|
||||
|
||||
function promptTextValue(value) {
|
||||
const extracted = promptTextRaw(value).replace(/\s+/gu, " ").trim();
|
||||
if (!extracted || extracted === "[object Object]") return "";
|
||||
return extracted;
|
||||
}
|
||||
|
||||
function messageAuthorityTextValue(value) {
|
||||
const extracted = promptTextRaw(value).replace(/\r\n?/gu, "\n");
|
||||
if (!extracted.trim() || extracted.trim() === "[object Object]") return "";
|
||||
return extracted;
|
||||
}
|
||||
|
||||
function promptTextRaw(value) {
|
||||
if (value === undefined || value === null) return "";
|
||||
if (typeof value === "string") return value;
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
if (Array.isArray(value)) return value.map(promptTextRaw).filter(Boolean).join("\n");
|
||||
if (typeof value === "object") {
|
||||
for (const key of ["text", "content", "message", "prompt", "value", "summary", "preview", "title"]) {
|
||||
const text = promptTextRaw(value[key]);
|
||||
if (text) return text;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function sha256Text(value) {
|
||||
return createHash("sha256").update(String(value)).digest("hex");
|
||||
}
|
||||
|
||||
function adapterError(code, message, details = {}) {
|
||||
return Object.assign(new Error(message), {
|
||||
code,
|
||||
@@ -2765,129 +2623,11 @@ function agentRunCommandExecutionEvent(base, payload = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function agentRunJson(fetchImpl, managerUrl, path, { method = "GET", body, timeoutMs = 20_000, env = process.env } = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
const performanceContext = currentBackendPerformanceContext();
|
||||
const startedAt = nowMs();
|
||||
let statusCode = 0;
|
||||
let outcome = "ok";
|
||||
try {
|
||||
const headers: Record<string, string> = {};
|
||||
const apiKey = resolveAgentRunApiKey(env);
|
||||
if (body !== undefined) headers["content-type"] = "application/json";
|
||||
if (apiKey) headers.authorization = `Bearer ${apiKey}`;
|
||||
const response = await fetchImpl(`${managerUrl}${path}`, {
|
||||
method,
|
||||
headers: Object.keys(headers).length === 0 ? undefined : headers,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal: controller.signal
|
||||
});
|
||||
statusCode = Number(response.status ?? 0);
|
||||
const text = await response.text();
|
||||
const parsed = text ? JSON.parse(text) : {};
|
||||
if (!response.ok || parsed?.ok === false) {
|
||||
outcome = "error";
|
||||
throw Object.assign(new Error(parsed?.message ?? `AgentRun ${method} ${path} failed with HTTP ${response.status}`), {
|
||||
code: parsed?.failureKind ?? "agentrun_request_failed",
|
||||
statusCode: response.status,
|
||||
agentRunError: parsed
|
||||
});
|
||||
}
|
||||
return parsed?.ok === true && Object.hasOwn(parsed, "data") ? parsed.data : parsed;
|
||||
} catch (error) {
|
||||
if (error?.name === "AbortError") {
|
||||
outcome = "timeout";
|
||||
statusCode = 504;
|
||||
throw Object.assign(new Error(`AgentRun ${method} ${path} timed out after ${timeoutMs}ms`), agentRunHttpErrorContext({ managerUrl, path, method, timeoutMs, code: "agentrun_timeout", statusCode: 504, category: "upstream-timeout" }));
|
||||
}
|
||||
if (outcome === "ok") outcome = "error";
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
performanceContext?.recordUpstream?.({
|
||||
component: "agentrun",
|
||||
operation: agentRunOperationName(method, path),
|
||||
route: backendPerformanceRouteTemplate(path),
|
||||
method,
|
||||
statusClass: statusClass(statusCode),
|
||||
outcome,
|
||||
durationMs: nowMs() - startedAt
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function agentRunOperationName(method, path) {
|
||||
const route = backendPerformanceRouteTemplate(path).replace(/^\/api\/v1\//u, "").replace(/\/:id/gu, "");
|
||||
return `${String(method ?? "GET").toLowerCase()}_${route || "root"}`.replace(/[^a-z0-9]+/giu, "_").replace(/^_+|_+$/gu, "").toLowerCase();
|
||||
}
|
||||
|
||||
function statusClass(value) {
|
||||
const status = Number(value);
|
||||
if (!Number.isFinite(status) || status <= 0) return "unknown";
|
||||
return `${Math.floor(status / 100)}xx`;
|
||||
}
|
||||
|
||||
function nowMs() {
|
||||
if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function agentRunHttpErrorContext({ managerUrl, path, method, timeoutMs, code, statusCode, category }) {
|
||||
return {
|
||||
code,
|
||||
statusCode,
|
||||
layer: "agentrun",
|
||||
category,
|
||||
retryable: true,
|
||||
method,
|
||||
route: path,
|
||||
path,
|
||||
timeoutMs,
|
||||
timeoutStage: "agentrun-http",
|
||||
managerHost: safeUrlHost(managerUrl),
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
function safeUrlHost(value) {
|
||||
try { return new URL(value).host; } catch { return null; }
|
||||
}
|
||||
|
||||
function resolveAgentRunManagerUrl(env = process.env, override = null) {
|
||||
const raw = firstNonEmpty(override, env.AGENTRUN_MGR_URL, env.HWLAB_CODE_AGENT_AGENTRUN_MGR_URL, DEFAULT_AGENTRUN_MGR_URL);
|
||||
const url = new URL(raw);
|
||||
const host = url.hostname.toLowerCase();
|
||||
const allowedByTest = truthyFlag(env.HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL) && ["127.0.0.1", "localhost"].includes(host);
|
||||
if (url.protocol !== "http:" || (!isAgentRunManagerInternalServiceHost(host) && !allowedByTest)) {
|
||||
throw Object.assign(new Error(`AGENTRUN_MGR_URL must use internal k3s Service DNS agentrun-mgr.<namespace>.svc.cluster.local; got ${redactUrl(raw)}`), {
|
||||
code: "agentrun_internal_url_required",
|
||||
statusCode: 500
|
||||
});
|
||||
}
|
||||
url.pathname = url.pathname.replace(/\/+$/u, "");
|
||||
url.search = "";
|
||||
url.hash = "";
|
||||
return url.toString().replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
function resolveAgentRunApiKey(env = process.env) {
|
||||
return firstNonEmpty(env.AGENTRUN_API_KEY, env.HWLAB_CODE_AGENT_AGENTRUN_API_KEY, env.HWLAB_API_KEY);
|
||||
}
|
||||
|
||||
function isAgentRunManagerInternalServiceHost(host) {
|
||||
return AGENTRUN_MANAGER_HOST_PATTERN.test(String(host ?? "").trim().toLowerCase());
|
||||
}
|
||||
|
||||
function resolveAgentRunBackendProfile(env = process.env, params = {}) {
|
||||
const requested = String(params.providerProfile ?? params.codeAgentProviderProfile ?? env.HWLAB_CODE_AGENT_AGENTRUN_BACKEND_PROFILE ?? env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE ?? "deepseek").trim().toLowerCase();
|
||||
const fallback = String(env.HWLAB_CODE_AGENT_AGENTRUN_DEFAULT_BACKEND_PROFILE ?? "deepseek").trim().toLowerCase() || "deepseek";
|
||||
const resolved = requested === "runtime-default" ? fallback : requested;
|
||||
if (!resolved || resolved === "runtime-default") return "deepseek";
|
||||
const mapped = AGENTRUN_BACKEND_ALIASES[resolved] ?? resolved;
|
||||
return AGENTRUN_BACKEND_PROFILE_ID_PATTERN.test(mapped) ? mapped : "deepseek";
|
||||
}
|
||||
|
||||
function agentRunMapping({ env, managerUrl, backendProfile, run, command, runnerJob, traceId, startedAt, params = {} }) {
|
||||
const threadReused = Boolean(safeOpaqueId(params.threadId));
|
||||
const runnerJobCreated = Boolean(runnerJob);
|
||||
@@ -3096,110 +2836,6 @@ function agentRunRunnerSummary(mapping = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunTraceMeta(env = process.env) {
|
||||
return {
|
||||
runnerKind: AGENTRUN_RUNNER_KIND,
|
||||
workspace: resolveAgentRunRepoUrl(env),
|
||||
sandbox: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_SANDBOX, env.HWLAB_CODE_AGENT_CODEX_SANDBOX, "danger-full-access"),
|
||||
protocol: AGENTRUN_PROVIDER_TRACE_PROTOCOL,
|
||||
sessionMode: AGENTRUN_SESSION_MODE,
|
||||
implementationType: AGENTRUN_IMPLEMENTATION_TYPE
|
||||
};
|
||||
}
|
||||
|
||||
function backendForBackendProfile(profile) {
|
||||
return `${AGENTRUN_BACKEND_PREFIX}/${resolveAgentRunBackendProfile({}, { providerProfile: profile })}`;
|
||||
}
|
||||
|
||||
function resolveAgentRunRepoUrl(env = process.env) {
|
||||
const raw = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_REPO_URL, env.HWLAB_BOOT_READ_URL, DEFAULT_REPO_URL);
|
||||
const url = new URL(raw);
|
||||
const host = url.hostname.toLowerCase();
|
||||
const allowedHost = "git-mirror-http.devops-infra.svc.cluster.local";
|
||||
const allowedByTest = truthyFlag(env.HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL) && ["127.0.0.1", "localhost"].includes(host);
|
||||
if (url.protocol !== "http:" || (host !== allowedHost && !allowedByTest)) {
|
||||
throw Object.assign(new Error(`HWLAB_CODE_AGENT_AGENTRUN_REPO_URL must use internal k3s git mirror ${DEFAULT_REPO_URL}; got ${redactUrl(raw)}`), {
|
||||
code: "agentrun_internal_repo_url_required",
|
||||
statusCode: 500
|
||||
});
|
||||
}
|
||||
url.pathname = url.pathname.replace(/\/+$/u, "");
|
||||
url.search = "";
|
||||
url.hash = "";
|
||||
return url.toString().replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
function requireAgentRunSourceCommit(env) {
|
||||
const text = String(env.HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT ?? "").trim().toLowerCase();
|
||||
if (/^[0-9a-f]{40}$/u.test(text)) return text;
|
||||
throw Object.assign(new Error("HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT must be a full 40-character source commit before AgentRun dispatch"), {
|
||||
code: "agentrun_bundle_source_commit_invalid",
|
||||
statusCode: 500
|
||||
});
|
||||
}
|
||||
|
||||
function resolveAgentRunProviderId(env = process.env) {
|
||||
const providerId = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID, env.AGENTRUN_PROVIDER_ID);
|
||||
if (!providerId) {
|
||||
throw Object.assign(new Error("HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID must be configured from the runtime lane node id before AgentRun dispatch"), {
|
||||
code: "agentrun_provider_id_required",
|
||||
statusCode: 500
|
||||
});
|
||||
}
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(providerId)) {
|
||||
throw Object.assign(new Error("HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID must be a simple node id"), {
|
||||
code: "agentrun_provider_id_invalid",
|
||||
statusCode: 500
|
||||
});
|
||||
}
|
||||
return providerId;
|
||||
}
|
||||
|
||||
function agentRunProviderIdOrNull(env = process.env) {
|
||||
try {
|
||||
return resolveAgentRunProviderId(env);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function modelForBackendProfile(profile, env = process.env) {
|
||||
const resolved = resolveAgentRunBackendProfile({}, { providerProfile: profile });
|
||||
if (resolved === "codex") return firstNonEmpty(env.HWLAB_CODE_AGENT_CODEX_API_MODEL, env.HWLAB_CODE_AGENT_MODEL, "gpt-5.5");
|
||||
if (resolved === "minimax-m3") return firstNonEmpty(env.HWLAB_CODE_AGENT_MINIMAX_M3_MODEL, "MiniMax-M3");
|
||||
if (resolved === "deepseek") return firstNonEmpty(env.HWLAB_CODE_AGENT_DEEPSEEK_MODEL, "deepseek-chat");
|
||||
if (resolved === "dsflash-go") return firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_DSFLASH_GO_MODEL, env.HWLAB_CODE_AGENT_DSFLASH_GO_MODEL, "deepseek-v4-flash");
|
||||
const profileEnvKey = dynamicBackendProfileEnvKey(resolved);
|
||||
return firstNonEmpty(env[`HWLAB_CODE_AGENT_AGENTRUN_${profileEnvKey}_MODEL`], env[`HWLAB_CODE_AGENT_${profileEnvKey}_MODEL`], resolved);
|
||||
}
|
||||
|
||||
function providerForBackendProfile(profile) {
|
||||
const resolved = resolveAgentRunBackendProfile({}, { providerProfile: profile });
|
||||
if (resolved === "codex") return "codex-api";
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function dynamicBackendProfileEnvKey(profile) {
|
||||
return String(profile ?? "")
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9]+/gu, "_")
|
||||
.replace(/^_+|_+$/gu, "") || "PROFILE";
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user