fix: reuse AgentRun runner for HWLAB turns (#662)
Co-authored-by: Codex <codex@local>
This commit is contained in:
@@ -158,9 +158,9 @@ Forwarder 不需要人工维护长驻进程。正式固化后,它应由 Kubern
|
||||
只有“真实 DEV 路由 + `completed` + 非空 assistant reply”能作为 DEV-LIVE 回复通过依据。
|
||||
不得把 mock、fixture、本地 echo、source report、静态检查或前端状态当作通过。
|
||||
|
||||
AgentRun 接入后,`agentrun-v01` 只能表示执行基础设施,不等同于真实 provider/model。Code Agent result `completed` 要被 Web 或 CLI 标记为真实完成,必须同时具备 final assistant response、真实 provider/model、traceId、conversationId/sessionId 和可回放 trace;否则应显示“完成证据不足”,不能用 SOURCE、fixture、echo、mock 或 stub 补齐。
|
||||
AgentRun 接入后,`agentrun-v01` 只能表示执行基础设施,不等同于真实 provider/model。Code Agent result `completed` 要被 Web 或 CLI 标记为真实完成,必须同时具备 final assistant response、真实 provider/model、`providerTrace`、traceId、conversationId/sessionId 和可回放 trace;否则应显示“完成证据不足”,不能用 SOURCE、fixture、echo、mock 或 stub 补齐。AgentRun adapter 应把真实执行形态暴露为 `codex-stdio` / Codex app-server stdio 等价长会话,把 `agentrun-v01` 保留在 adapter、runner 或 infrastructure 字段里。
|
||||
|
||||
同一 conversation/session 的后续消息应优先进入既有 runner/session 的新 turn,避免每次消息都重新 bundle、重新启动 runner、再把历史 thread 当作新 runner 可 resume 的 rollout。必须新建 runner 时,trace/result 要说明原因,便于区分 runner 生命周期缺口和业务回答失败。
|
||||
同一 conversation/session 的后续消息必须在 AgentRun runner lease 有效时进入既有 run/runner 的新 command/turn,避免每次消息都重新 bundle、重新启动 runner、再把历史 thread 当作新 runner 可 resume 的 rollout。每条消息都重新 bundle 或启动新 runner 是功能缺口,不得只靠 trace 说明包装成通过。只有 run 已 terminal、runner lease 过期或 AgentRun manager 明确拒绝复用时才允许新建 runner;trace/result 可以说明原因,但该说明只是诊断证据,不替代复用能力本身。
|
||||
|
||||
Trace 展示问题按“同源 row 转换优先”排查:先用 `hwlab-cli client agent trace <traceId> --render web` 输出 Cloud Web 同一 trace row 摘要;CLI 也乱说明 row 转换逻辑问题,CLI 正常而浏览器乱再查 DOM、CSS、滚动和增量 patch。
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
- `client agent send` 是 Cloud Web Code Agent composer 的非视觉等价入口。它必须支持 `--from-trace`、`--conversation-id`、`--session-id`、`--thread-id` 和 `--retry-of`,并在输出中返回 redacted continuation 摘要,证明本次 CLI 请求是否覆盖 Web 的继续会话路径。浏览器 issue 中已经给出 traceId 时,复现命令优先使用 `--from-trace <traceId>`,让 CLI 先走 `/v1/agent/chat/inspect` 读取 Web 上下文,再提交同源 `/v1/agent/chat`。
|
||||
- `client agent send` 默认先恢复账号 workspace,再向 `/v1/agent/chat` 发送 `workspaceId` 和 `expectedWorkspaceRevision`;服务端接受后 CLI 保存新的 workspace revision,终态轮询后再 PATCH workspace 清理终态 `activeTraceId`。只有显式 `--no-workspace` 才跳过这一默认恢复路径。
|
||||
- `client agent trace <traceId> --render web` 必须调用 Cloud Web trace row 的同一纯转换路径,输出 `render="web"`、renderer 标识、source event count、rendered row count 和 row 摘要。浏览器 trace 展示错乱时,必须先用该 CLI 入口确认 Web 渲染转换是否已经乱序、重复、缺 final response 或吞掉关键 row,再继续修浏览器 DOM/CSS。
|
||||
- AgentRun v0.1 短连接 runner 不保证历史 Web/Codex `threadId` 可在新 runner Job 内 resume;CLI 仍应把 Web 提交的 `threadId` 原样送到 Cloud Web API,以便验证 adapter 是否正确把它降级为 `requestedThreadId` 元数据,而不是让 runner 执行旧 thread resume。
|
||||
- AgentRun v0.1 短连接 runner 已要求支持同 run/runner 多轮 command。CLI 仍应把 Web 提交的 `conversationId/sessionId/threadId` 原样送到 Cloud Web API,用于验证 adapter 是否在 runner lease 有效时复用同一个 AgentRun `runId` / `jobName` 并创建新 `commandId`;每轮都新建 runner 或重新 bundle 不是通过状态,trace 中的原因说明只能用于定位。
|
||||
- `client harness`、`client harness-ops` 和 `client harness-opt` 吸收 G14 harness-ops 的短连接业务能力:health、submit、result、trace、wait 和 audit。
|
||||
- harness 系列命令只调用 Cloud Web/Code Agent 同源 API,不创建镜像、Job、常驻服务,也不执行 hot-sync、kubectl cp 或硬编码 namespace/pod 的运行面写路径。
|
||||
- 旧 `hwlab-cli cicd`、fixture MVP gate 和 CLI 镜像/Job 口径属于废弃路径;开发中遇到这些旧门禁、旧测试或旧预检时直接删除,不再维护兼容。
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
- Cloud Web 与 `hwlab-cli client` 必须共享同一组非视觉业务 API。浏览器遇到的 Code Agent continuation、trace/result、device-pod list/status 和 device-pod job 问题,必须能通过 `hwlab-cli client` 走同一 `19666` Cloud Web path 复现;不能让 CLI 长期绕到 `19667` Cloud API 后把 Web 路径缺口误判为业务已通过。
|
||||
- 浏览器启动后必须从 `GET /v1/workbench/workspace` hydrate 账号 workspace;同一个账号在多个浏览器标签页、多个浏览器或 CLI profile 中应看到同一个 `workspaceId`、selected conversation/session/thread、provider profile 和 active trace。浏览器 localStorage 只能作为短期缓存,并必须绑定 actor,不能作为 workspace authority。
|
||||
- Cloud Web trace 展示与 `hwlab-cli client agent trace --render web` 必须共享同一套 trace row 纯转换路径。Web 发生 row 顺序错乱、final response 缺失、assistant 消息被吞或噪声事件淹没时,先用 CLI 输出同一渲染 row 摘要复现;CLI 可复现说明是 trace row 转换问题,CLI 不可复现再进入 DOM/CSS/滚动状态调查。
|
||||
- Code Agent result `completed` 只有在同时包含真实 provider/model/trace/conversation 元数据和可展示的 final assistant response 时,才能被 Web 标记为真实完成;`provider=agentrun-v01` 只是执行基础设施标识,不得替代上游 provider/model,也不得把 SOURCE、fixture、echo、mock 或 stub 当成 DEV-LIVE 完成。
|
||||
- 同一 conversation/session 的后续用户消息应优先复用已存在的 AgentRun runner/session 继续 turn;只有 runner 不可用、已过期或协议明确要求新 runner 时才重新 bundle 和启动 runner。每条消息都重新 bundle/runner 属于 v0.2 AgentRun 接入缺口,必须在 trace 中显式可见。
|
||||
- Code Agent result `completed` 只有在同时包含真实 provider/model/trace/conversation 元数据、`providerTrace` 和可展示的 final assistant response 时,才能被 Web 标记为真实完成;`provider=agentrun-v01` 只是执行基础设施标识,不得替代上游 provider/model,也不得把 SOURCE、fixture、echo、mock 或 stub 当成 DEV-LIVE 完成。
|
||||
- 同一 conversation/session 的后续用户消息必须在 AgentRun runner lease 有效时复用已存在的 AgentRun run/runner 继续新 command/turn;只有 runner 不可用、已过期或协议明确要求新 runner 时才重新 bundle 和启动 runner。每条消息都重新 bundle/runner 属于 v0.2 AgentRun 接入缺口,不能只靠 trace 显示原因当成已完成。
|
||||
|
||||
## 内部架构
|
||||
|
||||
@@ -57,7 +57,7 @@ Cloud Web check 通过后仍需执行 bundle build 和 dist freshness 校验,
|
||||
|
||||
## T2.2
|
||||
|
||||
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:在同一 conversation/session 连续发送两条 Code Agent 消息,确认第二条不重新 materialize bundle/启动新 runner,或在 trace 中明确输出不能复用 runner 的原因;result completed 必须包含真实 provider/model/trace/conversation 和 final assistant response。
|
||||
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:在同一 conversation/session 连续发送两条 Code Agent 消息,确认第二条复用第一条的 AgentRun `runId` 和 runner `jobName`、生成新的 `commandId`,且不重新 materialize bundle/启动新 runner;result completed 必须包含真实 provider/model/`providerTrace`/trace/conversation 和 final assistant response。复用失败原因只能作为诊断,不作为本测试通过条件。
|
||||
|
||||
## T3
|
||||
|
||||
|
||||
@@ -587,6 +587,13 @@ class AccessController {
|
||||
return this.store.getAgentSessionByTraceId?.(id) ?? null;
|
||||
}
|
||||
|
||||
async getAgentSession(sessionId) {
|
||||
await this.ensureBootstrap();
|
||||
const id = safeAgentSessionId(sessionId);
|
||||
if (!id) return null;
|
||||
return this.store.getAgentSession?.(id) ?? null;
|
||||
}
|
||||
|
||||
async listAgentConversations(request, response, url) {
|
||||
await this.ensureBootstrap();
|
||||
const auth = await this.authenticate(request, { required: true });
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
||||
import {
|
||||
CODEX_APP_SERVER_PROTOCOL,
|
||||
CODEX_APP_SERVER_WIRE_API,
|
||||
CODEX_STDIO_BACKEND,
|
||||
CODEX_STDIO_CAPABILITY_LEVEL,
|
||||
CODEX_STDIO_IMPLEMENTATION_TYPE,
|
||||
CODEX_STDIO_PROVIDER,
|
||||
CODEX_STDIO_RUNNER_KIND,
|
||||
CODEX_STDIO_SESSION_MODE,
|
||||
longLivedSessionGate
|
||||
} from "./codex-stdio-session.ts";
|
||||
import {
|
||||
parsePositiveInteger,
|
||||
safeConversationId,
|
||||
@@ -19,8 +30,9 @@ const DEFAULT_REPO_URL = "http://git-mirror-http.devops-infra.svc.cluster.local/
|
||||
const DEFAULT_RUNNER_NAMESPACE = "agentrun-v01";
|
||||
const DEFAULT_TIMEOUT_MS = 1_200_000;
|
||||
const AGENTRUN_BACKENDS = Object.freeze(["codex", "deepseek"]);
|
||||
const THREAD_CONTINUITY_POLICY = "hwlab-agentrun-v01-new-runner-thread";
|
||||
const SESSION_POLICY_RUN_LOCAL = "hwlab-agentrun-v01-run-local-session";
|
||||
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"]);
|
||||
|
||||
export function codeAgentAgentRunAdapterEnabled(env = process.env) {
|
||||
const value = String(env.HWLAB_CODE_AGENT_ADAPTER ?? env.HWLAB_CODE_AGENT_PROVIDER ?? "").trim().toLowerCase();
|
||||
@@ -42,12 +54,13 @@ export function initialAgentRunChatResult({ params = {}, options = {}, traceId }
|
||||
messageId: `msg_${safeTraceId(traceId)?.slice(4) || randomUUID()}`,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
provider: ADAPTER_ID,
|
||||
provider: CODEX_STDIO_PROVIDER,
|
||||
model: modelForBackendProfile(backendProfile, env),
|
||||
backend: `agentrun-v01/${backendProfile}`,
|
||||
capabilityLevel: "agentrun-v01-manual-dispatch",
|
||||
sessionMode: "agentrun-v01-session-ref",
|
||||
implementationType: "hwlab-cloud-api-agentrun-v01-adapter",
|
||||
backend: CODEX_STDIO_BACKEND,
|
||||
infrastructureBackend: `agentrun-v01/${backendProfile}`,
|
||||
capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL,
|
||||
sessionMode: CODEX_STDIO_SESSION_MODE,
|
||||
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
|
||||
requestedThreadId: safeOpaqueId(params.threadId) || null,
|
||||
threadContinuityPolicy: THREAD_CONTINUITY_POLICY,
|
||||
agentRun: {
|
||||
@@ -76,14 +89,67 @@ 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 manually create run/command/runner-job over the k3s Service DNS.",
|
||||
waitingFor: "agentrun-run-create",
|
||||
message: "HWLAB Code Agent request accepted by the AgentRun v0.1 adapter; hwlab-cloud-api will reuse an active runner when the HWLAB session has a live AgentRun lease, otherwise it will create run/command/runner-job over the k3s Service DNS.",
|
||||
waitingFor: "agentrun-run-reuse-or-create",
|
||||
adapter: ADAPTER_ID,
|
||||
managerHost: new URL(managerUrl).hostname,
|
||||
valuesPrinted: false
|
||||
}, agentRunTraceMeta(env, params));
|
||||
|
||||
const runInput = buildAgentRunCreateRunInput({ params, env, traceId, backendProfile });
|
||||
const reusable = await resolveReusableAgentRun({ params, options, env, managerUrl, fetchImpl, timeoutMs, backendProfile, traceId, traceStore });
|
||||
if (reusable?.mapping) {
|
||||
try {
|
||||
const commandInput = buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId: reusable.sessionId });
|
||||
const command = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(reusable.mapping.runId)}/commands`, {
|
||||
method: "POST",
|
||||
body: commandInput,
|
||||
timeoutMs
|
||||
});
|
||||
const commandId = requiredString(command?.id, "command.id");
|
||||
const mapping = agentRunReusedMapping({ previous: reusable.mapping, run: reusable.run, command, traceId, startedAt, backendProfile, managerUrl, env });
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:command:created",
|
||||
message: `AgentRun command ${commandId} created on reused run ${mapping.runId}; hwlab-cloud-api will not start a new runner Job for this turn.`,
|
||||
runId: mapping.runId,
|
||||
commandId,
|
||||
backendProfile,
|
||||
waitingFor: "agentrun-result",
|
||||
valuesPrinted: false
|
||||
});
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:runner-job:reused",
|
||||
message: `AgentRun runner Job ${mapping.jobName ?? "unknown"} is reused for this HWLAB session turn; no new bundle materialization is requested.`,
|
||||
runId: mapping.runId,
|
||||
commandId: mapping.commandId,
|
||||
attemptId: mapping.attemptId,
|
||||
runnerId: mapping.runnerId,
|
||||
jobName: mapping.jobName,
|
||||
namespace: mapping.namespace,
|
||||
waitingFor: "agentrun-result",
|
||||
valuesPrinted: false
|
||||
});
|
||||
return decorateAgentRunRunningResult({ base: initialAgentRunChatResult({ params, options, traceId }), mapping, traceStore, traceId });
|
||||
} catch (error) {
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:run:reuse-command-failed",
|
||||
message: `AgentRun reused run ${reusable.mapping.runId} rejected the new command; hwlab-cloud-api will create a fresh run/runner Job for this turn.`,
|
||||
errorCode: error?.code ?? "agentrun_reuse_command_failed",
|
||||
runId: reusable.mapping.runId,
|
||||
commandId: reusable.mapping.commandId ?? null,
|
||||
waitingFor: "agentrun-run-create",
|
||||
valuesPrinted: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sessionId = agentRunSessionIdForParams(params, traceId);
|
||||
const runInput = buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, sessionId });
|
||||
const run = await agentRunJson(fetchImpl, managerUrl, "/api/v1/runs", {
|
||||
method: "POST",
|
||||
body: runInput,
|
||||
@@ -101,7 +167,7 @@ export async function submitAgentRunChatTurn({ params = {}, options = {}, traceI
|
||||
valuesPrinted: false
|
||||
});
|
||||
|
||||
const commandInput = buildAgentRunCommandInput({ params, traceId, backendProfile });
|
||||
const commandInput = buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId });
|
||||
const command = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(runId)}/commands`, {
|
||||
method: "POST",
|
||||
body: commandInput,
|
||||
@@ -263,12 +329,13 @@ export async function loadPersistedAgentRunResult(traceId, options = {}) {
|
||||
createdAt: session.startedAt ?? session.updatedAt ?? nowIso(options.now),
|
||||
updatedAt: session.updatedAt ?? nowIso(options.now),
|
||||
ownerUserId: session.ownerUserId,
|
||||
provider: ADAPTER_ID,
|
||||
provider: CODEX_STDIO_PROVIDER,
|
||||
model: modelForBackendProfile(agentRun.backendProfile, options.env ?? process.env),
|
||||
backend: `agentrun-v01/${agentRun.backendProfile ?? "deepseek"}`,
|
||||
capabilityLevel: "agentrun-v01-manual-dispatch",
|
||||
sessionMode: "agentrun-v01-session-ref",
|
||||
implementationType: "hwlab-cloud-api-agentrun-v01-adapter",
|
||||
backend: CODEX_STDIO_BACKEND,
|
||||
infrastructureBackend: `agentrun-v01/${agentRun.backendProfile ?? "deepseek"}`,
|
||||
capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL,
|
||||
sessionMode: CODEX_STDIO_SESSION_MODE,
|
||||
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
|
||||
agentRun: { ...agentRun, adapter: ADAPTER_ID, valuesPrinted: false },
|
||||
valuesPrinted: false
|
||||
};
|
||||
@@ -294,15 +361,17 @@ export function agentRunSessionEvidence(payload = {}) {
|
||||
terminalStatus: payload.agentRun.terminalStatus ?? null,
|
||||
lastSeq: payload.agentRun.lastSeq ?? 0,
|
||||
providerId: payload.agentRun.providerId ?? DEFAULT_PROVIDER_ID,
|
||||
reuseEligible: payload.agentRun.reuseEligible ?? false,
|
||||
sessionId: payload.sessionId ?? payload.agentRun.sessionId ?? null,
|
||||
conversationId: payload.conversationId ?? payload.agentRun.conversationId ?? null,
|
||||
threadId: payload.threadId ?? payload.agentRun.threadId ?? null,
|
||||
providerTrace: payload.providerTrace ?? payload.agentRun.providerTrace ?? null,
|
||||
valuesPrinted: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile }) {
|
||||
function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile, sessionId }) {
|
||||
const commitId = fullSourceCommit(env);
|
||||
const resourceBundleRef = commitId
|
||||
? {
|
||||
@@ -325,7 +394,7 @@ function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile }) {
|
||||
valuesPrinted: false
|
||||
},
|
||||
sessionRef: {
|
||||
sessionId: agentRunSessionId(traceId),
|
||||
sessionId: sessionId ?? agentRunSessionIdForParams(params, traceId),
|
||||
...(safeConversationId(params.conversationId) ? { conversationId: safeConversationId(params.conversationId) } : {}),
|
||||
metadata: {
|
||||
adapter: ADAPTER_ID,
|
||||
@@ -361,7 +430,7 @@ function buildAgentRunCreateRunInput({ params, env, traceId, backendProfile }) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildAgentRunCommandInput({ params, traceId, backendProfile }) {
|
||||
function buildAgentRunCommandInput({ params, traceId, backendProfile, sessionId }) {
|
||||
const prompt = String(params.message ?? params.prompt ?? "").trim();
|
||||
return {
|
||||
type: "turn",
|
||||
@@ -370,7 +439,7 @@ function buildAgentRunCommandInput({ params, traceId, backendProfile }) {
|
||||
message: prompt,
|
||||
traceId,
|
||||
conversationId: safeConversationId(params.conversationId) || null,
|
||||
sessionId: agentRunSessionId(traceId),
|
||||
sessionId: sessionId ?? agentRunSessionIdForParams(params, traceId),
|
||||
hwlabSessionId: safeSessionId(params.sessionId) || null,
|
||||
threadId: null,
|
||||
requestedThreadId: safeOpaqueId(params.threadId) || null,
|
||||
@@ -409,6 +478,122 @@ function buildAgentRunTransientEnv(env = process.env) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function resolveReusableAgentRun({ params = {}, options = {}, env = process.env, managerUrl, fetchImpl, timeoutMs, backendProfile, traceId, traceStore }) {
|
||||
const sessionId = agentRunSessionIdForParams(params, traceId);
|
||||
if (!safeSessionId(sessionId) || sessionId === agentRunSessionId(traceId) || typeof options.accessController?.getAgentSession !== "function") {
|
||||
return null;
|
||||
}
|
||||
const session = await options.accessController.getAgentSession(sessionId);
|
||||
if (!canReuseAgentRunSessionForOwner(session, params, options)) {
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:run:reuse-owner-skipped",
|
||||
message: "Stored AgentRun session is not visible to the current actor; a new runner Job will be created for this request.",
|
||||
sessionId,
|
||||
waitingFor: "agentrun-run-create",
|
||||
valuesPrinted: false
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const mapping = session?.session?.agentRun;
|
||||
if (!isReusableAgentRunMapping(mapping, backendProfile)) {
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:run:reuse-skipped",
|
||||
message: "No reusable AgentRun run was found for this HWLAB session; a new runner Job will be created.",
|
||||
sessionId,
|
||||
waitingFor: "agentrun-run-create",
|
||||
valuesPrinted: false
|
||||
});
|
||||
return null;
|
||||
}
|
||||
let run = null;
|
||||
try {
|
||||
run = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(mapping.runId)}`, {
|
||||
method: "GET",
|
||||
timeoutMs
|
||||
});
|
||||
} catch (error) {
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:run:reuse-unavailable",
|
||||
message: `Stored AgentRun run ${mapping.runId} could not be read; a new runner Job will be created.`,
|
||||
errorCode: error?.code ?? "agentrun_run_lookup_failed",
|
||||
runId: mapping.runId,
|
||||
sessionId,
|
||||
waitingFor: "agentrun-run-create",
|
||||
valuesPrinted: false
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const reason = agentRunReuseBlocker(run, mapping);
|
||||
if (reason) {
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:run:reuse-blocked",
|
||||
message: `Stored AgentRun run ${mapping.runId} cannot be reused: ${reason}; a new runner Job will be created.`,
|
||||
runId: mapping.runId,
|
||||
sessionId,
|
||||
waitingFor: "agentrun-run-create",
|
||||
valuesPrinted: false
|
||||
});
|
||||
return null;
|
||||
}
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:run:reused",
|
||||
message: `AgentRun run ${mapping.runId} is reused for HWLAB session ${sessionId}; runner lease is active and no new bundle will be requested for this turn.`,
|
||||
runId: mapping.runId,
|
||||
commandId: mapping.commandId ?? null,
|
||||
runnerId: mapping.runnerId ?? null,
|
||||
jobName: mapping.jobName ?? null,
|
||||
namespace: mapping.namespace ?? null,
|
||||
sessionId,
|
||||
waitingFor: "agentrun-command-create",
|
||||
valuesPrinted: false
|
||||
});
|
||||
return { sessionId, session, mapping, run };
|
||||
}
|
||||
|
||||
function isReusableAgentRunMapping(mapping, backendProfile) {
|
||||
return Boolean(
|
||||
mapping &&
|
||||
typeof mapping === "object" &&
|
||||
mapping.adapter === ADAPTER_ID &&
|
||||
mapping.reuseEligible !== false &&
|
||||
mapping.runId &&
|
||||
mapping.jobName &&
|
||||
(!mapping.backendProfile || mapping.backendProfile === backendProfile)
|
||||
);
|
||||
}
|
||||
|
||||
function canReuseAgentRunSessionForOwner(session, params = {}, options = {}) {
|
||||
if (!session) return false;
|
||||
const ownerUserId = options.actor?.id ?? params.ownerUserId ?? null;
|
||||
const ownerRole = options.actor?.role ?? params.ownerRole ?? null;
|
||||
if (ownerRole === "admin") return true;
|
||||
if (!session.ownerUserId) return true;
|
||||
return Boolean(ownerUserId && session.ownerUserId === ownerUserId);
|
||||
}
|
||||
|
||||
function agentRunReuseBlocker(run = {}, mapping = {}) {
|
||||
const status = String(run?.terminalStatus ?? run?.status ?? mapping.runStatus ?? "").toLowerCase();
|
||||
if (TERMINAL_RUN_STATUSES.has(status)) return `run_status_${status}`;
|
||||
if (!run?.claimedBy) return "runner_not_claimed";
|
||||
if (runLeaseExpired(run.leaseExpiresAt)) return "runner_lease_expired";
|
||||
return null;
|
||||
}
|
||||
|
||||
function runLeaseExpired(leaseExpiresAt) {
|
||||
const expiresMs = Date.parse(String(leaseExpiresAt ?? ""));
|
||||
return !Number.isFinite(expiresMs) || expiresMs <= Date.now();
|
||||
}
|
||||
|
||||
function providerCredentialSecretRef(profile, env) {
|
||||
return {
|
||||
profile,
|
||||
@@ -421,6 +606,57 @@ function providerCredentialSecretRef(profile, env) {
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunProviderTrace({ base = {}, result = {}, terminalStatus = null } = {}) {
|
||||
return {
|
||||
transport: "stdio",
|
||||
protocol: CODEX_APP_SERVER_PROTOCOL,
|
||||
wireApi: CODEX_APP_SERVER_WIRE_API,
|
||||
runnerKind: CODEX_STDIO_RUNNER_KIND,
|
||||
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
|
||||
command: "codex app-server --listen stdio://",
|
||||
toolName: "agentrun.v01.command.turn",
|
||||
source: ADAPTER_ID,
|
||||
backendProfile: base.agentRun?.backendProfile ?? null,
|
||||
runId: base.agentRun?.runId ?? result?.runId ?? null,
|
||||
commandId: base.agentRun?.commandId ?? result?.commandId ?? null,
|
||||
runnerId: base.agentRun?.runnerId ?? result?.runnerId ?? null,
|
||||
jobName: base.agentRun?.jobName ?? result?.jobName ?? null,
|
||||
namespace: base.agentRun?.namespace ?? result?.namespace ?? null,
|
||||
traceId: base.traceId ?? base.agentRun?.traceId ?? null,
|
||||
threadId: result?.sessionRef?.threadId ?? base.agentRun?.threadId ?? base.threadId ?? null,
|
||||
turnId: result?.turnId ?? null,
|
||||
terminalStatus: terminalStatus ?? result?.terminalStatus ?? null,
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunLongLivedSessionGate(base = {}) {
|
||||
return longLivedSessionGate({
|
||||
provider: CODEX_STDIO_PROVIDER,
|
||||
runnerKind: CODEX_STDIO_RUNNER_KIND,
|
||||
session: agentRunSessionSummary(base, "idle"),
|
||||
sessionMode: CODEX_STDIO_SESSION_MODE,
|
||||
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
|
||||
codexStdioFeasibility: {
|
||||
ready: true,
|
||||
canStartLongLivedCodexStdio: true,
|
||||
source: ADAPTER_ID,
|
||||
valuesPrinted: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function agentRunToolCalls(result = {}, status = "completed") {
|
||||
return [{
|
||||
name: "codex-app-server.turn",
|
||||
status,
|
||||
runId: result?.runId ?? null,
|
||||
commandId: result?.commandId ?? null,
|
||||
outputSummary: result?.reply ? `assistantChars=${String(result.reply).length}` : null,
|
||||
valuesPrinted: false
|
||||
}];
|
||||
}
|
||||
|
||||
function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId }) {
|
||||
const terminalStatus = String(result?.terminalStatus ?? "");
|
||||
const terminal = terminalStatus === "completed" || terminalStatus === "failed" || terminalStatus === "blocked" || terminalStatus === "cancelled";
|
||||
@@ -444,11 +680,16 @@ function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId })
|
||||
...base,
|
||||
status: "completed",
|
||||
updatedAt: now,
|
||||
workspace: base.workspace ?? "/home/agentrun/workspace",
|
||||
sandbox: base.sandbox ?? "danger-full-access",
|
||||
session: agentRunSessionSummary(base, "idle"),
|
||||
sessionReuse: agentRunSessionReuseSummary(base, base.agentRun.reused === true),
|
||||
runner: agentRunRunnerSummary(base.agentRun),
|
||||
runnerTrace: traceStore.snapshot(traceId, agentRunTraceMeta({}, {})),
|
||||
toolCalls: [],
|
||||
skills: { status: "delegated", provider: ADAPTER_ID, items: [], valuesPrinted: false },
|
||||
toolCalls: agentRunToolCalls(result, "completed"),
|
||||
skills: { status: "delegated", provider: ADAPTER_ID, count: 0, items: [], valuesPrinted: false },
|
||||
longLivedSessionGate: agentRunLongLivedSessionGate(base),
|
||||
providerTrace: agentRunProviderTrace({ base, result, terminalStatus }),
|
||||
reply: {
|
||||
messageId: base.messageId ?? `msg_${traceId.slice(4)}`,
|
||||
role: "assistant",
|
||||
@@ -456,7 +697,7 @@ function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId })
|
||||
createdAt: now
|
||||
},
|
||||
usage: null,
|
||||
agentRun: { ...base.agentRun, terminalStatus, completed: true, valuesPrinted: false },
|
||||
agentRun: { ...base.agentRun, terminalStatus, completed: true, reuseEligible: true, valuesPrinted: false },
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
@@ -480,10 +721,12 @@ function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId })
|
||||
canceled,
|
||||
updatedAt: now,
|
||||
session: agentRunSessionSummary(base, canceled ? "canceled" : "failed"),
|
||||
sessionReuse: agentRunSessionReuseSummary(base, base.agentRun.reused === true),
|
||||
runner: agentRunRunnerSummary(base.agentRun),
|
||||
runnerTrace,
|
||||
toolCalls: [],
|
||||
skills: { status: "delegated", provider: ADAPTER_ID, items: [], valuesPrinted: false },
|
||||
toolCalls: agentRunToolCalls(result, canceled ? "canceled" : "failed"),
|
||||
skills: { status: "delegated", provider: ADAPTER_ID, count: 0, items: [], valuesPrinted: false },
|
||||
providerTrace: agentRunProviderTrace({ base, result, terminalStatus }),
|
||||
error: {
|
||||
code,
|
||||
layer: "agentrun",
|
||||
@@ -518,6 +761,7 @@ function decorateAgentRunRunningResult({ base, mapping, traceStore, traceId }) {
|
||||
shortConnection: true,
|
||||
updatedAt: nowIso(),
|
||||
session: agentRunSessionSummary({ ...base, agentRun: mapping }, "running"),
|
||||
sessionReuse: agentRunSessionReuseSummary({ ...base, agentRun: mapping }, mapping.reused === true),
|
||||
runner: agentRunRunnerSummary(mapping),
|
||||
runnerTrace: traceStore.snapshot(traceId, agentRunTraceMeta({}, {})),
|
||||
agentRun: { ...mapping, adapter: ADAPTER_ID, valuesPrinted: false },
|
||||
@@ -646,20 +890,60 @@ function agentRunMapping({ env, managerUrl, backendProfile, run, command, runner
|
||||
terminalStatus: null,
|
||||
lastSeq: 0,
|
||||
traceId,
|
||||
sessionId: run?.sessionRef?.sessionId ?? null,
|
||||
conversationId: run?.sessionRef?.conversationId ?? null,
|
||||
threadId: run?.sessionRef?.threadId ?? null,
|
||||
reused: false,
|
||||
reuseEligible: true,
|
||||
createdAt: startedAt,
|
||||
updatedAt: nowIso(),
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunReusedMapping({ previous = {}, run = {}, command = {}, traceId, startedAt, backendProfile, managerUrl, env }) {
|
||||
return {
|
||||
...previous,
|
||||
adapter: ADAPTER_ID,
|
||||
managerUrl,
|
||||
backendProfile,
|
||||
providerId: previous.providerId ?? firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID, env.AGENTRUN_PROVIDER_ID, DEFAULT_PROVIDER_ID),
|
||||
runId: previous.runId ?? run?.id,
|
||||
commandId: command.id,
|
||||
status: "runner-job-reused",
|
||||
runStatus: run?.status ?? previous.runStatus ?? null,
|
||||
commandState: command.state ?? null,
|
||||
terminalStatus: null,
|
||||
lastSeq: previous.lastSeq ?? 0,
|
||||
traceId,
|
||||
sessionId: run?.sessionRef?.sessionId ?? previous.sessionId ?? null,
|
||||
conversationId: run?.sessionRef?.conversationId ?? previous.conversationId ?? null,
|
||||
threadId: run?.sessionRef?.threadId ?? previous.threadId ?? null,
|
||||
reused: true,
|
||||
reuseEligible: true,
|
||||
createdAt: previous.createdAt ?? startedAt,
|
||||
updatedAt: nowIso(),
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunResultRefs(result = {}) {
|
||||
const refs = {};
|
||||
for (const key of ["attemptId", "runnerId", "jobName", "namespace", "runnerJobCount"]) {
|
||||
if (result?.[key] !== undefined && result?.[key] !== null) refs[key] = result[key];
|
||||
}
|
||||
if (result?.sessionRef && typeof result.sessionRef === "object") {
|
||||
if (result.sessionRef.sessionId) refs.sessionId = result.sessionRef.sessionId;
|
||||
if (result.sessionRef.conversationId) refs.conversationId = result.sessionRef.conversationId;
|
||||
if (result.sessionRef.threadId) refs.threadId = result.sessionRef.threadId;
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
function agentRunSessionIdForParams(params = {}, traceId) {
|
||||
return safeSessionId(params.sessionId) || agentRunSessionId(traceId);
|
||||
}
|
||||
|
||||
function agentRunSessionId(traceId) {
|
||||
return `ses_agentrun_${String(safeTraceId(traceId) || `trc_${randomUUID()}`).slice(4)}`;
|
||||
}
|
||||
@@ -670,16 +954,35 @@ function agentRunSessionSummary(base, status) {
|
||||
conversationId: base.conversationId ?? base.agentRun?.conversationId ?? null,
|
||||
threadId: base.threadId ?? base.agentRun?.threadId ?? null,
|
||||
status,
|
||||
sessionMode: "agentrun-v01-session-ref",
|
||||
runnerKind: "agentrun-v01-runner-job",
|
||||
sessionMode: CODEX_STDIO_SESSION_MODE,
|
||||
runnerKind: CODEX_STDIO_RUNNER_KIND,
|
||||
lastTraceId: base.traceId ?? base.agentRun?.traceId ?? null,
|
||||
longLivedSession: true,
|
||||
codexStdio: true,
|
||||
writeCapable: true,
|
||||
durable: true,
|
||||
durableSession: true,
|
||||
idleTimeoutMs: parsePositiveInteger(base.agentRun?.idleTimeoutMs, 600000),
|
||||
secretMaterialStored: false,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunSessionReuseSummary(base, reused) {
|
||||
return {
|
||||
sessionId: base.sessionId ?? base.agentRun?.sessionId ?? null,
|
||||
conversationId: base.conversationId ?? base.agentRun?.conversationId ?? null,
|
||||
threadId: base.threadId ?? base.agentRun?.threadId ?? null,
|
||||
reused: Boolean(reused),
|
||||
status: reused ? "reused" : "new",
|
||||
runnerReused: Boolean(reused),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunRunnerSummary(mapping = {}) {
|
||||
return {
|
||||
kind: "agentrun-v01-runner-job",
|
||||
kind: CODEX_STDIO_RUNNER_KIND,
|
||||
adapter: ADAPTER_ID,
|
||||
runId: mapping.runId ?? null,
|
||||
commandId: mapping.commandId ?? null,
|
||||
@@ -688,17 +991,27 @@ function agentRunRunnerSummary(mapping = {}) {
|
||||
jobName: mapping.jobName ?? null,
|
||||
namespace: mapping.namespace ?? null,
|
||||
backendProfile: mapping.backendProfile ?? null,
|
||||
provider: CODEX_STDIO_PROVIDER,
|
||||
sessionMode: CODEX_STDIO_SESSION_MODE,
|
||||
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
|
||||
capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL,
|
||||
codexStdio: true,
|
||||
writeCapable: true,
|
||||
durableSession: true,
|
||||
longLivedSession: true,
|
||||
sandbox: "danger-full-access",
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunTraceMeta(env = process.env) {
|
||||
return {
|
||||
runnerKind: "agentrun-v01-runner-job",
|
||||
runnerKind: CODEX_STDIO_RUNNER_KIND,
|
||||
workspace: resolveAgentRunRepoUrl(env),
|
||||
sandbox: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_SANDBOX, env.HWLAB_CODE_AGENT_CODEX_SANDBOX, "danger-full-access"),
|
||||
sessionMode: "agentrun-v01-session-ref",
|
||||
implementationType: "hwlab-cloud-api-agentrun-v01-adapter"
|
||||
protocol: CODEX_APP_SERVER_PROTOCOL,
|
||||
sessionMode: CODEX_STDIO_SESSION_MODE,
|
||||
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,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 () => {
|
||||
const calls = [];
|
||||
const ownerSessions = new Map();
|
||||
const agentRunServer = createHttpServer(async (request, response) => {
|
||||
const url = new URL(request.url || "/", "http://127.0.0.1");
|
||||
const chunks = [];
|
||||
@@ -149,30 +150,46 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_agentrun" })}\n`);
|
||||
};
|
||||
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_adapter") {
|
||||
return send({
|
||||
id: "run_hwlab_adapter",
|
||||
status: "claimed",
|
||||
terminalStatus: null,
|
||||
claimedBy: "runner_hwlab_adapter",
|
||||
leaseExpiresAt: "2999-01-01T00:00:00.000Z",
|
||||
backendProfile: "deepseek",
|
||||
sessionRef: {
|
||||
sessionId: "ses_server-test-agentrun",
|
||||
conversationId: "cnv_server-test-agentrun",
|
||||
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
||||
metadata: {}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/v1/runs") {
|
||||
assert.equal(body.tenantId, "hwlab");
|
||||
assert.equal(body.backendProfile, "deepseek");
|
||||
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(Object.hasOwn(body.sessionRef, "threadId"), false);
|
||||
assert.equal(body.sessionRef.sessionId, "ses_agentrun_server-test-agentrun-adapter");
|
||||
assert.notEqual(body.sessionRef.sessionId, "ses_server-test-agentrun");
|
||||
assert.equal(body.sessionRef.sessionId, "ses_server-test-agentrun");
|
||||
assert.equal(body.sessionRef.metadata.hwlabSessionId, "ses_server-test-agentrun");
|
||||
assert.equal(body.sessionRef.metadata.requestedThreadId, "019e8078-db67-7750-a5d9-1a99f3abd445");
|
||||
assert.equal(body.sessionRef.metadata.threadContinuityPolicy, "hwlab-agentrun-v01-new-runner-thread");
|
||||
assert.equal(body.sessionRef.metadata.sessionPolicy, "hwlab-agentrun-v01-run-local-session");
|
||||
assert.equal(body.sessionRef.metadata.threadContinuityPolicy, "hwlab-agentrun-v01-reuse-runner-thread");
|
||||
assert.equal(body.sessionRef.metadata.sessionPolicy, "hwlab-agentrun-v01-session-runner-reuse");
|
||||
return send({ id: "run_hwlab_adapter", status: "pending", backendProfile: "deepseek", sessionRef: body.sessionRef, resourceBundleRef: body.resourceBundleRef });
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_adapter/commands") {
|
||||
assert.equal(body.type, "turn");
|
||||
assert.match(body.payload.prompt, /AgentRun adapter/u);
|
||||
assert.equal(body.payload.sessionId, "ses_agentrun_server-test-agentrun-adapter");
|
||||
assert.equal(body.payload.sessionId, "ses_server-test-agentrun");
|
||||
assert.equal(body.payload.hwlabSessionId, "ses_server-test-agentrun");
|
||||
assert.equal(body.payload.threadId, null);
|
||||
assert.equal(body.payload.requestedThreadId, "019e8078-db67-7750-a5d9-1a99f3abd445");
|
||||
assert.equal(body.payload.threadContinuityPolicy, "hwlab-agentrun-v01-new-runner-thread");
|
||||
assert.equal(body.payload.sessionPolicy, "hwlab-agentrun-v01-run-local-session");
|
||||
return send({ id: "cmd_hwlab_adapter", runId: "run_hwlab_adapter", state: "pending", type: "turn", seq: 1 });
|
||||
assert.equal(body.payload.threadContinuityPolicy, "hwlab-agentrun-v01-reuse-runner-thread");
|
||||
assert.equal(body.payload.sessionPolicy, "hwlab-agentrun-v01-session-runner-reuse");
|
||||
const secondTurn = /第二轮/u.test(body.payload.prompt);
|
||||
return send({ id: secondTurn ? "cmd_hwlab_adapter_second" : "cmd_hwlab_adapter", runId: "run_hwlab_adapter", state: "pending", type: "turn", seq: secondTurn ? 2 : 1 });
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_adapter/runner-jobs") {
|
||||
assert.equal(body.commandId, "cmd_hwlab_adapter");
|
||||
@@ -194,10 +211,11 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
});
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_adapter/events") {
|
||||
const second = url.searchParams.get("afterSeq") === "3" || calls.some((call) => call.path === "/api/v1/runs/run_hwlab_adapter/commands/cmd_hwlab_adapter_second/result");
|
||||
return send({ items: [
|
||||
{ id: "evt_1", runId: "run_hwlab_adapter", seq: 1, type: "backend_status", payload: { phase: "runner-job-created", commandId: "cmd_hwlab_adapter", attemptId: "attempt_hwlab_adapter", jobName: "agentrun-v01-runner-hwlab-adapter", namespace: "agentrun-v01" }, createdAt: "2026-06-01T00:00:00.000Z" },
|
||||
{ id: "evt_2", runId: "run_hwlab_adapter", seq: 2, type: "assistant_message", payload: { text: "AgentRun adapter 已接管 HWLAB Code Agent。" }, createdAt: "2026-06-01T00:00:01.000Z" },
|
||||
{ id: "evt_3", runId: "run_hwlab_adapter", seq: 3, type: "terminal_status", payload: { terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:02.000Z" }
|
||||
{ id: second ? "evt_4" : "evt_1", runId: "run_hwlab_adapter", seq: second ? 4 : 1, type: "backend_status", payload: { phase: second ? "turn-started" : "runner-job-created", commandId: second ? "cmd_hwlab_adapter_second" : "cmd_hwlab_adapter", attemptId: "attempt_hwlab_adapter", jobName: "agentrun-v01-runner-hwlab-adapter", namespace: "agentrun-v01" }, createdAt: "2026-06-01T00:00:00.000Z" },
|
||||
{ id: second ? "evt_5" : "evt_2", runId: "run_hwlab_adapter", seq: second ? 5 : 2, type: "assistant_message", payload: { commandId: second ? "cmd_hwlab_adapter_second" : "cmd_hwlab_adapter", text: second ? "AgentRun adapter 复用已有 runner 完成第二轮。" : "AgentRun adapter 已接管 HWLAB Code Agent。" }, createdAt: "2026-06-01T00:00:01.000Z" },
|
||||
{ id: second ? "evt_6" : "evt_3", runId: "run_hwlab_adapter", seq: second ? 6 : 3, type: "terminal_status", payload: { commandId: second ? "cmd_hwlab_adapter_second" : "cmd_hwlab_adapter", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:02.000Z" }
|
||||
] });
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_adapter/commands/cmd_hwlab_adapter/result") {
|
||||
@@ -215,7 +233,27 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
completed: true,
|
||||
reply: "AgentRun adapter 已接管 HWLAB Code Agent。",
|
||||
lastSeq: 3,
|
||||
eventCount: 3
|
||||
eventCount: 3,
|
||||
sessionRef: { sessionId: "ses_server-test-agentrun", conversationId: "cnv_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445" }
|
||||
});
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_adapter/commands/cmd_hwlab_adapter_second/result") {
|
||||
return send({
|
||||
runId: "run_hwlab_adapter",
|
||||
commandId: "cmd_hwlab_adapter_second",
|
||||
attemptId: "attempt_hwlab_adapter",
|
||||
runnerId: "runner_hwlab_adapter",
|
||||
jobName: "agentrun-v01-runner-hwlab-adapter",
|
||||
namespace: "agentrun-v01",
|
||||
status: "completed",
|
||||
runStatus: "claimed",
|
||||
commandState: "completed",
|
||||
terminalStatus: "completed",
|
||||
completed: true,
|
||||
reply: "AgentRun adapter 复用已有 runner 完成第二轮。",
|
||||
lastSeq: 6,
|
||||
eventCount: 6,
|
||||
sessionRef: { sessionId: "ses_server-test-agentrun", conversationId: "cnv_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445" }
|
||||
});
|
||||
}
|
||||
response.writeHead(404, { "content-type": "application/json" });
|
||||
@@ -244,6 +282,13 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
},
|
||||
async createCodeAgentDevicePodSession() {
|
||||
return { token: "test-device-pod-session-token" };
|
||||
},
|
||||
async recordAgentSessionOwner(input) {
|
||||
ownerSessions.set(input.sessionId, input);
|
||||
return input;
|
||||
},
|
||||
async getAgentSession(sessionId) {
|
||||
return ownerSessions.get(sessionId) ?? null;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -258,6 +303,8 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
body: JSON.stringify({
|
||||
conversationId: "cnv_server-test-agentrun",
|
||||
sessionId: "ses_server-test-agentrun",
|
||||
ownerUserId: "usr_agent_owner",
|
||||
ownerRole: "user",
|
||||
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
||||
retryOf: "trc_previous-agentrun-web",
|
||||
message: "AgentRun adapter smoke"
|
||||
@@ -270,8 +317,12 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
const payload = await pollAgentResult(port, traceId);
|
||||
validateCodeAgentChatSchema(payload);
|
||||
assert.equal(payload.status, "completed");
|
||||
assert.equal(payload.provider, "agentrun-v01");
|
||||
assert.equal(payload.backend, "agentrun-v01/deepseek");
|
||||
assert.equal(payload.provider, "codex-stdio");
|
||||
assert.equal(payload.infrastructureBackend, "agentrun-v01/deepseek");
|
||||
assert.equal(payload.providerTrace.protocol, "codex-app-server-jsonrpc-stdio");
|
||||
assert.equal(payload.providerTrace.command, "codex app-server --listen stdio://");
|
||||
assert.equal(payload.providerTrace.terminalStatus, "completed");
|
||||
assert.equal(payload.longLivedSessionGate.status, "pass");
|
||||
assert.equal(payload.agentRun.runId, "run_hwlab_adapter");
|
||||
assert.equal(payload.agentRun.commandId, "cmd_hwlab_adapter");
|
||||
assert.equal(payload.agentRun.jobName, "agentrun-v01-runner-hwlab-adapter");
|
||||
@@ -284,6 +335,33 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
const traceBody = await trace.json();
|
||||
assert.ok(traceBody.events.some((event) => event.runId === "run_hwlab_adapter"));
|
||||
assert.ok(traceBody.events.some((event) => event.label === "agentrun:assistant:message"));
|
||||
|
||||
const secondTraceId = "trc_server-test-agentrun-adapter-second";
|
||||
const second = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", "x-trace-id": secondTraceId },
|
||||
body: JSON.stringify({
|
||||
conversationId: "cnv_server-test-agentrun",
|
||||
sessionId: "ses_server-test-agentrun",
|
||||
ownerUserId: "usr_agent_owner",
|
||||
ownerRole: "user",
|
||||
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
||||
message: "AgentRun adapter smoke 第二轮"
|
||||
})
|
||||
});
|
||||
assert.equal(second.status, 202);
|
||||
const secondPayload = await pollAgentResult(port, secondTraceId);
|
||||
validateCodeAgentChatSchema(secondPayload);
|
||||
assert.equal(secondPayload.status, "completed");
|
||||
assert.equal(secondPayload.agentRun.runId, "run_hwlab_adapter");
|
||||
assert.equal(secondPayload.agentRun.commandId, "cmd_hwlab_adapter_second");
|
||||
assert.equal(secondPayload.agentRun.jobName, "agentrun-v01-runner-hwlab-adapter");
|
||||
assert.equal(secondPayload.sessionReuse.reused, true);
|
||||
assert.match(secondPayload.reply.content, /复用已有 runner/u);
|
||||
assert.ok(secondPayload.runnerTrace.events.some((event) => event.label === "agentrun:run:reused"));
|
||||
assert.ok(secondPayload.runnerTrace.events.some((event) => event.label === "agentrun:runner-job:reused"));
|
||||
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs").length, 1);
|
||||
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs/run_hwlab_adapter/runner-jobs").length, 1);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
|
||||
|
||||
@@ -130,6 +130,75 @@ test("long-lived Codex app-server stdio session fields are shown as full Code Ag
|
||||
assert.match(runtimePath.summary, /Responses wire API/u);
|
||||
});
|
||||
|
||||
test("AgentRun carried Codex app-server session is shown as full Code Agent session", () => {
|
||||
const message = {
|
||||
status: "completed",
|
||||
provider: "codex-stdio",
|
||||
model: "deepseek-chat",
|
||||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||||
infrastructureBackend: "agentrun-v01/deepseek",
|
||||
workspace: "/home/agentrun/workspace",
|
||||
sandbox: "danger-full-access",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
session: {
|
||||
sessionId: "ses_web_agentrun",
|
||||
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
||||
status: "idle",
|
||||
lastTraceId: "trc_web_agentrun",
|
||||
longLivedSession: true,
|
||||
codexStdio: true,
|
||||
writeCapable: true,
|
||||
durable: true,
|
||||
durableSession: true
|
||||
},
|
||||
sessionReuse: {
|
||||
sessionId: "ses_web_agentrun",
|
||||
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
||||
reused: true
|
||||
},
|
||||
runner: {
|
||||
kind: "codex-app-server-stdio-runner",
|
||||
codexStdio: true,
|
||||
writeCapable: true,
|
||||
durableSession: true,
|
||||
longLivedSession: true
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "pass",
|
||||
pass: true
|
||||
},
|
||||
toolCalls: [{ name: "codex-app-server.turn", status: "completed" }],
|
||||
skills: { status: "delegated", count: 0 },
|
||||
runnerTrace: {
|
||||
traceId: "trc_web_agentrun",
|
||||
runnerKind: "codex-app-server-stdio-runner",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session"
|
||||
},
|
||||
providerTrace: {
|
||||
transport: "stdio",
|
||||
protocol: "codex-app-server-jsonrpc-stdio",
|
||||
wireApi: "responses",
|
||||
command: "codex app-server --listen stdio://",
|
||||
terminalStatus: "completed",
|
||||
source: "agentrun-v01"
|
||||
},
|
||||
traceId: "trc_web_agentrun"
|
||||
};
|
||||
const facts = codeAgentFactsFromMessage(message);
|
||||
const runtimePath = codeAgentRuntimePathFromMessage({ role: "agent", ...message });
|
||||
|
||||
assert.equal(facts.kind, "codex-stdio-long-lived");
|
||||
assert.equal(facts.fullCodeAgent, true);
|
||||
assert.equal(runtimePath.kind, "codex-app-server-stdio");
|
||||
assert.equal(runtimePath.tone, "ok");
|
||||
assert.equal(runtimePath.providerTraceMissing, false);
|
||||
assert.equal(runtimePath.criticalMissing, false);
|
||||
assert.equal(runtimePath.terminalStatusOk, true);
|
||||
});
|
||||
|
||||
test("Codex app-server without providerTrace is degraded with structured missing fields", () => {
|
||||
const message = {
|
||||
role: "agent",
|
||||
|
||||
Reference in New Issue
Block a user