Status: split lightweight gateway agent list

This commit is contained in:
Vincent Koc 2026-03-15 21:58:50 -07:00
parent d47fc009de
commit c4b18ab3c9
6 changed files with 115 additions and 18 deletions

View File

@ -3,7 +3,7 @@ import path from "node:path";
import { resolveAgentWorkspaceDir } from "../../agents/agent-scope.js";
import type { OpenClawConfig } from "../../config/config.js";
import { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
import { listAgentsForGateway } from "../../gateway/session-utils.js";
import { listGatewayAgentsBasic } from "../../gateway/agent-list.js";
async function fileExists(p: string): Promise<boolean> {
try {
@ -15,7 +15,7 @@ async function fileExists(p: string): Promise<boolean> {
}
export async function getAgentLocalStatuses(cfg: OpenClawConfig) {
const agentList = listAgentsForGateway(cfg);
const agentList = listGatewayAgentsBasic(cfg);
const now = Date.now();
const agents = await Promise.all(

View File

@ -4,7 +4,7 @@ import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
import type { OpenClawConfig } from "../config/config.js";
import { loadConfig } from "../config/config.js";
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
import { listAgentsForGateway } from "../gateway/session-utils.js";
import { listGatewayAgentsBasic } from "../gateway/agent-list.js";
export type AgentLocalStatus = {
id: string;
@ -36,7 +36,7 @@ async function fileExists(p: string): Promise<boolean> {
export async function getAgentLocalStatuses(
cfg: OpenClawConfig = loadConfig(),
): Promise<AgentLocalStatusesResult> {
const agentList = listAgentsForGateway(cfg);
const agentList = listGatewayAgentsBasic(cfg);
const now = Date.now();
const statuses: AgentLocalStatus[] = [];

View File

@ -32,12 +32,15 @@ vi.mock("../config/sessions.js", () => ({
resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
}));
vi.mock("../gateway/session-utils.js", () => ({
classifySessionKey: vi.fn(() => "direct"),
listAgentsForGateway: vi.fn(() => ({
vi.mock("../gateway/agent-list.js", () => ({
listGatewayAgentsBasic: vi.fn(() => ({
defaultId: "main",
agents: [{ id: "main" }],
})),
}));
vi.mock("../gateway/session-utils.js", () => ({
classifySessionKey: vi.fn(() => "direct"),
resolveSessionModelRef: vi.fn(() => ({
provider: "openai",
model: "gpt-5.2",
@ -61,6 +64,8 @@ vi.mock("../infra/system-events.js", () => ({
}));
vi.mock("../routing/session-key.js", () => ({
normalizeAgentId: vi.fn((value: string) => value),
normalizeMainKey: vi.fn((value?: string) => value ?? "main"),
parseAgentSessionKey: vi.fn(() => null),
}));

View File

@ -11,11 +11,8 @@ import {
resolveStorePath,
type SessionEntry,
} from "../config/sessions.js";
import {
classifySessionKey,
listAgentsForGateway,
resolveSessionModelRef,
} from "../gateway/session-utils.js";
import { listGatewayAgentsBasic } from "../gateway/agent-list.js";
import { classifySessionKey, resolveSessionModelRef } from "../gateway/session-utils.js";
import { resolveHeartbeatSummaryForAgent } from "../infra/heartbeat-summary.js";
import { peekSystemEvents } from "../infra/system-events.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
@ -107,7 +104,7 @@ export async function getStatusSummary(
resolveLinkChannelContext(cfg),
)
: null;
const agentList = listAgentsForGateway(cfg);
const agentList = listGatewayAgentsBasic(cfg);
const heartbeatAgents: HeartbeatStatus[] = agentList.agents.map((agent) => {
const summary = resolveHeartbeatSummaryForAgent(cfg, agent.id);
return {

View File

@ -168,7 +168,7 @@ const mocks = vi.hoisted(() => ({
configSnapshot: null,
}),
callGateway: vi.fn().mockResolvedValue({}),
listAgentsForGateway: vi.fn().mockReturnValue({
listGatewayAgentsBasic: vi.fn().mockReturnValue({
defaultId: "main",
mainKey: "agent:main:main",
scope: "per-sender",
@ -299,11 +299,18 @@ vi.mock("../gateway/call.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../gateway/call.js")>();
return { ...actual, callGateway: mocks.callGateway };
});
vi.mock("../gateway/agent-list.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../gateway/agent-list.js")>();
return {
...actual,
listGatewayAgentsBasic: mocks.listGatewayAgentsBasic,
};
});
vi.mock("../gateway/session-utils.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../gateway/session-utils.js")>();
return {
...actual,
listAgentsForGateway: mocks.listAgentsForGateway,
};
});
vi.mock("../infra/openclaw-root.js", () => ({
@ -608,11 +615,11 @@ describe("statusCommand", () => {
});
it("includes sessions across agents in JSON output", async () => {
const originalAgents = mocks.listAgentsForGateway.getMockImplementation();
const originalAgents = mocks.listGatewayAgentsBasic.getMockImplementation();
const originalResolveStorePath = mocks.resolveStorePath.getMockImplementation();
const originalLoadSessionStore = mocks.loadSessionStore.getMockImplementation();
mocks.listAgentsForGateway.mockReturnValue({
mocks.listGatewayAgentsBasic.mockReturnValue({
defaultId: "main",
mainKey: "agent:main:main",
scope: "per-sender",
@ -651,7 +658,7 @@ describe("statusCommand", () => {
).toBe(true);
if (originalAgents) {
mocks.listAgentsForGateway.mockImplementation(originalAgents);
mocks.listGatewayAgentsBasic.mockImplementation(originalAgents);
}
if (originalResolveStorePath) {
mocks.resolveStorePath.mockImplementation(originalResolveStorePath);

88
src/gateway/agent-list.ts Normal file
View File

@ -0,0 +1,88 @@
import fs from "node:fs";
import path from "node:path";
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import type { SessionScope } from "../config/sessions.js";
import { normalizeAgentId, normalizeMainKey } from "../routing/session-key.js";
export type GatewayAgentListRow = {
id: string;
name?: string;
};
function listExistingAgentIdsFromDisk(): string[] {
const root = resolveStateDir();
const agentsDir = path.join(root, "agents");
try {
const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
return entries
.filter((entry) => entry.isDirectory())
.map((entry) => normalizeAgentId(entry.name))
.filter(Boolean);
} catch {
return [];
}
}
function listConfiguredAgentIds(cfg: OpenClawConfig): string[] {
const ids = new Set<string>();
const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg));
ids.add(defaultId);
for (const entry of cfg.agents?.list ?? []) {
if (entry?.id) {
ids.add(normalizeAgentId(entry.id));
}
}
for (const id of listExistingAgentIdsFromDisk()) {
ids.add(id);
}
const sorted = Array.from(ids).filter(Boolean);
sorted.sort((a, b) => a.localeCompare(b));
return sorted.includes(defaultId)
? [defaultId, ...sorted.filter((id) => id !== defaultId)]
: sorted;
}
export function listGatewayAgentsBasic(cfg: OpenClawConfig): {
defaultId: string;
mainKey: string;
scope: SessionScope;
agents: GatewayAgentListRow[];
} {
const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg));
const mainKey = normalizeMainKey(cfg.session?.mainKey);
const scope = cfg.session?.scope ?? "per-sender";
const configuredById = new Map<string, { name?: string }>();
for (const entry of cfg.agents?.list ?? []) {
if (!entry?.id) {
continue;
}
configuredById.set(normalizeAgentId(entry.id), {
name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : undefined,
});
}
const explicitIds = new Set(
(cfg.agents?.list ?? [])
.map((entry) => (entry?.id ? normalizeAgentId(entry.id) : ""))
.filter(Boolean),
);
const allowedIds = explicitIds.size > 0 ? new Set([...explicitIds, defaultId]) : null;
let agentIds = listConfiguredAgentIds(cfg).filter((id) =>
allowedIds ? allowedIds.has(id) : true,
);
if (mainKey && !agentIds.includes(mainKey) && (!allowedIds || allowedIds.has(mainKey))) {
agentIds = [...agentIds, mainKey];
}
const agents = agentIds.map((id) => {
const meta = configuredById.get(id);
return {
id,
name: meta?.name,
};
});
return { defaultId, mainKey, scope, agents };
}