308 lines
10 KiB
JavaScript
308 lines
10 KiB
JavaScript
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";
|
|
|
|
export function codeAgentFactsFromMessage(message) {
|
|
if (!hasCodeAgentFields(message)) return null;
|
|
const classification = classifyCodeAgentFacts(message);
|
|
const rows = [
|
|
{
|
|
label: "runner/provider/backend",
|
|
value: compactFields([
|
|
field("runner", message.runner?.kind ?? runnerKindFromMessage(message)),
|
|
field("provider", message.provider),
|
|
field("backend", message.backend)
|
|
])
|
|
},
|
|
{
|
|
label: "session",
|
|
value: compactFields([
|
|
field("id", message.session?.sessionId ?? message.sessionId ?? message.runner?.sessionId),
|
|
field("status", message.session?.status),
|
|
field("sessionMode", message.sessionMode ?? message.runner?.sessionMode ?? message.runner?.session)
|
|
])
|
|
},
|
|
{
|
|
label: "workspace",
|
|
value: message.workspace ?? message.runner?.workspace ?? "none"
|
|
},
|
|
{
|
|
label: "sandbox",
|
|
value: message.sandbox ?? message.runner?.sandbox ?? "none"
|
|
},
|
|
{
|
|
label: "capabilityLevel",
|
|
value: message.capabilityLevel ?? message.runner?.capabilityLevel ?? "unknown"
|
|
},
|
|
{
|
|
label: "toolCalls",
|
|
value: toolCallsSummary(message.toolCalls)
|
|
},
|
|
{
|
|
label: "skills",
|
|
value: skillsSummary(message.skills)
|
|
},
|
|
{
|
|
label: "runnerTrace/traceId",
|
|
value: compactFields([
|
|
field("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: "完整 Code Agent session",
|
|
tone: "dev-live",
|
|
summary: "Codex stdio long-lived session gate passed; session lifecycle and trace capture are available.",
|
|
fullCodeAgent: true,
|
|
codexSessionGatePass: true
|
|
};
|
|
}
|
|
|
|
if (provider === "hwlab-skill-cli" || runnerKind === HWLAB_SKILL_RUNNER || hasM3IoToolCall(message?.toolCalls)) {
|
|
return {
|
|
kind: "hwlab-skill-cli",
|
|
statusLabel: "HWLAB Skill CLI",
|
|
tone: "source",
|
|
summary: "Controlled Skill CLI -> HWLAB API path only; this is not OpenAI fallback and not a generic 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: "stateless-one-shot",
|
|
tone: "source",
|
|
summary: "Ephemeral one-shot runner result; do not treat it as a reusable or long-lived Code Agent session.",
|
|
fullCodeAgent: false,
|
|
codexSessionGatePass: false
|
|
};
|
|
}
|
|
|
|
if (runnerKind === TEXT_FALLBACK_RUNNER || provider === "openai-responses" || capabilityLevel === "text-chat-only") {
|
|
return {
|
|
kind: "text-chat-only",
|
|
statusLabel: "openai-responses-fallback / text-chat-only",
|
|
tone: "source",
|
|
summary: "Text chat fallback can answer ordinary messages, but it has no workspace, tools, skills, or Codex session gate.",
|
|
fullCodeAgent: false,
|
|
codexSessionGatePass: false
|
|
};
|
|
}
|
|
|
|
if (provider === "codex-readonly-runner" || runnerKind === "hwlab-readonly-runner" || sessionMode === READONLY_SESSION_MODE) {
|
|
return {
|
|
kind: READONLY_SESSION_MODE,
|
|
statusLabel: "只读 session registry",
|
|
tone: "source",
|
|
summary: "Bounded read-only tools and session mapping are available, but Codex stdio, write capability, and durable session remain blocked.",
|
|
fullCodeAgent: false,
|
|
codexSessionGatePass: false
|
|
};
|
|
}
|
|
|
|
if (provider === "codex-stdio" || sessionMode === CODEX_STDIO_SESSION_MODE) {
|
|
return {
|
|
kind: "codex-stdio-blocked",
|
|
statusLabel: "Codex stdio blocked",
|
|
tone: "blocked",
|
|
summary: "Codex stdio fields are present, but the long-lived session gate has not passed.",
|
|
fullCodeAgent: false,
|
|
codexSessionGatePass: false
|
|
};
|
|
}
|
|
|
|
return {
|
|
kind: "unknown-code-agent-path",
|
|
statusLabel: "Code Agent facts",
|
|
tone: message?.status === "failed" ? "blocked" : "source",
|
|
summary: "Provider response is shown with its raw runner/session facts; completion is not promoted without a passing gate.",
|
|
fullCodeAgent: false,
|
|
codexSessionGatePass: false
|
|
};
|
|
}
|
|
|
|
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,
|
|
operationId: nullableText(toolCall.operationId ?? parsed?.operationId),
|
|
traceId: nullableText(toolCall.traceId ?? parsed?.traceId ?? message?.traceId),
|
|
auditId: nullableText(toolCall.audit?.auditId ?? parsed?.audit?.auditId ?? parsed?.auditId),
|
|
evidenceId: nullableText(toolCall.evidence?.evidenceId ?? parsed?.evidence?.evidenceId ?? parsed?.evidenceId),
|
|
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("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
|
|
)
|
|
);
|
|
}
|
|
|
|
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 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();
|
|
}
|