fix: complete v03 AgentRun workbench projection

This commit is contained in:
lyon
2026-06-19 03:29:55 +08:00
parent 99a8d92b0e
commit cee702726c
5 changed files with 378 additions and 32 deletions
+111 -2
View File
@@ -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");
+110 -8
View File
@@ -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";
+92 -13
View File
@@ -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);