Skip to content

Commit a372e4a

Browse files
committed
perf(agents): isolate agent scope config helpers
1 parent 2c59ba2 commit a372e4a

3 files changed

Lines changed: 185 additions & 159 deletions

File tree

src/agents/agent-scope-config.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import path from "node:path";
2+
import { resolveStateDir } from "../config/paths.js";
3+
import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js";
4+
import type { OpenClawConfig } from "../config/types.js";
5+
import { createSubsystemLogger } from "../logging/subsystem.js";
6+
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js";
7+
import { readStringValue } from "../shared/string-coerce.js";
8+
import { resolveUserPath } from "../utils.js";
9+
import { resolveDefaultAgentWorkspaceDir } from "./workspace.js";
10+
11+
type AgentEntry = NonNullable<NonNullable<OpenClawConfig["agents"]>["list"]>[number];
12+
13+
export type ResolvedAgentConfig = {
14+
name?: string;
15+
workspace?: string;
16+
agentDir?: string;
17+
systemPromptOverride?: AgentEntry["systemPromptOverride"];
18+
model?: AgentEntry["model"];
19+
thinkingDefault?: AgentEntry["thinkingDefault"];
20+
verboseDefault?: AgentDefaultsConfig["verboseDefault"];
21+
reasoningDefault?: AgentEntry["reasoningDefault"];
22+
fastModeDefault?: AgentEntry["fastModeDefault"];
23+
skills?: AgentEntry["skills"];
24+
memorySearch?: AgentEntry["memorySearch"];
25+
humanDelay?: AgentEntry["humanDelay"];
26+
heartbeat?: AgentEntry["heartbeat"];
27+
identity?: AgentEntry["identity"];
28+
groupChat?: AgentEntry["groupChat"];
29+
subagents?: AgentEntry["subagents"];
30+
embeddedPi?: AgentEntry["embeddedPi"];
31+
sandbox?: AgentEntry["sandbox"];
32+
tools?: AgentEntry["tools"];
33+
};
34+
35+
let log: ReturnType<typeof createSubsystemLogger> | null = null;
36+
let defaultAgentWarned = false;
37+
38+
function getLog(): ReturnType<typeof createSubsystemLogger> {
39+
log ??= createSubsystemLogger("agent-scope");
40+
return log;
41+
}
42+
43+
/** Strip null bytes from paths to prevent ENOTDIR errors. */
44+
function stripNullBytes(s: string): string {
45+
// eslint-disable-next-line no-control-regex
46+
return s.replace(/\0/g, "");
47+
}
48+
49+
export function listAgentEntries(cfg: OpenClawConfig): AgentEntry[] {
50+
const list = cfg.agents?.list;
51+
if (!Array.isArray(list)) {
52+
return [];
53+
}
54+
return list.filter((entry): entry is AgentEntry => entry !== null && typeof entry === "object");
55+
}
56+
57+
export function listAgentIds(cfg: OpenClawConfig): string[] {
58+
const agents = listAgentEntries(cfg);
59+
if (agents.length === 0) {
60+
return [DEFAULT_AGENT_ID];
61+
}
62+
const seen = new Set<string>();
63+
const ids: string[] = [];
64+
for (const entry of agents) {
65+
const id = normalizeAgentId(entry?.id);
66+
if (seen.has(id)) {
67+
continue;
68+
}
69+
seen.add(id);
70+
ids.push(id);
71+
}
72+
return ids.length > 0 ? ids : [DEFAULT_AGENT_ID];
73+
}
74+
75+
export function resolveDefaultAgentId(cfg: OpenClawConfig): string {
76+
const agents = listAgentEntries(cfg);
77+
if (agents.length === 0) {
78+
return DEFAULT_AGENT_ID;
79+
}
80+
const defaults = agents.filter((agent) => agent?.default);
81+
if (defaults.length > 1 && !defaultAgentWarned) {
82+
defaultAgentWarned = true;
83+
getLog().warn("Multiple agents marked default=true; using the first entry as default.");
84+
}
85+
const chosen = (defaults[0] ?? agents[0])?.id?.trim();
86+
return normalizeAgentId(chosen || DEFAULT_AGENT_ID);
87+
}
88+
89+
function resolveAgentEntry(cfg: OpenClawConfig, agentId: string): AgentEntry | undefined {
90+
const id = normalizeAgentId(agentId);
91+
return listAgentEntries(cfg).find((entry) => normalizeAgentId(entry.id) === id);
92+
}
93+
94+
export function resolveAgentConfig(
95+
cfg: OpenClawConfig,
96+
agentId: string,
97+
): ResolvedAgentConfig | undefined {
98+
const id = normalizeAgentId(agentId);
99+
const entry = resolveAgentEntry(cfg, id);
100+
if (!entry) {
101+
return undefined;
102+
}
103+
const agentDefaults = cfg.agents?.defaults;
104+
return {
105+
name: readStringValue(entry.name),
106+
workspace: readStringValue(entry.workspace),
107+
agentDir: readStringValue(entry.agentDir),
108+
systemPromptOverride: readStringValue(entry.systemPromptOverride),
109+
model:
110+
typeof entry.model === "string" || (entry.model && typeof entry.model === "object")
111+
? entry.model
112+
: undefined,
113+
thinkingDefault: entry.thinkingDefault,
114+
verboseDefault: entry.verboseDefault ?? agentDefaults?.verboseDefault,
115+
reasoningDefault: entry.reasoningDefault,
116+
fastModeDefault: entry.fastModeDefault,
117+
skills: Array.isArray(entry.skills) ? entry.skills : undefined,
118+
memorySearch: entry.memorySearch,
119+
humanDelay: entry.humanDelay,
120+
heartbeat: entry.heartbeat,
121+
identity: entry.identity,
122+
groupChat: entry.groupChat,
123+
subagents: typeof entry.subagents === "object" && entry.subagents ? entry.subagents : undefined,
124+
embeddedPi:
125+
typeof entry.embeddedPi === "object" && entry.embeddedPi ? entry.embeddedPi : undefined,
126+
sandbox: entry.sandbox,
127+
tools: entry.tools,
128+
};
129+
}
130+
131+
export function resolveAgentWorkspaceDir(cfg: OpenClawConfig, agentId: string) {
132+
const id = normalizeAgentId(agentId);
133+
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
134+
if (configured) {
135+
return stripNullBytes(resolveUserPath(configured));
136+
}
137+
const defaultAgentId = resolveDefaultAgentId(cfg);
138+
const fallback = cfg.agents?.defaults?.workspace?.trim();
139+
if (id === defaultAgentId) {
140+
if (fallback) {
141+
return stripNullBytes(resolveUserPath(fallback));
142+
}
143+
return stripNullBytes(resolveDefaultAgentWorkspaceDir(process.env));
144+
}
145+
if (fallback) {
146+
return stripNullBytes(path.join(resolveUserPath(fallback), id));
147+
}
148+
const stateDir = resolveStateDir(process.env);
149+
return stripNullBytes(path.join(stateDir, `workspace-${id}`));
150+
}
151+
152+
export function resolveAgentDir(
153+
cfg: OpenClawConfig,
154+
agentId: string,
155+
env: NodeJS.ProcessEnv = process.env,
156+
) {
157+
const id = normalizeAgentId(agentId);
158+
const configured = resolveAgentConfig(cfg, id)?.agentDir?.trim();
159+
if (configured) {
160+
return resolveUserPath(configured, env);
161+
}
162+
const root = resolveStateDir(env);
163+
return path.join(root, "agents", id, "agent");
164+
}

src/agents/agent-scope.ts

Lines changed: 18 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import fs from "node:fs";
22
import path from "node:path";
33
import { resolveAgentModelFallbackValues } from "../config/model-input.js";
4-
import { resolveStateDir } from "../config/paths.js";
54
import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js";
65
import type { OpenClawConfig } from "../config/types.js";
7-
import { createSubsystemLogger } from "../logging/subsystem.js";
86
import {
9-
DEFAULT_AGENT_ID,
107
normalizeAgentId,
118
parseAgentSessionKey,
129
resolveAgentIdFromSessionKey,
@@ -15,19 +12,28 @@ import {
1512
lowercasePreservingWhitespace,
1613
normalizeLowercaseStringOrEmpty,
1714
normalizeOptionalString,
18-
readStringValue,
1915
resolvePrimaryStringValue,
2016
} from "../shared/string-coerce.js";
2117
import { resolveUserPath } from "../utils.js";
18+
import {
19+
listAgentEntries,
20+
listAgentIds,
21+
resolveAgentConfig,
22+
resolveAgentDir,
23+
resolveAgentWorkspaceDir,
24+
resolveDefaultAgentId,
25+
type ResolvedAgentConfig,
26+
} from "./agent-scope-config.js";
2227
import { resolveEffectiveAgentSkillFilter } from "./skills/agent-filter.js";
23-
import { resolveDefaultAgentWorkspaceDir } from "./workspace.js";
24-
25-
let log: ReturnType<typeof createSubsystemLogger> | null = null;
26-
27-
function getLog(): ReturnType<typeof createSubsystemLogger> {
28-
log ??= createSubsystemLogger("agent-scope");
29-
return log;
30-
}
28+
export {
29+
listAgentEntries,
30+
listAgentIds,
31+
resolveAgentConfig,
32+
resolveAgentDir,
33+
resolveAgentWorkspaceDir,
34+
resolveDefaultAgentId,
35+
type ResolvedAgentConfig,
36+
} from "./agent-scope-config.js";
3137

3238
/** Strip null bytes from paths to prevent ENOTDIR errors. */
3339
function stripNullBytes(s: string): string {
@@ -37,72 +43,6 @@ function stripNullBytes(s: string): string {
3743

3844
export { resolveAgentIdFromSessionKey };
3945

40-
type AgentEntry = NonNullable<NonNullable<OpenClawConfig["agents"]>["list"]>[number];
41-
42-
type ResolvedAgentConfig = {
43-
name?: string;
44-
workspace?: string;
45-
agentDir?: string;
46-
systemPromptOverride?: AgentEntry["systemPromptOverride"];
47-
model?: AgentEntry["model"];
48-
thinkingDefault?: AgentEntry["thinkingDefault"];
49-
verboseDefault?: AgentDefaultsConfig["verboseDefault"];
50-
reasoningDefault?: AgentEntry["reasoningDefault"];
51-
fastModeDefault?: AgentEntry["fastModeDefault"];
52-
skills?: AgentEntry["skills"];
53-
memorySearch?: AgentEntry["memorySearch"];
54-
humanDelay?: AgentEntry["humanDelay"];
55-
heartbeat?: AgentEntry["heartbeat"];
56-
identity?: AgentEntry["identity"];
57-
groupChat?: AgentEntry["groupChat"];
58-
subagents?: AgentEntry["subagents"];
59-
embeddedPi?: AgentEntry["embeddedPi"];
60-
sandbox?: AgentEntry["sandbox"];
61-
tools?: AgentEntry["tools"];
62-
};
63-
64-
let defaultAgentWarned = false;
65-
66-
export function listAgentEntries(cfg: OpenClawConfig): AgentEntry[] {
67-
const list = cfg.agents?.list;
68-
if (!Array.isArray(list)) {
69-
return [];
70-
}
71-
return list.filter((entry): entry is AgentEntry => entry !== null && typeof entry === "object");
72-
}
73-
74-
export function listAgentIds(cfg: OpenClawConfig): string[] {
75-
const agents = listAgentEntries(cfg);
76-
if (agents.length === 0) {
77-
return [DEFAULT_AGENT_ID];
78-
}
79-
const seen = new Set<string>();
80-
const ids: string[] = [];
81-
for (const entry of agents) {
82-
const id = normalizeAgentId(entry?.id);
83-
if (seen.has(id)) {
84-
continue;
85-
}
86-
seen.add(id);
87-
ids.push(id);
88-
}
89-
return ids.length > 0 ? ids : [DEFAULT_AGENT_ID];
90-
}
91-
92-
export function resolveDefaultAgentId(cfg: OpenClawConfig): string {
93-
const agents = listAgentEntries(cfg);
94-
if (agents.length === 0) {
95-
return DEFAULT_AGENT_ID;
96-
}
97-
const defaults = agents.filter((agent) => agent?.default);
98-
if (defaults.length > 1 && !defaultAgentWarned) {
99-
defaultAgentWarned = true;
100-
getLog().warn("Multiple agents marked default=true; using the first entry as default.");
101-
}
102-
const chosen = (defaults[0] ?? agents[0])?.id?.trim();
103-
return normalizeAgentId(chosen || DEFAULT_AGENT_ID);
104-
}
105-
10646
export function resolveSessionAgentIds(params: {
10747
sessionKey?: string;
10848
config?: OpenClawConfig;
@@ -129,48 +69,6 @@ export function resolveSessionAgentId(params: {
12969
return resolveSessionAgentIds(params).sessionAgentId;
13070
}
13171

132-
function resolveAgentEntry(cfg: OpenClawConfig, agentId: string): AgentEntry | undefined {
133-
const id = normalizeAgentId(agentId);
134-
return listAgentEntries(cfg).find((entry) => normalizeAgentId(entry.id) === id);
135-
}
136-
137-
export function resolveAgentConfig(
138-
cfg: OpenClawConfig,
139-
agentId: string,
140-
): ResolvedAgentConfig | undefined {
141-
const id = normalizeAgentId(agentId);
142-
const entry = resolveAgentEntry(cfg, id);
143-
if (!entry) {
144-
return undefined;
145-
}
146-
const agentDefaults = cfg.agents?.defaults;
147-
return {
148-
name: readStringValue(entry.name),
149-
workspace: readStringValue(entry.workspace),
150-
agentDir: readStringValue(entry.agentDir),
151-
systemPromptOverride: readStringValue(entry.systemPromptOverride),
152-
model:
153-
typeof entry.model === "string" || (entry.model && typeof entry.model === "object")
154-
? entry.model
155-
: undefined,
156-
thinkingDefault: entry.thinkingDefault,
157-
verboseDefault: entry.verboseDefault ?? agentDefaults?.verboseDefault,
158-
reasoningDefault: entry.reasoningDefault,
159-
fastModeDefault: entry.fastModeDefault,
160-
skills: Array.isArray(entry.skills) ? entry.skills : undefined,
161-
memorySearch: entry.memorySearch,
162-
humanDelay: entry.humanDelay,
163-
heartbeat: entry.heartbeat,
164-
identity: entry.identity,
165-
groupChat: entry.groupChat,
166-
subagents: typeof entry.subagents === "object" && entry.subagents ? entry.subagents : undefined,
167-
embeddedPi:
168-
typeof entry.embeddedPi === "object" && entry.embeddedPi ? entry.embeddedPi : undefined,
169-
sandbox: entry.sandbox,
170-
tools: entry.tools,
171-
};
172-
}
173-
17472
export function resolveAgentExecutionContract(
17573
cfg: OpenClawConfig | undefined,
17674
agentId?: string | null,
@@ -276,29 +174,6 @@ export function resolveEffectiveModelFallbacks(params: {
276174
return agentFallbacksOverride ?? defaultFallbacks;
277175
}
278176

279-
export function resolveAgentWorkspaceDir(cfg: OpenClawConfig, agentId: string) {
280-
const id = normalizeAgentId(agentId);
281-
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
282-
if (configured) {
283-
return stripNullBytes(resolveUserPath(configured));
284-
}
285-
const defaultAgentId = resolveDefaultAgentId(cfg);
286-
const fallback = cfg.agents?.defaults?.workspace?.trim();
287-
if (id === defaultAgentId) {
288-
if (fallback) {
289-
return stripNullBytes(resolveUserPath(fallback));
290-
}
291-
return stripNullBytes(resolveDefaultAgentWorkspaceDir(process.env));
292-
}
293-
// Non-default agents: use the configured default workspace as a base so that
294-
// agents.defaults.workspace is respected for all agents, not just the default.
295-
if (fallback) {
296-
return stripNullBytes(path.join(resolveUserPath(fallback), id));
297-
}
298-
const stateDir = resolveStateDir(process.env);
299-
return stripNullBytes(path.join(stateDir, `workspace-${id}`));
300-
}
301-
302177
function normalizePathForComparison(input: string): string {
303178
const resolved = path.resolve(stripNullBytes(resolveUserPath(input)));
304179
let normalized = resolved;
@@ -354,17 +229,3 @@ export function resolveAgentIdByWorkspacePath(
354229
): string | undefined {
355230
return resolveAgentIdsByWorkspacePath(cfg, workspacePath)[0];
356231
}
357-
358-
export function resolveAgentDir(
359-
cfg: OpenClawConfig,
360-
agentId: string,
361-
env: NodeJS.ProcessEnv = process.env,
362-
) {
363-
const id = normalizeAgentId(agentId);
364-
const configured = resolveAgentConfig(cfg, id)?.agentDir?.trim();
365-
if (configured) {
366-
return resolveUserPath(configured, env);
367-
}
368-
const root = resolveStateDir(env);
369-
return path.join(root, "agents", id, "agent");
370-
}

src/cron/isolated-agent/run.runtime.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ export {
33
resolveAgentDir,
44
resolveAgentWorkspaceDir,
55
resolveDefaultAgentId,
6-
resolveAgentSkillsFilter,
7-
} from "../../agents/agent-scope.js";
6+
type ResolvedAgentConfig,
7+
} from "../../agents/agent-scope-config.js";
8+
export { resolveAgentSkillsFilter } from "../../agents/agent-scope.js";
89
export { resolveCronStyleNow } from "../../agents/current-time.js";
910
export { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
1011
export { isCliProvider } from "../../agents/model-selection-cli.js";

0 commit comments

Comments
 (0)