diff --git a/src/agents/agent-scope-config.ts b/src/agents/agent-scope-config.ts new file mode 100644 index 00000000000..6c1df3d33a2 --- /dev/null +++ b/src/agents/agent-scope-config.ts @@ -0,0 +1,164 @@ +import path from "node:path"; +import { resolveStateDir } from "../config/paths.js"; +import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js"; +import type { OpenClawConfig } from "../config/types.js"; +import { createSubsystemLogger } from "../logging/subsystem.js"; +import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js"; +import { readStringValue } from "../shared/string-coerce.js"; +import { resolveUserPath } from "../utils.js"; +import { resolveDefaultAgentWorkspaceDir } from "./workspace.js"; + +type AgentEntry = NonNullable["list"]>[number]; + +export type ResolvedAgentConfig = { + name?: string; + workspace?: string; + agentDir?: string; + systemPromptOverride?: AgentEntry["systemPromptOverride"]; + model?: AgentEntry["model"]; + thinkingDefault?: AgentEntry["thinkingDefault"]; + verboseDefault?: AgentDefaultsConfig["verboseDefault"]; + reasoningDefault?: AgentEntry["reasoningDefault"]; + fastModeDefault?: AgentEntry["fastModeDefault"]; + skills?: AgentEntry["skills"]; + memorySearch?: AgentEntry["memorySearch"]; + humanDelay?: AgentEntry["humanDelay"]; + heartbeat?: AgentEntry["heartbeat"]; + identity?: AgentEntry["identity"]; + groupChat?: AgentEntry["groupChat"]; + subagents?: AgentEntry["subagents"]; + embeddedPi?: AgentEntry["embeddedPi"]; + sandbox?: AgentEntry["sandbox"]; + tools?: AgentEntry["tools"]; +}; + +let log: ReturnType | null = null; +let defaultAgentWarned = false; + +function getLog(): ReturnType { + log ??= createSubsystemLogger("agent-scope"); + return log; +} + +/** Strip null bytes from paths to prevent ENOTDIR errors. */ +function stripNullBytes(s: string): string { + // eslint-disable-next-line no-control-regex + return s.replace(/\0/g, ""); +} + +export function listAgentEntries(cfg: OpenClawConfig): AgentEntry[] { + const list = cfg.agents?.list; + if (!Array.isArray(list)) { + return []; + } + return list.filter((entry): entry is AgentEntry => entry !== null && typeof entry === "object"); +} + +export function listAgentIds(cfg: OpenClawConfig): string[] { + const agents = listAgentEntries(cfg); + if (agents.length === 0) { + return [DEFAULT_AGENT_ID]; + } + const seen = new Set(); + const ids: string[] = []; + for (const entry of agents) { + const id = normalizeAgentId(entry?.id); + if (seen.has(id)) { + continue; + } + seen.add(id); + ids.push(id); + } + return ids.length > 0 ? ids : [DEFAULT_AGENT_ID]; +} + +export function resolveDefaultAgentId(cfg: OpenClawConfig): string { + const agents = listAgentEntries(cfg); + if (agents.length === 0) { + return DEFAULT_AGENT_ID; + } + const defaults = agents.filter((agent) => agent?.default); + if (defaults.length > 1 && !defaultAgentWarned) { + defaultAgentWarned = true; + getLog().warn("Multiple agents marked default=true; using the first entry as default."); + } + const chosen = (defaults[0] ?? agents[0])?.id?.trim(); + return normalizeAgentId(chosen || DEFAULT_AGENT_ID); +} + +function resolveAgentEntry(cfg: OpenClawConfig, agentId: string): AgentEntry | undefined { + const id = normalizeAgentId(agentId); + return listAgentEntries(cfg).find((entry) => normalizeAgentId(entry.id) === id); +} + +export function resolveAgentConfig( + cfg: OpenClawConfig, + agentId: string, +): ResolvedAgentConfig | undefined { + const id = normalizeAgentId(agentId); + const entry = resolveAgentEntry(cfg, id); + if (!entry) { + return undefined; + } + const agentDefaults = cfg.agents?.defaults; + return { + name: readStringValue(entry.name), + workspace: readStringValue(entry.workspace), + agentDir: readStringValue(entry.agentDir), + systemPromptOverride: readStringValue(entry.systemPromptOverride), + model: + typeof entry.model === "string" || (entry.model && typeof entry.model === "object") + ? entry.model + : undefined, + thinkingDefault: entry.thinkingDefault, + verboseDefault: entry.verboseDefault ?? agentDefaults?.verboseDefault, + reasoningDefault: entry.reasoningDefault, + fastModeDefault: entry.fastModeDefault, + skills: Array.isArray(entry.skills) ? entry.skills : undefined, + memorySearch: entry.memorySearch, + humanDelay: entry.humanDelay, + heartbeat: entry.heartbeat, + identity: entry.identity, + groupChat: entry.groupChat, + subagents: typeof entry.subagents === "object" && entry.subagents ? entry.subagents : undefined, + embeddedPi: + typeof entry.embeddedPi === "object" && entry.embeddedPi ? entry.embeddedPi : undefined, + sandbox: entry.sandbox, + tools: entry.tools, + }; +} + +export function resolveAgentWorkspaceDir(cfg: OpenClawConfig, agentId: string) { + const id = normalizeAgentId(agentId); + const configured = resolveAgentConfig(cfg, id)?.workspace?.trim(); + if (configured) { + return stripNullBytes(resolveUserPath(configured)); + } + const defaultAgentId = resolveDefaultAgentId(cfg); + const fallback = cfg.agents?.defaults?.workspace?.trim(); + if (id === defaultAgentId) { + if (fallback) { + return stripNullBytes(resolveUserPath(fallback)); + } + return stripNullBytes(resolveDefaultAgentWorkspaceDir(process.env)); + } + if (fallback) { + return stripNullBytes(path.join(resolveUserPath(fallback), id)); + } + const stateDir = resolveStateDir(process.env); + return stripNullBytes(path.join(stateDir, `workspace-${id}`)); +} + +export function resolveAgentDir( + cfg: OpenClawConfig, + agentId: string, + env: NodeJS.ProcessEnv = process.env, +) { + const id = normalizeAgentId(agentId); + const configured = resolveAgentConfig(cfg, id)?.agentDir?.trim(); + if (configured) { + return resolveUserPath(configured, env); + } + const root = resolveStateDir(env); + return path.join(root, "agents", id, "agent"); +} diff --git a/src/agents/agent-scope.ts b/src/agents/agent-scope.ts index 6092948e5f2..de421fd9f51 100644 --- a/src/agents/agent-scope.ts +++ b/src/agents/agent-scope.ts @@ -1,12 +1,9 @@ import fs from "node:fs"; import path from "node:path"; import { resolveAgentModelFallbackValues } from "../config/model-input.js"; -import { resolveStateDir } from "../config/paths.js"; import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js"; import type { OpenClawConfig } from "../config/types.js"; -import { createSubsystemLogger } from "../logging/subsystem.js"; import { - DEFAULT_AGENT_ID, normalizeAgentId, parseAgentSessionKey, resolveAgentIdFromSessionKey, @@ -15,19 +12,28 @@ import { lowercasePreservingWhitespace, normalizeLowercaseStringOrEmpty, normalizeOptionalString, - readStringValue, resolvePrimaryStringValue, } from "../shared/string-coerce.js"; import { resolveUserPath } from "../utils.js"; +import { + listAgentEntries, + listAgentIds, + resolveAgentConfig, + resolveAgentDir, + resolveAgentWorkspaceDir, + resolveDefaultAgentId, + type ResolvedAgentConfig, +} from "./agent-scope-config.js"; import { resolveEffectiveAgentSkillFilter } from "./skills/agent-filter.js"; -import { resolveDefaultAgentWorkspaceDir } from "./workspace.js"; - -let log: ReturnType | null = null; - -function getLog(): ReturnType { - log ??= createSubsystemLogger("agent-scope"); - return log; -} +export { + listAgentEntries, + listAgentIds, + resolveAgentConfig, + resolveAgentDir, + resolveAgentWorkspaceDir, + resolveDefaultAgentId, + type ResolvedAgentConfig, +} from "./agent-scope-config.js"; /** Strip null bytes from paths to prevent ENOTDIR errors. */ function stripNullBytes(s: string): string { @@ -37,72 +43,6 @@ function stripNullBytes(s: string): string { export { resolveAgentIdFromSessionKey }; -type AgentEntry = NonNullable["list"]>[number]; - -type ResolvedAgentConfig = { - name?: string; - workspace?: string; - agentDir?: string; - systemPromptOverride?: AgentEntry["systemPromptOverride"]; - model?: AgentEntry["model"]; - thinkingDefault?: AgentEntry["thinkingDefault"]; - verboseDefault?: AgentDefaultsConfig["verboseDefault"]; - reasoningDefault?: AgentEntry["reasoningDefault"]; - fastModeDefault?: AgentEntry["fastModeDefault"]; - skills?: AgentEntry["skills"]; - memorySearch?: AgentEntry["memorySearch"]; - humanDelay?: AgentEntry["humanDelay"]; - heartbeat?: AgentEntry["heartbeat"]; - identity?: AgentEntry["identity"]; - groupChat?: AgentEntry["groupChat"]; - subagents?: AgentEntry["subagents"]; - embeddedPi?: AgentEntry["embeddedPi"]; - sandbox?: AgentEntry["sandbox"]; - tools?: AgentEntry["tools"]; -}; - -let defaultAgentWarned = false; - -export function listAgentEntries(cfg: OpenClawConfig): AgentEntry[] { - const list = cfg.agents?.list; - if (!Array.isArray(list)) { - return []; - } - return list.filter((entry): entry is AgentEntry => entry !== null && typeof entry === "object"); -} - -export function listAgentIds(cfg: OpenClawConfig): string[] { - const agents = listAgentEntries(cfg); - if (agents.length === 0) { - return [DEFAULT_AGENT_ID]; - } - const seen = new Set(); - const ids: string[] = []; - for (const entry of agents) { - const id = normalizeAgentId(entry?.id); - if (seen.has(id)) { - continue; - } - seen.add(id); - ids.push(id); - } - return ids.length > 0 ? ids : [DEFAULT_AGENT_ID]; -} - -export function resolveDefaultAgentId(cfg: OpenClawConfig): string { - const agents = listAgentEntries(cfg); - if (agents.length === 0) { - return DEFAULT_AGENT_ID; - } - const defaults = agents.filter((agent) => agent?.default); - if (defaults.length > 1 && !defaultAgentWarned) { - defaultAgentWarned = true; - getLog().warn("Multiple agents marked default=true; using the first entry as default."); - } - const chosen = (defaults[0] ?? agents[0])?.id?.trim(); - return normalizeAgentId(chosen || DEFAULT_AGENT_ID); -} - export function resolveSessionAgentIds(params: { sessionKey?: string; config?: OpenClawConfig; @@ -129,48 +69,6 @@ export function resolveSessionAgentId(params: { return resolveSessionAgentIds(params).sessionAgentId; } -function resolveAgentEntry(cfg: OpenClawConfig, agentId: string): AgentEntry | undefined { - const id = normalizeAgentId(agentId); - return listAgentEntries(cfg).find((entry) => normalizeAgentId(entry.id) === id); -} - -export function resolveAgentConfig( - cfg: OpenClawConfig, - agentId: string, -): ResolvedAgentConfig | undefined { - const id = normalizeAgentId(agentId); - const entry = resolveAgentEntry(cfg, id); - if (!entry) { - return undefined; - } - const agentDefaults = cfg.agents?.defaults; - return { - name: readStringValue(entry.name), - workspace: readStringValue(entry.workspace), - agentDir: readStringValue(entry.agentDir), - systemPromptOverride: readStringValue(entry.systemPromptOverride), - model: - typeof entry.model === "string" || (entry.model && typeof entry.model === "object") - ? entry.model - : undefined, - thinkingDefault: entry.thinkingDefault, - verboseDefault: entry.verboseDefault ?? agentDefaults?.verboseDefault, - reasoningDefault: entry.reasoningDefault, - fastModeDefault: entry.fastModeDefault, - skills: Array.isArray(entry.skills) ? entry.skills : undefined, - memorySearch: entry.memorySearch, - humanDelay: entry.humanDelay, - heartbeat: entry.heartbeat, - identity: entry.identity, - groupChat: entry.groupChat, - subagents: typeof entry.subagents === "object" && entry.subagents ? entry.subagents : undefined, - embeddedPi: - typeof entry.embeddedPi === "object" && entry.embeddedPi ? entry.embeddedPi : undefined, - sandbox: entry.sandbox, - tools: entry.tools, - }; -} - export function resolveAgentExecutionContract( cfg: OpenClawConfig | undefined, agentId?: string | null, @@ -276,29 +174,6 @@ export function resolveEffectiveModelFallbacks(params: { return agentFallbacksOverride ?? defaultFallbacks; } -export function resolveAgentWorkspaceDir(cfg: OpenClawConfig, agentId: string) { - const id = normalizeAgentId(agentId); - const configured = resolveAgentConfig(cfg, id)?.workspace?.trim(); - if (configured) { - return stripNullBytes(resolveUserPath(configured)); - } - const defaultAgentId = resolveDefaultAgentId(cfg); - const fallback = cfg.agents?.defaults?.workspace?.trim(); - if (id === defaultAgentId) { - if (fallback) { - return stripNullBytes(resolveUserPath(fallback)); - } - return stripNullBytes(resolveDefaultAgentWorkspaceDir(process.env)); - } - // Non-default agents: use the configured default workspace as a base so that - // agents.defaults.workspace is respected for all agents, not just the default. - if (fallback) { - return stripNullBytes(path.join(resolveUserPath(fallback), id)); - } - const stateDir = resolveStateDir(process.env); - return stripNullBytes(path.join(stateDir, `workspace-${id}`)); -} - function normalizePathForComparison(input: string): string { const resolved = path.resolve(stripNullBytes(resolveUserPath(input))); let normalized = resolved; @@ -354,17 +229,3 @@ export function resolveAgentIdByWorkspacePath( ): string | undefined { return resolveAgentIdsByWorkspacePath(cfg, workspacePath)[0]; } - -export function resolveAgentDir( - cfg: OpenClawConfig, - agentId: string, - env: NodeJS.ProcessEnv = process.env, -) { - const id = normalizeAgentId(agentId); - const configured = resolveAgentConfig(cfg, id)?.agentDir?.trim(); - if (configured) { - return resolveUserPath(configured, env); - } - const root = resolveStateDir(env); - return path.join(root, "agents", id, "agent"); -} diff --git a/src/cron/isolated-agent/run.runtime.ts b/src/cron/isolated-agent/run.runtime.ts index fff003c988c..a9f9b6e38a9 100644 --- a/src/cron/isolated-agent/run.runtime.ts +++ b/src/cron/isolated-agent/run.runtime.ts @@ -3,8 +3,9 @@ export { resolveAgentDir, resolveAgentWorkspaceDir, resolveDefaultAgentId, - resolveAgentSkillsFilter, -} from "../../agents/agent-scope.js"; + type ResolvedAgentConfig, +} from "../../agents/agent-scope-config.js"; +export { resolveAgentSkillsFilter } from "../../agents/agent-scope.js"; export { resolveCronStyleNow } from "../../agents/current-time.js"; export { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js"; export { isCliProvider } from "../../agents/model-selection-cli.js";