569 lines
24 KiB
JavaScript
569 lines
24 KiB
JavaScript
const TEXT_FALLBACK_RUNNER = "openai-responses-fallback";
|
||
const CODEX_ONE_SHOT_RUNNER = "codex-cli-one-shot-ephemeral";
|
||
const READONLY_SESSION_MODE = "controlled-readonly-session-registry";
|
||
const CODEX_APP_SERVER_RUNNER = "codex-app-server-stdio-runner";
|
||
const CODEX_APP_SERVER_SESSION_MODE = "codex-app-server-stdio-long-lived";
|
||
const CODEX_APP_SERVER_IMPLEMENTATION = "repo-owned-codex-app-server-stdio-session";
|
||
const CODEX_APP_SERVER_PROTOCOL = "codex-app-server-jsonrpc-stdio";
|
||
const CODEX_MCP_RUNNER = "codex-mcp-stdio-runner";
|
||
const CODEX_MCP_SESSION_MODE = "codex-mcp-stdio-long-lived";
|
||
const CODEX_MCP_IMPLEMENTATION = "repo-owned-codex-mcp-stdio-session";
|
||
const MISSING_FIELD_VALUE = "字段缺失:证据不足";
|
||
const REQUIRED_ATTRIBUTION_KEYS = Object.freeze([
|
||
"provider",
|
||
"backend",
|
||
"runnerKind",
|
||
"protocol",
|
||
"implementationType",
|
||
"providerTrace.command",
|
||
"providerTrace.terminalStatus",
|
||
"sessionMode",
|
||
"capabilityLevel",
|
||
"toolCalls",
|
||
"traceId",
|
||
"operationId",
|
||
"audit",
|
||
"evidence"
|
||
]);
|
||
|
||
export function codeAgentFactsFromMessage(message) {
|
||
if (!hasCodeAgentFields(message)) return null;
|
||
const classification = classifyCodeAgentFacts(message);
|
||
const rows = [
|
||
{
|
||
label: "runner/provider/backend",
|
||
value: compactFields([
|
||
requiredField("runner", message.runner?.kind ?? runnerKindFromMessage(message)),
|
||
requiredField("provider", message.provider),
|
||
requiredField("backend", message.backend)
|
||
])
|
||
},
|
||
{
|
||
label: "session",
|
||
value: compactFields([
|
||
field("id", message.session?.sessionId ?? message.sessionId ?? message.runner?.sessionId),
|
||
field("status", message.session?.status),
|
||
requiredField("sessionMode", message.sessionMode ?? message.runner?.sessionMode ?? message.runner?.session)
|
||
])
|
||
},
|
||
{
|
||
label: "workspace",
|
||
value: valueOrMissing(message.workspace ?? message.runner?.workspace)
|
||
},
|
||
{
|
||
label: "sandbox",
|
||
value: valueOrMissing(message.sandbox ?? message.runner?.sandbox)
|
||
},
|
||
{
|
||
label: "capabilityLevel",
|
||
value: valueOrMissing(message.capabilityLevel ?? message.runner?.capabilityLevel)
|
||
},
|
||
{
|
||
label: "toolCalls",
|
||
value: toolCallsSummary(message.toolCalls)
|
||
},
|
||
{
|
||
label: "skills",
|
||
value: skillsSummary(message.skills)
|
||
},
|
||
{
|
||
label: "runnerTrace/traceId",
|
||
value: compactFields([
|
||
requiredField("traceId", message.traceId),
|
||
field("runnerTrace", runnerTraceSummary(message.runnerTrace))
|
||
])
|
||
}
|
||
];
|
||
|
||
return {
|
||
...classification,
|
||
rows
|
||
};
|
||
}
|
||
|
||
export function classifyCodeAgentFacts(message) {
|
||
const runnerKind = runnerKindFromMessage(message);
|
||
const provider = String(message?.provider ?? "").trim();
|
||
const sessionMode = String(message?.sessionMode ?? message?.runner?.sessionMode ?? message?.runner?.session ?? "").trim();
|
||
const implementationType = String(message?.implementationType ?? message?.runner?.implementationType ?? "").trim();
|
||
const capabilityLevel = String(message?.capabilityLevel ?? message?.runner?.capabilityLevel ?? "").trim();
|
||
const longLivedGate = message?.longLivedSessionGate;
|
||
const runtimePath = codeAgentRuntimePathFromMessage(message);
|
||
|
||
if (
|
||
provider === "codex-stdio" &&
|
||
longLivedGate?.status === "pass" &&
|
||
longLivedGate?.pass === true &&
|
||
runtimePath?.kind === "codex-app-server-stdio" &&
|
||
runtimePath?.criticalMissing === false &&
|
||
runtimePath?.terminalStatusOk === true
|
||
) {
|
||
return {
|
||
kind: "codex-stdio-long-lived",
|
||
statusLabel: "真实 runner:Codex app-server stdio 长会话",
|
||
tone: "dev-live",
|
||
summary: "repo-owned Codex app-server stdio runner 已通过长会话 gate;providerTrace 明确显示 Responses wire API、stdio command 和 terminalStatus。",
|
||
fullCodeAgent: true,
|
||
codexSessionGatePass: true
|
||
};
|
||
}
|
||
|
||
if (isCodexMcpPath({ runnerKind, sessionMode, implementationType, protocol: runtimePath?.protocol })) {
|
||
return {
|
||
kind: "codex-mcp-or-other-runner",
|
||
statusLabel: "MCP/其他 runner:非当前完整 Code Agent",
|
||
tone: "source",
|
||
summary: "这是 Codex stdio/MCP 或其他 runner 形态;当前完整 Code Agent 口径要求 repo-owned codex app-server --listen stdio:// 和 providerTrace 终态证据。",
|
||
fullCodeAgent: false,
|
||
codexSessionGatePass: false
|
||
};
|
||
}
|
||
|
||
if (runnerKind === CODEX_ONE_SHOT_RUNNER || sessionMode === "ephemeral-one-shot" || implementationType === "codex-cli-one-shot-ephemeral") {
|
||
return {
|
||
kind: "stateless-one-shot",
|
||
statusLabel: "一次性 runner:非长会话",
|
||
tone: "source",
|
||
summary: "回复来自一次性 runner;不可当作可复用或长会话 Code Agent。",
|
||
fullCodeAgent: false,
|
||
codexSessionGatePass: false
|
||
};
|
||
}
|
||
|
||
if (runnerKind === TEXT_FALLBACK_RUNNER || provider === "openai-responses" || capabilityLevel === "text-chat-only") {
|
||
return {
|
||
kind: "text-chat-only",
|
||
statusLabel: "OpenAI fallback:只是文本回答 / text-chat-only",
|
||
tone: "source",
|
||
summary: "只是文本回答;当前不能执行 workspace、tools、skills 或 Codex session 控制。",
|
||
fullCodeAgent: false,
|
||
codexSessionGatePass: false
|
||
};
|
||
}
|
||
|
||
if (provider === "codex-readonly-runner" || runnerKind === "hwlab-readonly-runner" || sessionMode === READONLY_SESSION_MODE) {
|
||
return {
|
||
kind: READONLY_SESSION_MODE,
|
||
statusLabel: "只读长会话:controlled-readonly-session-registry",
|
||
tone: "source",
|
||
summary: "只读 session registry 可复用 conversation/session;只能使用 read-only-session-tools,不能写工作区,也不能冒充完整 Codex stdio Code Agent。",
|
||
fullCodeAgent: false,
|
||
codexSessionGatePass: false
|
||
};
|
||
}
|
||
|
||
if (provider === "codex-stdio" || sessionMode === CODEX_APP_SERVER_SESSION_MODE) {
|
||
return {
|
||
kind: runtimePath?.providerTraceMissing ? "codex-stdio-provider-trace-missing" : "codex-stdio-blocked",
|
||
statusLabel: runtimePath?.providerTraceMissing ? "Codex stdio 降级:providerTrace 缺失" : "Codex stdio 受阻:当前不能执行",
|
||
tone: "blocked",
|
||
summary: runtimePath?.providerTraceMissing
|
||
? "Codex stdio 字段存在,但 providerTrace 缺失;界面不会猜测它是完整 app-server runner。"
|
||
: "Codex stdio 字段存在,但长会话 gate 或 providerTrace 终态未通过;当前不能标记为完整 runner 控制。",
|
||
fullCodeAgent: false,
|
||
codexSessionGatePass: false
|
||
};
|
||
}
|
||
|
||
return {
|
||
kind: "unknown-code-agent-path",
|
||
statusLabel: message?.status === "failed" ? "BLOCKED:当前不能执行" : "证据不足:字段缺失",
|
||
tone: message?.status === "failed" ? "blocked" : "source",
|
||
summary: "payload 缺少足够 provider/backend/runner/session 证据;只展示结构化字段缺失,不假定具备执行能力。",
|
||
fullCodeAgent: false,
|
||
codexSessionGatePass: false
|
||
};
|
||
}
|
||
|
||
export function codeAgentAttributionFromMessage(message) {
|
||
if (!message || typeof message !== "object") return null;
|
||
if (message.role && message.role !== "agent") return null;
|
||
if (!hasCodeAgentFields(message)) return null;
|
||
|
||
const classification = classifyCodeAgentFacts(message);
|
||
const ids = operationAuditEvidenceIds(message);
|
||
const runtimePath = codeAgentRuntimePathFromMessage(message);
|
||
const runnerKind = message.runner?.kind ?? runnerKindFromMessage(message);
|
||
const sessionMode = message.sessionMode ?? message.runner?.sessionMode ?? message.runner?.session;
|
||
const capabilityLevel = message.capabilityLevel ?? message.runner?.capabilityLevel;
|
||
const fields = [
|
||
attributionField("provider", message.provider, { message }),
|
||
attributionField("backend", message.backend, { message }),
|
||
attributionField("runnerKind", runnerKind, { message }),
|
||
attributionField("protocol", runtimePath?.protocol, { message }),
|
||
attributionField("implementationType", runtimePath?.implementationType, { message }),
|
||
attributionField("providerTrace.command", runtimePath?.command, { message }),
|
||
attributionField("providerTrace.terminalStatus", runtimePath?.terminalStatus, { message }),
|
||
attributionField("sessionMode", sessionMode, { message }),
|
||
attributionField("capabilityLevel", capabilityLevel, { message }),
|
||
attributionField("toolCalls", toolCallsAttributionSummary(message.toolCalls), { message, missingWhenNone: false }),
|
||
attributionField("traceId", message.traceId ?? message.runnerTrace?.traceId ?? message.providerTrace?.traceId, { message }),
|
||
attributionField("operationId", ids.operationId, { message }),
|
||
attributionField("audit", ids.auditId, { message }),
|
||
attributionField("evidence", ids.evidenceId, { message })
|
||
];
|
||
const missingFields = fields.filter((field) => field.missing).map((field) => field.key);
|
||
return {
|
||
kind: classification.kind,
|
||
tone: classification.tone,
|
||
label: messageAttributionLabel(message, classification),
|
||
summary: messageAttributionSummary(message, classification, missingFields, runtimePath),
|
||
fields,
|
||
missingFields
|
||
};
|
||
}
|
||
|
||
export function codeAgentRuntimePathFromMessage(message) {
|
||
if (!message || typeof message !== "object") return null;
|
||
if (message.role && message.role !== "agent") return null;
|
||
if (!hasCodeAgentFields(message)) return null;
|
||
const providerTrace = objectOrNull(message.providerTrace);
|
||
const runnerTrace = objectOrNull(message.runnerTrace);
|
||
const runnerKind = firstText(message.runner?.kind, runnerTrace?.runnerKind, runnerKindFromMessage(message));
|
||
const provider = nullableText(message.provider);
|
||
const protocol = firstText(providerTrace?.protocol, runnerTrace?.protocol);
|
||
const implementationType = firstText(message.implementationType, message.runner?.implementationType, runnerTrace?.implementationType);
|
||
const command = firstText(providerTrace?.command);
|
||
const terminalStatus = firstText(providerTrace?.terminalStatus);
|
||
const wireApi = firstText(providerTrace?.wireApi, isCodexAppServerPath({ provider, runnerKind, protocol, implementationType, command }) ? "responses" : null);
|
||
const providerTraceMissing = !providerTrace;
|
||
const required = {
|
||
provider,
|
||
runnerKind,
|
||
protocol,
|
||
implementationType,
|
||
"providerTrace.command": command,
|
||
"providerTrace.terminalStatus": terminalStatus
|
||
};
|
||
const missingFields = Object.entries(required)
|
||
.filter(([, value]) => !nullableText(value))
|
||
.map(([key]) => key);
|
||
const isAppServer = isCodexAppServerPath({ provider, runnerKind, protocol, implementationType, command });
|
||
const isMcp = isCodexMcpPath({ runnerKind, sessionMode: message.sessionMode, implementationType, protocol });
|
||
const isFallback = provider === "openai-responses" || runnerKind === TEXT_FALLBACK_RUNNER || message.capabilityLevel === "text-chat-only";
|
||
const isReadonly = provider === "codex-readonly-runner" || runnerKind === "hwlab-readonly-runner" || message.sessionMode === READONLY_SESSION_MODE;
|
||
const terminalStatusOk = terminalStatus === "completed" || terminalStatus === "succeeded";
|
||
const criticalMissing = missingFields.length > 0;
|
||
const blocked = ["failed", "blocked", "timeout", "error", "canceled"].includes(String(message.status ?? "").toLowerCase());
|
||
let kind = "unknown";
|
||
let label = "运行路径证据不足";
|
||
let summary = "payload 缺少 providerTrace 或 runner 关键字段;界面不会推断完整 Code Agent。";
|
||
let tone = blocked ? "blocked" : "warn";
|
||
|
||
if (isAppServer) {
|
||
kind = "codex-app-server-stdio";
|
||
if (!providerTrace) {
|
||
label = "DEGRADED:providerTrace 缺失";
|
||
summary = "检测到 Codex app-server 形态字段,但 providerTrace 缺失;不能确认 command、protocol 或 terminalStatus。";
|
||
} else if (criticalMissing) {
|
||
label = "DEGRADED:运行路径字段不完整";
|
||
summary = `repo-owned Codex app-server stdio 已暴露部分字段,但仍缺少 ${missingFields.join(", ")}。`;
|
||
} else if (!terminalStatusOk) {
|
||
label = "BLOCKED:Codex app-server 终态未完成";
|
||
summary = `repo-owned Codex app-server stdio 返回 terminalStatus=${terminalStatus};本次不标为完整完成。`;
|
||
tone = "blocked";
|
||
} else {
|
||
label = "repo-owned Codex app-server stdio";
|
||
summary = `Codex app-server 通过 stdio JSON-RPC 运行,使用 Responses wire API(wire_api=${wireApi ?? "未观测"}),不是 MCP、fallback 或 Cloud Web 直连 OpenAI。`;
|
||
tone = "ok";
|
||
}
|
||
} else if (isFallback) {
|
||
kind = "openai-text-fallback";
|
||
label = "DEGRADED:OpenAI text fallback";
|
||
summary = "这是 OpenAI text fallback/text-chat-only;没有 workspace runner、Codex app-server session 或工具控制能力。";
|
||
tone = blocked ? "blocked" : "warn";
|
||
} else if (isMcp) {
|
||
kind = "mcp-or-other-runner";
|
||
label = "DEGRADED:MCP/其他 runner";
|
||
summary = "这是 MCP 或其他 runner 形态,不等同于 repo-owned Codex app-server stdio。";
|
||
tone = blocked ? "blocked" : "warn";
|
||
} else if (isReadonly) {
|
||
kind = "readonly-runner";
|
||
label = "DEGRADED:read-only/source runner";
|
||
summary = "这是只读或 source runner;不能写 workspace,也不能冒充完整 Code Agent 完成。";
|
||
tone = blocked ? "blocked" : "warn";
|
||
} else if (providerTraceMissing) {
|
||
kind = "provider-trace-missing";
|
||
label = blocked ? "BLOCKED:providerTrace 缺失" : "DEGRADED:providerTrace 缺失";
|
||
summary = "providerTrace 缺失;不会把 fallback/read-only/source 或未知路径显示成完整 Code Agent。";
|
||
}
|
||
|
||
return {
|
||
kind,
|
||
label,
|
||
summary,
|
||
tone,
|
||
provider,
|
||
runnerKind,
|
||
protocol,
|
||
implementationType,
|
||
command,
|
||
terminalStatus,
|
||
wireApi,
|
||
providerTraceMissing,
|
||
criticalMissing,
|
||
missingFields,
|
||
terminalStatusOk,
|
||
rows: [
|
||
runtimePathRow("provider", provider),
|
||
runtimePathRow("runnerKind", runnerKind),
|
||
runtimePathRow("protocol", protocol),
|
||
runtimePathRow("implementationType", implementationType),
|
||
runtimePathRow("providerTrace.command", command),
|
||
runtimePathRow("providerTrace.terminalStatus", terminalStatus),
|
||
runtimePathRow("wireApi", wireApi)
|
||
]
|
||
};
|
||
}
|
||
|
||
export function toolCallsSummary(toolCalls) {
|
||
if (!Array.isArray(toolCalls) || toolCalls.length === 0) return "none";
|
||
const completed = toolCalls.filter((tool) => tool?.status === "completed").length;
|
||
const names = toolCalls
|
||
.map((tool) => compactFields([
|
||
firstText(tool?.name, "tool"),
|
||
tool?.status ? `(${tool.status})` : null,
|
||
tool?.route ? `@${tool.route}` : null
|
||
], ""))
|
||
.filter(Boolean)
|
||
.slice(0, 4)
|
||
.join(", ");
|
||
return `${completed}/${toolCalls.length}${names ? ` ${names}` : ""}`;
|
||
}
|
||
|
||
export function skillsSummary(skills) {
|
||
if (!skills || typeof skills !== "object") return "none";
|
||
const count = skills.totalCount ?? skills.count ?? (Array.isArray(skills.items) ? skills.items.length : 0);
|
||
const names = Array.isArray(skills.items)
|
||
? skills.items.map((item) => item?.name).filter(Boolean).slice(0, 3).join(",")
|
||
: "";
|
||
const blockers = Array.isArray(skills.blockers)
|
||
? skills.blockers.map((blocker) => blocker?.code).filter(Boolean).slice(0, 2).join(",")
|
||
: "";
|
||
return compactFields([
|
||
skills.status ?? "unknown",
|
||
`count=${count}`,
|
||
names ? `items=${names}` : null,
|
||
blockers ? `blockers=${blockers}` : null
|
||
], ":");
|
||
}
|
||
|
||
export function runnerTraceSummary(runnerTrace) {
|
||
if (!runnerTrace || typeof runnerTrace !== "object") return "none";
|
||
return compactFields([
|
||
field("traceId", runnerTrace.traceId),
|
||
field("runnerKind", runnerTrace.runnerKind),
|
||
field("sessionMode", runnerTrace.sessionMode),
|
||
field("implementationType", runnerTrace.implementationType),
|
||
field("protocol", runnerTrace.protocol),
|
||
typeof runnerTrace.turn === "number" ? field("turn", runnerTrace.turn) : null,
|
||
field("route", runnerTrace.route),
|
||
field("status", runnerTrace.status)
|
||
]);
|
||
}
|
||
|
||
function hasCodeAgentFields(message) {
|
||
return Boolean(
|
||
message &&
|
||
typeof message === "object" &&
|
||
(
|
||
message.provider ||
|
||
message.backend ||
|
||
message.runner ||
|
||
message.session ||
|
||
message.sessionMode ||
|
||
message.capabilityLevel ||
|
||
Array.isArray(message.toolCalls) ||
|
||
message.runnerTrace ||
|
||
message.providerTrace ||
|
||
(message.role === "agent" && (message.traceId || message.error || message.status === "failed" || message.status === "running"))
|
||
)
|
||
);
|
||
}
|
||
|
||
function messageAttributionLabel(message, classification) {
|
||
if (message?.status === "failed" || message?.sourceKind === "BLOCKED") return "BLOCKED:当前不能执行";
|
||
if (classification.kind === "text-chat-only") return "OpenAI fallback:只是文本回答";
|
||
if (classification.kind === READONLY_SESSION_MODE) return "read-only-session-tools:只读长会话";
|
||
if (classification.kind === "codex-stdio-long-lived") return "真实 runner:Codex app-server stdio 长会话";
|
||
if (classification.kind === "codex-mcp-or-other-runner") return "MCP/其他 runner:非当前完整 Code Agent";
|
||
if (classification.kind === "stateless-one-shot") return "一次性 runner:非长会话";
|
||
if (classification.kind === "codex-stdio-provider-trace-missing") return "DEGRADED:providerTrace 缺失";
|
||
if (classification.kind === "codex-stdio-blocked") return "Codex stdio 受阻:当前不能执行";
|
||
return classification.statusLabel;
|
||
}
|
||
|
||
function messageAttributionSummary(message, classification, missingFields, runtimePath = null) {
|
||
const prefix = messageAttributionLabel(message, classification);
|
||
const missing = missingFields.length > 0
|
||
? `证据不足:${missingFields.join(",")} 字段缺失。`
|
||
: "关键归因字段已观测。";
|
||
const runtime = runtimePath?.summary ? `运行路径:${runtimePath.summary}` : "";
|
||
if (classification.kind === "text-chat-only") {
|
||
return `${prefix};只是文本回答,当前不能执行工具或 runner 控制。${missing}${runtime}`;
|
||
}
|
||
if (message?.status === "failed" || message?.sourceKind === "BLOCKED") {
|
||
return `${prefix};本次回复没有执行能力。${missing}${runtime}`;
|
||
}
|
||
return `${prefix};${missing}${runtime}`;
|
||
}
|
||
|
||
function attributionField(key, rawValue, { message, missingWhenNone = true } = {}) {
|
||
const text = nullableText(rawValue);
|
||
const noneLike = !text || ["none", "unknown", "null", "undefined"].includes(text.toLowerCase());
|
||
const missing = missingWhenNone && noneLike;
|
||
const value = missing ? MISSING_FIELD_VALUE : localizedAttributionValue(key, text ?? "none", message);
|
||
return {
|
||
key,
|
||
label: key,
|
||
value,
|
||
missing,
|
||
chip: `${key}=${value}`
|
||
};
|
||
}
|
||
|
||
function localizedAttributionValue(key, value, message) {
|
||
const normalized = String(value ?? "").trim();
|
||
if (key === "provider" && normalized === "openai-responses") return `${normalized} / OpenAI fallback:只是文本回答`;
|
||
if (key === "runnerKind" && normalized === CODEX_APP_SERVER_RUNNER) return `${normalized} / repo-owned Codex app-server stdio`;
|
||
if (key === "runnerKind" && normalized === CODEX_MCP_RUNNER) return `${normalized} / MCP/其他 runner`;
|
||
if (key === "runnerKind" && normalized === TEXT_FALLBACK_RUNNER) return `${normalized} / 不是 runner 控制`;
|
||
if (key === "runnerKind" && normalized === "hwlab-readonly-runner") return `${normalized} / 只读 runner`;
|
||
if (key === "protocol" && normalized === CODEX_APP_SERVER_PROTOCOL) return `${normalized} / Responses wire API`;
|
||
if (key === "implementationType" && normalized === CODEX_APP_SERVER_IMPLEMENTATION) return `${normalized} / repo-owned app-server stdio session`;
|
||
if (key === "implementationType" && normalized === CODEX_MCP_IMPLEMENTATION) return `${normalized} / MCP stdio session`;
|
||
if (key === "sessionMode" && normalized === READONLY_SESSION_MODE) return `${normalized} / 只读 session registry`;
|
||
if (key === "sessionMode" && normalized === CODEX_APP_SERVER_SESSION_MODE) return `${normalized} / app-server stdio 长会话`;
|
||
if (key === "sessionMode" && normalized === CODEX_MCP_SESSION_MODE) return `${normalized} / MCP stdio 长会话`;
|
||
if (key === "capabilityLevel" && normalized === "text-chat-only") return "text-chat-only / 只是文本回答";
|
||
if (key === "capabilityLevel" && normalized === "read-only-session-tools") return `${normalized} / 只读工具`;
|
||
if (key === "toolCalls" && normalized === "none") {
|
||
return message?.capabilityLevel === "text-chat-only" ? "0/0 / 无工具调用:只是文本回答" : "0/0 / 无工具调用";
|
||
}
|
||
return normalized;
|
||
}
|
||
|
||
function operationAuditEvidenceIds(message) {
|
||
const providerTrace = objectOrNull(message?.providerTrace);
|
||
const runnerTrace = objectOrNull(message?.runnerTrace);
|
||
const toolCalls = Array.isArray(message?.toolCalls) ? message.toolCalls : [];
|
||
const parsedToolPayloads = toolCalls.map((toolCall) => parseToolStdout(toolCall?.stdout)).filter(Boolean);
|
||
return {
|
||
operationId: firstText(
|
||
message?.operationId,
|
||
message?.operation?.operationId,
|
||
providerTrace?.operationId,
|
||
runnerTrace?.operationId,
|
||
...toolCalls.map((toolCall) => toolCall?.operationId ?? toolCall?.hwlabApi?.operationId),
|
||
...parsedToolPayloads.map((payload) => payload?.operationId ?? payload?.operation?.operationId)
|
||
),
|
||
auditId: firstText(
|
||
message?.auditId,
|
||
message?.audit?.auditId,
|
||
providerTrace?.auditId,
|
||
runnerTrace?.auditId,
|
||
...toolCalls.map((toolCall) => toolCall?.auditId ?? toolCall?.audit?.auditId ?? toolCall?.hwlabApi?.auditId),
|
||
...parsedToolPayloads.map((payload) => payload?.auditId ?? payload?.audit?.auditId)
|
||
),
|
||
evidenceId: firstText(
|
||
message?.evidenceId,
|
||
message?.evidence?.evidenceId,
|
||
providerTrace?.evidenceId,
|
||
runnerTrace?.evidenceId,
|
||
...toolCalls.map((toolCall) => toolCall?.evidenceId ?? toolCall?.evidence?.evidenceId ?? toolCall?.hwlabApi?.evidenceId),
|
||
...parsedToolPayloads.map((payload) => payload?.evidenceId ?? payload?.evidence?.evidenceId)
|
||
)
|
||
};
|
||
}
|
||
|
||
function toolCallsAttributionSummary(toolCalls) {
|
||
const summary = toolCallsSummary(toolCalls);
|
||
return summary === "none" ? "none" : summary;
|
||
}
|
||
|
||
function objectOrNull(value) {
|
||
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
||
}
|
||
|
||
function runnerKindFromMessage(message) {
|
||
return firstText(
|
||
message?.runner?.kind,
|
||
message?.runnerTrace?.runnerKind,
|
||
message?.provider === "openai-responses" ? TEXT_FALLBACK_RUNNER : null
|
||
);
|
||
}
|
||
|
||
function runtimePathRow(label, value) {
|
||
const text = nullableText(value);
|
||
return {
|
||
label,
|
||
value: text ?? MISSING_FIELD_VALUE,
|
||
missing: !text
|
||
};
|
||
}
|
||
|
||
function isCodexAppServerPath({ provider, runnerKind, protocol, implementationType, command }) {
|
||
return provider === "codex-stdio" && (
|
||
runnerKind === CODEX_APP_SERVER_RUNNER ||
|
||
protocol === CODEX_APP_SERVER_PROTOCOL ||
|
||
implementationType === CODEX_APP_SERVER_IMPLEMENTATION ||
|
||
/\bcodex\s+app-server\s+--listen\s+stdio:\/\//u.test(String(command ?? ""))
|
||
);
|
||
}
|
||
|
||
function isCodexMcpPath({ runnerKind, sessionMode, implementationType, protocol }) {
|
||
return (
|
||
runnerKind === CODEX_MCP_RUNNER ||
|
||
sessionMode === CODEX_MCP_SESSION_MODE ||
|
||
implementationType === CODEX_MCP_IMPLEMENTATION ||
|
||
/mcp/iu.test(String(protocol ?? ""))
|
||
);
|
||
}
|
||
|
||
function parseToolStdout(stdout) {
|
||
if (typeof stdout !== "string" || !stdout.trim()) return null;
|
||
const text = stdout.trim();
|
||
const direct = parseJson(text);
|
||
if (direct) return direct;
|
||
const start = text.indexOf("{");
|
||
const end = text.lastIndexOf("}");
|
||
if (start === -1 || end <= start) return null;
|
||
return parseJson(text.slice(start, end + 1));
|
||
}
|
||
|
||
function parseJson(text) {
|
||
try {
|
||
const parsed = JSON.parse(text);
|
||
return parsed && typeof parsed === "object" ? parsed : null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function compactFields(fields, separator = " / ") {
|
||
return fields.filter((item) => item !== null && item !== undefined && String(item).trim()).map((item) => String(item).trim()).join(separator);
|
||
}
|
||
|
||
function field(label, value) {
|
||
const normalized = nullableText(value);
|
||
return normalized ? `${label}=${normalized}` : null;
|
||
}
|
||
|
||
function requiredField(label, value) {
|
||
const normalized = nullableText(value);
|
||
return `${label}=${normalized ?? MISSING_FIELD_VALUE}`;
|
||
}
|
||
|
||
function valueOrMissing(value) {
|
||
return nullableText(value) ?? MISSING_FIELD_VALUE;
|
||
}
|
||
|
||
function firstText(...values) {
|
||
return values.map(nullableText).find(Boolean) ?? null;
|
||
}
|
||
|
||
function nullableText(value) {
|
||
if (value === undefined || value === null || value === "") return null;
|
||
return String(value).replace(/\s+/gu, " ").trim();
|
||
}
|