fix: reuse warm AgentRun runner for HWLAB turns

This commit is contained in:
lyon
2026-06-26 08:57:42 +08:00
parent ff15725fcb
commit c9dd8150f2
2 changed files with 221 additions and 80 deletions
+109 -80
View File
@@ -1,4 +1,4 @@
// 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.
// 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; draft-2026-06-25-p0-session-warm-runner-contract; 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 { randomUUID } from "node:crypto";
@@ -320,8 +320,8 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
type: "request",
status: "accepted",
label: "agentrun:request:accepted",
message: "HWLAB Code Agent request accepted by the AgentRun v0.1 adapter; hwlab-cloud-api will create a fresh run/command tied to the session-scoped AgentRun PVC, then ensure the runner Job outside the browser admission path.",
waitingFor: "agentrun-run-create",
message: "HWLAB Code Agent request accepted by the AgentRun v0.1 adapter; hwlab-cloud-api will reuse a warm session run when its runner lease is valid, otherwise create a fresh run and runner Job outside the browser admission path.",
waitingFor: "agentrun-run-reuse-check",
adapter: ADAPTER_ID,
managerHost: new URL(managerUrl).hostname,
...codeAgentOtelTraceFields(traceId, env),
@@ -331,10 +331,12 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
const reusable = await resolveReusableAgentRun({ params, options, env, managerUrl, fetchImpl, timeoutMs, backendProfile, traceId, traceStore });
if (reusable?.mapping) {
const reusableParams = paramsWithAgentRunSessionThread(params, reusable.mapping.threadId);
const commandInput = buildAgentRunCommandInput({ params: reusableParams, traceId, backendProfile, sessionId: reusable.sessionId });
let command = null;
let commandId = null;
try {
const reusableParams = paramsWithAgentRunSessionThread(params, reusable.mapping.threadId);
const commandInput = buildAgentRunCommandInput({ params: reusableParams, traceId, backendProfile, sessionId: reusable.sessionId });
const command = await agentRunDispatchJson({ fetchImpl, managerUrl, path: `/api/v1/runs/${encodeURIComponent(reusable.mapping.runId)}/commands`,
command = await agentRunDispatchJson({ fetchImpl, managerUrl, path: `/api/v1/runs/${encodeURIComponent(reusable.mapping.runId)}/commands`,
method: "POST",
body: commandInput,
timeoutMs,
@@ -346,69 +348,7 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
stage: "command-create-reused",
terminalOnFailure: false
});
const commandId = requiredString(command?.id, "command.id");
traceStore.append(traceId, agentRunTraceEvent({
type: "backend",
status: "running",
label: "agentrun:command:created",
message: `AgentRun command ${commandId} created on reused run ${reusable.mapping.runId}; hwlab-cloud-api will ensure a runner Job is available for this turn.`,
runId: reusable.mapping.runId,
commandId,
backendProfile,
waitingFor: "agentrun-runner-job-ensure",
valuesPrinted: false
}, backendProfile));
let runnerJob = null;
try {
const runnerJobInput = buildAgentRunRunnerJobInput({ env, traceId, commandId, ownerApiKey, toolCapabilities, backendProfile });
runnerJob = await agentRunDispatchJson({ fetchImpl, managerUrl, path: `/api/v1/runs/${encodeURIComponent(reusable.mapping.runId)}/runner-jobs`,
method: "POST",
body: runnerJobInput,
timeoutMs,
env,
traceStore,
traceId,
backendProfile,
runId: reusable.mapping.runId,
commandId,
stage: "runner-job-ensure-reused",
terminalOnFailure: false
});
} catch (error) {
if (!isAgentRunCommandAlreadyClaimed(error)) throw error;
traceStore.append(traceId, agentRunTraceEvent({
type: "backend",
status: "running",
label: "agentrun:runner-job:already-active",
message: `AgentRun command ${commandId} is already claimed by an active runner; no replacement runner Job is needed for this turn.`,
errorCode: error?.code ?? "agentrun_command_already_claimed",
runId: reusable.mapping.runId,
commandId,
runnerId: reusable.mapping.runnerId ?? null,
jobName: reusable.mapping.jobName ?? null,
namespace: reusable.mapping.namespace ?? null,
waitingFor: "agentrun-result",
valuesPrinted: false
}, backendProfile));
}
const mapping = agentRunReusedMapping({ previous: reusable.mapping, run: reusable.run, command, runnerJob, traceId, startedAt, backendProfile, managerUrl, env });
traceStore.append(traceId, agentRunTraceEvent({
type: "backend",
status: "running",
label: runnerJob ? "agentrun:runner-job:ensured" : "agentrun:runner-job:reused",
message: runnerJob
? `AgentRun runner Job ${mapping.jobName ?? "unknown"} ensured for reused run ${mapping.runId}; this keeps the persistent session resumable after pod replacement.`
: `AgentRun runner Job ${mapping.jobName ?? "unknown"} is already active for this HWLAB session turn.`,
runId: mapping.runId,
commandId: mapping.commandId,
attemptId: mapping.attemptId,
runnerId: mapping.runnerId,
jobName: mapping.jobName,
namespace: mapping.namespace,
waitingFor: "agentrun-result",
valuesPrinted: false
}, mapping));
return decorateAgentRunRunningResult({ base: initialAgentRunChatResult({ params: reusableParams, options, traceId }), mapping, traceStore, traceId });
commandId = requiredString(command?.id, "command.id");
} catch (error) {
traceStore.append(traceId, agentRunTraceEvent({
type: "backend",
@@ -421,6 +361,40 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
waitingFor: "agentrun-run-create",
valuesPrinted: false
}, backendProfile));
command = null;
commandId = null;
}
if (command && commandId) {
traceStore.append(traceId, agentRunTraceEvent({
type: "backend",
status: "running",
label: "agentrun:command:created",
message: `AgentRun command ${commandId} created on reused run ${reusable.mapping.runId}; the active runner will pick it up without creating another runner Job.`,
runId: reusable.mapping.runId,
commandId,
backendProfile,
waitingFor: "agentrun-result",
valuesPrinted: false
}, backendProfile));
const runnerJob = null;
const mapping = agentRunReusedMapping({ previous: reusable.mapping, run: reusable.run, command, runnerJob, traceId, startedAt, backendProfile, managerUrl, env });
traceStore.append(traceId, agentRunTraceEvent({
type: "backend",
status: "running",
label: runnerJob ? "agentrun:runner-job:ensured" : "agentrun:runner-job:reused",
message: runnerJob
? `AgentRun runner Job ${mapping.jobName ?? "unknown"} ensured for reused run ${mapping.runId}; this keeps the persistent session resumable after pod replacement.`
: `AgentRun runner Job ${mapping.jobName ?? "unknown"} remains active for this HWLAB session turn; no replacement Job was created.`,
runId: mapping.runId,
commandId: mapping.commandId,
attemptId: mapping.attemptId,
runnerId: mapping.runnerId,
jobName: mapping.jobName,
namespace: mapping.namespace,
waitingFor: "agentrun-result",
valuesPrinted: false
}, mapping));
return decorateAgentRunRunningResult({ base: initialAgentRunChatResult({ params: reusableParams, options, traceId }), mapping, traceStore, traceId });
}
}
@@ -698,12 +672,6 @@ function sleepAgentRunDispatchRetry(ms) {
return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
}
function isAgentRunCommandAlreadyClaimed(error) {
const statusCode = Number(error?.statusCode ?? 0);
const message = String(error?.message ?? error?.agentRunError?.message ?? "");
return statusCode === 409 && /command\s+[^\s]+\s+is not pending:/u.test(message);
}
export async function syncAgentRunChatResult({ traceId, currentResult = null, options = {}, traceStore = defaultCodeAgentTraceStore, appendResultEvent = true, refreshEvents = true, forceResultSync = false }) {
const initial = currentResult ?? await loadPersistedAgentRunResult(traceId, options);
if (isAgentRunUserCancelSealed(initial)) {
@@ -1683,22 +1651,83 @@ async function resolveReusableAgentRun({ params = {}, options = {}, env = proces
}, backendProfile));
return null;
}
let run = null;
try {
run = await agentRunDispatchJson({
fetchImpl,
managerUrl,
path: `/api/v1/runs/${encodeURIComponent(mapping.runId)}`,
method: "GET",
timeoutMs,
env,
traceStore,
traceId,
backendProfile,
runId: mapping.runId,
stage: "run-reuse-check",
terminalOnFailure: false
});
} catch (error) {
traceStore.append(traceId, agentRunTraceEvent({
type: "backend",
status: "running",
label: "agentrun:run:reuse-blocked",
message: `Stored AgentRun run ${mapping.runId} could not be read from AgentRun manager; hwlab-cloud-api will create a fresh run.`,
errorCode: error?.code ?? error?.agentRunError?.failureKind ?? "agentrun_reuse_check_failed",
runId: mapping.runId,
commandId: mapping.commandId ?? null,
runnerId: mapping.runnerId ?? null,
jobName: mapping.jobName ?? null,
namespace: mapping.namespace ?? null,
sessionId: hwlabSessionId,
agentRunSessionId: mapping.sessionId ?? null,
waitingFor: "agentrun-run-create",
valuesPrinted: false
}, backendProfile));
return null;
}
const blocker = agentRunReuseBlocker(run, mapping);
if (blocker) {
traceStore.append(traceId, agentRunTraceEvent({
type: "backend",
status: "running",
label: "agentrun:run:reuse-blocked",
message: `Stored AgentRun run ${mapping.runId} cannot be reused (${blocker}); hwlab-cloud-api will create a fresh run while preserving the session-scoped AgentRun PVC.`,
runId: mapping.runId,
commandId: mapping.commandId ?? null,
runnerId: mapping.runnerId ?? null,
jobName: mapping.jobName ?? null,
namespace: mapping.namespace ?? null,
sessionId: hwlabSessionId,
agentRunSessionId: mapping.sessionId ?? null,
reuseBlocker: blocker,
runStatus: run?.terminalStatus ?? run?.status ?? mapping.runStatus ?? null,
leaseExpiresAt: run?.leaseExpiresAt ?? null,
waitingFor: "agentrun-run-create",
valuesPrinted: false
}, backendProfile));
return null;
}
const agentRunScopedSessionId = safeSessionId(run?.sessionRef?.sessionId) || safeSessionId(mapping.sessionId) || scopedAgentRunSessionIdForParams(params, traceId);
traceStore.append(traceId, agentRunTraceEvent({
type: "backend",
status: "running",
label: "agentrun:run:reuse-disabled",
message: `Stored AgentRun run ${mapping.runId} is not reused for a new HWLAB turn; hwlab-cloud-api will create a fresh run while preserving the session-scoped AgentRun PVC.`,
label: "agentrun:run:reuse-enabled",
message: `Stored AgentRun run ${mapping.runId} is still claimed by an active runner; hwlab-cloud-api will append the next command without creating another runner Job.`,
runId: mapping.runId,
commandId: mapping.commandId ?? null,
runnerId: mapping.runnerId ?? null,
jobName: mapping.jobName ?? null,
namespace: mapping.namespace ?? null,
sessionId: hwlabSessionId,
agentRunSessionId: mapping.sessionId ?? null,
waitingFor: "agentrun-run-create",
agentRunSessionId: agentRunScopedSessionId,
runStatus: run?.status ?? mapping.runStatus ?? null,
claimedBy: run?.claimedBy ?? null,
leaseExpiresAt: run?.leaseExpiresAt ?? null,
waitingFor: "agentrun-command-create",
valuesPrinted: false
}, backendProfile));
return null;
return { mapping, run, sessionId: agentRunScopedSessionId };
}
function isReusableAgentRunMapping(mapping, backendProfile) {