fix: complete v03 AgentRun workbench projection
This commit is contained in:
@@ -922,6 +922,7 @@ function newSessionIdAfterEviction(baseSessionId, traceId) {
|
||||
}
|
||||
function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, sessionId, toolCapabilities = null }) {
|
||||
const commitId = requireAgentRunSourceCommit(env);
|
||||
const runtimeAuthority = requireAgentRunRuntimeAuthority(env);
|
||||
const threadId = safeOpaqueId(params.threadId);
|
||||
const hwlabProjectId = firstNonEmpty(params.projectId);
|
||||
rejectRemovedResourceWorkspaceFiles(params.workspaceFiles ?? params.resourceWorkspaceFiles);
|
||||
@@ -940,8 +941,8 @@ function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, ses
|
||||
workspaceRef: {
|
||||
kind: "opaque",
|
||||
repo: DEFAULT_PROJECT_ID,
|
||||
branch: firstNonEmpty(env.HWLAB_BOOT_REF, env.HWLAB_CODE_AGENT_AGENTRUN_BRANCH, "v0.2"),
|
||||
runtimeNamespace: firstNonEmpty(env.HWLAB_RUNTIME_NAMESPACE, env.POD_NAMESPACE, env.HWLAB_NAMESPACE, "hwlab-v02"),
|
||||
branch: firstNonEmpty(env.HWLAB_BOOT_REF, env.HWLAB_CODE_AGENT_AGENTRUN_BRANCH, defaultAgentRunBranchForRuntimeLane(runtimeAuthority.lane), "v0.2"),
|
||||
runtimeNamespace: runtimeAuthority.namespace,
|
||||
hwlabTraceId: traceId,
|
||||
valuesPrinted: false
|
||||
},
|
||||
@@ -1196,7 +1197,9 @@ async function resolveOwnerApiKey({ params, options, now }) {
|
||||
return text(key.displaySecret ?? "");
|
||||
}
|
||||
function buildAgentRunRunnerJobInput({ env, traceId, commandId, ownerApiKey, toolCapabilities = null, backendProfile }) {
|
||||
const runtimeAuthority = requireAgentRunRuntimeAuthority(env);
|
||||
const baseTransient = buildAgentRunTransientEnv(env, { providerProfile: backendProfile, parentTraceId: traceId });
|
||||
assertAgentRunTransientRuntimeAuthority(baseTransient, runtimeAuthority);
|
||||
const hwpodAllowed = toolCapabilityAllowed(toolCapabilities, "hwpod");
|
||||
const transientEnv = ownerApiKey && hwpodAllowed
|
||||
? baseTransient.concat([{ name: "HWLAB_API_KEY", value: ownerApiKey, sensitive: true }])
|
||||
@@ -1236,6 +1239,112 @@ function buildAgentRunTransientEnv(env = process.env, options = {}) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
function requireAgentRunRuntimeAuthority(env = process.env) {
|
||||
const explicitLane = normalizeAgentRunRuntimeLane(firstNonEmpty(
|
||||
env.HWLAB_RUNTIME_LANE,
|
||||
env.HWLAB_GITOPS_LANE,
|
||||
env.HWLAB_GITOPS_PROFILE,
|
||||
env.HWLAB_ENVIRONMENT,
|
||||
env.HWLAB_BOOT_REF
|
||||
));
|
||||
const namespace = firstNonEmpty(
|
||||
env.HWLAB_RUNTIME_NAMESPACE,
|
||||
env.POD_NAMESPACE,
|
||||
env.HWLAB_NAMESPACE,
|
||||
namespaceForAgentRunRuntimeLane(explicitLane)
|
||||
);
|
||||
if (!namespace) {
|
||||
throw agentRunRuntimeAuthorityError("agentrun_runtime_namespace_required", "AgentRun dispatch requires HWLAB_RUNTIME_NAMESPACE/POD_NAMESPACE/HWLAB_NAMESPACE or a supported runtime lane");
|
||||
}
|
||||
const namespaceLane = agentRunRuntimeLaneForNamespace(namespace);
|
||||
if (!namespaceLane) {
|
||||
throw agentRunRuntimeAuthorityError("agentrun_runtime_namespace_unsupported", `unsupported AgentRun HWLAB runtime namespace: ${namespace}`);
|
||||
}
|
||||
if (explicitLane && explicitLane !== namespaceLane) {
|
||||
throw agentRunRuntimeAuthorityError("agentrun_runtime_authority_mismatch", `AgentRun HWLAB runtime lane ${explicitLane} does not match namespace ${namespace} (${namespaceLane})`);
|
||||
}
|
||||
assertAgentRunEndpointNamespace(env.HWLAB_RUNTIME_API_URL, namespace, "HWLAB_RUNTIME_API_URL");
|
||||
assertAgentRunEndpointNamespace(env.HWLAB_RUNTIME_WEB_URL, namespace, "HWLAB_RUNTIME_WEB_URL");
|
||||
return { namespace, lane: namespaceLane };
|
||||
}
|
||||
|
||||
function assertAgentRunTransientRuntimeAuthority(entries, runtimeAuthority) {
|
||||
const values = new Map((Array.isArray(entries) ? entries : []).map((entry) => [entry?.name, firstNonEmpty(entry?.value)]));
|
||||
if (values.get("HWLAB_RUNTIME_NAMESPACE") !== runtimeAuthority.namespace) {
|
||||
throw agentRunRuntimeAuthorityError("agentrun_transient_namespace_mismatch", `AgentRun transient HWLAB_RUNTIME_NAMESPACE must be ${runtimeAuthority.namespace}`);
|
||||
}
|
||||
if (normalizeAgentRunRuntimeLane(values.get("HWLAB_RUNTIME_LANE")) !== runtimeAuthority.lane) {
|
||||
throw agentRunRuntimeAuthorityError("agentrun_transient_lane_mismatch", `AgentRun transient HWLAB_RUNTIME_LANE must be ${runtimeAuthority.lane}`);
|
||||
}
|
||||
for (const name of ["HWLAB_RUNTIME_API_URL", "HWLAB_RUNTIME_WEB_URL"]) {
|
||||
const value = values.get(name);
|
||||
if (!value) {
|
||||
throw agentRunRuntimeAuthorityError("agentrun_transient_endpoint_required", `AgentRun transient ${name} is required`);
|
||||
}
|
||||
assertAgentRunEndpointNamespace(value, runtimeAuthority.namespace, name);
|
||||
}
|
||||
}
|
||||
|
||||
function assertAgentRunEndpointNamespace(value, namespace, fieldName) {
|
||||
const endpointNamespace = agentRunRuntimeNamespaceForEndpoint(value);
|
||||
if (endpointNamespace && endpointNamespace !== namespace) {
|
||||
throw agentRunRuntimeAuthorityError("agentrun_runtime_endpoint_mismatch", `${fieldName} points at ${endpointNamespace}, not ${namespace}`);
|
||||
}
|
||||
}
|
||||
|
||||
function agentRunRuntimeNamespaceForEndpoint(value) {
|
||||
const match = String(value ?? "").match(/\.((?:hwlab-[a-z0-9-]+))\.svc\.cluster\.local(?::|\/|$)/iu);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function agentRunRuntimeLaneForNamespace(namespace) {
|
||||
const value = String(namespace ?? "").trim().toLowerCase();
|
||||
if (value === "hwlab-prod") return "prod";
|
||||
if (value === "hwlab-dev") return "dev";
|
||||
const versionMatch = value.match(/^hwlab-v(\d+)$/u);
|
||||
if (versionMatch) return versionAgentRunRuntimeLane(versionMatch[1]);
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeAgentRunRuntimeLane(value) {
|
||||
const profile = String(value ?? "").trim().toLowerCase();
|
||||
if (!profile) return null;
|
||||
if (profile === "prod" || profile === "production") return "prod";
|
||||
if (profile === "dev" || profile === "development" || profile === "local") return "dev";
|
||||
const dotted = profile.match(/^v?0+\.(\d+)$/u);
|
||||
if (dotted) return versionAgentRunRuntimeLane(dotted[1]);
|
||||
const compact = profile.match(/^v?0*(\d+)$/u);
|
||||
if (compact) return versionAgentRunRuntimeLane(compact[1]);
|
||||
throw agentRunRuntimeAuthorityError("agentrun_runtime_lane_unsupported", `unsupported AgentRun HWLAB runtime lane: ${String(value).trim()}`);
|
||||
}
|
||||
|
||||
function versionAgentRunRuntimeLane(value) {
|
||||
const version = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(version) || version <= 0) return null;
|
||||
return `v${String(version).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function namespaceForAgentRunRuntimeLane(lane) {
|
||||
if (lane === "prod") return "hwlab-prod";
|
||||
if (lane === "dev") return "hwlab-dev";
|
||||
if (lane) return `hwlab-${lane}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
function defaultAgentRunBranchForRuntimeLane(lane) {
|
||||
const match = String(lane ?? "").match(/^v(\d+)$/u);
|
||||
if (!match) return null;
|
||||
return `v0.${Number.parseInt(match[1], 10)}`;
|
||||
}
|
||||
|
||||
function agentRunRuntimeAuthorityError(code, message) {
|
||||
return Object.assign(new Error(message), {
|
||||
code,
|
||||
statusCode: 500,
|
||||
valuesPrinted: false
|
||||
});
|
||||
}
|
||||
|
||||
function agentRunToolCredentials(env = process.env, toolCapabilities = null) {
|
||||
const namespace = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_TOOL_SECRET_NAMESPACE, env.HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE, DEFAULT_RUNNER_NAMESPACE);
|
||||
const githubSecretName = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME, "agentrun-v01-tool-github-pr");
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createCloudApiServer } from "./server.ts";
|
||||
import { validateCodeAgentChatSchema } from "./code-agent-chat.ts";
|
||||
import { createCodexStdioSessionManager } from "./codex-stdio-session.ts";
|
||||
import { createCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
||||
import { submitAgentRunChatTurn } from "./code-agent-agentrun-adapter.ts";
|
||||
import { submitAgentRunChatTurn, syncAgentRunChatResult } from "./code-agent-agentrun-adapter.ts";
|
||||
import {
|
||||
codexStdioChatFixture,
|
||||
codexStdioReadyFixture,
|
||||
@@ -370,7 +370,7 @@ test("cloud api /v1/agent/chat supports short submit and result polling", async
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapter", async () => {
|
||||
test("cloud api /v1/agent/chat delegates v0.3 turns to AgentRun v0.1 over adapter", async () => {
|
||||
const calls = [];
|
||||
const ownerSessions = new Map();
|
||||
const conversationMessages = [];
|
||||
@@ -404,6 +404,8 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
assert.equal(body.tenantId, "hwlab");
|
||||
assert.equal(body.projectId, "pikasTech/HWLAB");
|
||||
assert.equal(body.backendProfile, "deepseek");
|
||||
assert.equal(body.workspaceRef.branch, "v0.3");
|
||||
assert.equal(body.workspaceRef.runtimeNamespace, "hwlab-v03");
|
||||
assert.equal(body.resourceBundleRef.repoUrl, "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
|
||||
assert.equal(body.resourceBundleRef.commitId, "0123456789abcdef0123456789abcdef01234567");
|
||||
assert.equal(body.resourceBundleRef.kind, "gitbundle");
|
||||
@@ -476,10 +478,10 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
const secondRunnerJob = body.commandId === "cmd_hwlab_adapter_second";
|
||||
assert.ok(body.commandId === "cmd_hwlab_adapter" || secondRunnerJob);
|
||||
const transientEnv = Object.fromEntries(body.transientEnv.map((entry) => [entry.name, entry.value]));
|
||||
assert.equal(transientEnv.HWLAB_RUNTIME_API_URL, "http://hwlab-cloud-api.hwlab-v02.svc.cluster.local:6667");
|
||||
assert.equal(transientEnv.HWLAB_RUNTIME_WEB_URL, "http://hwlab-cloud-web.hwlab-v02.svc.cluster.local:8080");
|
||||
assert.equal(transientEnv.HWLAB_RUNTIME_NAMESPACE, "hwlab-v02");
|
||||
assert.equal(transientEnv.HWLAB_RUNTIME_LANE, "v02");
|
||||
assert.equal(transientEnv.HWLAB_RUNTIME_API_URL, "http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667");
|
||||
assert.equal(transientEnv.HWLAB_RUNTIME_WEB_URL, "http://hwlab-cloud-web.hwlab-v03.svc.cluster.local:8080");
|
||||
assert.equal(transientEnv.HWLAB_RUNTIME_NAMESPACE, "hwlab-v03");
|
||||
assert.equal(transientEnv.HWLAB_RUNTIME_LANE, "v03");
|
||||
assert.equal(transientEnv.HWLAB_RUNTIME_ENDPOINT_LOCKED, "1");
|
||||
assert.equal(transientEnv.HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME, "1");
|
||||
assert.equal(transientEnv.UNIDESK_MAIN_SERVER_IP, "http://74.48.78.17:18081");
|
||||
@@ -575,8 +577,9 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
||||
HWLAB_CODE_AGENT_DEEPSEEK_MODEL: "deepseek-chat",
|
||||
HWLAB_ENVIRONMENT: "v02",
|
||||
HWLAB_GITOPS_PROFILE: "v02",
|
||||
HWLAB_ENVIRONMENT: "v03",
|
||||
HWLAB_GITOPS_PROFILE: "v03",
|
||||
HWLAB_BOOT_REF: "v0.3",
|
||||
UNIDESK_MAIN_SERVER_IP: "http://74.48.78.17:18081"
|
||||
},
|
||||
accessController: {
|
||||
@@ -829,6 +832,105 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
}
|
||||
});
|
||||
|
||||
test("AgentRun sync converts terminal command result even when run remains claimed (#1555)", async () => {
|
||||
const calls = [];
|
||||
const traceId = "trc_issue1555_terminal_command";
|
||||
const runId = "run_issue1555_claimed";
|
||||
const commandId = "cmd_issue1555_completed";
|
||||
const agentRunServer = createHttpServer(async (request, response) => {
|
||||
const url = new URL(request.url || "/", "http://127.0.0.1");
|
||||
calls.push({ method: request.method, path: url.pathname, search: url.search });
|
||||
const send = (data) => {
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_issue1555" })}\n`);
|
||||
};
|
||||
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/events`) {
|
||||
return send({ items: [
|
||||
{ id: "evt_issue1555_terminal", runId, seq: 139, type: "terminal_status", payload: { commandId, terminalStatus: "completed" }, createdAt: "2026-06-18T00:00:00.000Z" }
|
||||
] });
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands`) {
|
||||
return send({ items: [
|
||||
{ id: commandId, runId, state: "completed", type: "turn", seq: 1, idempotencyKey: traceId, payload: { traceId, conversationId: "cnv_issue1555", hwlabSessionId: "ses_issue1555", threadId: "thread_issue1555" } }
|
||||
] });
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands/${commandId}/result`) {
|
||||
return send({
|
||||
runId,
|
||||
commandId,
|
||||
attemptId: "attempt_issue1555",
|
||||
runnerId: "runner_issue1555",
|
||||
jobName: "agentrun-v01-runner-issue1555",
|
||||
namespace: "agentrun-v02",
|
||||
status: "completed",
|
||||
runStatus: "claimed",
|
||||
commandState: "completed",
|
||||
terminalStatus: "completed",
|
||||
completed: true,
|
||||
reply: "issue 1555 command result is terminal even though the reusable run is still claimed.",
|
||||
lastSeq: 139,
|
||||
eventCount: 139,
|
||||
sessionRef: { sessionId: "ses_issue1555", conversationId: "cnv_issue1555", threadId: "thread_issue1555" }
|
||||
});
|
||||
}
|
||||
response.writeHead(404, { "content-type": "application/json" });
|
||||
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
|
||||
});
|
||||
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
|
||||
const agentRunPort = agentRunServer.address().port;
|
||||
const traceStore = createCodeAgentTraceStore();
|
||||
try {
|
||||
const currentResult = {
|
||||
ok: true,
|
||||
accepted: true,
|
||||
shortConnection: true,
|
||||
status: "running",
|
||||
traceId,
|
||||
conversationId: "cnv_issue1555",
|
||||
sessionId: "ses_issue1555",
|
||||
threadId: "thread_issue1555",
|
||||
agentRun: {
|
||||
adapter: "agentrun-v01",
|
||||
managerUrl: `http://127.0.0.1:${agentRunPort}`,
|
||||
runId,
|
||||
commandId,
|
||||
status: "pending",
|
||||
runStatus: "claimed",
|
||||
commandState: "pending",
|
||||
terminalStatus: null,
|
||||
lastSeq: 0,
|
||||
valuesPrinted: false
|
||||
},
|
||||
valuesPrinted: false
|
||||
};
|
||||
const synced = await syncAgentRunChatResult({
|
||||
traceId,
|
||||
currentResult,
|
||||
traceStore,
|
||||
options: {
|
||||
env: {
|
||||
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
|
||||
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.equal(synced.resultSynced, true);
|
||||
assert.equal(synced.result.status, "completed");
|
||||
assert.equal(synced.result.agentRun.runStatus, "claimed");
|
||||
assert.equal(synced.result.agentRun.commandState, "completed");
|
||||
assert.equal(synced.result.agentRun.terminalStatus, "completed");
|
||||
assert.match(synced.result.reply.content, /issue 1555 command result is terminal/u);
|
||||
assert.deepEqual(calls.map((call) => call.path), [
|
||||
`/api/v1/runs/${runId}/commands`,
|
||||
`/api/v1/runs/${runId}/events`,
|
||||
`/api/v1/runs/${runId}/commands/${commandId}/result`
|
||||
]);
|
||||
assert.ok(traceStore.snapshot(traceId).events.some((event) => event.label === "agentrun:result:completed"));
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api AgentRun adapter reports persistent thread resume when a completed run needs a new runner", async () => {
|
||||
const calls = [];
|
||||
const hwlabSessionId = "ses_server-test-thread-resume";
|
||||
|
||||
@@ -483,8 +483,10 @@ async function codeAgentAuthEnv(options = {}, env = process.env, params = {}) {
|
||||
const key = ownerUserId && store?.findActiveDefaultApiKeyForUser
|
||||
? await store.findActiveDefaultApiKeyForUser(ownerUserId)
|
||||
: null;
|
||||
const envRuntimeLane = runtimeLaneForEnv(env);
|
||||
const runtimeNamespace = runtimeNamespaceForEnv(env);
|
||||
const runtimeLane = runtimeLaneForNamespace(runtimeNamespace) ?? runtimeLaneForEnv(env);
|
||||
const namespaceRuntimeLane = runtimeLaneForNamespace(runtimeNamespace);
|
||||
const runtimeLane = resolveRuntimeLaneAuthority({ envRuntimeLane, runtimeNamespace, namespaceRuntimeLane });
|
||||
const inClusterApiUrl = internalCodeAgentApiUrl(env, runtimeNamespace);
|
||||
const inClusterWebUrl = internalCodeAgentWebUrl(env, runtimeNamespace);
|
||||
const apiUrl = firstNonEmptyValue(
|
||||
@@ -529,16 +531,21 @@ function runtimeNamespaceForEnv(env = process.env) {
|
||||
}
|
||||
|
||||
function runtimeLaneForEnv(env = process.env) {
|
||||
const profile = String(firstNonEmptyValue(env.HWLAB_RUNTIME_LANE, env.HWLAB_GITOPS_LANE, env.HWLAB_GITOPS_PROFILE, env.HWLAB_ENVIRONMENT, env.HWLAB_BOOT_REF, "dev") ?? "dev").trim().toLowerCase();
|
||||
if (profile === "prod" || profile === "production") return "prod";
|
||||
if (profile === "v02" || profile === "v0.2" || profile === "0.2") return "v02";
|
||||
return "dev";
|
||||
const raw = firstNonEmptyValue(env.HWLAB_RUNTIME_LANE, env.HWLAB_GITOPS_LANE, env.HWLAB_GITOPS_PROFILE, env.HWLAB_ENVIRONMENT, env.HWLAB_BOOT_REF);
|
||||
if (!raw) return null;
|
||||
const lane = normalizeRuntimeLane(raw);
|
||||
if (!lane) {
|
||||
throw runtimeAuthorityError("unsupported_runtime_lane", `unsupported HWLAB runtime lane: ${String(raw).trim()}`);
|
||||
}
|
||||
return lane;
|
||||
}
|
||||
|
||||
function runtimeLaneForNamespace(namespace) {
|
||||
if (namespace === "hwlab-v02") return "v02";
|
||||
if (namespace === "hwlab-prod") return "prod";
|
||||
if (namespace === "hwlab-dev") return "dev";
|
||||
const value = String(namespace ?? "").trim().toLowerCase();
|
||||
if (value === "hwlab-prod") return "prod";
|
||||
if (value === "hwlab-dev") return "dev";
|
||||
const versionMatch = value.match(/^hwlab-v(\d+)$/u);
|
||||
if (versionMatch) return versionRuntimeLane(versionMatch[1]);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -559,17 +566,57 @@ function runtimeNamespaceForEndpoint(value) {
|
||||
|
||||
function runtimeLaneForEndpoint(value) {
|
||||
const text = String(value ?? "").toLowerCase();
|
||||
if (/hwlab-v02|:19666(?:\/|$)|:19667(?:\/|$)/u.test(text)) return "v02";
|
||||
const namespaceLane = runtimeLaneForNamespace(runtimeNamespaceForEndpoint(text));
|
||||
if (namespaceLane) return namespaceLane;
|
||||
if (/hwlab-prod|:18666(?:\/|$)|:18667(?:\/|$)/u.test(text)) return "prod";
|
||||
if (/hwlab-dev|:17666(?:\/|$)|:17667(?:\/|$)|:16666(?:\/|$)|:16667(?:\/|$)/u.test(text)) return "dev";
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeRuntimeLane(value) {
|
||||
const profile = String(value ?? "").trim().toLowerCase();
|
||||
if (!profile) return null;
|
||||
if (profile === "prod" || profile === "production") return "prod";
|
||||
if (profile === "dev" || profile === "development" || profile === "local") return "dev";
|
||||
const dotted = profile.match(/^v?0+\.(\d+)$/u);
|
||||
if (dotted) return versionRuntimeLane(dotted[1]);
|
||||
const compact = profile.match(/^v?0*(\d+)$/u);
|
||||
if (compact) return versionRuntimeLane(compact[1]);
|
||||
return null;
|
||||
}
|
||||
|
||||
function versionRuntimeLane(value) {
|
||||
const version = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(version) || version <= 0) return null;
|
||||
return `v${String(version).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function resolveRuntimeLaneAuthority({ envRuntimeLane, runtimeNamespace, namespaceRuntimeLane }) {
|
||||
if (!namespaceRuntimeLane) {
|
||||
throw runtimeAuthorityError("unsupported_runtime_namespace", `unsupported HWLAB runtime namespace: ${runtimeNamespace}`);
|
||||
}
|
||||
if (envRuntimeLane && envRuntimeLane !== namespaceRuntimeLane) {
|
||||
throw runtimeAuthorityError(
|
||||
"runtime_authority_mismatch",
|
||||
`HWLAB runtime lane ${envRuntimeLane} does not match namespace ${runtimeNamespace} (${namespaceRuntimeLane})`
|
||||
);
|
||||
}
|
||||
return namespaceRuntimeLane;
|
||||
}
|
||||
|
||||
function runtimeAuthorityError(code, message) {
|
||||
return Object.assign(new Error(message), {
|
||||
code,
|
||||
statusCode: 500,
|
||||
valuesPrinted: false
|
||||
});
|
||||
}
|
||||
|
||||
function defaultRuntimeNamespace(env = process.env) {
|
||||
const profile = runtimeLaneForEnv(env);
|
||||
const profile = runtimeLaneForEnv(env) ?? "dev";
|
||||
if (profile === "prod") return "hwlab-prod";
|
||||
if (profile === "v02") return "hwlab-v02";
|
||||
return "hwlab-dev";
|
||||
if (profile === "dev") return "hwlab-dev";
|
||||
return `hwlab-${profile}`;
|
||||
}
|
||||
|
||||
function localCloudApiUrl(env = process.env) {
|
||||
@@ -807,7 +854,13 @@ function scheduleAgentRunProjectionSync({ traceId, params = {}, options = {}, tr
|
||||
isTerminal: isTraceCommandTerminalStatus,
|
||||
activitySignature: agentRunProjectionActivitySignature,
|
||||
sleep: sleepAgentRunProjectionSync,
|
||||
syncResult: ({ result }) => syncAgentRunChatResult({ traceId, currentResult: result, options: refreshOptions, traceStore }),
|
||||
syncResult: ({ result }) => syncAgentRunChatResult({
|
||||
traceId,
|
||||
currentResult: result,
|
||||
options: refreshOptions,
|
||||
traceStore,
|
||||
forceResultSync: agentRunProjectionObservedTerminal(result, traceStore?.snapshot?.(traceId))
|
||||
}),
|
||||
onTerminal: (payload, finalizerOptions = {}) => scheduleCodeAgentTerminalTurnStatusEffects({ payload, params, options, preserveLastTraceId: finalizerOptions.preserveLastTraceId === true })
|
||||
});
|
||||
}
|
||||
@@ -839,6 +892,32 @@ function agentRunProjectionActivitySignature(result, runnerTrace) {
|
||||
].map((value) => value ?? "").join("|");
|
||||
}
|
||||
|
||||
function agentRunProjectionObservedTerminal(result, runnerTrace) {
|
||||
const agentRun = result?.agentRun && typeof result.agentRun === "object" ? result.agentRun : {};
|
||||
if (
|
||||
isTraceCommandTerminalStatus(result?.status) ||
|
||||
isTraceCommandTerminalStatus(agentRun.status) ||
|
||||
isTraceCommandTerminalStatus(agentRun.runStatus) ||
|
||||
isTraceCommandTerminalStatus(agentRun.commandState) ||
|
||||
isTraceCommandTerminalStatus(agentRun.terminalStatus)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const commandId = textValue(agentRun.commandId);
|
||||
const trace = runnerTrace && typeof runnerTrace === "object" ? runnerTrace : result?.runnerTrace;
|
||||
const events = Array.isArray(trace?.events) ? trace.events : [];
|
||||
return events.some((event) => {
|
||||
const payload = event?.payload && typeof event.payload === "object" ? event.payload : {};
|
||||
const eventCommandId = textValue(event?.commandId ?? payload.commandId);
|
||||
if (commandId && eventCommandId && eventCommandId !== commandId) return false;
|
||||
return event?.terminal === true ||
|
||||
isTraceCommandTerminalStatus(event?.status) ||
|
||||
isTraceCommandTerminalStatus(payload.status) ||
|
||||
isTraceCommandTerminalStatus(payload.terminalStatus) ||
|
||||
payload.phase === "command-terminal";
|
||||
});
|
||||
}
|
||||
|
||||
function sleepAgentRunProjectionSync(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
||||
}
|
||||
|
||||
@@ -649,33 +649,70 @@ function acceptChatTurn(body: JsonRecord): JsonRecord {
|
||||
|
||||
function liveBackfillEarlyTrace(sessionId: string, threadId: string | null, traceId: string): JsonRecord {
|
||||
const createdAt = new Date().toISOString();
|
||||
const agentRun = liveBackfillAgentRun("pending", null, 4);
|
||||
return {
|
||||
traceId,
|
||||
status: "running",
|
||||
running: true,
|
||||
terminal: false,
|
||||
sessionId,
|
||||
threadId,
|
||||
agentRun,
|
||||
events: [
|
||||
{ seq: 1, sourceSeq: 1, createdAt, label: "agentrun:request:accepted", type: "backend_status", status: "running" },
|
||||
{ seq: 2, sourceSeq: 2, createdAt, label: "agentrun:run:created", type: "backend_status", status: "running" },
|
||||
{ seq: 3, sourceSeq: 3, createdAt, label: "agentrun:command:created", type: "backend_status", status: "running" },
|
||||
{ seq: 4, sourceSeq: 4, createdAt, label: "agentrun:runner-job:created", type: "backend_status", status: "running" }
|
||||
{ seq: 1, sourceSeq: 1, createdAt, label: "agentrun:request:accepted", type: "backend_status", status: "running", payload: { commandId: agentRun.commandId } },
|
||||
{ seq: 2, sourceSeq: 2, createdAt, label: "agentrun:run:created", type: "backend_status", status: "running", payload: { runId: agentRun.runId, runStatus: agentRun.runStatus } },
|
||||
{ seq: 3, sourceSeq: 3, createdAt, label: "agentrun:command:created", type: "backend_status", status: "running", payload: { commandId: agentRun.commandId, commandState: agentRun.commandState } },
|
||||
{ seq: 4, sourceSeq: 4, createdAt, label: "agentrun:runner-job:created", type: "backend_status", status: "running", payload: { commandId: agentRun.commandId, jobName: agentRun.jobName, namespace: agentRun.namespace } }
|
||||
],
|
||||
eventCount: 4,
|
||||
fullTraceLoaded: true,
|
||||
hasMore: false
|
||||
hasMore: false,
|
||||
traceSummary: { source: "agentrun-events", agentRun, valuesPrinted: false }
|
||||
};
|
||||
}
|
||||
|
||||
function liveBackfillCompletedTrace(sessionId: string, threadId: string | null, traceId: string): JsonRecord {
|
||||
const createdAt = new Date().toISOString();
|
||||
const finalText = "fake AgentRun completed after REST backfill without terminal SSE.";
|
||||
const agentRun = liveBackfillAgentRun("completed", "completed", 23);
|
||||
const events = [
|
||||
...(liveBackfillEarlyTrace(sessionId, threadId, traceId).events as JsonRecord[]),
|
||||
{ seq: 5, sourceSeq: 21, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "completed", message: finalText },
|
||||
{ seq: 6, sourceSeq: 22, createdAt, label: "item/commandExecution:completed", type: "commandExecution", status: "completed", stdout: "OK\n" },
|
||||
{ seq: 7, sourceSeq: 23, createdAt, label: "agentrun:backend:turn/completed", type: "backend_status", status: "completed", terminal: true }
|
||||
{ seq: 5, sourceSeq: 21, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "completed", message: finalText, payload: { commandId: agentRun.commandId } },
|
||||
{ seq: 6, sourceSeq: 22, createdAt, label: "item/commandExecution:completed", type: "commandExecution", status: "completed", stdout: "OK\n", payload: { commandId: agentRun.commandId, exitCode: 0 } },
|
||||
{ seq: 7, sourceSeq: 23, createdAt, label: "agentrun:backend:turn/completed", type: "backend_status", status: "completed", terminal: true, payload: { commandId: agentRun.commandId, phase: "command-terminal", terminalStatus: "completed" } }
|
||||
];
|
||||
return { traceId, status: "completed", sessionId, threadId, events, eventCount: events.length, fullTraceLoaded: true, hasMore: false, finalResponse: { text: finalText, status: "completed" } };
|
||||
return {
|
||||
traceId,
|
||||
status: "completed",
|
||||
running: false,
|
||||
terminal: true,
|
||||
sessionId,
|
||||
threadId,
|
||||
agentRun,
|
||||
events,
|
||||
eventCount: events.length,
|
||||
fullTraceLoaded: true,
|
||||
hasMore: false,
|
||||
finalResponse: { text: finalText, status: "completed" },
|
||||
traceSummary: { source: "agentrun-command-result", terminalStatus: "completed", agentRun, valuesPrinted: false }
|
||||
};
|
||||
}
|
||||
|
||||
function liveBackfillAgentRun(commandState: string, terminalStatus: string | null, lastSeq: number): JsonRecord {
|
||||
return {
|
||||
adapter: "agentrun-v01",
|
||||
runId: "run_live_backfill_issue1555",
|
||||
commandId: "cmd_live_backfill_issue1555",
|
||||
status: commandState,
|
||||
runStatus: "claimed",
|
||||
commandState,
|
||||
terminalStatus,
|
||||
runnerStatus: "running",
|
||||
namespace: "agentrun-v02",
|
||||
jobName: "agentrun-v01-runner-live-backfill-issue1555",
|
||||
lastSeq,
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleLiveBackfillProjection(sessionId: string, threadId: string | null, traceId: string, readyAtMs: number): void {
|
||||
|
||||
@@ -20,6 +20,25 @@ test.describe("live REST backfill without terminal SSE", () => {
|
||||
return response.ok() ? ((await response.json()).turn?.status as string | undefined) : undefined;
|
||||
}).toBe("completed");
|
||||
|
||||
const turnResponse = await page.request.get("/v1/workbench/turns/trc_live_backfill");
|
||||
expect(turnResponse.ok()).toBe(true);
|
||||
const turnPayload = await turnResponse.json() as {
|
||||
turn?: {
|
||||
running?: boolean;
|
||||
terminal?: boolean;
|
||||
agentRun?: { runStatus?: string; commandState?: string; terminalStatus?: string | null; namespace?: string };
|
||||
traceSummary?: { source?: string; terminalStatus?: string };
|
||||
};
|
||||
};
|
||||
expect(turnPayload.turn?.running).toBe(false);
|
||||
expect(turnPayload.turn?.terminal).toBe(true);
|
||||
expect(turnPayload.turn?.agentRun?.runStatus).toBe("claimed");
|
||||
expect(turnPayload.turn?.agentRun?.commandState).toBe("completed");
|
||||
expect(turnPayload.turn?.agentRun?.terminalStatus).toBe("completed");
|
||||
expect(turnPayload.turn?.agentRun?.namespace).toBe("agentrun-v02");
|
||||
expect(turnPayload.turn?.traceSummary?.source).toBe("agentrun-command-result");
|
||||
expect(turnPayload.turn?.traceSummary?.terminalStatus).toBe("completed");
|
||||
|
||||
const listResponse = await page.request.get("/v1/workbench/sessions?includeSessionId=ses_completed");
|
||||
const messagesResponse = await page.request.get("/v1/workbench/sessions/ses_completed/messages?limit=100");
|
||||
expect(listResponse.ok()).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user