Files
pikasTech-HWLAB/web/hwlab-cloud-web/code-agent-facts.mjs
T
2026-05-23 16:22:40 +00:00

467 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const M3_IO_ROUTE = "/v1/m3/io";
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_STDIO_SESSION_MODE = "codex-mcp-stdio-long-lived";
const HWLAB_SKILL_RUNNER = "hwlab-m3-io-skill-cli";
const MISSING_FIELD_VALUE = "字段缺失:证据不足";
const REQUIRED_ATTRIBUTION_KEYS = Object.freeze([
"provider",
"backend",
"runner",
"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,
hwlabApiFacts: hwlabApiFactsFromMessage(message)
};
}
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;
if (provider === "codex-stdio" && longLivedGate?.status === "pass" && longLivedGate?.pass === true) {
return {
kind: "codex-stdio-long-lived",
statusLabel: "真实 runnerCodex stdio 长会话",
tone: "dev-live",
summary: "真实 Codex stdio runner 已通过长会话 gate;具备 session 生命周期和 trace capture 证据。",
fullCodeAgent: true,
codexSessionGatePass: true
};
}
if (provider === "hwlab-skill-cli" || runnerKind === HWLAB_SKILL_RUNNER || hasM3IoToolCall(message?.toolCalls)) {
return {
kind: "hwlab-skill-cli",
statusLabel: "Skill CLIHWLAB API 受控路径",
tone: "source",
summary: "回复来自 hwlab-skill-cli 受控路径;它只代表 Skill CLI -> HWLAB API,不是 OpenAI fallback,也不是通用 Codex session。",
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 可用;只能使用 read-only-session-tools,不能写工作区,也不能冒充完整 Codex stdio 长会话。",
fullCodeAgent: false,
codexSessionGatePass: false
};
}
if (provider === "codex-stdio" || sessionMode === CODEX_STDIO_SESSION_MODE) {
return {
kind: "codex-stdio-blocked",
statusLabel: "Codex stdio 受阻:当前不能执行",
tone: "blocked",
summary: "Codex stdio 字段存在,但长会话 gate 未通过;当前不能执行 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 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("runner", runnerKind, { 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),
fields,
missingFields
};
}
export function hwlabApiFactsFromMessage(message) {
const facts = [];
for (const toolCall of Array.isArray(message?.toolCalls) ? message.toolCalls : []) {
const parsed = parseToolStdout(toolCall.stdout);
const route = firstText(toolCall.route, toolCall.hwlabApi?.route, parsed?.route);
if (route !== M3_IO_ROUTE) continue;
const blocker = firstText(
toolCall.blocker?.code,
toolCall.capabilityBlocker?.code,
toolCall.trustBlocker?.code,
firstBlockerCode(toolCall.blockers),
parsed?.blocker?.code,
parsed?.blocker,
parsed?.durable?.blocker
);
facts.push({
tool: firstText(toolCall.name, "skill-cli"),
route,
method: firstText(toolCall.method, parsed?.method, "POST"),
operationId: nullableText(toolCall.operationId ?? parsed?.operationId),
traceId: nullableText(toolCall.traceId ?? parsed?.traceId ?? message?.traceId),
auditId: nullableText(toolCall.auditId ?? toolCall.audit?.auditId ?? parsed?.auditId ?? parsed?.audit?.auditId),
evidenceId: nullableText(toolCall.evidenceId ?? toolCall.evidence?.evidenceId ?? parsed?.evidenceId ?? parsed?.evidence?.evidenceId),
accepted: toolCall.accepted ?? parsed?.accepted ?? null,
readback: toolCall.readback ?? parsed?.readback ?? parsed?.result?.targetReadback ?? null,
blocker: nullableText(blocker),
status: firstText(toolCall.status, parsed?.status, "unknown")
});
}
return facts;
}
export function compactHwlabApiFact(fact) {
if (!fact) return "";
return compactFields([
field("route", fact.route),
field("method", fact.method),
field("operationId", fact.operationId),
field("traceId", fact.traceId),
field("auditId", fact.auditId),
field("evidenceId", fact.evidenceId),
field("blocker", fact.blocker)
]);
}
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),
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 === "hwlab-skill-cli") return "hwlab-skill-cliSkill CLI 受控路径";
if (classification.kind === "codex-stdio-long-lived") return "真实 runnerCodex stdio 长会话";
if (classification.kind === "stateless-one-shot") return "一次性 runner:非长会话";
if (classification.kind === "codex-stdio-blocked") return "Codex stdio 受阻:当前不能执行";
return classification.statusLabel;
}
function messageAttributionSummary(message, classification, missingFields) {
const prefix = messageAttributionLabel(message, classification);
const missing = missingFields.length > 0
? `证据不足:${missingFields.join(",")} 字段缺失。`
: "关键归因字段已观测。";
if (classification.kind === "text-chat-only") {
return `${prefix};只是文本回答,当前不能执行工具、Skill CLI 或 runner 控制。${missing}`;
}
if (message?.status === "failed" || message?.sourceKind === "BLOCKED") {
return `${prefix};本次回复没有执行能力。${missing}`;
}
return `${prefix}${missing}`;
}
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 === "provider" && normalized === "hwlab-skill-cli") return `${normalized} / Skill CLI 受控路径`;
if (key === "runner" && normalized === TEXT_FALLBACK_RUNNER) return `${normalized} / 不是 runner 控制`;
if (key === "runner" && normalized === HWLAB_SKILL_RUNNER) return `${normalized} / Skill CLI runner`;
if (key === "runner" && normalized === "hwlab-readonly-runner") return `${normalized} / 只读 runner`;
if (key === "sessionMode" && normalized === READONLY_SESSION_MODE) return `${normalized} / 只读 session registry`;
if (key === "capabilityLevel" && normalized === "text-chat-only") return "text-chat-only / 只是文本回答";
if (key === "capabilityLevel" && normalized === "read-only-session-tools") return `${normalized} / 只读工具`;
if (key === "capabilityLevel" && normalized === "hwlab-api-control-ready") return `${normalized} / HWLAB API 受控路径`;
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 hasM3IoToolCall(toolCalls) {
return Array.isArray(toolCalls) && toolCalls.some((toolCall) =>
toolCall?.route === M3_IO_ROUTE ||
toolCall?.hwlabApi?.route === M3_IO_ROUTE ||
parseToolStdout(toolCall?.stdout)?.route === M3_IO_ROUTE
);
}
function runnerKindFromMessage(message) {
return firstText(
message?.runner?.kind,
message?.runnerTrace?.runnerKind,
message?.provider === "openai-responses" ? TEXT_FALLBACK_RUNNER : null
);
}
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 firstBlockerCode(blockers) {
if (!Array.isArray(blockers)) return null;
return blockers.map((blocker) => blocker?.code).find(Boolean) ?? 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();
}