diff --git a/CHANGELOG.md b/CHANGELOG.md index ad7cd786cda..39680186aa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai - Media/audio: skip empty structured sherpa-onnx transcripts instead of treating the raw JSON payload as spoken text. (#84667) Thanks @TurboTheTurtle. - CLI/perf: keep `setup --help`, `onboard --help`, and `configure --help` out of the full wizard runtime while preserving the existing help output. (#84488) Thanks @frankekn. +- CLI/perf: keep `agents --help` out of agents action/runtime imports so help, completion, and command discovery paths avoid loading the full agents runtime. (#84483) Thanks @frankekn. ## 2026.5.20 diff --git a/src/cli/help-cold-imports.test.ts b/src/cli/help-cold-imports.test.ts index 091c4109117..79582d92111 100644 --- a/src/cli/help-cold-imports.test.ts +++ b/src/cli/help-cold-imports.test.ts @@ -150,6 +150,40 @@ vi.mock("../commands/setup.js", () => { return { setupCommand: vi.fn(async () => {}) }; }); +vi.mock("../commands/agent-via-gateway.js", () => { + loaded.mark("agent-via-gateway-command"); + return { agentCliCommand: vi.fn(async () => {}) }; +}); + +vi.mock("../commands/agents.commands.add.js", () => { + loaded.mark("agents-add-command"); + return { agentsAddCommand: vi.fn(async () => {}) }; +}); + +vi.mock("../commands/agents.commands.bind.js", () => { + loaded.mark("agents-bind-command"); + return { + agentsBindingsCommand: vi.fn(async () => {}), + agentsBindCommand: vi.fn(async () => {}), + agentsUnbindCommand: vi.fn(async () => {}), + }; +}); + +vi.mock("../commands/agents.commands.delete.js", () => { + loaded.mark("agents-delete-command"); + return { agentsDeleteCommand: vi.fn(async () => {}) }; +}); + +vi.mock("../commands/agents.commands.identity.js", () => { + loaded.mark("agents-identity-command"); + return { agentsSetIdentityCommand: vi.fn(async () => {}) }; +}); + +vi.mock("../commands/agents.commands.list.js", () => { + loaded.mark("agents-list-command"); + return { agentsListCommand: vi.fn(async () => {}) }; +}); + function makeProgram(): Command { const program = new Command(); program.name("openclaw"); @@ -248,4 +282,19 @@ describe("subcommand help cold imports", () => { expect(loaded.modules).not.toContain("onboard-command"); expect(loaded.modules).not.toContain("default-runtime"); }); + + it("keeps agents help out of agent action modules", async () => { + const { registerAgentCommands } = await import("./program/register.agent.js"); + const program = makeProgram(); + + registerAgentCommands(program, { agentChannelOptions: "last|telegram|discord" }); + await expectHelpExit(program, ["agents", "--help"]); + + expect(loaded.modules).not.toContain("agent-via-gateway-command"); + expect(loaded.modules).not.toContain("agents-add-command"); + expect(loaded.modules).not.toContain("agents-bind-command"); + expect(loaded.modules).not.toContain("agents-delete-command"); + expect(loaded.modules).not.toContain("agents-identity-command"); + expect(loaded.modules).not.toContain("agents-list-command"); + }); }); diff --git a/src/cli/program/register.agent.test.ts b/src/cli/program/register.agent.test.ts index 8551438ec5e..81c73236714 100644 --- a/src/cli/program/register.agent.test.ts +++ b/src/cli/program/register.agent.test.ts @@ -36,17 +36,29 @@ vi.mock("../../commands/agent-via-gateway.js", () => ({ agentCliCommand: mocks.agentCliCommandMock, })); -vi.mock("../../commands/agents.js", () => ({ +vi.mock("../../commands/agents.commands.add.js", () => ({ agentsAddCommand: mocks.agentsAddCommandMock, +})); + +vi.mock("../../commands/agents.commands.bind.js", () => ({ agentsBindingsCommand: mocks.agentsBindingsCommandMock, agentsBindCommand: mocks.agentsBindCommandMock, - agentsDeleteCommand: mocks.agentsDeleteCommandMock, - agentsListCommand: mocks.agentsListCommandMock, - agentsSetIdentityCommand: mocks.agentsSetIdentityCommandMock, agentsUnbindCommand: mocks.agentsUnbindCommandMock, })); -vi.mock("../../globals.js", () => ({ +vi.mock("../../commands/agents.commands.delete.js", () => ({ + agentsDeleteCommand: mocks.agentsDeleteCommandMock, +})); + +vi.mock("../../commands/agents.commands.identity.js", () => ({ + agentsSetIdentityCommand: mocks.agentsSetIdentityCommandMock, +})); + +vi.mock("../../commands/agents.commands.list.js", () => ({ + agentsListCommand: mocks.agentsListCommandMock, +})); + +vi.mock("../../global-state.js", () => ({ setVerbose: mocks.setVerboseMock, })); diff --git a/src/cli/program/register.agent.ts b/src/cli/program/register.agent.ts index 6addea2479c..88ecaf2f9ff 100644 --- a/src/cli/program/register.agent.ts +++ b/src/cli/program/register.agent.ts @@ -1,26 +1,68 @@ import type { Command } from "commander"; -import { agentCliCommand } from "../../commands/agent-via-gateway.js"; -import { - agentsAddCommand, - agentsBindingsCommand, - agentsBindCommand, - agentsDeleteCommand, - agentsListCommand, - agentsSetIdentityCommand, - agentsUnbindCommand, -} from "../../commands/agents.js"; -import { setVerbose } from "../../globals.js"; import { defaultRuntime } from "../../runtime.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { formatDocsLink } from "../../terminal/links.js"; import { theme } from "../../terminal/theme.js"; import { runCommandWithRuntime } from "../cli-utils.js"; import { hasExplicitOptions } from "../command-options.js"; -import { createDefaultDeps } from "../deps.js"; import { formatHelpExamples } from "../help-format.js"; import { collectOption } from "./helpers.js"; -export function registerAgentCommands(program: Command, args: { agentChannelOptions: string }) { +type AgentViaGatewayModule = typeof import("../../commands/agent-via-gateway.js"); +type AgentsAddModule = typeof import("../../commands/agents.commands.add.js"); +type AgentsBindModule = typeof import("../../commands/agents.commands.bind.js"); +type AgentsDeleteModule = typeof import("../../commands/agents.commands.delete.js"); +type AgentsIdentityModule = typeof import("../../commands/agents.commands.identity.js"); +type AgentsListModule = typeof import("../../commands/agents.commands.list.js"); +type CliDepsModule = typeof import("../deps.js"); +type GlobalStateModule = typeof import("../../global-state.js"); + +async function loadAgentCliCommand(): Promise { + return (await import("../../commands/agent-via-gateway.js")).agentCliCommand; +} + +async function loadAgentsAddCommand(): Promise { + return (await import("../../commands/agents.commands.add.js")).agentsAddCommand; +} + +async function loadAgentsBindCommand(): Promise { + return (await import("../../commands/agents.commands.bind.js")).agentsBindCommand; +} + +async function loadAgentsBindingsCommand(): Promise { + return (await import("../../commands/agents.commands.bind.js")).agentsBindingsCommand; +} + +async function loadAgentsUnbindCommand(): Promise { + return (await import("../../commands/agents.commands.bind.js")).agentsUnbindCommand; +} + +async function loadAgentsDeleteCommand(): Promise { + return (await import("../../commands/agents.commands.delete.js")).agentsDeleteCommand; +} + +async function loadAgentsSetIdentityCommand(): Promise< + AgentsIdentityModule["agentsSetIdentityCommand"] +> { + return (await import("../../commands/agents.commands.identity.js")).agentsSetIdentityCommand; +} + +async function loadAgentsListCommand(): Promise { + return (await import("../../commands/agents.commands.list.js")).agentsListCommand; +} + +async function loadCreateDefaultDeps(): Promise { + return (await import("../deps.js")).createDefaultDeps; +} + +async function loadSetVerbose(): Promise { + return (await import("../../global-state.js")).setVerbose; +} + +export function registerAgentCommands( + program: Command, + args: { agentChannelOptions: string }, +): void { program .command("agent") .description("Run an agent turn via the Gateway (use --local for embedded)") @@ -77,13 +119,16 @@ ${formatHelpExamples([ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/agent")}`, ) - .action(async (opts) => { + .action(async (opts): Promise => { const verboseLevel = typeof opts.verbose === "string" ? normalizeLowercaseStringOrEmpty(opts.verbose) : ""; - setVerbose(verboseLevel === "on"); - // Build default deps (keeps parity with other commands; future-proofing). - const deps = createDefaultDeps(); await runCommandWithRuntime(defaultRuntime, async () => { + const setVerbose = await loadSetVerbose(); + setVerbose(verboseLevel === "on"); + // Build default deps (keeps parity with other commands; future-proofing). + const createDefaultDeps = await loadCreateDefaultDeps(); + const deps = createDefaultDeps(); + const agentCliCommand = await loadAgentCliCommand(); await agentCliCommand(opts, defaultRuntime, deps); }); }); @@ -102,8 +147,9 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age .description("List configured agents") .option("--json", "Output JSON instead of text", false) .option("--bindings", "Include routing bindings", false) - .action(async (opts) => { + .action(async (opts): Promise => { await runCommandWithRuntime(defaultRuntime, async () => { + const agentsListCommand = await loadAgentsListCommand(); await agentsListCommand( { json: Boolean(opts.json), bindings: Boolean(opts.bindings) }, defaultRuntime, @@ -116,8 +162,9 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age .description("List routing bindings") .option("--agent ", "Filter by agent id") .option("--json", "Output JSON instead of text", false) - .action(async (opts) => { + .action(async (opts): Promise => { await runCommandWithRuntime(defaultRuntime, async () => { + const agentsBindingsCommand = await loadAgentsBindingsCommand(); await agentsBindingsCommand( { agent: opts.agent as string | undefined, @@ -139,8 +186,9 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age [], ) .option("--json", "Output JSON summary", false) - .action(async (opts) => { + .action(async (opts): Promise => { await runCommandWithRuntime(defaultRuntime, async () => { + const agentsBindCommand = await loadAgentsBindCommand(); await agentsBindCommand( { agent: opts.agent as string | undefined, @@ -159,8 +207,9 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age .option("--bind ", "Binding to remove (repeatable)", collectOption, []) .option("--all", "Remove all bindings for this agent", false) .option("--json", "Output JSON summary", false) - .action(async (opts) => { + .action(async (opts): Promise => { await runCommandWithRuntime(defaultRuntime, async () => { + const agentsUnbindCommand = await loadAgentsUnbindCommand(); await agentsUnbindCommand( { agent: opts.agent as string | undefined, @@ -182,7 +231,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age .option("--bind ", "Route channel binding (repeatable)", collectOption, []) .option("--non-interactive", "Disable prompts; requires --workspace", false) .option("--json", "Output JSON summary", false) - .action(async (name, opts, command) => { + .action(async (name, opts, command): Promise => { await runCommandWithRuntime(defaultRuntime, async () => { const hasFlags = hasExplicitOptions(command, [ "workspace", @@ -191,6 +240,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age "bind", "nonInteractive", ]); + const agentsAddCommand = await loadAgentsAddCommand(); await agentsAddCommand( { name: typeof name === "string" ? name : undefined, @@ -238,8 +288,9 @@ ${formatHelpExamples([ ])} `, ) - .action(async (opts) => { + .action(async (opts): Promise => { await runCommandWithRuntime(defaultRuntime, async () => { + const agentsSetIdentityCommand = await loadAgentsSetIdentityCommand(); await agentsSetIdentityCommand( { agent: opts.agent as string | undefined, @@ -262,8 +313,9 @@ ${formatHelpExamples([ .description("Delete an agent and prune workspace/state") .option("--force", "Skip confirmation", false) .option("--json", "Output JSON summary", false) - .action(async (id, opts) => { + .action(async (id, opts): Promise => { await runCommandWithRuntime(defaultRuntime, async () => { + const agentsDeleteCommand = await loadAgentsDeleteCommand(); await agentsDeleteCommand( { id: String(id), @@ -275,8 +327,9 @@ ${formatHelpExamples([ }); }); - agents.action(async () => { + agents.action(async (): Promise => { await runCommandWithRuntime(defaultRuntime, async () => { + const agentsListCommand = await loadAgentsListCommand(); await agentsListCommand({}, defaultRuntime); }); }); diff --git a/src/cli/program/routed-command-definitions.ts b/src/cli/program/routed-command-definitions.ts index ff4ee4fc78f..dd7d3f2f8a4 100644 --- a/src/cli/program/routed-command-definitions.ts +++ b/src/cli/program/routed-command-definitions.ts @@ -20,6 +20,7 @@ import { type RouteArgParser = (argv: string[]) => TArgs | null; type ParsedRouteArgs> = Exclude, null>; +type AgentsListCommandModule = typeof import("../../commands/agents.commands.list.js"); type ConfigCliModule = typeof import("../config-cli.js"); type ModelsListCommandModule = typeof import("../../commands/models/list.list-command.js"); type ModelsStatusCommandModule = typeof import("../../commands/models/list.status-command.js"); @@ -41,6 +42,9 @@ function defineRoutedCommand>( } const configCliLoader = createLazyImportLoader(() => import("../config-cli.js")); +const agentsListCommandLoader = createLazyImportLoader( + () => import("../../commands/agents.commands.list.js"), +); const modelsListCommandLoader = createLazyImportLoader( () => import("../../commands/models/list.list-command.js"), ); @@ -52,6 +56,10 @@ function loadConfigCli(): Promise { return configCliLoader.load(); } +function loadAgentsListCommand(): Promise { + return agentsListCommandLoader.load(); +} + function loadModelsListCommand(): Promise { return modelsListCommandLoader.load(); } @@ -105,7 +113,7 @@ export const routedCommandDefinitions = { "agents-list": defineRoutedCommand({ parseArgs: parseAgentsListRouteArgs, runParsedArgs: async (args) => { - const { agentsListCommand } = await import("../../commands/agents.js"); + const { agentsListCommand } = await loadAgentsListCommand(); await agentsListCommand(args, defaultRuntime); }, }), diff --git a/src/cli/program/routes.test.ts b/src/cli/program/routes.test.ts index 804dff02d17..74f9a9a7e69 100644 --- a/src/cli/program/routes.test.ts +++ b/src/cli/program/routes.test.ts @@ -53,7 +53,7 @@ vi.mock("../../commands/channels/status.js", () => ({ channelsStatusCommand: channelsStatusCommandMock, })); -vi.mock("../../commands/agents.js", () => ({ +vi.mock("../../commands/agents.commands.list.js", () => ({ agentsListCommand: agentsListCommandMock, })); diff --git a/src/commands/agents.add.test.ts b/src/commands/agents.add.test.ts index eb26be42312..2eef86e9f62 100644 --- a/src/commands/agents.add.test.ts +++ b/src/commands/agents.add.test.ts @@ -78,8 +78,7 @@ vi.mock("../wizard/clack-prompter.js", () => ({ })); import { WizardCancelledError } from "../wizard/prompts.js"; -import { testing } from "./agents.commands.add.js"; -import { agentsAddCommand } from "./agents.js"; +import { agentsAddCommand, testing } from "./agents.commands.add.js"; const runtime = createTestRuntime(); diff --git a/src/commands/agents.delete.test.ts b/src/commands/agents.delete.test.ts index 65e0c5ca0ad..4ce9de3637b 100644 --- a/src/commands/agents.delete.test.ts +++ b/src/commands/agents.delete.test.ts @@ -35,7 +35,7 @@ vi.mock("../process/exec.js", () => ({ runCommandWithTimeout: processMocks.runCommandWithTimeout, })); -import { agentsDeleteCommand } from "./agents.js"; +import { agentsDeleteCommand } from "./agents.commands.delete.js"; const runtime = createTestRuntime(); diff --git a/src/commands/agents.identity.test.ts b/src/commands/agents.identity.test.ts index 4f05e8896b0..a069b1634ad 100644 --- a/src/commands/agents.identity.test.ts +++ b/src/commands/agents.identity.test.ts @@ -22,7 +22,7 @@ vi.mock("../config/config.js", async () => ({ replaceConfigFile: configMocks.replaceConfigFile, })); -import { agentsSetIdentityCommand } from "./agents.js"; +import { agentsSetIdentityCommand } from "./agents.commands.identity.js"; const runtime = createTestRuntime(); type ConfigWritePayload = { diff --git a/src/commands/agents.test.ts b/src/commands/agents.test.ts index 73030c4a55f..660765dc285 100644 --- a/src/commands/agents.test.ts +++ b/src/commands/agents.test.ts @@ -1,13 +1,8 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { - applyAgentBindings, - applyAgentConfig, - buildAgentSummaries, - pruneAgentConfig, - removeAgentBindings, -} from "./agents.js"; +import { applyAgentBindings, removeAgentBindings } from "./agents.bindings.js"; +import { applyAgentConfig, buildAgentSummaries, pruneAgentConfig } from "./agents.config.js"; function requireAgentSummary( summaries: ReturnType, diff --git a/src/commands/agents.ts b/src/commands/agents.ts deleted file mode 100644 index 5f5bdcd3c7b..00000000000 --- a/src/commands/agents.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./agents.bindings.js"; -export * from "./agents.commands.bind.js"; -export * from "./agents.commands.add.js"; -export * from "./agents.commands.delete.js"; -export * from "./agents.commands.identity.js"; -export * from "./agents.commands.list.js"; -export * from "./agents.config.js";