Skip to content

Commit 168f8a7

Browse files
authored
perf(cli): lazy-load agents actions for help (#84483)
Lazy-load agents CLI action modules from command callbacks so agents --help avoids importing the full agents runtime. Validated by GitHub required checks and local focused CLI gates.
1 parent 46030f5 commit 168f8a7

11 files changed

Lines changed: 159 additions & 49 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
1212

1313
- Media/audio: skip empty structured sherpa-onnx transcripts instead of treating the raw JSON payload as spoken text. (#84667) Thanks @TurboTheTurtle.
1414
- 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.
15+
- 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.
1516

1617
## 2026.5.20
1718

src/cli/help-cold-imports.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,40 @@ vi.mock("../commands/setup.js", () => {
150150
return { setupCommand: vi.fn(async () => {}) };
151151
});
152152

153+
vi.mock("../commands/agent-via-gateway.js", () => {
154+
loaded.mark("agent-via-gateway-command");
155+
return { agentCliCommand: vi.fn(async () => {}) };
156+
});
157+
158+
vi.mock("../commands/agents.commands.add.js", () => {
159+
loaded.mark("agents-add-command");
160+
return { agentsAddCommand: vi.fn(async () => {}) };
161+
});
162+
163+
vi.mock("../commands/agents.commands.bind.js", () => {
164+
loaded.mark("agents-bind-command");
165+
return {
166+
agentsBindingsCommand: vi.fn(async () => {}),
167+
agentsBindCommand: vi.fn(async () => {}),
168+
agentsUnbindCommand: vi.fn(async () => {}),
169+
};
170+
});
171+
172+
vi.mock("../commands/agents.commands.delete.js", () => {
173+
loaded.mark("agents-delete-command");
174+
return { agentsDeleteCommand: vi.fn(async () => {}) };
175+
});
176+
177+
vi.mock("../commands/agents.commands.identity.js", () => {
178+
loaded.mark("agents-identity-command");
179+
return { agentsSetIdentityCommand: vi.fn(async () => {}) };
180+
});
181+
182+
vi.mock("../commands/agents.commands.list.js", () => {
183+
loaded.mark("agents-list-command");
184+
return { agentsListCommand: vi.fn(async () => {}) };
185+
});
186+
153187
function makeProgram(): Command {
154188
const program = new Command();
155189
program.name("openclaw");
@@ -248,4 +282,19 @@ describe("subcommand help cold imports", () => {
248282
expect(loaded.modules).not.toContain("onboard-command");
249283
expect(loaded.modules).not.toContain("default-runtime");
250284
});
285+
286+
it("keeps agents help out of agent action modules", async () => {
287+
const { registerAgentCommands } = await import("./program/register.agent.js");
288+
const program = makeProgram();
289+
290+
registerAgentCommands(program, { agentChannelOptions: "last|telegram|discord" });
291+
await expectHelpExit(program, ["agents", "--help"]);
292+
293+
expect(loaded.modules).not.toContain("agent-via-gateway-command");
294+
expect(loaded.modules).not.toContain("agents-add-command");
295+
expect(loaded.modules).not.toContain("agents-bind-command");
296+
expect(loaded.modules).not.toContain("agents-delete-command");
297+
expect(loaded.modules).not.toContain("agents-identity-command");
298+
expect(loaded.modules).not.toContain("agents-list-command");
299+
});
251300
});

src/cli/program/register.agent.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,29 @@ vi.mock("../../commands/agent-via-gateway.js", () => ({
3636
agentCliCommand: mocks.agentCliCommandMock,
3737
}));
3838

39-
vi.mock("../../commands/agents.js", () => ({
39+
vi.mock("../../commands/agents.commands.add.js", () => ({
4040
agentsAddCommand: mocks.agentsAddCommandMock,
41+
}));
42+
43+
vi.mock("../../commands/agents.commands.bind.js", () => ({
4144
agentsBindingsCommand: mocks.agentsBindingsCommandMock,
4245
agentsBindCommand: mocks.agentsBindCommandMock,
46+
agentsUnbindCommand: mocks.agentsUnbindCommandMock,
47+
}));
48+
49+
vi.mock("../../commands/agents.commands.delete.js", () => ({
4350
agentsDeleteCommand: mocks.agentsDeleteCommandMock,
44-
agentsListCommand: mocks.agentsListCommandMock,
51+
}));
52+
53+
vi.mock("../../commands/agents.commands.identity.js", () => ({
4554
agentsSetIdentityCommand: mocks.agentsSetIdentityCommandMock,
46-
agentsUnbindCommand: mocks.agentsUnbindCommandMock,
4755
}));
4856

49-
vi.mock("../../globals.js", () => ({
57+
vi.mock("../../commands/agents.commands.list.js", () => ({
58+
agentsListCommand: mocks.agentsListCommandMock,
59+
}));
60+
61+
vi.mock("../../global-state.js", () => ({
5062
setVerbose: mocks.setVerboseMock,
5163
}));
5264

src/cli/program/register.agent.ts

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,68 @@
11
import type { Command } from "commander";
2-
import { agentCliCommand } from "../../commands/agent-via-gateway.js";
3-
import {
4-
agentsAddCommand,
5-
agentsBindingsCommand,
6-
agentsBindCommand,
7-
agentsDeleteCommand,
8-
agentsListCommand,
9-
agentsSetIdentityCommand,
10-
agentsUnbindCommand,
11-
} from "../../commands/agents.js";
12-
import { setVerbose } from "../../globals.js";
132
import { defaultRuntime } from "../../runtime.js";
143
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
154
import { formatDocsLink } from "../../terminal/links.js";
165
import { theme } from "../../terminal/theme.js";
176
import { runCommandWithRuntime } from "../cli-utils.js";
187
import { hasExplicitOptions } from "../command-options.js";
19-
import { createDefaultDeps } from "../deps.js";
208
import { formatHelpExamples } from "../help-format.js";
219
import { collectOption } from "./helpers.js";
2210

23-
export function registerAgentCommands(program: Command, args: { agentChannelOptions: string }) {
11+
type AgentViaGatewayModule = typeof import("../../commands/agent-via-gateway.js");
12+
type AgentsAddModule = typeof import("../../commands/agents.commands.add.js");
13+
type AgentsBindModule = typeof import("../../commands/agents.commands.bind.js");
14+
type AgentsDeleteModule = typeof import("../../commands/agents.commands.delete.js");
15+
type AgentsIdentityModule = typeof import("../../commands/agents.commands.identity.js");
16+
type AgentsListModule = typeof import("../../commands/agents.commands.list.js");
17+
type CliDepsModule = typeof import("../deps.js");
18+
type GlobalStateModule = typeof import("../../global-state.js");
19+
20+
async function loadAgentCliCommand(): Promise<AgentViaGatewayModule["agentCliCommand"]> {
21+
return (await import("../../commands/agent-via-gateway.js")).agentCliCommand;
22+
}
23+
24+
async function loadAgentsAddCommand(): Promise<AgentsAddModule["agentsAddCommand"]> {
25+
return (await import("../../commands/agents.commands.add.js")).agentsAddCommand;
26+
}
27+
28+
async function loadAgentsBindCommand(): Promise<AgentsBindModule["agentsBindCommand"]> {
29+
return (await import("../../commands/agents.commands.bind.js")).agentsBindCommand;
30+
}
31+
32+
async function loadAgentsBindingsCommand(): Promise<AgentsBindModule["agentsBindingsCommand"]> {
33+
return (await import("../../commands/agents.commands.bind.js")).agentsBindingsCommand;
34+
}
35+
36+
async function loadAgentsUnbindCommand(): Promise<AgentsBindModule["agentsUnbindCommand"]> {
37+
return (await import("../../commands/agents.commands.bind.js")).agentsUnbindCommand;
38+
}
39+
40+
async function loadAgentsDeleteCommand(): Promise<AgentsDeleteModule["agentsDeleteCommand"]> {
41+
return (await import("../../commands/agents.commands.delete.js")).agentsDeleteCommand;
42+
}
43+
44+
async function loadAgentsSetIdentityCommand(): Promise<
45+
AgentsIdentityModule["agentsSetIdentityCommand"]
46+
> {
47+
return (await import("../../commands/agents.commands.identity.js")).agentsSetIdentityCommand;
48+
}
49+
50+
async function loadAgentsListCommand(): Promise<AgentsListModule["agentsListCommand"]> {
51+
return (await import("../../commands/agents.commands.list.js")).agentsListCommand;
52+
}
53+
54+
async function loadCreateDefaultDeps(): Promise<CliDepsModule["createDefaultDeps"]> {
55+
return (await import("../deps.js")).createDefaultDeps;
56+
}
57+
58+
async function loadSetVerbose(): Promise<GlobalStateModule["setVerbose"]> {
59+
return (await import("../../global-state.js")).setVerbose;
60+
}
61+
62+
export function registerAgentCommands(
63+
program: Command,
64+
args: { agentChannelOptions: string },
65+
): void {
2466
program
2567
.command("agent")
2668
.description("Run an agent turn via the Gateway (use --local for embedded)")
@@ -77,13 +119,16 @@ ${formatHelpExamples([
77119
78120
${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/agent")}`,
79121
)
80-
.action(async (opts) => {
122+
.action(async (opts): Promise<void> => {
81123
const verboseLevel =
82124
typeof opts.verbose === "string" ? normalizeLowercaseStringOrEmpty(opts.verbose) : "";
83-
setVerbose(verboseLevel === "on");
84-
// Build default deps (keeps parity with other commands; future-proofing).
85-
const deps = createDefaultDeps();
86125
await runCommandWithRuntime(defaultRuntime, async () => {
126+
const setVerbose = await loadSetVerbose();
127+
setVerbose(verboseLevel === "on");
128+
// Build default deps (keeps parity with other commands; future-proofing).
129+
const createDefaultDeps = await loadCreateDefaultDeps();
130+
const deps = createDefaultDeps();
131+
const agentCliCommand = await loadAgentCliCommand();
87132
await agentCliCommand(opts, defaultRuntime, deps);
88133
});
89134
});
@@ -102,8 +147,9 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age
102147
.description("List configured agents")
103148
.option("--json", "Output JSON instead of text", false)
104149
.option("--bindings", "Include routing bindings", false)
105-
.action(async (opts) => {
150+
.action(async (opts): Promise<void> => {
106151
await runCommandWithRuntime(defaultRuntime, async () => {
152+
const agentsListCommand = await loadAgentsListCommand();
107153
await agentsListCommand(
108154
{ json: Boolean(opts.json), bindings: Boolean(opts.bindings) },
109155
defaultRuntime,
@@ -116,8 +162,9 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age
116162
.description("List routing bindings")
117163
.option("--agent <id>", "Filter by agent id")
118164
.option("--json", "Output JSON instead of text", false)
119-
.action(async (opts) => {
165+
.action(async (opts): Promise<void> => {
120166
await runCommandWithRuntime(defaultRuntime, async () => {
167+
const agentsBindingsCommand = await loadAgentsBindingsCommand();
121168
await agentsBindingsCommand(
122169
{
123170
agent: opts.agent as string | undefined,
@@ -139,8 +186,9 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age
139186
[],
140187
)
141188
.option("--json", "Output JSON summary", false)
142-
.action(async (opts) => {
189+
.action(async (opts): Promise<void> => {
143190
await runCommandWithRuntime(defaultRuntime, async () => {
191+
const agentsBindCommand = await loadAgentsBindCommand();
144192
await agentsBindCommand(
145193
{
146194
agent: opts.agent as string | undefined,
@@ -159,8 +207,9 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age
159207
.option("--bind <channel[:accountId]>", "Binding to remove (repeatable)", collectOption, [])
160208
.option("--all", "Remove all bindings for this agent", false)
161209
.option("--json", "Output JSON summary", false)
162-
.action(async (opts) => {
210+
.action(async (opts): Promise<void> => {
163211
await runCommandWithRuntime(defaultRuntime, async () => {
212+
const agentsUnbindCommand = await loadAgentsUnbindCommand();
164213
await agentsUnbindCommand(
165214
{
166215
agent: opts.agent as string | undefined,
@@ -182,7 +231,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age
182231
.option("--bind <channel[:accountId]>", "Route channel binding (repeatable)", collectOption, [])
183232
.option("--non-interactive", "Disable prompts; requires --workspace", false)
184233
.option("--json", "Output JSON summary", false)
185-
.action(async (name, opts, command) => {
234+
.action(async (name, opts, command): Promise<void> => {
186235
await runCommandWithRuntime(defaultRuntime, async () => {
187236
const hasFlags = hasExplicitOptions(command, [
188237
"workspace",
@@ -191,6 +240,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age
191240
"bind",
192241
"nonInteractive",
193242
]);
243+
const agentsAddCommand = await loadAgentsAddCommand();
194244
await agentsAddCommand(
195245
{
196246
name: typeof name === "string" ? name : undefined,
@@ -238,8 +288,9 @@ ${formatHelpExamples([
238288
])}
239289
`,
240290
)
241-
.action(async (opts) => {
291+
.action(async (opts): Promise<void> => {
242292
await runCommandWithRuntime(defaultRuntime, async () => {
293+
const agentsSetIdentityCommand = await loadAgentsSetIdentityCommand();
243294
await agentsSetIdentityCommand(
244295
{
245296
agent: opts.agent as string | undefined,
@@ -262,8 +313,9 @@ ${formatHelpExamples([
262313
.description("Delete an agent and prune workspace/state")
263314
.option("--force", "Skip confirmation", false)
264315
.option("--json", "Output JSON summary", false)
265-
.action(async (id, opts) => {
316+
.action(async (id, opts): Promise<void> => {
266317
await runCommandWithRuntime(defaultRuntime, async () => {
318+
const agentsDeleteCommand = await loadAgentsDeleteCommand();
267319
await agentsDeleteCommand(
268320
{
269321
id: String(id),
@@ -275,8 +327,9 @@ ${formatHelpExamples([
275327
});
276328
});
277329

278-
agents.action(async () => {
330+
agents.action(async (): Promise<void> => {
279331
await runCommandWithRuntime(defaultRuntime, async () => {
332+
const agentsListCommand = await loadAgentsListCommand();
280333
await agentsListCommand({}, defaultRuntime);
281334
});
282335
});

src/cli/program/routed-command-definitions.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
type RouteArgParser<TArgs> = (argv: string[]) => TArgs | null;
2121

2222
type ParsedRouteArgs<TParse extends RouteArgParser<unknown>> = Exclude<ReturnType<TParse>, null>;
23+
type AgentsListCommandModule = typeof import("../../commands/agents.commands.list.js");
2324
type ConfigCliModule = typeof import("../config-cli.js");
2425
type ModelsListCommandModule = typeof import("../../commands/models/list.list-command.js");
2526
type ModelsStatusCommandModule = typeof import("../../commands/models/list.status-command.js");
@@ -41,6 +42,9 @@ function defineRoutedCommand<TParse extends RouteArgParser<unknown>>(
4142
}
4243

4344
const configCliLoader = createLazyImportLoader<ConfigCliModule>(() => import("../config-cli.js"));
45+
const agentsListCommandLoader = createLazyImportLoader<AgentsListCommandModule>(
46+
() => import("../../commands/agents.commands.list.js"),
47+
);
4448
const modelsListCommandLoader = createLazyImportLoader<ModelsListCommandModule>(
4549
() => import("../../commands/models/list.list-command.js"),
4650
);
@@ -52,6 +56,10 @@ function loadConfigCli(): Promise<ConfigCliModule> {
5256
return configCliLoader.load();
5357
}
5458

59+
function loadAgentsListCommand(): Promise<AgentsListCommandModule> {
60+
return agentsListCommandLoader.load();
61+
}
62+
5563
function loadModelsListCommand(): Promise<ModelsListCommandModule> {
5664
return modelsListCommandLoader.load();
5765
}
@@ -105,7 +113,7 @@ export const routedCommandDefinitions = {
105113
"agents-list": defineRoutedCommand({
106114
parseArgs: parseAgentsListRouteArgs,
107115
runParsedArgs: async (args) => {
108-
const { agentsListCommand } = await import("../../commands/agents.js");
116+
const { agentsListCommand } = await loadAgentsListCommand();
109117
await agentsListCommand(args, defaultRuntime);
110118
},
111119
}),

src/cli/program/routes.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ vi.mock("../../commands/channels/status.js", () => ({
5353
channelsStatusCommand: channelsStatusCommandMock,
5454
}));
5555

56-
vi.mock("../../commands/agents.js", () => ({
56+
vi.mock("../../commands/agents.commands.list.js", () => ({
5757
agentsListCommand: agentsListCommandMock,
5858
}));
5959

src/commands/agents.add.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,7 @@ vi.mock("../wizard/clack-prompter.js", () => ({
7878
}));
7979

8080
import { WizardCancelledError } from "../wizard/prompts.js";
81-
import { testing } from "./agents.commands.add.js";
82-
import { agentsAddCommand } from "./agents.js";
81+
import { agentsAddCommand, testing } from "./agents.commands.add.js";
8382

8483
const runtime = createTestRuntime();
8584

src/commands/agents.delete.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ vi.mock("../process/exec.js", () => ({
3535
runCommandWithTimeout: processMocks.runCommandWithTimeout,
3636
}));
3737

38-
import { agentsDeleteCommand } from "./agents.js";
38+
import { agentsDeleteCommand } from "./agents.commands.delete.js";
3939

4040
const runtime = createTestRuntime();
4141

src/commands/agents.identity.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ vi.mock("../config/config.js", async () => ({
2222
replaceConfigFile: configMocks.replaceConfigFile,
2323
}));
2424

25-
import { agentsSetIdentityCommand } from "./agents.js";
25+
import { agentsSetIdentityCommand } from "./agents.commands.identity.js";
2626

2727
const runtime = createTestRuntime();
2828
type ConfigWritePayload = {

src/commands/agents.test.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import path from "node:path";
22
import { describe, expect, it } from "vitest";
33
import type { OpenClawConfig } from "../config/config.js";
4-
import {
5-
applyAgentBindings,
6-
applyAgentConfig,
7-
buildAgentSummaries,
8-
pruneAgentConfig,
9-
removeAgentBindings,
10-
} from "./agents.js";
4+
import { applyAgentBindings, removeAgentBindings } from "./agents.bindings.js";
5+
import { applyAgentConfig, buildAgentSummaries, pruneAgentConfig } from "./agents.config.js";
116

127
function requireAgentSummary(
138
summaries: ReturnType<typeof buildAgentSummaries>,

0 commit comments

Comments
 (0)