mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-06 23:55:12 +00:00
fix: expose agent runtime status metadata
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
145
src/agents/agent-runtime-metadata.ts
Normal file
145
src/agents/agent-runtime-metadata.ts
Normal 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",
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 } : {},
|
||||
);
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 "-";
|
||||
|
||||
Reference in New Issue
Block a user