import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js"; import type { OpenClawConfig } from "../config/config.js"; import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; import { isSubagentSessionKey, parseAgentSessionKey } from "../routing/session-key.js"; import { getSubagentDepthFromSessionStore } from "./subagent-depth.js"; export const SUBAGENT_SESSION_ROLES = ["main", "orchestrator", "leaf"] as const; export type SubagentSessionRole = (typeof SUBAGENT_SESSION_ROLES)[number]; export const SUBAGENT_CONTROL_SCOPES = ["children", "none"] as const; export type SubagentControlScope = (typeof SUBAGENT_CONTROL_SCOPES)[number]; type SessionCapabilityEntry = { sessionId?: unknown; spawnDepth?: unknown; subagentRole?: unknown; subagentControlScope?: unknown; }; function normalizeSessionKey(value: unknown): string | undefined { if (typeof value !== "string") { return undefined; } const trimmed = value.trim(); return trimmed || undefined; } function normalizeSubagentRole(value: unknown): SubagentSessionRole | undefined { if (typeof value !== "string") { return undefined; } const trimmed = value.trim().toLowerCase(); return SUBAGENT_SESSION_ROLES.find((entry) => entry === trimmed); } function normalizeSubagentControlScope(value: unknown): SubagentControlScope | undefined { if (typeof value !== "string") { return undefined; } const trimmed = value.trim().toLowerCase(); return SUBAGENT_CONTROL_SCOPES.find((entry) => entry === trimmed); } function readSessionStore(storePath: string): Record { try { return loadSessionStore(storePath); } catch { return {}; } } function findEntryBySessionId( store: Record, sessionId: string, ): SessionCapabilityEntry | undefined { const normalizedSessionId = normalizeSessionKey(sessionId); if (!normalizedSessionId) { return undefined; } for (const entry of Object.values(store)) { const candidateSessionId = normalizeSessionKey(entry?.sessionId); if (candidateSessionId === normalizedSessionId) { return entry; } } return undefined; } function resolveSessionCapabilityEntry(params: { sessionKey: string; cfg?: OpenClawConfig; store?: Record; }): SessionCapabilityEntry | undefined { if (params.store) { return params.store[params.sessionKey] ?? findEntryBySessionId(params.store, params.sessionKey); } if (!params.cfg) { return undefined; } const parsed = parseAgentSessionKey(params.sessionKey); if (!parsed?.agentId) { return undefined; } const storePath = resolveStorePath(params.cfg.session?.store, { agentId: parsed.agentId }); const store = readSessionStore(storePath); return store[params.sessionKey] ?? findEntryBySessionId(store, params.sessionKey); } export function resolveSubagentRoleForDepth(params: { depth: number; maxSpawnDepth?: number; }): SubagentSessionRole { const depth = Number.isInteger(params.depth) ? Math.max(0, params.depth) : 0; const maxSpawnDepth = typeof params.maxSpawnDepth === "number" && Number.isFinite(params.maxSpawnDepth) ? Math.max(1, Math.floor(params.maxSpawnDepth)) : DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH; if (depth <= 0) { return "main"; } return depth < maxSpawnDepth ? "orchestrator" : "leaf"; } export function resolveSubagentControlScopeForRole( role: SubagentSessionRole, ): SubagentControlScope { return role === "leaf" ? "none" : "children"; } export function resolveSubagentCapabilities(params: { depth: number; maxSpawnDepth?: number }) { const role = resolveSubagentRoleForDepth(params); const controlScope = resolveSubagentControlScopeForRole(role); return { depth: Math.max(0, Math.floor(params.depth)), role, controlScope, canSpawn: role === "main" || role === "orchestrator", canControlChildren: controlScope === "children", }; } export function resolveStoredSubagentCapabilities( sessionKey: string | undefined | null, opts?: { cfg?: OpenClawConfig; store?: Record; }, ) { const normalizedSessionKey = normalizeSessionKey(sessionKey); const maxSpawnDepth = opts?.cfg?.agents?.defaults?.subagents?.maxSpawnDepth ?? DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH; const depth = getSubagentDepthFromSessionStore(normalizedSessionKey, { cfg: opts?.cfg, store: opts?.store, }); if (!normalizedSessionKey || !isSubagentSessionKey(normalizedSessionKey)) { return resolveSubagentCapabilities({ depth, maxSpawnDepth }); } const entry = resolveSessionCapabilityEntry({ sessionKey: normalizedSessionKey, cfg: opts?.cfg, store: opts?.store, }); const storedRole = normalizeSubagentRole(entry?.subagentRole); const storedControlScope = normalizeSubagentControlScope(entry?.subagentControlScope); const fallback = resolveSubagentCapabilities({ depth, maxSpawnDepth }); const role = storedRole ?? fallback.role; const controlScope = storedControlScope ?? resolveSubagentControlScopeForRole(role); return { depth, role, controlScope, canSpawn: role === "main" || role === "orchestrator", canControlChildren: controlScope === "children", }; }