fix: reuse AgentRun runner for HWLAB turns (#662)

Co-authored-by: Codex <codex@local>
This commit is contained in:
Lyon
2026-06-01 23:18:40 +08:00
committed by GitHub
parent b24dc9b1db
commit 24060853e7
7 changed files with 518 additions and 51 deletions
+2 -2
View File
@@ -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。
+1 -1
View File
@@ -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 内 resumeCLI 仍应把 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 口径属于废弃路径;开发中遇到这些旧门禁、旧测试或旧预检时直接删除,不再维护兼容。
+3 -3
View File
@@ -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/启动新 runnerresult completed 必须包含真实 provider/model/`providerTrace`/trace/conversation 和 final assistant response。复用失败原因只能作为诊断,不作为本测试通过条件。
## T3
+7
View File
@@ -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 });
+344 -31
View File
@@ -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
};
}
+92 -14
View File
@@ -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",