mirror of https://github.com/openclaw/openclaw.git
Agents UI: fix effective model and file hydration
This commit is contained in:
parent
27188fa39f
commit
384a590e54
|
|
@ -28,6 +28,16 @@ export const AgentSummarySchema = Type.Object(
|
|||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
workspace: Type.Optional(NonEmptyString),
|
||||
model: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
primary: Type.Optional(NonEmptyString),
|
||||
fallbacks: Type.Optional(Type.Array(NonEmptyString)),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -548,6 +548,60 @@ describe("gateway session utils", () => {
|
|||
expect(agents.map((agent) => agent.id)).toEqual(["main"]);
|
||||
});
|
||||
});
|
||||
|
||||
test("listAgentsForGateway includes effective workspace + model for default agent", () => {
|
||||
const cfg = {
|
||||
session: { mainKey: "main" },
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/tmp/default-workspace",
|
||||
model: {
|
||||
primary: "openai/gpt-5.4",
|
||||
fallbacks: ["openai-codex/gpt-5.2-codex"],
|
||||
},
|
||||
},
|
||||
list: [{ id: "main", default: true }],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = listAgentsForGateway(cfg);
|
||||
expect(result.agents[0]).toMatchObject({
|
||||
id: "main",
|
||||
workspace: "/tmp/default-workspace",
|
||||
model: {
|
||||
primary: "openai/gpt-5.4",
|
||||
fallbacks: ["openai-codex/gpt-5.2-codex"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("listAgentsForGateway respects per-agent fallback override (including explicit empty list)", () => {
|
||||
const cfg = {
|
||||
session: { mainKey: "main" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai/gpt-5.4",
|
||||
fallbacks: ["openai-codex/gpt-5.2-codex"],
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{ id: "main", default: true },
|
||||
{
|
||||
id: "ops",
|
||||
model: {
|
||||
primary: "anthropic/claude-opus-4-6",
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = listAgentsForGateway(cfg);
|
||||
const ops = result.agents.find((agent) => agent.id === "ops");
|
||||
expect(ops?.model).toEqual({ primary: "anthropic/claude-opus-4-6" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSessionModelRef", () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import {
|
||||
resolveAgentEffectiveModelPrimary,
|
||||
resolveAgentModelFallbacksOverride,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { lookupContextTokens, resolveContextTokensForModel } from "../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import {
|
||||
|
|
@ -17,6 +22,7 @@ import {
|
|||
resolveSubagentSessionStatus,
|
||||
} from "../agents/subagent-registry-read.js";
|
||||
import { type OpenClawConfig, loadConfig } from "../config/config.js";
|
||||
import { resolveAgentModelFallbackValues } from "../config/model-input.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import {
|
||||
buildGroupDisplayName,
|
||||
|
|
@ -576,6 +582,41 @@ function listConfiguredAgentIds(cfg: OpenClawConfig): string[] {
|
|||
: sorted;
|
||||
}
|
||||
|
||||
function normalizeFallbackList(values: readonly string[]): string[] {
|
||||
const out: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const value of values) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
const key = trimmed.toLowerCase();
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
out.push(trimmed);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function resolveGatewayAgentModel(
|
||||
cfg: OpenClawConfig,
|
||||
agentId: string,
|
||||
): GatewayAgentRow["model"] | undefined {
|
||||
const primary = resolveAgentEffectiveModelPrimary(cfg, agentId)?.trim();
|
||||
const fallbackOverride = resolveAgentModelFallbacksOverride(cfg, agentId);
|
||||
const defaultFallbacks = resolveAgentModelFallbackValues(cfg.agents?.defaults?.model);
|
||||
const fallbacks = normalizeFallbackList(fallbackOverride ?? defaultFallbacks);
|
||||
if (!primary && fallbacks.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(primary ? { primary } : {}),
|
||||
...(fallbacks.length > 0 ? { fallbacks } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function listAgentsForGateway(cfg: OpenClawConfig): {
|
||||
defaultId: string;
|
||||
mainKey: string;
|
||||
|
|
@ -625,10 +666,13 @@ export function listAgentsForGateway(cfg: OpenClawConfig): {
|
|||
}
|
||||
const agents = agentIds.map((id) => {
|
||||
const meta = configuredById.get(id);
|
||||
const model = resolveGatewayAgentModel(cfg, id);
|
||||
return {
|
||||
id,
|
||||
name: meta?.name,
|
||||
identity: meta?.identity,
|
||||
workspace: resolveAgentWorkspaceDir(cfg, id),
|
||||
...(model ? { model } : {}),
|
||||
};
|
||||
});
|
||||
return { defaultId, mainKey, scope, agents };
|
||||
|
|
|
|||
|
|
@ -6,10 +6,17 @@ export type GatewayAgentIdentity = {
|
|||
avatarUrl?: string;
|
||||
};
|
||||
|
||||
export type GatewayAgentModel = {
|
||||
primary?: string;
|
||||
fallbacks?: string[];
|
||||
};
|
||||
|
||||
export type GatewayAgentRow = {
|
||||
id: string;
|
||||
name?: string;
|
||||
identity?: GatewayAgentIdentity;
|
||||
workspace?: string;
|
||||
model?: GatewayAgentModel;
|
||||
};
|
||||
|
||||
export type SessionsListResultBase<TDefaults, TRow> = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadAgentsMock = vi.hoisted(() =>
|
||||
vi.fn(async (host: { agentsList?: unknown }) => {
|
||||
host.agentsList = {
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }],
|
||||
};
|
||||
}),
|
||||
);
|
||||
const loadConfigMock = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
const loadAgentIdentitiesMock = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
const loadAgentIdentityMock = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
const loadAgentSkillsMock = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
const loadAgentFilesMock = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
const loadChannelsMock = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
|
||||
vi.mock("../ui/src/ui/controllers/agents.ts", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../ui/src/ui/controllers/agents.ts")>();
|
||||
return { ...actual, loadAgents: loadAgentsMock };
|
||||
});
|
||||
|
||||
vi.mock("../ui/src/ui/controllers/config.ts", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../ui/src/ui/controllers/config.ts")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: loadConfigMock,
|
||||
loadConfigSchema: vi.fn(async () => undefined),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../ui/src/ui/controllers/agent-identity.ts", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("../ui/src/ui/controllers/agent-identity.ts")>();
|
||||
return {
|
||||
...actual,
|
||||
loadAgentIdentities: loadAgentIdentitiesMock,
|
||||
loadAgentIdentity: loadAgentIdentityMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../ui/src/ui/controllers/agent-skills.ts", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../ui/src/ui/controllers/agent-skills.ts")>();
|
||||
return { ...actual, loadAgentSkills: loadAgentSkillsMock };
|
||||
});
|
||||
|
||||
vi.mock("../ui/src/ui/controllers/agent-files.ts", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../ui/src/ui/controllers/agent-files.ts")>();
|
||||
return { ...actual, loadAgentFiles: loadAgentFilesMock };
|
||||
});
|
||||
|
||||
vi.mock("../ui/src/ui/controllers/channels.ts", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../ui/src/ui/controllers/channels.ts")>();
|
||||
return { ...actual, loadChannels: loadChannelsMock };
|
||||
});
|
||||
|
||||
vi.mock("../ui/src/ui/controllers/cron.ts", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../ui/src/ui/controllers/cron.ts")>();
|
||||
return {
|
||||
...actual,
|
||||
loadCronJobs: vi.fn(async () => undefined),
|
||||
loadCronRuns: vi.fn(async () => undefined),
|
||||
loadCronStatus: vi.fn(async () => undefined),
|
||||
};
|
||||
});
|
||||
|
||||
import { refreshActiveTab } from "../ui/src/ui/app-settings.ts";
|
||||
|
||||
type AgentsPanel = "overview" | "files" | "tools" | "skills" | "channels" | "cron";
|
||||
|
||||
function createHost(agentsPanel: AgentsPanel): Parameters<typeof refreshActiveTab>[0] {
|
||||
return {
|
||||
tab: "agents",
|
||||
connected: true,
|
||||
agentsPanel,
|
||||
agentsList: null,
|
||||
agentsSelectedId: null,
|
||||
settings: {
|
||||
gatewayUrl: "",
|
||||
token: "",
|
||||
sessionKey: "main",
|
||||
lastActiveSessionKey: "main",
|
||||
theme: "claw",
|
||||
themeMode: "system",
|
||||
chatFocusMode: false,
|
||||
chatShowThinking: true,
|
||||
chatShowToolCalls: true,
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
navGroupsCollapsed: {},
|
||||
borderRadius: 50,
|
||||
},
|
||||
theme: "claw",
|
||||
themeMode: "system",
|
||||
themeResolved: "dark",
|
||||
applySessionKey: "main",
|
||||
sessionKey: "main",
|
||||
chatHasAutoScrolled: false,
|
||||
logsAtBottom: false,
|
||||
eventLog: [],
|
||||
eventLogBuffer: [],
|
||||
basePath: "",
|
||||
} as Parameters<typeof refreshActiveTab>[0];
|
||||
}
|
||||
|
||||
describe("refreshActiveTab (agents/files)", () => {
|
||||
beforeEach(() => {
|
||||
loadAgentsMock.mockClear();
|
||||
loadConfigMock.mockClear();
|
||||
loadAgentIdentitiesMock.mockClear();
|
||||
loadAgentIdentityMock.mockClear();
|
||||
loadAgentSkillsMock.mockClear();
|
||||
loadAgentFilesMock.mockClear();
|
||||
loadChannelsMock.mockClear();
|
||||
});
|
||||
|
||||
it("loads agent files when the active agents panel is files", async () => {
|
||||
const host = createHost("files");
|
||||
await refreshActiveTab(host);
|
||||
|
||||
expect(loadAgentFilesMock).toHaveBeenCalledTimes(1);
|
||||
expect(loadAgentFilesMock).toHaveBeenCalledWith(host, "main");
|
||||
});
|
||||
|
||||
it("does not load agent files on non-files panels", async () => {
|
||||
const host = createHost("overview");
|
||||
await refreshActiveTab(host);
|
||||
|
||||
expect(loadAgentFilesMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from "./app-polling.ts";
|
||||
import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll.ts";
|
||||
import type { OpenClawApp } from "./app.ts";
|
||||
import { loadAgentFiles } from "./controllers/agent-files.ts";
|
||||
import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity.ts";
|
||||
import { loadAgentSkills } from "./controllers/agent-skills.ts";
|
||||
import { loadAgents } from "./controllers/agents.ts";
|
||||
|
|
@ -246,6 +247,9 @@ export async function refreshActiveTab(host: SettingsHost) {
|
|||
host.agentsSelectedId ?? host.agentsList?.defaultId ?? host.agentsList?.agents?.[0]?.id;
|
||||
if (agentId) {
|
||||
void loadAgentIdentity(host as unknown as OpenClawApp, agentId);
|
||||
if (host.agentsPanel === "files") {
|
||||
void loadAgentFiles(host as unknown as OpenClawApp, agentId);
|
||||
}
|
||||
if (host.agentsPanel === "skills") {
|
||||
void loadAgentSkills(host as unknown as OpenClawApp, agentId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,20 +49,31 @@ export function renderAgentOverview(params: {
|
|||
onSelectPanel,
|
||||
} = params;
|
||||
const config = resolveAgentConfig(configForm, agent.id);
|
||||
const agentModel = agent.model;
|
||||
const workspaceFromFiles =
|
||||
agentFilesList && agentFilesList.agentId === agent.id ? agentFilesList.workspace : null;
|
||||
const workspace =
|
||||
workspaceFromFiles || config.entry?.workspace || config.defaults?.workspace || "default";
|
||||
workspaceFromFiles ||
|
||||
config.entry?.workspace ||
|
||||
config.defaults?.workspace ||
|
||||
agent.workspace ||
|
||||
"default";
|
||||
const model = config.entry?.model
|
||||
? resolveModelLabel(config.entry?.model)
|
||||
: resolveModelLabel(config.defaults?.model);
|
||||
const defaultModel = resolveModelLabel(config.defaults?.model);
|
||||
: config.defaults?.model
|
||||
? resolveModelLabel(config.defaults?.model)
|
||||
: resolveModelLabel(agentModel);
|
||||
const defaultModel = resolveModelLabel(config.defaults?.model ?? agentModel);
|
||||
const entryPrimary = resolveModelPrimary(config.entry?.model);
|
||||
const defaultPrimary =
|
||||
resolveModelPrimary(config.defaults?.model) ||
|
||||
(defaultModel !== "-" ? normalizeModelValue(defaultModel) : null);
|
||||
(defaultModel !== "-" ? normalizeModelValue(defaultModel) : null) ||
|
||||
(configForm ? null : resolveModelPrimary(agentModel));
|
||||
const effectivePrimary = entryPrimary ?? defaultPrimary ?? null;
|
||||
const modelFallbacks = resolveModelFallbacks(config.entry?.model);
|
||||
const modelFallbacks =
|
||||
resolveModelFallbacks(config.entry?.model) ??
|
||||
resolveModelFallbacks(config.defaults?.model) ??
|
||||
(configForm ? null : resolveModelFallbacks(agentModel));
|
||||
const fallbackChips = modelFallbacks ?? [];
|
||||
const skillFilter = Array.isArray(config.entry?.skills) ? config.entry?.skills : null;
|
||||
const skillCount = skillFilter?.length ?? null;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
agentLogoUrl,
|
||||
buildAgentContext,
|
||||
resolveConfiguredCronModelSuggestions,
|
||||
resolveAgentAvatarUrl,
|
||||
resolveEffectiveModelFallbacks,
|
||||
|
|
@ -131,3 +132,50 @@ describe("resolveAgentAvatarUrl", () => {
|
|||
expect(resolveAgentAvatarUrl({ identity: { avatar: "🦞" } })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildAgentContext", () => {
|
||||
it("falls back to agent payload workspace/model when config form is unavailable", () => {
|
||||
const context = buildAgentContext(
|
||||
{
|
||||
id: "main",
|
||||
workspace: "/tmp/agent-workspace",
|
||||
model: {
|
||||
primary: "openai/gpt-5.4",
|
||||
fallbacks: ["openai-codex/gpt-5.2-codex"],
|
||||
},
|
||||
},
|
||||
null,
|
||||
null,
|
||||
"main",
|
||||
null,
|
||||
);
|
||||
|
||||
expect(context.workspace).toBe("/tmp/agent-workspace");
|
||||
expect(context.model).toBe("openai/gpt-5.4 (+1 fallback)");
|
||||
expect(context.isDefault).toBe(true);
|
||||
});
|
||||
|
||||
it("uses configured defaults when agent-specific overrides are absent", () => {
|
||||
const context = buildAgentContext(
|
||||
{ id: "main" },
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/tmp/default-workspace",
|
||||
model: {
|
||||
primary: "openai/gpt-5.4",
|
||||
fallbacks: ["openai-codex/gpt-5.2-codex"],
|
||||
},
|
||||
},
|
||||
list: [{ id: "main" }],
|
||||
},
|
||||
},
|
||||
null,
|
||||
"main",
|
||||
null,
|
||||
);
|
||||
|
||||
expect(context.workspace).toBe("/tmp/default-workspace");
|
||||
expect(context.model).toBe("openai/gpt-5.4 (+1 fallback)");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -329,10 +329,16 @@ export function buildAgentContext(
|
|||
const workspaceFromFiles =
|
||||
agentFilesList && agentFilesList.agentId === agent.id ? agentFilesList.workspace : null;
|
||||
const workspace =
|
||||
workspaceFromFiles || config.entry?.workspace || config.defaults?.workspace || "default";
|
||||
workspaceFromFiles ||
|
||||
config.entry?.workspace ||
|
||||
config.defaults?.workspace ||
|
||||
agent.workspace ||
|
||||
"default";
|
||||
const modelLabel = config.entry?.model
|
||||
? resolveModelLabel(config.entry?.model)
|
||||
: resolveModelLabel(config.defaults?.model);
|
||||
: config.defaults?.model
|
||||
? resolveModelLabel(config.defaults?.model)
|
||||
: resolveModelLabel(agent.model);
|
||||
const identityName =
|
||||
agentIdentity?.name?.trim() ||
|
||||
agent.identity?.name?.trim() ||
|
||||
|
|
|
|||
Loading…
Reference in New Issue