From 7db1afe74e7bc365d4afb47bbf6127825050ac6e Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Sat, 23 May 2026 16:04:59 +0000 Subject: [PATCH] feat: expose code agent session facts --- internal/cloud/code-agent-chat.mjs | 2 ++ .../cloud/code-agent-session-registry.mjs | 8 ++++++ .../code-agent-session-registry.test.mjs | 14 ++++++++++ internal/cloud/codex-stdio-session.mjs | 2 ++ web/hwlab-cloud-web/app.mjs | 7 +++++ web/hwlab-cloud-web/code-agent-status.mjs | 28 +++++++++++++++++++ .../code-agent-status.test.mjs | 25 ++++++++++++++++- web/hwlab-cloud-web/scripts/check.mjs | 7 +++++ 8 files changed, 92 insertions(+), 1 deletion(-) diff --git a/internal/cloud/code-agent-chat.mjs b/internal/cloud/code-agent-chat.mjs index 59beccca..345add9c 100644 --- a/internal/cloud/code-agent-chat.mjs +++ b/internal/cloud/code-agent-chat.mjs @@ -790,6 +790,7 @@ async function callReadOnlyRunner({ intent, conversationId, sessionId, traceId, workspace: resolvedWorkspace, sandbox: READONLY_RUNNER_SANDBOX, runnerKind: READONLY_RUNNER_KIND, + sessionMode: READONLY_SESSION_MODE, capabilityLevel: READONLY_SESSION_CAPABILITY_LEVEL, implementationType: READONLY_IMPLEMENTATION_TYPE, traceId, @@ -1131,6 +1132,7 @@ async function callM3IoSkillRunner({ intent, conversationId, sessionId, traceId, workspace: resolvedWorkspace, sandbox: M3_IO_SKILL_SANDBOX, runnerKind: M3_IO_SKILL_RUNNER_KIND, + sessionMode: M3_IO_SKILL_SESSION_MODE, capabilityLevel: sessionCapabilityLevel, implementationType: M3_IO_SKILL_IMPLEMENTATION_TYPE, traceId, diff --git a/internal/cloud/code-agent-session-registry.mjs b/internal/cloud/code-agent-session-registry.mjs index 5e4b756e..0287fbd2 100644 --- a/internal/cloud/code-agent-session-registry.mjs +++ b/internal/cloud/code-agent-session-registry.mjs @@ -75,6 +75,7 @@ export function createCodeAgentSessionRegistry(options = {}) { workspace: params.workspace ?? null, sandbox: params.sandbox ?? "read-only", runnerKind: params.runnerKind ?? "hwlab-readonly-runner", + sessionMode: params.sessionMode ?? params.implementationType ?? "controlled-readonly-session-registry", capabilityLevel: params.capabilityLevel ?? "read-only-session-tools", implementationType: params.implementationType ?? "controlled-readonly-session-registry", createdAt: timestamp, @@ -96,6 +97,7 @@ export function createCodeAgentSessionRegistry(options = {}) { session.workspace = params.workspace ?? session.workspace; session.sandbox = params.sandbox ?? session.sandbox; session.runnerKind = params.runnerKind ?? session.runnerKind; + session.sessionMode = params.sessionMode ?? session.sessionMode; session.capabilityLevel = params.capabilityLevel ?? session.capabilityLevel; session.implementationType = params.implementationType ?? session.implementationType; session.status = "busy"; @@ -173,6 +175,11 @@ export function createCodeAgentSessionRegistry(options = {}) { maxSessions, idleTimeoutMs, statuses: [...CODE_AGENT_SESSION_STATUSES], + sessionMode: "controlled-readonly-session-registry", + recentSessions: [...sessions.values()] + .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))) + .slice(0, 10) + .map((session) => publicSession(session)), longLivedCodexStdio: false, durable: false, secretMaterialStored: false, @@ -245,6 +252,7 @@ function publicSession(session, { conversationId = null, reused = null } = {}) { workspace: session.workspace, sandbox: session.sandbox, runnerKind: session.runnerKind, + sessionMode: session.sessionMode, capabilityLevel: session.capabilityLevel, implementationType: session.implementationType, createdAt: session.createdAt, diff --git a/internal/cloud/code-agent-session-registry.test.mjs b/internal/cloud/code-agent-session-registry.test.mjs index 6f306512..36df452a 100644 --- a/internal/cloud/code-agent-session-registry.test.mjs +++ b/internal/cloud/code-agent-session-registry.test.mjs @@ -32,10 +32,21 @@ test("code agent session registry creates, reuses, and expires sessions without assert.equal(first.reused, false); assert.equal(first.session.sessionId, "ses_test-registry"); assert.equal(first.session.status, "busy"); + assert.equal(first.session.workspace, "/workspace/hwlab"); + assert.equal(first.session.sandbox, "read-only"); + assert.equal(first.session.runnerKind, "hwlab-readonly-runner"); + assert.equal(first.session.sessionMode, "controlled-readonly-session-registry"); + assert.equal(first.session.capabilityLevel, "read-only-session-tools"); assert.equal(first.session.idleTimeoutMs, 1000); assert.equal(first.session.lastTraceId, "trc_registry_1"); assert.equal(first.session.secretMaterialStored, false); assert.equal(first.session.valuesRedacted, true); + const describedCreating = registry.describe(); + assert.equal(describedCreating.sessionMode, "controlled-readonly-session-registry"); + assert.equal(describedCreating.recentSessions[0].sessionId, "ses_test-registry"); + assert.equal(describedCreating.recentSessions[0].sessionMode, "controlled-readonly-session-registry"); + assert.equal(describedCreating.recentSessions[0].workspace, "/workspace/hwlab"); + assert.equal(describedCreating.recentSessions[0].secretMaterialStored, false); registry.release(first.session.sessionId, { conversationId: "cnv_registry", traceId: "trc_registry_1", @@ -132,6 +143,7 @@ test("read-only runner returns session registry evidence and blocked long-lived assert.equal(first.sessionId, "ses_chat_registry"); assert.equal(first.session.sessionId, "ses_chat_registry"); assert.equal(first.session.status, "idle"); + assert.equal(first.session.sessionMode, "controlled-readonly-session-registry"); assert.equal(first.session.workspace, process.cwd()); assert.equal(first.session.lastTraceId, "trc_chat_registry_pwd"); assert.equal(first.capabilityLevel, "read-only-session-tools"); @@ -387,6 +399,7 @@ test("Code Agent M3 DO write uses Skill CLI to call only HWLAB API /v1/m3/io", a assert.equal(payload.provider, "hwlab-skill-cli"); assert.equal(payload.backend, "hwlab-cloud-api/hwlab-agent-runtime-skill-cli"); assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready); + assert.equal(payload.session.sessionMode, "controlled-m3-io-skill-cli"); assert.equal(payload.toolCalls.length, 1); assert.equal(payload.toolCalls[0].name, "hwlab-agent-runtime.m3-io"); assert.equal(payload.toolCalls[0].status, "completed"); @@ -1038,6 +1051,7 @@ test("Codex stdio runner startup failure marks session failed and blocks long-li assert.equal(payload.error.code, "codex_stdio_failed"); assert.equal(payload.session.sessionId, "ses_stdio_start_failed"); assert.equal(payload.session.status, "failed"); + assert.equal(payload.session.sessionMode, "codex-mcp-stdio-long-lived"); assert.equal(payload.session.statusReason, "codex_stdio_failed"); assert.equal(payload.longLivedSessionGate.status, "blocked"); assert.equal(payload.longLivedSessionGate.pass, false); diff --git a/internal/cloud/codex-stdio-session.mjs b/internal/cloud/codex-stdio-session.mjs index dc6eff17..9aea0df5 100644 --- a/internal/cloud/codex-stdio-session.mjs +++ b/internal/cloud/codex-stdio-session.mjs @@ -450,6 +450,7 @@ export function createCodexStdioSessionManager(options = {}) { workspace: params.workspace, sandbox: params.sandbox, runnerKind: CODEX_STDIO_RUNNER_KIND, + sessionMode: CODEX_STDIO_SESSION_MODE, capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, createdAt: timestamp, @@ -759,6 +760,7 @@ function publicSession(session, { conversationId = null, reused = null } = {}) { workspace: session.workspace, sandbox: session.sandbox, runnerKind: session.runnerKind, + sessionMode: session.sessionMode, capabilityLevel: session.capabilityLevel, implementationType: session.implementationType, createdAt: session.createdAt, diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 442d28e2..24de9e9b 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -2023,6 +2023,13 @@ function codeAgentSummaryRows(summary) { codeAgentSummaryRow("provider/mode/backend", summary.providerModeBackend, "source"), codeAgentSummaryRow("capabilityLevel", summary.capabilityLevel, summary.tone), codeAgentSummaryRow("session", summary.sessionSummary, sessionSummaryTone(summary)), + codeAgentSummaryRow("sessionId/status", summary.sessionIdStatus, sessionSummaryTone(summary)), + codeAgentSummaryRow("workspace", summary.workspace, "source"), + codeAgentSummaryRow("sandbox", summary.sandbox, "source"), + codeAgentSummaryRow("runner", summary.runnerKind, summary.kind === "long-lived-session" ? "ok" : "source"), + codeAgentSummaryRow("toolCalls", summary.toolCalls, summary.toolCalls === "none" ? "source" : summary.tone), + codeAgentSummaryRow("skills", summary.skills, summary.skills === "none" ? "source" : summary.tone), + codeAgentSummaryRow("runnerTrace", summary.runnerTrace, "source"), codeAgentSummaryRow("readiness blockers", summary.blockersLabel, summary.readinessBlockers.length > 0 ? "blocked" : "source"), codeAgentSummaryRow("last traceId", summary.lastTraceId, "source"), codeAgentSummaryRow("当前部署 revision", summary.deploymentRevision, "source") diff --git a/web/hwlab-cloud-web/code-agent-status.mjs b/web/hwlab-cloud-web/code-agent-status.mjs index a81db190..ce8148b1 100644 --- a/web/hwlab-cloud-web/code-agent-status.mjs +++ b/web/hwlab-cloud-web/code-agent-status.mjs @@ -1,3 +1,9 @@ +import { + runnerTraceSummary as compactRunnerTraceSummary, + skillsSummary as compactSkillsSummary, + toolCallsSummary as compactToolCallsSummary +} from "./code-agent-facts.mjs"; + const GREEN_TONES = new Set(["ok", "available", "ready", "dev-live", "live", "pass"]); const SECRET_MARKER = /\b(?:OPENAI_API_KEY|DATABASE_URL|secret|secretref|token|password|passwd|credential|private key|kubeconfig|密钥|密码|凭证)\b/iu; const NON_LIVE_VALUES = new Set(["", "n/a", "none", "null", "undefined", "unknown", "not_observed", "not-observed"]); @@ -15,7 +21,14 @@ export function classifyCodeAgentStatusSummary({ availability = null, latestMess const codeAgentStatus = safeField(firstString(payload.status, payload.readinessStatus, observedAvailability?.status)); const capabilityLevel = safeField(firstString(payload.capabilityLevel, payload.runner?.capabilityLevel, observedAvailability?.capabilityLevel)); const sessionMode = safeField(firstString(payload.sessionMode, payload.session?.sessionMode, payload.runnerTrace?.sessionMode, payload.runner?.sessionMode)); + const sessionId = safeField(firstString(payload.session?.sessionId, payload.sessionId, payload.runner?.sessionId, payload.sessionReuse?.sessionId)); const sessionStatus = safeField(firstString(payload.session?.status, payload.sessionStatus, payload.runnerTrace?.sessionStatus, payload.sessionReuse?.status)); + const workspace = safeField(firstString(payload.workspace, payload.session?.workspace, payload.runner?.workspace, payload.sessionReuse?.workspace)); + const sandbox = safeField(firstString(payload.sandbox, payload.session?.sandbox, payload.runner?.sandbox)); + const runnerKind = safeField(firstString(payload.runner?.kind, payload.session?.runnerKind, payload.runnerTrace?.runnerKind)); + const toolCalls = safeField(compactToolCallsSummary(payload.toolCalls)); + const skills = safeField(compactSkillsSummary(payload.skills)); + const runnerTrace = safeField(compactRunnerTraceSummary(payload.runnerTrace)); const lastTraceId = safeTraceId(firstString( latestMessage?.traceId, payload.traceId, @@ -37,8 +50,16 @@ export function classifyCodeAgentStatusSummary({ availability = null, latestMess codeAgentStatus, capabilityLevel, sessionMode, + sessionId, sessionStatus, sessionSummary: compactJoin([sessionMode, sessionStatus], " / "), + sessionIdStatus: compactJoin([sessionId, sessionStatus], " / "), + workspace, + sandbox, + runnerKind, + toolCalls, + skills, + runnerTrace, readinessBlockers: blockers, blockersLabel: blockers.length > 0 ? blockers.join(", ") : "无", lastTraceId: lastTraceId || "未观测", @@ -50,7 +71,14 @@ export function classifyCodeAgentStatusSummary({ availability = null, latestMess codeAgentStatus, capabilityLevel, sessionMode, + sessionId, sessionStatus, + workspace, + sandbox, + runnerKind, + toolCalls, + skills, + runnerTrace, readinessBlockers: blockers, lastTraceId: lastTraceId || null, deploymentRevision diff --git a/web/hwlab-cloud-web/code-agent-status.test.mjs b/web/hwlab-cloud-web/code-agent-status.test.mjs index b2c9e4b4..38ef5f2d 100644 --- a/web/hwlab-cloud-web/code-agent-status.test.mjs +++ b/web/hwlab-cloud-web/code-agent-status.test.mjs @@ -61,14 +61,29 @@ test("maps degraded readonly session tools to warning, not green", () => { backend: "hwlab-cloud-api/controlled-readonly-session-registry", capabilityLevel: "read-only-session-tools", sessionMode: "controlled-readonly-session-registry", + workspace: "/workspace/hwlab", + sandbox: "read-only", session: { + sessionId: "ses_readonly", status: "idle", - lastTraceId: "trc_readonly" + lastTraceId: "trc_readonly", + workspace: "/workspace/hwlab", + sandbox: "read-only", + runnerKind: "hwlab-readonly-runner", + sessionMode: "controlled-readonly-session-registry" }, runner: { kind: "hwlab-readonly-runner", writeCapable: false }, + 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 + }, blockers: [ { code: "stdio_protocol_not_wired" } ] @@ -78,6 +93,14 @@ test("maps degraded readonly session tools to warning, not green", () => { assert.equal(summary.kind, "read-only-session-tools"); assert.equal(summary.label, "只读会话工具"); assert.equal(summary.tone, "warn"); + assert.equal(summary.sessionId, "ses_readonly"); + assert.equal(summary.sessionIdStatus, "ses_readonly / idle"); + assert.equal(summary.workspace, "/workspace/hwlab"); + assert.equal(summary.sandbox, "read-only"); + assert.equal(summary.runnerKind, "hwlab-readonly-runner"); + assert.equal(summary.toolCalls, "1/1 pwd(completed)"); + assert.equal(summary.skills, "not_requested:count=0"); + assert.match(summary.runnerTrace, /runnerKind=hwlab-readonly-runner/u); assert.equal(summary.readinessBlockers.includes("stdio_protocol_not_wired"), true); assert.equal(summary.lastTraceId, "trc_readonly"); }); diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index cf525dd8..fc28c224 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -701,6 +701,13 @@ assert.match(app, /recordField\("implementation", message\.implementationType\)/ assert.match(app, /recordField\("limitations", limitationsSummary\(message\.runnerLimitations\)\)/); assert.match(app, /recordField\("codexStdio", codexStdioFeasibilitySummary\(message\.codexStdioFeasibility\)\)/); assert.match(app, /recordField\("longLivedGate", longLivedSessionGateSummary\(message\.longLivedSessionGate\)\)/); +assert.match(app, /codeAgentSummaryRow\("sessionId\/status", summary\.sessionIdStatus/); +assert.match(app, /codeAgentSummaryRow\("workspace", summary\.workspace/); +assert.match(app, /codeAgentSummaryRow\("sandbox", summary\.sandbox/); +assert.match(app, /codeAgentSummaryRow\("runner", summary\.runnerKind/); +assert.match(app, /codeAgentSummaryRow\("toolCalls", summary\.toolCalls/); +assert.match(app, /codeAgentSummaryRow\("skills", summary\.skills/); +assert.match(app, /codeAgentSummaryRow\("runnerTrace", summary\.runnerTrace/); assert.match(app, /function sessionSummary/); assert.match(app, /function longLivedSessionGateSummary/); assert.match(app, /read-only-session-tools/);