fix: expose agent runtime status metadata

This commit is contained in:
Peter Steinberger
2026-04-29 05:01:45 +01:00
parent 0015d34fda
commit e5dc0e6d15
23 changed files with 451 additions and 64 deletions

View File

@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
- Gateway: expose `gateway.handshakeTimeoutMs` in config, schema, and docs while preserving `OPENCLAW_HANDSHAKE_TIMEOUT_MS` precedence, so loaded or low-powered hosts can tune local WebSocket pre-auth handshakes without patching dist files. Supersedes #51282; refs #73592 and #73652. Thanks @henry-the-frog.
- Agents/model selection: resolve slash-form aliases before provider/model parsing and keep alias-resolved primary models subject to transient provider cooldowns, so cron and persisted sessions do not retry cooled-down raw aliases. Fixes #73573 and #73657. Thanks @akai-shuuichi and @hashslingers.
- Agents/Claude CLI: reuse already-cached macOS Keychain credentials for no-prompt Claude credential reads, so doctor/runtime checks do not miss fresh interactive Claude auth. Fixes #73682. Thanks @RyanSandoval.
- Agents/runtime status: expose effective agent runtime metadata in `agents.list`, Control UI agent panels, and `/agents`, and avoid rendering stale or cumulative CLI token totals as live context usage. Fixes #73660, #73578, and #45268. Thanks @spartman, @DashLabsDev, and @xyooz.
- Agents/transcripts: strip empty assistant text blocks while preserving valid text, images, and signatures, so Anthropic-style providers no longer reject sanitized transcript turns. Fixes #73640. Thanks @jowhee327.
- Providers/Bedrock: omit deprecated `temperature` for Claude Opus 4.7 Bedrock model ids, named and application inference profiles, including dotted `opus-4.7` refs, and classify the nested validation response for failover. Fixes #73663. Thanks @bstanbury.
- Gateway: raise the preauth/connect-challenge timeout to 15s so cold CLI starts on slower hosts have more time to process the WebSocket challenge before the Gateway closes the connection. Fixes #51469; refs #73592 and #62060. Thanks @GothicFox and @jackychen-png.

View File

@@ -2866,19 +2866,22 @@ public struct AgentSummary: Codable, Sendable {
public let identity: [String: AnyCodable]?
public let workspace: String?
public let model: [String: AnyCodable]?
public let agentruntime: [String: AnyCodable]?
public init(
id: String,
name: String?,
identity: [String: AnyCodable]?,
workspace: String?,
model: [String: AnyCodable]?)
model: [String: AnyCodable]?,
agentruntime: [String: AnyCodable]?)
{
self.id = id
self.name = name
self.identity = identity
self.workspace = workspace
self.model = model
self.agentruntime = agentruntime
}
private enum CodingKeys: String, CodingKey {
@@ -2887,6 +2890,7 @@ public struct AgentSummary: Codable, Sendable {
case identity
case workspace
case model
case agentruntime = "agentRuntime"
}
}

View File

@@ -2866,19 +2866,22 @@ public struct AgentSummary: Codable, Sendable {
public let identity: [String: AnyCodable]?
public let workspace: String?
public let model: [String: AnyCodable]?
public let agentruntime: [String: AnyCodable]?
public init(
id: String,
name: String?,
identity: [String: AnyCodable]?,
workspace: String?,
model: [String: AnyCodable]?)
model: [String: AnyCodable]?,
agentruntime: [String: AnyCodable]?)
{
self.id = id
self.name = name
self.identity = identity
self.workspace = workspace
self.model = model
self.agentruntime = agentruntime
}
private enum CodingKeys: String, CodingKey {
@@ -2887,6 +2890,7 @@ public struct AgentSummary: Codable, Sendable {
case identity
case workspace
case model
case agentruntime = "agentRuntime"
}
}

View File

@@ -378,7 +378,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
</Accordion>
<Accordion title="Agent and workspace helpers">
- `agents.list` returns configured agent entries.
- `agents.list` returns configured agent entries, including effective model and runtime metadata.
- `agents.create`, `agents.update`, and `agents.delete` manage agent records and workspace wiring.
- `agents.files.list`, `agents.files.get`, and `agents.files.set` manage the bootstrap workspace files exposed for an agent.
- `agent.identity.get` returns the effective assistant identity for an agent or session.

View File

@@ -0,0 +1,145 @@
import type { AgentRuntimePolicyConfig } from "../config/types.agents-shared.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { resolveAgentRuntimePolicy } from "./agent-runtime-policy.js";
import { listAgentEntries } from "./agent-scope.js";
import {
normalizeEmbeddedAgentRuntime,
resolveEmbeddedAgentHarnessFallback,
type EmbeddedAgentHarnessFallback,
type EmbeddedAgentRuntime,
} from "./pi-embedded-runner/runtime.js";
export type AgentRuntimeMetadata = {
id: string;
fallback?: "pi" | "none";
source: "env" | "agent" | "defaults" | "implicit";
};
function normalizeRuntimeValue(value: unknown): EmbeddedAgentRuntime | undefined {
const normalized = typeof value === "string" ? normalizeLowercaseStringOrEmpty(value) : "";
return normalized ? normalizeEmbeddedAgentRuntime(normalized) : undefined;
}
function normalizeAgentHarnessFallback(
value: EmbeddedAgentHarnessFallback | undefined,
runtime: EmbeddedAgentRuntime,
): EmbeddedAgentHarnessFallback {
if (value) {
return value === "none" ? "none" : "pi";
}
return runtime === "auto" ? "pi" : "none";
}
function isPluginAgentRuntime(runtime: string): boolean {
return runtime !== "auto" && runtime !== "pi";
}
function resolveEffectiveFallback(params: {
envFallback?: EmbeddedAgentHarnessFallback;
envRuntime?: string;
runtime: EmbeddedAgentRuntime;
agentPolicy?: AgentRuntimePolicyConfig;
defaultsPolicy?: AgentRuntimePolicyConfig;
}): EmbeddedAgentHarnessFallback | undefined {
if (params.envFallback) {
return params.envFallback;
}
if (params.envRuntime && isPluginAgentRuntime(params.runtime)) {
return normalizeAgentHarnessFallback(undefined, params.runtime);
}
if (params.agentPolicy?.id) {
return normalizeAgentHarnessFallback(params.agentPolicy.fallback, params.runtime);
}
if (
params.envRuntime ||
params.defaultsPolicy?.id ||
params.agentPolicy?.fallback ||
params.defaultsPolicy?.fallback
) {
return normalizeAgentHarnessFallback(
params.agentPolicy?.fallback ?? params.defaultsPolicy?.fallback,
params.runtime,
);
}
return undefined;
}
export function resolveAgentRuntimeMetadata(
cfg: OpenClawConfig,
agentId: string,
env: NodeJS.ProcessEnv = process.env,
): AgentRuntimeMetadata {
const envFallback = resolveEmbeddedAgentHarnessFallback(env);
const envRuntime = normalizeRuntimeValue(env.OPENCLAW_AGENT_RUNTIME);
const normalizedAgentId = normalizeAgentId(agentId);
const agentEntry = listAgentEntries(cfg).find(
(entry) => normalizeAgentId(entry.id) === normalizedAgentId,
);
const agentPolicy = resolveAgentRuntimePolicy(agentEntry);
const defaultsPolicy = resolveAgentRuntimePolicy(cfg.agents?.defaults);
if (envRuntime) {
return {
id: envRuntime,
fallback: resolveEffectiveFallback({
envFallback,
envRuntime,
runtime: envRuntime,
agentPolicy,
defaultsPolicy,
}),
source: "env",
};
}
const agentRuntime = normalizeRuntimeValue(agentPolicy?.id);
if (agentRuntime) {
return {
id: agentRuntime,
fallback: resolveEffectiveFallback({
envFallback,
runtime: agentRuntime,
agentPolicy,
defaultsPolicy,
}),
source: envFallback ? "env" : "agent",
};
}
const defaultsRuntime = normalizeRuntimeValue(defaultsPolicy?.id);
if (defaultsRuntime) {
return {
id: defaultsRuntime,
fallback: resolveEffectiveFallback({
envFallback,
runtime: defaultsRuntime,
agentPolicy,
defaultsPolicy,
}),
source: envFallback ? "env" : agentPolicy?.fallback ? "agent" : "defaults",
};
}
return {
id: "pi",
fallback: resolveEffectiveFallback({
envFallback,
runtime: "pi",
agentPolicy,
defaultsPolicy,
}),
source: envFallback
? "env"
: agentPolicy?.fallback
? "agent"
: defaultsPolicy?.fallback
? "defaults"
: "implicit",
};
}

View File

@@ -460,7 +460,7 @@ describe("updateSessionStoreAfterAgentRun", () => {
agents: {
defaults: {
cliBackends: {
"claude-cli": {},
"claude-cli": { command: "claude" },
},
},
},
@@ -564,6 +564,63 @@ describe("updateSessionStoreAfterAgentRun", () => {
});
});
it("does not treat CLI cumulative usage as a fresh context snapshot", async () => {
await withTempSessionStore(async ({ storePath }) => {
const cfg = {
agents: {
defaults: {
cliBackends: {
"claude-cli": { command: "claude" },
},
},
},
} as OpenClawConfig;
const sessionKey = "agent:main:explicit:test-cli-cumulative-usage";
const sessionId = "test-cli-cumulative-usage-session";
const sessionStore: Record<string, SessionEntry> = {
[sessionKey]: {
sessionId,
updatedAt: 1,
totalTokens: 95_000,
totalTokensFresh: true,
},
};
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2));
await updateSessionStoreAfterAgentRun({
cfg,
contextTokensOverride: 1_000_000,
sessionId,
sessionKey,
storePath,
sessionStore,
defaultProvider: "claude-cli",
defaultModel: "claude-opus-4-7",
result: {
meta: {
durationMs: 1,
executionTrace: { runner: "cli" },
agentMeta: {
sessionId,
provider: "claude-cli",
model: "claude-opus-4-7",
usage: {
input: 3_800_000,
output: 20_000,
total: 3_820_000,
},
},
},
},
});
expect(sessionStore[sessionKey]?.inputTokens).toBe(3_800_000);
expect(sessionStore[sessionKey]?.outputTokens).toBe(20_000);
expect(sessionStore[sessionKey]?.totalTokens).toBeUndefined();
expect(sessionStore[sessionKey]?.totalTokensFresh).toBe(false);
});
});
it("persists compaction tokensAfter when provider usage is unavailable", async () => {
await withTempSessionStore(async ({ storePath }) => {
const cfg = {} as OpenClawConfig;

View File

@@ -133,8 +133,9 @@ export async function updateSessionStoreAfterAgentRun(params: {
const { estimateUsageCost, resolveModelCostConfig } = await getUsageFormatModule();
const input = usage.input ?? 0;
const output = usage.output ?? 0;
const usageForContext = isCliProvider(providerUsed, cfg) ? undefined : usage;
const totalTokens = deriveSessionTotalTokens({
usage: promptTokens ? undefined : usage,
usage: promptTokens ? undefined : usageForContext,
contextTokens,
promptTokens,
});

View File

@@ -83,12 +83,14 @@ describe("agents_list tool", () => {
});
});
it("marks OPENCLAW_AGENT_RUNTIME as the effective runtime source", async () => {
it("marks OPENCLAW_AGENT_RUNTIME and fallback env overrides as effective", async () => {
vi.stubEnv("OPENCLAW_AGENT_RUNTIME", "codex");
vi.stubEnv("OPENCLAW_AGENT_HARNESS_FALLBACK", "pi");
loadConfigMock.mockReturnValue({
agents: {
defaults: {
model: "openai/gpt-5.5",
agentRuntime: { fallback: "none" },
},
list: [{ id: "main", default: true }],
},
@@ -104,7 +106,37 @@ describe("agents_list tool", () => {
agents: [
{
id: "main",
agentRuntime: { id: "codex", source: "env" },
agentRuntime: { id: "codex", fallback: "pi", source: "env" },
},
],
});
});
it("preserves agent fallback-only overrides while inheriting default runtime id", async () => {
loadConfigMock.mockReturnValue({
agents: {
defaults: {
agentRuntime: { id: "auto", fallback: "pi" },
subagents: { allowAgents: ["strict"] },
},
list: [
{ id: "main", default: true },
{ id: "strict", agentRuntime: { fallback: "none" } },
],
},
} satisfies OpenClawConfig);
const { createAgentsListTool } = await import("./agents-list-tool.js");
const result = await createAgentsListTool({ agentSessionKey: "agent:main:main" }).execute(
"call",
{},
);
expect(result.details).toMatchObject({
agents: [
{
id: "strict",
agentRuntime: { id: "auto", fallback: "none", source: "agent" },
},
],
});

View File

@@ -5,12 +5,8 @@ import {
normalizeAgentId,
parseAgentSessionKey,
} from "../../routing/session-key.js";
import { resolveAgentRuntimePolicy } from "../agent-runtime-policy.js";
import {
listAgentEntries,
resolveAgentConfig,
resolveAgentEffectiveModelPrimary,
} from "../agent-scope.js";
import { resolveAgentRuntimeMetadata } from "../agent-runtime-metadata.js";
import { resolveAgentConfig, resolveAgentEffectiveModelPrimary } from "../agent-scope.js";
import { resolveSubagentAllowedTargetIds } from "../subagent-target-policy.js";
import type { AnyAgentTool } from "./common.js";
import { jsonResult } from "./common.js";
@@ -30,49 +26,6 @@ type AgentListEntry = {
};
};
function normalizeRuntimeValue(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : undefined;
}
function resolveAgentRuntimeMetadata(
cfg: ReturnType<typeof getRuntimeConfig>,
agentId: string,
): NonNullable<AgentListEntry["agentRuntime"]> {
const envRuntime = normalizeRuntimeValue(process.env.OPENCLAW_AGENT_RUNTIME);
if (envRuntime) {
return {
id: envRuntime,
source: "env",
};
}
const agentEntry = listAgentEntries(cfg).find((entry) => normalizeAgentId(entry.id) === agentId);
const agentPolicy = resolveAgentRuntimePolicy(agentEntry);
const agentRuntime = normalizeRuntimeValue(agentPolicy?.id);
if (agentRuntime) {
return {
id: agentRuntime,
fallback: agentPolicy?.fallback,
source: "agent",
};
}
const defaultsPolicy = resolveAgentRuntimePolicy(cfg.agents?.defaults);
const defaultsRuntime = normalizeRuntimeValue(defaultsPolicy?.id);
if (defaultsRuntime) {
return {
id: defaultsRuntime,
fallback: defaultsPolicy?.fallback,
source: "defaults",
};
}
return {
id: "pi",
source: "implicit",
};
}
export function createAgentsListTool(opts?: {
agentSessionKey?: string;
/** Explicit agent ID override for cron/hook sessions. */

View File

@@ -1316,7 +1316,6 @@ export async function runReplyAgent(params: {
systemPromptReport: runResult.meta?.systemPromptReport,
cliSessionId,
cliSessionBinding,
usageIsContextSnapshot: isCliProvider(providerUsed, cfg),
});
// Drain any late tool/block deliveries before deciding there's "nothing to send".

View File

@@ -1287,9 +1287,9 @@ describe("createFollowupRunner messaging delivery and dedupe", () => {
expect(persistSpy).toHaveBeenCalledWith(
expect.objectContaining({
providerUsed: "anthropic",
usageIsContextSnapshot: true,
}),
);
expect(persistSpy.mock.calls[0]?.[0]?.usageIsContextSnapshot).toBeUndefined();
persistSpy.mockRestore();
});

View File

@@ -7,7 +7,6 @@ import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-bu
import { resolveContextTokensForModel } from "../../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
import { runWithModelFallback } from "../../agents/model-fallback.js";
import { isCliProvider } from "../../agents/model-selection.js";
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
import {
buildAgentRuntimeDeliveryPlan,
@@ -401,7 +400,6 @@ export function createFollowupRunner(params: {
contextTokensUsed,
systemPromptReport: runResult.meta?.systemPromptReport,
cliSessionBinding: runResult.meta?.agentMeta?.cliSessionBinding,
usageIsContextSnapshot: isCliProvider(providerUsed, runtimeConfig),
logLabel: "followup",
});
}

View File

@@ -103,6 +103,33 @@ describe("buildStatusMessage", () => {
expect(normalized).toContain("Queue: collect");
});
it("does not render stale totalTokens as current context usage", () => {
const text = buildStatusMessage({
agent: {
model: "anthropic/pi:opus",
contextTokens: 1_000_000,
},
sessionEntry: {
sessionId: "abc",
updatedAt: 0,
inputTokens: 3_800_000,
outputTokens: 20_000,
totalTokens: 3_800_000,
totalTokensFresh: false,
contextTokens: 1_000_000,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
queue: { mode: "collect", depth: 0 },
modelAuth: "api-key",
now: 10 * 60_000,
});
const normalized = normalizeTestText(text);
expect(normalized).toContain("Context: ?/1.0m");
expect(normalized).not.toContain("Context: 3.8m/1.0m");
});
it("shows sanitized TTS provider details in the voice status line", async () => {
await withTempHome(async () => {
const text = buildStatusMessage({
@@ -1433,6 +1460,53 @@ describe("buildStatusMessage", () => {
);
});
it("does not render stale context usage from transcript fallback", async () => {
await withTempHome(
async (dir) => {
const sessionId = "sess-stale-transcript-context";
writeTranscriptUsageLog({
dir,
agentId: "main",
sessionId,
usage: {
input: 3_800_000,
output: 20_000,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 3_820_000,
},
});
const text = buildStatusMessage({
agent: {
model: "anthropic/claude-opus-4-6",
contextTokens: 1_000_000,
},
sessionEntry: {
sessionId,
updatedAt: 0,
inputTokens: 3_800_000,
outputTokens: 20_000,
totalTokens: 3_800_000,
totalTokensFresh: false,
contextTokens: 1_000_000,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
queue: { mode: "collect", depth: 0 },
includeTranscriptUsage: true,
modelAuth: "api-key",
});
const normalized = normalizeTestText(text);
expect(normalized).toContain("Context: ?/1.0m");
expect(normalized).not.toContain("Context: 3.8m/1.0m");
expect(normalized).not.toContain("Context: 3.82m/1.0m");
},
{ prefix: "openclaw-status-" },
);
});
it("reads transcript usage for non-default agents", async () => {
await withTempHome(
async (dir) => {

View File

@@ -39,6 +39,21 @@ export const AgentSummarySchema = Type.Object(
{ additionalProperties: false },
),
),
agentRuntime: Type.Optional(
Type.Object(
{
id: NonEmptyString,
fallback: Type.Optional(Type.Union([Type.Literal("pi"), Type.Literal("none")])),
source: Type.Union([
Type.Literal("env"),
Type.Literal("agent"),
Type.Literal("defaults"),
Type.Literal("implicit"),
]),
},
{ additionalProperties: false },
),
),
},
{ additionalProperties: false },
);

View File

@@ -842,8 +842,9 @@ describe("gateway session utils", () => {
primary: "openai/gpt-5.4",
fallbacks: ["openai-codex/gpt-5.4"],
},
agentRuntime: { id: "pi", fallback: "pi" },
},
list: [{ id: "main", default: true }],
list: [{ id: "main", default: true, agentRuntime: { id: "claude-cli", fallback: "none" } }],
},
} as OpenClawConfig;
@@ -855,6 +856,65 @@ describe("gateway session utils", () => {
primary: "openai/gpt-5.4",
fallbacks: ["openai-codex/gpt-5.4"],
},
agentRuntime: {
id: "claude-cli",
fallback: "none",
source: "agent",
},
});
});
test("listAgentsForGateway reports effective env runtime fallback override", () => {
const previousFallback = process.env.OPENCLAW_AGENT_HARNESS_FALLBACK;
process.env.OPENCLAW_AGENT_HARNESS_FALLBACK = "pi";
try {
const cfg = {
session: { mainKey: "main" },
agents: {
defaults: {
agentRuntime: { id: "codex", fallback: "none" },
},
list: [{ id: "main", default: true }],
},
} as OpenClawConfig;
const result = listAgentsForGateway(cfg);
expect(result.agents[0]).toMatchObject({
id: "main",
agentRuntime: {
id: "codex",
fallback: "pi",
source: "env",
},
});
} finally {
if (previousFallback === undefined) {
delete process.env.OPENCLAW_AGENT_HARNESS_FALLBACK;
} else {
process.env.OPENCLAW_AGENT_HARNESS_FALLBACK = previousFallback;
}
}
});
test("listAgentsForGateway preserves fallback-only agent runtime overrides", () => {
const cfg = {
session: { mainKey: "main" },
agents: {
defaults: {
agentRuntime: { id: "auto", fallback: "pi" },
},
list: [{ id: "main", default: true, agentRuntime: { fallback: "none" } }],
},
} as OpenClawConfig;
const result = listAgentsForGateway(cfg);
expect(result.agents[0]).toMatchObject({
id: "main",
agentRuntime: {
id: "auto",
fallback: "none",
source: "agent",
},
});
});

View File

@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { resolveAgentRuntimeMetadata } from "../agents/agent-runtime-metadata.js";
import {
listAgentIds,
resolveAgentConfig,
@@ -794,6 +795,7 @@ export function listAgentsForGateway(cfg: OpenClawConfig): {
name: meta?.name,
identity: meta?.identity,
workspace: resolveAgentWorkspaceDir(cfg, id),
agentRuntime: resolveAgentRuntimeMetadata(cfg, id),
},
model ? { model } : {},
);

View File

@@ -11,12 +11,19 @@ export type GatewayAgentModel = {
fallbacks?: string[];
};
export type GatewayAgentRuntime = {
id: string;
fallback?: "pi" | "none";
source: "env" | "agent" | "defaults" | "implicit";
};
export type GatewayAgentRow = {
id: string;
name?: string;
identity?: GatewayAgentIdentity;
workspace?: string;
model?: GatewayAgentModel;
agentRuntime?: GatewayAgentRuntime;
};
export type SessionsListResultBase<TDefaults, TRow> = {

View File

@@ -28,6 +28,7 @@ import {
resolveSessionFilePathOptions,
resolveSessionPluginStatusLines,
resolveSessionPluginTraceLines,
resolveFreshSessionTotalTokens,
type SessionEntry,
type SessionScope,
} from "../config/sessions.js";
@@ -571,7 +572,13 @@ export function buildStatusMessage(args: StatusArgs): string {
let outputTokens = entry?.outputTokens;
let cacheRead = entry?.cacheRead;
let cacheWrite = entry?.cacheWrite;
let totalTokens = entry?.totalTokens ?? (entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0);
const freshTotalTokens = resolveFreshSessionTotalTokens(entry);
const allowTranscriptContextUsage = entry?.totalTokensFresh !== false;
let totalTokens =
freshTotalTokens ??
(entry?.totalTokensFresh === false
? undefined
: (entry?.totalTokens ?? (entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0)));
// Prefer prompt-size tokens from the session transcript when it looks larger
// (cached prompt tokens are often missing from agent meta/store).
@@ -585,7 +592,10 @@ export function buildStatusMessage(args: StatusArgs): string {
);
if (logUsage) {
const candidate = logUsage.promptTokens || logUsage.total;
if (!totalTokens || totalTokens === 0 || candidate > totalTokens) {
if (
allowTranscriptContextUsage &&
(!totalTokens || totalTokens === 0 || candidate > totalTokens)
) {
totalTokens = candidate;
}
if (!entry?.model && logUsage.model) {

View File

@@ -426,7 +426,8 @@ async function executeAgents(client: GatewayBrowserClient): Promise<SlashCommand
const isDefault = agent.id === result?.defaultId;
const name = agent.identity?.name || agent.name || agent.id;
const marker = isDefault ? " *(default)*" : "";
lines.push(`- \`${agent.id}\`${name}${marker}`);
const runtime = agent.agentRuntime?.id ? ` · runtime \`${agent.agentRuntime.id}\`` : "";
lines.push(`- \`${agent.id}\`${name}${marker}${runtime}`);
}
return { content: lines.join("\n") };
} catch (err) {

View File

@@ -11,6 +11,7 @@ import {
normalizeModelValue,
parseFallbackList,
resolveAgentConfig,
resolveAgentRuntimeLabel,
resolveModelFallbacks,
resolveModelLabel,
resolveModelPrimary,
@@ -64,6 +65,7 @@ export function renderAgentOverview(params: {
: config.defaults?.model
? resolveModelLabel(config.defaults?.model)
: resolveModelLabel(agentModel);
const runtime = resolveAgentRuntimeLabel(agent.agentRuntime);
const defaultModel = resolveModelLabel(config.defaults?.model ?? agentModel);
const entryPrimary = resolveModelPrimary(config.entry?.model);
const defaultPrimary =
@@ -121,6 +123,10 @@ export function renderAgentOverview(params: {
<div class="label">Primary Model</div>
<div class="mono">${model}</div>
</div>
<div class="agent-kv">
<div class="label">Runtime</div>
<div class="mono">${runtime}</div>
</div>
<div class="agent-kv">
<div class="label">Skills Filter</div>
<div>${skillFilter ? `${skillCount} selected` : "all skills"}</div>

View File

@@ -112,6 +112,10 @@ function renderAgentContextCard(
<div class="label">Primary Model</div>
<div class="mono">${context.model}</div>
</div>
<div class="agent-kv">
<div class="label">Runtime</div>
<div class="mono">${context.runtime}</div>
</div>
<div class="agent-kv">
<div class="label">Identity Name</div>
<div>${context.identityName}</div>

View File

@@ -204,6 +204,7 @@ describe("buildAgentContext", () => {
primary: "openai/gpt-5.5",
fallbacks: ["openai-codex/gpt-5.2-codex"],
},
agentRuntime: { id: "claude-cli", fallback: "none", source: "agent" },
},
null,
null,
@@ -213,6 +214,7 @@ describe("buildAgentContext", () => {
expect(context.workspace).toBe("/tmp/agent-workspace");
expect(context.model).toBe("openai/gpt-5.5 (+1 fallback)");
expect(context.runtime).toBe("claude-cli (fallback none)");
expect(context.isDefault).toBe(true);
});

View File

@@ -167,6 +167,7 @@ type AgentConfigEntry = {
workspace?: string;
agentDir?: string;
model?: unknown;
agentRuntime?: unknown;
skills?: string[];
tools?: {
profile?: string;
@@ -386,6 +387,7 @@ export function resolveAgentConfig(config: Record<string, unknown> | null, agent
export type AgentContext = {
workspace: string;
model: string;
runtime: string;
identityName: string;
identityAvatar: string;
skillsLabel: string;
@@ -413,6 +415,7 @@ export function buildAgentContext(
: config.defaults?.model
? resolveModelLabel(config.defaults?.model)
: resolveModelLabel(agent.model);
const runtime = resolveAgentRuntimeLabel(agent.agentRuntime);
const identityName =
normalizeOptionalString(agent.identity?.name) ||
normalizeOptionalString(agent.name) ||
@@ -427,6 +430,7 @@ export function buildAgentContext(
return {
workspace,
model: modelLabel,
runtime,
identityName,
identityAvatar,
skillsLabel: skillFilter ? `${skillCount} selected` : "all skills",
@@ -434,6 +438,14 @@ export function buildAgentContext(
};
}
export function resolveAgentRuntimeLabel(
agentRuntime?: AgentsListResult["agents"][number]["agentRuntime"],
): string {
const id = normalizeOptionalString(agentRuntime?.id) ?? "pi";
const fallback = normalizeOptionalString(agentRuntime?.fallback);
return fallback ? `${id} (fallback ${fallback})` : id;
}
export function resolveModelLabel(model?: unknown): string {
if (!model) {
return "-";