Merge pull request #355 from pikasTech/feat/code-agent-facts-78
feat: expose Code Agent capability facts
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
+57
-15
@@ -1,4 +1,11 @@
|
||||
import { gateSummary } from "./gate-summary.mjs";
|
||||
import {
|
||||
codeAgentFactsFromMessage,
|
||||
compactHwlabApiFact,
|
||||
runnerTraceSummary as compactRunnerTraceSummary,
|
||||
skillsSummary as compactSkillsSummary,
|
||||
toolCallsSummary as compactToolCallsSummary
|
||||
} from "./code-agent-facts.mjs";
|
||||
import { classifyWorkbenchLiveStatus } from "./live-status.mjs";
|
||||
import { marked } from "./third_party/marked/marked.esm.js";
|
||||
import { wiringPresentationFromSummary } from "./wiring-status.mjs";
|
||||
@@ -2397,11 +2404,55 @@ function messageCard(message) {
|
||||
article.append(textSpan(roleLabel(message.role), "message-role"));
|
||||
article.append(textSpan(message.title, "message-title"));
|
||||
article.append(textSpan(message.text, "message-copy"));
|
||||
const capabilityFacts = codeAgentCapabilityFactsPanel(message);
|
||||
if (capabilityFacts) article.append(capabilityFacts);
|
||||
const messageEvidence = messageEvidencePanel(message);
|
||||
if (messageEvidence) article.append(messageEvidence);
|
||||
return article;
|
||||
}
|
||||
|
||||
function codeAgentCapabilityFactsPanel(message) {
|
||||
const facts = codeAgentFactsFromMessage(message);
|
||||
if (!facts) return null;
|
||||
const section = document.createElement("section");
|
||||
section.className = `code-agent-facts tone-border-${toneClass(facts.tone)}`;
|
||||
section.dataset.codeAgentFactKind = facts.kind;
|
||||
section.append(codeAgentFactsHeader(facts));
|
||||
|
||||
const grid = document.createElement("dl");
|
||||
grid.className = "code-agent-fact-grid";
|
||||
for (const row of facts.rows) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "code-agent-fact-row";
|
||||
item.append(textSpan(row.label, "code-agent-fact-key"), textSpan(row.value ?? "none", "code-agent-fact-value mono"));
|
||||
grid.append(item);
|
||||
}
|
||||
section.append(grid);
|
||||
|
||||
if (facts.hwlabApiFacts.length > 0) {
|
||||
const apiList = document.createElement("div");
|
||||
apiList.className = "hwlab-api-facts";
|
||||
apiList.append(textSpan("HWLAB API", "hwlab-api-facts-title"));
|
||||
for (const fact of facts.hwlabApiFacts) {
|
||||
const code = document.createElement("code");
|
||||
code.className = "hwlab-api-fact-code";
|
||||
code.textContent = compactHwlabApiFact(fact);
|
||||
apiList.append(code);
|
||||
}
|
||||
section.append(apiList);
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
function codeAgentFactsHeader(facts) {
|
||||
const header = document.createElement("div");
|
||||
header.className = "code-agent-facts-head";
|
||||
header.append(badge(facts.statusLabel, facts.tone));
|
||||
header.append(textSpan(facts.summary, "code-agent-facts-summary"));
|
||||
return header;
|
||||
}
|
||||
|
||||
function messageEvidencePanel(message) {
|
||||
const fields = messageEvidenceFields(message).map(boundedEvidenceField);
|
||||
if (fields.length === 0) return null;
|
||||
@@ -2464,27 +2515,18 @@ function providerTraceSummary(providerTrace) {
|
||||
}
|
||||
|
||||
function toolCallsSummary(toolCalls) {
|
||||
if (!Array.isArray(toolCalls) || toolCalls.length === 0) return null;
|
||||
const completed = toolCalls.filter((tool) => tool?.status === "completed").length;
|
||||
const names = toolCalls.map((tool) => tool?.name).filter(Boolean).slice(0, 3).join(",");
|
||||
return `${completed}/${toolCalls.length}${names ? `:${names}` : ""}`;
|
||||
const summary = compactToolCallsSummary(toolCalls);
|
||||
return summary === "none" ? null : summary;
|
||||
}
|
||||
|
||||
function skillsSummary(skills) {
|
||||
if (!skills || typeof skills !== "object") return null;
|
||||
if (skills.status === "not_requested") return "not_requested";
|
||||
const count = skills.totalCount ?? skills.count ?? (Array.isArray(skills.items) ? skills.items.length : 0);
|
||||
return `${skills.status ?? "unknown"}:${count}`;
|
||||
const summary = compactSkillsSummary(skills);
|
||||
return summary === "none" ? null : summary;
|
||||
}
|
||||
|
||||
function runnerTraceSummary(runnerTrace) {
|
||||
if (!runnerTrace || typeof runnerTrace !== "object") return null;
|
||||
const parts = [
|
||||
runnerTrace.runnerKind ?? "present",
|
||||
runnerTrace.sessionMode,
|
||||
typeof runnerTrace.turn === "number" ? `turn${runnerTrace.turn}` : null
|
||||
].filter(Boolean);
|
||||
return parts.join(":");
|
||||
const summary = compactRunnerTraceSummary(runnerTrace);
|
||||
return summary === "none" ? null : summary;
|
||||
}
|
||||
|
||||
function sessionReuseSummary(sessionReuse) {
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
codeAgentFactsFromMessage,
|
||||
compactHwlabApiFact
|
||||
} from "./code-agent-facts.mjs";
|
||||
|
||||
test("codex-readonly-runner is shown as partial read-only session registry", () => {
|
||||
const facts = codeAgentFactsFromMessage({
|
||||
status: "source",
|
||||
provider: "codex-readonly-runner",
|
||||
model: "read-only-tools",
|
||||
backend: "hwlab-cloud-api/codex-readonly-runner",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "read-only",
|
||||
capabilityLevel: "read-only-session-tools",
|
||||
sessionMode: "controlled-readonly-session-registry",
|
||||
implementationType: "controlled-readonly-session-registry",
|
||||
session: {
|
||||
sessionId: "ses_readonly",
|
||||
status: "idle",
|
||||
turn: 1,
|
||||
lastTraceId: "trc_readonly"
|
||||
},
|
||||
sessionReuse: {
|
||||
reused: false,
|
||||
turn: 1,
|
||||
status: "idle"
|
||||
},
|
||||
runner: {
|
||||
kind: "hwlab-readonly-runner",
|
||||
codexStdio: false,
|
||||
writeCapable: false,
|
||||
durableSession: false
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "blocked",
|
||||
pass: false,
|
||||
blockers: [{ code: "controlled_readonly_not_long_lived_stdio" }]
|
||||
},
|
||||
toolCalls: [{ name: "pwd", status: "completed" }],
|
||||
skills: { status: "not_requested", count: 0 },
|
||||
runnerTrace: {
|
||||
traceId: "trc_readonly",
|
||||
runnerKind: "hwlab-readonly-runner",
|
||||
sessionMode: "controlled-readonly-session-registry",
|
||||
turn: 1
|
||||
},
|
||||
traceId: "trc_readonly"
|
||||
});
|
||||
|
||||
assert.equal(facts.kind, "controlled-readonly-session-registry");
|
||||
assert.equal(facts.fullCodeAgent, false);
|
||||
assert.equal(facts.codexSessionGatePass, false);
|
||||
assert.match(facts.summary, /Codex stdio/u);
|
||||
assert.match(facts.rows.find((row) => row.label === "runner/provider/backend").value, /runner=hwlab-readonly-runner/u);
|
||||
assert.match(facts.rows.find((row) => row.label === "toolCalls").value, /1\/1 pwd\(completed\)/u);
|
||||
});
|
||||
|
||||
test("long-lived Codex stdio session fields are shown as full Code Agent session", () => {
|
||||
const facts = codeAgentFactsFromMessage({
|
||||
status: "completed",
|
||||
provider: "codex-stdio",
|
||||
model: "gpt-5.5",
|
||||
backend: "hwlab-cloud-api/codex-mcp-stdio",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-mcp-stdio-session",
|
||||
session: {
|
||||
sessionId: "ses_stdio",
|
||||
status: "idle",
|
||||
turn: 3,
|
||||
idleTimeoutMs: 1800000,
|
||||
lastTraceId: "trc_stdio",
|
||||
longLivedSession: true,
|
||||
codexStdio: true
|
||||
},
|
||||
runner: {
|
||||
kind: "codex-mcp-stdio-runner",
|
||||
codexStdio: true,
|
||||
writeCapable: true,
|
||||
durableSession: true
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "pass",
|
||||
pass: true
|
||||
},
|
||||
toolCalls: [{ name: "codex-reply", status: "completed" }],
|
||||
skills: { status: "not_requested", count: 0 },
|
||||
runnerTrace: {
|
||||
traceId: "trc_stdio",
|
||||
runnerKind: "codex-mcp-stdio-runner",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
turn: 3
|
||||
},
|
||||
traceId: "trc_stdio"
|
||||
});
|
||||
|
||||
assert.equal(facts.kind, "codex-stdio-long-lived");
|
||||
assert.equal(facts.statusLabel, "完整 Code Agent session");
|
||||
assert.equal(facts.fullCodeAgent, true);
|
||||
assert.equal(facts.codexSessionGatePass, true);
|
||||
assert.match(facts.rows.find((row) => row.label === "sandbox").value, /workspace-write/u);
|
||||
});
|
||||
|
||||
test("openai-responses fallback is explicitly text-chat-only", () => {
|
||||
const facts = codeAgentFactsFromMessage({
|
||||
status: "source",
|
||||
provider: "openai-responses",
|
||||
model: "gpt-5.5",
|
||||
backend: "hwlab-cloud-api/openai-responses",
|
||||
capabilityLevel: "text-chat-only",
|
||||
sessionMode: "provider-text-request",
|
||||
runner: {
|
||||
kind: "openai-responses-fallback",
|
||||
codexStdio: false,
|
||||
writeCapable: false,
|
||||
durableSession: false
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "blocked",
|
||||
pass: false,
|
||||
blockers: [{ code: "openai_responses_fallback_not_session" }]
|
||||
},
|
||||
toolCalls: [],
|
||||
skills: { status: "not_requested", count: 0 },
|
||||
runnerTrace: {
|
||||
traceId: "trc_fallback",
|
||||
runnerKind: "openai-responses-fallback",
|
||||
events: ["fallback:text-chat-only"]
|
||||
},
|
||||
traceId: "trc_fallback"
|
||||
});
|
||||
|
||||
assert.equal(facts.kind, "text-chat-only");
|
||||
assert.equal(facts.statusLabel, "openai-responses-fallback / text-chat-only");
|
||||
assert.equal(facts.fullCodeAgent, false);
|
||||
assert.equal(facts.codexSessionGatePass, false);
|
||||
assert.match(facts.summary, /no workspace, tools, skills, or Codex session gate/u);
|
||||
});
|
||||
|
||||
test("stateless one-shot is not wrapped as a long-lived session", () => {
|
||||
const facts = codeAgentFactsFromMessage({
|
||||
status: "source",
|
||||
provider: "codex-cli",
|
||||
model: "gpt-5.5",
|
||||
backend: "hwlab-cloud-api/codex-cli",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "read-only",
|
||||
capabilityLevel: "text-chat-only",
|
||||
sessionMode: "ephemeral-one-shot",
|
||||
implementationType: "codex-cli-one-shot-ephemeral",
|
||||
session: {
|
||||
sessionId: "cnv_one_shot",
|
||||
status: "expired",
|
||||
idleTimeoutMs: 0,
|
||||
lastTraceId: "trc_one_shot"
|
||||
},
|
||||
runner: {
|
||||
kind: "codex-cli-one-shot-ephemeral",
|
||||
codexStdio: false,
|
||||
writeCapable: false,
|
||||
durableSession: false
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "blocked",
|
||||
pass: false,
|
||||
blockers: [{ code: "one_shot_runner_not_long_lived" }]
|
||||
},
|
||||
traceId: "trc_one_shot"
|
||||
});
|
||||
|
||||
assert.equal(facts.kind, "stateless-one-shot");
|
||||
assert.equal(facts.fullCodeAgent, false);
|
||||
assert.equal(facts.codexSessionGatePass, false);
|
||||
assert.match(facts.summary, /Ephemeral one-shot runner/u);
|
||||
});
|
||||
|
||||
test("HWLAB Skill CLI toolCall shows compact copyable /v1/m3/io facts", () => {
|
||||
const facts = codeAgentFactsFromMessage({
|
||||
status: "source",
|
||||
provider: "hwlab-skill-cli",
|
||||
model: "controlled-m3-io",
|
||||
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "hwlab-api-route-only",
|
||||
capabilityLevel: "hwlab-api-control-ready",
|
||||
sessionMode: "controlled-m3-io-skill-cli",
|
||||
runner: {
|
||||
kind: "hwlab-m3-io-skill-cli",
|
||||
codexStdio: false,
|
||||
writeCapable: true,
|
||||
durableSession: false
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "blocked",
|
||||
pass: false
|
||||
},
|
||||
toolCalls: [
|
||||
{
|
||||
name: "hwlab-agent-runtime.m3-io",
|
||||
type: "skill-cli",
|
||||
status: "completed",
|
||||
route: "/v1/m3/io",
|
||||
operationId: "op_m3_do_write_cli",
|
||||
traceId: "trc_m3_skill_cli",
|
||||
audit: { auditId: "aud_m3_do_write_cli_succeeded" },
|
||||
evidence: { evidenceId: "evd_m3_do_write_cli_succeeded" },
|
||||
blocker: { code: "runtime_durable_not_green" },
|
||||
stdout: JSON.stringify({
|
||||
route: "/v1/m3/io",
|
||||
status: "succeeded",
|
||||
traceId: "trc_m3_skill_cli",
|
||||
operationId: "op_m3_do_write_cli",
|
||||
audit: { auditId: "aud_m3_do_write_cli_succeeded" },
|
||||
evidence: { evidenceId: "evd_m3_do_write_cli_succeeded" },
|
||||
durable: { blocker: "runtime_durable_not_green" }
|
||||
})
|
||||
}
|
||||
],
|
||||
skills: {
|
||||
status: "used",
|
||||
count: 1,
|
||||
items: [{ name: "hwlab-agent-runtime.m3-io" }]
|
||||
},
|
||||
runnerTrace: {
|
||||
traceId: "trc_m3_skill_cli",
|
||||
runnerKind: "hwlab-m3-io-skill-cli",
|
||||
sessionMode: "controlled-m3-io-skill-cli",
|
||||
route: "/v1/m3/io",
|
||||
status: "succeeded"
|
||||
},
|
||||
traceId: "trc_m3_skill_cli"
|
||||
});
|
||||
|
||||
assert.equal(facts.kind, "hwlab-skill-cli");
|
||||
assert.equal(facts.fullCodeAgent, false);
|
||||
assert.equal(facts.hwlabApiFacts.length, 1);
|
||||
assert.deepEqual(facts.hwlabApiFacts[0], {
|
||||
tool: "hwlab-agent-runtime.m3-io",
|
||||
route: "/v1/m3/io",
|
||||
operationId: "op_m3_do_write_cli",
|
||||
traceId: "trc_m3_skill_cli",
|
||||
auditId: "aud_m3_do_write_cli_succeeded",
|
||||
evidenceId: "evd_m3_do_write_cli_succeeded",
|
||||
blocker: "runtime_durable_not_green",
|
||||
status: "completed"
|
||||
});
|
||||
assert.equal(
|
||||
compactHwlabApiFact(facts.hwlabApiFacts[0]),
|
||||
"route=/v1/m3/io / operationId=op_m3_do_write_cli / traceId=trc_m3_skill_cli / auditId=aud_m3_do_write_cli_succeeded / evidenceId=evd_m3_do_write_cli_succeeded / blocker=runtime_durable_not_green"
|
||||
);
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"m3-readonly": "node scripts/m3-readonly-contract.mjs",
|
||||
"check": "node --test wiring-status.test.mjs && node scripts/check.mjs",
|
||||
"check": "node --test code-agent-facts.test.mjs wiring-status.test.mjs && node scripts/check.mjs",
|
||||
"build": "node scripts/build.mjs",
|
||||
"layout": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --static --report ../../reports/dev-gate/dev-cloud-workbench-layout.json",
|
||||
"layout:build": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --build --report ../../reports/dev-gate/dev-cloud-workbench-layout-build.json",
|
||||
|
||||
@@ -22,6 +22,7 @@ const requiredFiles = [
|
||||
"index.html",
|
||||
"styles.css",
|
||||
"app.mjs",
|
||||
"code-agent-facts.mjs",
|
||||
"live-status.mjs",
|
||||
"gate-summary.mjs",
|
||||
"runtime.mjs",
|
||||
@@ -42,6 +43,7 @@ for (const file of requiredFiles) {
|
||||
const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8");
|
||||
const styles = fs.readFileSync(path.resolve(rootDir, "styles.css"), "utf8");
|
||||
const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8");
|
||||
const codeAgentFacts = fs.readFileSync(path.resolve(rootDir, "code-agent-facts.mjs"), "utf8");
|
||||
const liveStatus = fs.readFileSync(path.resolve(rootDir, "live-status.mjs"), "utf8");
|
||||
const wiringStatus = fs.readFileSync(path.resolve(rootDir, "wiring-status.mjs"), "utf8");
|
||||
const helpMarkdown = fs.readFileSync(path.resolve(rootDir, "help.md"), "utf8");
|
||||
@@ -640,6 +642,19 @@ assert.match(app, /SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 D
|
||||
assert.match(app, /function messageEvidencePanel/);
|
||||
assert.match(app, /function messageEvidenceSummary/);
|
||||
assert.match(app, /function boundedEvidenceField/);
|
||||
assert.match(app, /codeAgentFactsFromMessage/);
|
||||
assert.match(app, /function codeAgentCapabilityFactsPanel/);
|
||||
assert.match(app, /compactHwlabApiFact/);
|
||||
assert.match(codeAgentFacts, /openai-responses-fallback/);
|
||||
assert.match(codeAgentFacts, /text-chat-only/);
|
||||
assert.match(codeAgentFacts, /stateless-one-shot/);
|
||||
assert.match(codeAgentFacts, /controlled-readonly-session-registry/);
|
||||
assert.match(codeAgentFacts, /hwlab-m3-io-skill-cli/);
|
||||
assert.match(codeAgentFacts, /route", fact\.route/);
|
||||
assert.match(codeAgentFacts, /operationId", fact\.operationId/);
|
||||
assert.match(codeAgentFacts, /auditId", fact\.auditId/);
|
||||
assert.match(codeAgentFacts, /evidenceId", fact\.evidenceId/);
|
||||
assert.match(codeAgentFacts, /blocker", fact\.blocker/);
|
||||
assert.match(app, /sourceKind:\s*completion\.sourceKind/);
|
||||
assert.match(app, /status === "completed" \? "dev-live"/);
|
||||
assert.doesNotMatch(app, /status === "failed" \? "dev-live"/);
|
||||
@@ -652,12 +667,17 @@ assert.match(styles, /\.message-user\s*{/);
|
||||
assert.match(styles, /\.message-agent,\s*\n\.message-system\s*{/);
|
||||
assert.match(styles, /\.message-evidence\s*{/);
|
||||
assert.match(styles, /\.message-evidence-chip\s*{/);
|
||||
assert.match(styles, /\.code-agent-facts\s*{/);
|
||||
assert.match(styles, /\.code-agent-fact-grid\s*{/);
|
||||
assert.match(styles, /\.hwlab-api-fact-code\s*{/);
|
||||
assert.match(styles, /\.hwlab-api-fact-code\s*{[^}]*user-select:\s*all;/s);
|
||||
assert.match(styles, /\.rail-button\s*{[^}]*min-width:\s*0;[^}]*min-height:\s*36px;[^}]*text-align:\s*center;[^}]*word-break:\s*keep-all;/s);
|
||||
assert.match(styles, /\.side-tab\s*{[^}]*min-width:\s*0;[^}]*min-height:\s*34px;[^}]*overflow-wrap:\s*anywhere;[^}]*word-break:\s*keep-all;/s);
|
||||
assert.match(styles, /\.(?:status-dot|state-tag|badge)[^{]*{[^}]*max-width:\s*100%;[^}]*line-height:\s*1\.25;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/s);
|
||||
assert.match(styles, /\.probe-card\s*{[^}]*min-width:\s*0;/s);
|
||||
assert.match(styles, /\.probe-card strong\s*{[^}]*line-height:\s*1\.2;[^}]*overflow-wrap:\s*anywhere;/s);
|
||||
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.message-evidence\s*{[\s\S]*?grid-column:\s*1;/);
|
||||
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.code-agent-fact-grid\s*{[\s\S]*?grid-template-columns:\s*1fr;/);
|
||||
for (const userFacingFunctionName of [
|
||||
"codeAgentStatusMessage",
|
||||
"codeAgentPromptText",
|
||||
|
||||
@@ -9,6 +9,7 @@ export const cloudWebDistRuntimeFiles = Object.freeze([
|
||||
"index.html",
|
||||
"styles.css",
|
||||
"app.mjs",
|
||||
"code-agent-facts.mjs",
|
||||
"live-status.mjs",
|
||||
"gate-summary.mjs",
|
||||
"runtime.mjs",
|
||||
|
||||
@@ -663,6 +663,99 @@ h3 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.code-agent-facts {
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
padding: 8px;
|
||||
background: rgba(15, 17, 16, 0.72);
|
||||
border: 1px solid var(--line);
|
||||
border-left-width: 3px;
|
||||
}
|
||||
|
||||
.code-agent-facts-head {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 7px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.code-agent-facts-summary {
|
||||
min-width: 0;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.code-agent-fact-grid {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-agent-fact-row {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding: 6px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.code-agent-fact-key,
|
||||
.code-agent-fact-value {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.code-agent-fact-key {
|
||||
color: var(--dim);
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
font-weight: 760;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.code-agent-fact-value {
|
||||
color: var(--text);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.hwlab-api-facts {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.hwlab-api-facts-title {
|
||||
color: var(--accent-2);
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
font-weight: 760;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hwlab-api-fact-code {
|
||||
min-width: 0;
|
||||
display: block;
|
||||
padding: 6px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
color: var(--text);
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
line-height: 1.35;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.message-evidence summary {
|
||||
cursor: pointer;
|
||||
list-style-position: inside;
|
||||
@@ -1628,9 +1721,15 @@ tbody tr:last-child td {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.code-agent-facts,
|
||||
.message-evidence {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.code-agent-facts-head,
|
||||
.code-agent-fact-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
|
||||
Reference in New Issue
Block a user