fix: reuse warm AgentRun runner for HWLAB turns
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user