Skip to content

Commit e5dc0e6

Browse files
committed
fix: expose agent runtime status metadata
1 parent 0015d34 commit e5dc0e6

23 files changed

Lines changed: 451 additions & 64 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
3838
- 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.
3939
- 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.
4040
- 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.
41+
- 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.
4142
- 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.
4243
- 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.
4344
- 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.

apps/macos/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2866,19 +2866,22 @@ public struct AgentSummary: Codable, Sendable {
28662866
public let identity: [String: AnyCodable]?
28672867
public let workspace: String?
28682868
public let model: [String: AnyCodable]?
2869+
public let agentruntime: [String: AnyCodable]?
28692870

28702871
public init(
28712872
id: String,
28722873
name: String?,
28732874
identity: [String: AnyCodable]?,
28742875
workspace: String?,
2875-
model: [String: AnyCodable]?)
2876+
model: [String: AnyCodable]?,
2877+
agentruntime: [String: AnyCodable]?)
28762878
{
28772879
self.id = id
28782880
self.name = name
28792881
self.identity = identity
28802882
self.workspace = workspace
28812883
self.model = model
2884+
self.agentruntime = agentruntime
28822885
}
28832886

28842887
private enum CodingKeys: String, CodingKey {
@@ -2887,6 +2890,7 @@ public struct AgentSummary: Codable, Sendable {
28872890
case identity
28882891
case workspace
28892892
case model
2893+
case agentruntime = "agentRuntime"
28902894
}
28912895
}
28922896

apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2866,19 +2866,22 @@ public struct AgentSummary: Codable, Sendable {
28662866
public let identity: [String: AnyCodable]?
28672867
public let workspace: String?
28682868
public let model: [String: AnyCodable]?
2869+
public let agentruntime: [String: AnyCodable]?
28692870

28702871
public init(
28712872
id: String,
28722873
name: String?,
28732874
identity: [String: AnyCodable]?,
28742875
workspace: String?,
2875-
model: [String: AnyCodable]?)
2876+
model: [String: AnyCodable]?,
2877+
agentruntime: [String: AnyCodable]?)
28762878
{
28772879
self.id = id
28782880
self.name = name
28792881
self.identity = identity
28802882
self.workspace = workspace
28812883
self.model = model
2884+
self.agentruntime = agentruntime
28822885
}
28832886

28842887
private enum CodingKeys: String, CodingKey {
@@ -2887,6 +2890,7 @@ public struct AgentSummary: Codable, Sendable {
28872890
case identity
28882891
case workspace
28892892
case model
2893+
case agentruntime = "agentRuntime"
28902894
}
28912895
}
28922896

docs/gateway/protocol.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
378378
</Accordion>
379379

380380
<Accordion title="Agent and workspace helpers">
381-
- `agents.list` returns configured agent entries.
381+
- `agents.list` returns configured agent entries, including effective model and runtime metadata.
382382
- `agents.create`, `agents.update`, and `agents.delete` manage agent records and workspace wiring.
383383
- `agents.files.list`, `agents.files.get`, and `agents.files.set` manage the bootstrap workspace files exposed for an agent.
384384
- `agent.identity.get` returns the effective assistant identity for an agent or session.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import type { AgentRuntimePolicyConfig } from "../config/types.agents-shared.js";
2+
import type { OpenClawConfig } from "../config/types.openclaw.js";
3+
import { normalizeAgentId } from "../routing/session-key.js";
4+
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
5+
import { resolveAgentRuntimePolicy } from "./agent-runtime-policy.js";
6+
import { listAgentEntries } from "./agent-scope.js";
7+
import {
8+
normalizeEmbeddedAgentRuntime,
9+
resolveEmbeddedAgentHarnessFallback,
10+
type EmbeddedAgentHarnessFallback,
11+
type EmbeddedAgentRuntime,
12+
} from "./pi-embedded-runner/runtime.js";
13+
14+
export type AgentRuntimeMetadata = {
15+
id: string;
16+
fallback?: "pi" | "none";
17+
source: "env" | "agent" | "defaults" | "implicit";
18+
};
19+
20+
function normalizeRuntimeValue(value: unknown): EmbeddedAgentRuntime | undefined {
21+
const normalized = typeof value === "string" ? normalizeLowercaseStringOrEmpty(value) : "";
22+
return normalized ? normalizeEmbeddedAgentRuntime(normalized) : undefined;
23+
}
24+
25+
function normalizeAgentHarnessFallback(
26+
value: EmbeddedAgentHarnessFallback | undefined,
27+
runtime: EmbeddedAgentRuntime,
28+
): EmbeddedAgentHarnessFallback {
29+
if (value) {
30+
return value === "none" ? "none" : "pi";
31+
}
32+
return runtime === "auto" ? "pi" : "none";
33+
}
34+
35+
function isPluginAgentRuntime(runtime: string): boolean {
36+
return runtime !== "auto" && runtime !== "pi";
37+
}
38+
39+
function resolveEffectiveFallback(params: {
40+
envFallback?: EmbeddedAgentHarnessFallback;
41+
envRuntime?: string;
42+
runtime: EmbeddedAgentRuntime;
43+
agentPolicy?: AgentRuntimePolicyConfig;
44+
defaultsPolicy?: AgentRuntimePolicyConfig;
45+
}): EmbeddedAgentHarnessFallback | undefined {
46+
if (params.envFallback) {
47+
return params.envFallback;
48+
}
49+
50+
if (params.envRuntime && isPluginAgentRuntime(params.runtime)) {
51+
return normalizeAgentHarnessFallback(undefined, params.runtime);
52+
}
53+
54+
if (params.agentPolicy?.id) {
55+
return normalizeAgentHarnessFallback(params.agentPolicy.fallback, params.runtime);
56+
}
57+
58+
if (
59+
params.envRuntime ||
60+
params.defaultsPolicy?.id ||
61+
params.agentPolicy?.fallback ||
62+
params.defaultsPolicy?.fallback
63+
) {
64+
return normalizeAgentHarnessFallback(
65+
params.agentPolicy?.fallback ?? params.defaultsPolicy?.fallback,
66+
params.runtime,
67+
);
68+
}
69+
70+
return undefined;
71+
}
72+
73+
export function resolveAgentRuntimeMetadata(
74+
cfg: OpenClawConfig,
75+
agentId: string,
76+
env: NodeJS.ProcessEnv = process.env,
77+
): AgentRuntimeMetadata {
78+
const envFallback = resolveEmbeddedAgentHarnessFallback(env);
79+
const envRuntime = normalizeRuntimeValue(env.OPENCLAW_AGENT_RUNTIME);
80+
const normalizedAgentId = normalizeAgentId(agentId);
81+
const agentEntry = listAgentEntries(cfg).find(
82+
(entry) => normalizeAgentId(entry.id) === normalizedAgentId,
83+
);
84+
const agentPolicy = resolveAgentRuntimePolicy(agentEntry);
85+
const defaultsPolicy = resolveAgentRuntimePolicy(cfg.agents?.defaults);
86+
87+
if (envRuntime) {
88+
return {
89+
id: envRuntime,
90+
fallback: resolveEffectiveFallback({
91+
envFallback,
92+
envRuntime,
93+
runtime: envRuntime,
94+
agentPolicy,
95+
defaultsPolicy,
96+
}),
97+
source: "env",
98+
};
99+
}
100+
101+
const agentRuntime = normalizeRuntimeValue(agentPolicy?.id);
102+
if (agentRuntime) {
103+
return {
104+
id: agentRuntime,
105+
fallback: resolveEffectiveFallback({
106+
envFallback,
107+
runtime: agentRuntime,
108+
agentPolicy,
109+
defaultsPolicy,
110+
}),
111+
source: envFallback ? "env" : "agent",
112+
};
113+
}
114+
115+
const defaultsRuntime = normalizeRuntimeValue(defaultsPolicy?.id);
116+
if (defaultsRuntime) {
117+
return {
118+
id: defaultsRuntime,
119+
fallback: resolveEffectiveFallback({
120+
envFallback,
121+
runtime: defaultsRuntime,
122+
agentPolicy,
123+
defaultsPolicy,
124+
}),
125+
source: envFallback ? "env" : agentPolicy?.fallback ? "agent" : "defaults",
126+
};
127+
}
128+
129+
return {
130+
id: "pi",
131+
fallback: resolveEffectiveFallback({
132+
envFallback,
133+
runtime: "pi",
134+
agentPolicy,
135+
defaultsPolicy,
136+
}),
137+
source: envFallback
138+
? "env"
139+
: agentPolicy?.fallback
140+
? "agent"
141+
: defaultsPolicy?.fallback
142+
? "defaults"
143+
: "implicit",
144+
};
145+
}

src/agents/command/session-store.test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ describe("updateSessionStoreAfterAgentRun", () => {
460460
agents: {
461461
defaults: {
462462
cliBackends: {
463-
"claude-cli": {},
463+
"claude-cli": { command: "claude" },
464464
},
465465
},
466466
},
@@ -564,6 +564,63 @@ describe("updateSessionStoreAfterAgentRun", () => {
564564
});
565565
});
566566

567+
it("does not treat CLI cumulative usage as a fresh context snapshot", async () => {
568+
await withTempSessionStore(async ({ storePath }) => {
569+
const cfg = {
570+
agents: {
571+
defaults: {
572+
cliBackends: {
573+
"claude-cli": { command: "claude" },
574+
},
575+
},
576+
},
577+
} as OpenClawConfig;
578+
const sessionKey = "agent:main:explicit:test-cli-cumulative-usage";
579+
const sessionId = "test-cli-cumulative-usage-session";
580+
const sessionStore: Record<string, SessionEntry> = {
581+
[sessionKey]: {
582+
sessionId,
583+
updatedAt: 1,
584+
totalTokens: 95_000,
585+
totalTokensFresh: true,
586+
},
587+
};
588+
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2));
589+
590+
await updateSessionStoreAfterAgentRun({
591+
cfg,
592+
contextTokensOverride: 1_000_000,
593+
sessionId,
594+
sessionKey,
595+
storePath,
596+
sessionStore,
597+
defaultProvider: "claude-cli",
598+
defaultModel: "claude-opus-4-7",
599+
result: {
600+
meta: {
601+
durationMs: 1,
602+
executionTrace: { runner: "cli" },
603+
agentMeta: {
604+
sessionId,
605+
provider: "claude-cli",
606+
model: "claude-opus-4-7",
607+
usage: {
608+
input: 3_800_000,
609+
output: 20_000,
610+
total: 3_820_000,
611+
},
612+
},
613+
},
614+
},
615+
});
616+
617+
expect(sessionStore[sessionKey]?.inputTokens).toBe(3_800_000);
618+
expect(sessionStore[sessionKey]?.outputTokens).toBe(20_000);
619+
expect(sessionStore[sessionKey]?.totalTokens).toBeUndefined();
620+
expect(sessionStore[sessionKey]?.totalTokensFresh).toBe(false);
621+
});
622+
});
623+
567624
it("persists compaction tokensAfter when provider usage is unavailable", async () => {
568625
await withTempSessionStore(async ({ storePath }) => {
569626
const cfg = {} as OpenClawConfig;

src/agents/command/session-store.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,9 @@ export async function updateSessionStoreAfterAgentRun(params: {
133133
const { estimateUsageCost, resolveModelCostConfig } = await getUsageFormatModule();
134134
const input = usage.input ?? 0;
135135
const output = usage.output ?? 0;
136+
const usageForContext = isCliProvider(providerUsed, cfg) ? undefined : usage;
136137
const totalTokens = deriveSessionTotalTokens({
137-
usage: promptTokens ? undefined : usage,
138+
usage: promptTokens ? undefined : usageForContext,
138139
contextTokens,
139140
promptTokens,
140141
});

src/agents/tools/agents-list-tool.test.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,14 @@ describe("agents_list tool", () => {
8383
});
8484
});
8585

86-
it("marks OPENCLAW_AGENT_RUNTIME as the effective runtime source", async () => {
86+
it("marks OPENCLAW_AGENT_RUNTIME and fallback env overrides as effective", async () => {
8787
vi.stubEnv("OPENCLAW_AGENT_RUNTIME", "codex");
88+
vi.stubEnv("OPENCLAW_AGENT_HARNESS_FALLBACK", "pi");
8889
loadConfigMock.mockReturnValue({
8990
agents: {
9091
defaults: {
9192
model: "openai/gpt-5.5",
93+
agentRuntime: { fallback: "none" },
9294
},
9395
list: [{ id: "main", default: true }],
9496
},
@@ -104,7 +106,37 @@ describe("agents_list tool", () => {
104106
agents: [
105107
{
106108
id: "main",
107-
agentRuntime: { id: "codex", source: "env" },
109+
agentRuntime: { id: "codex", fallback: "pi", source: "env" },
110+
},
111+
],
112+
});
113+
});
114+
115+
it("preserves agent fallback-only overrides while inheriting default runtime id", async () => {
116+
loadConfigMock.mockReturnValue({
117+
agents: {
118+
defaults: {
119+
agentRuntime: { id: "auto", fallback: "pi" },
120+
subagents: { allowAgents: ["strict"] },
121+
},
122+
list: [
123+
{ id: "main", default: true },
124+
{ id: "strict", agentRuntime: { fallback: "none" } },
125+
],
126+
},
127+
} satisfies OpenClawConfig);
128+
129+
const { createAgentsListTool } = await import("./agents-list-tool.js");
130+
const result = await createAgentsListTool({ agentSessionKey: "agent:main:main" }).execute(
131+
"call",
132+
{},
133+
);
134+
135+
expect(result.details).toMatchObject({
136+
agents: [
137+
{
138+
id: "strict",
139+
agentRuntime: { id: "auto", fallback: "none", source: "agent" },
108140
},
109141
],
110142
});

0 commit comments

Comments
 (0)