diff --git a/src/agents/subagent-spawn-child-plan.ts b/src/agents/subagent-spawn-child-plan.ts index ea895c4fe5b1..42c8f2c5c529 100644 --- a/src/agents/subagent-spawn-child-plan.ts +++ b/src/agents/subagent-spawn-child-plan.ts @@ -3,6 +3,10 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { isIncognitoSessionKey } from "../routing/session-key.js"; import { resolveUserPath } from "../utils.js"; import { resolveAgentDir } from "./agent-scope-config.js"; +import { findModelCatalogEntry } from "./model-catalog-lookup.js"; +import { resolveDefaultModelForAgent } from "./model-selection.js"; +import { supportsModelTools } from "./model-tool-support.js"; +import { summarizeSpawnError } from "./spawn-pipeline.js"; import { resolveSpawnSandboxError, mintSpawnSessionKey } from "./spawn-plan.js"; import { resolveRequesterOriginForChild } from "./spawn-requester-origin.js"; import { @@ -15,21 +19,68 @@ import type { SpawnSubagentParams, SpawnSubagentResult, } from "./subagent-spawn-contract.js"; -import { - buildResolvedSubagentModelMetadata, - resolveCollectorOutputModelError, -} from "./subagent-spawn-model.js"; +import { getSubagentSpawnDeps } from "./subagent-spawn-deps.js"; import { resolveSubagentModelAndThinkingPlan, splitModelRef } from "./subagent-spawn-plan.js"; import { readRequesterFastMode, readRequesterThinkingLevel, } from "./subagent-spawn-requester-prefs.js"; import { + loadPreparedModelCatalog, normalizeDeliveryContext, resolveAgentConfig, resolveSandboxRuntimeStatus, } from "./subagent-spawn.runtime.js"; +function buildResolvedSubagentModelMetadata(resolvedModel?: string): { + resolvedModel?: string; + resolvedProvider?: string; +} { + const modelRef = resolvedModel?.trim(); + if (!modelRef) { + return {}; + } + const { provider } = splitModelRef(modelRef); + return { + resolvedModel: modelRef, + ...(provider ? { resolvedProvider: provider } : {}), + }; +} + +async function resolveCollectorOutputModelError(params: { + cfg: OpenClawConfig; + targetAgentId: string; + targetAgentDir: string; + workspaceDir?: string; + resolvedModel?: string; +}): Promise { + const selected = splitModelRef(params.resolvedModel); + const fallback = resolveDefaultModelForAgent({ + cfg: params.cfg, + agentId: params.targetAgentId, + }); + const provider = selected.provider ?? fallback.provider; + const model = selected.model ?? fallback.model; + if (!provider || !model) { + return undefined; + } + let catalog: Awaited>; + try { + catalog = await getSubagentSpawnDeps().loadPreparedModelCatalog({ + config: params.cfg, + agentDir: params.targetAgentDir, + workspaceDir: params.workspaceDir, + }); + } catch (error) { + return `sessions_spawn could not verify outputSchema model capabilities: ${summarizeSpawnError(error)}`; + } + const entry = findModelCatalogEntry(catalog, { provider, modelId: model }); + if (!entry || supportsModelTools(entry)) { + return undefined; + } + return `sessions_spawn outputSchema requires a tool-capable target model; "${provider}/${model}" declares compat.supportsTools=false.`; +} + type ResolvedSubagentChildPlan = { spawnedCwd?: string; toolSpawnMetadata: ReturnType; diff --git a/src/agents/subagent-spawn-model.ts b/src/agents/subagent-spawn-model.ts deleted file mode 100644 index 0ca406c7ceb4..000000000000 --- a/src/agents/subagent-spawn-model.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { findModelCatalogEntry } from "./model-catalog-lookup.js"; -import { resolveDefaultModelForAgent } from "./model-selection.js"; -import { supportsModelTools } from "./model-tool-support.js"; -import { summarizeSpawnError } from "./spawn-pipeline.js"; -import { getSubagentSpawnDeps } from "./subagent-spawn-deps.js"; -import { splitModelRef } from "./subagent-spawn-plan.js"; -import { loadPreparedModelCatalog } from "./subagent-spawn.runtime.js"; - -export function buildResolvedSubagentModelMetadata(resolvedModel?: string): { - resolvedModel?: string; - resolvedProvider?: string; -} { - const modelRef = resolvedModel?.trim(); - if (!modelRef) { - return {}; - } - const { provider } = splitModelRef(modelRef); - return { - resolvedModel: modelRef, - ...(provider ? { resolvedProvider: provider } : {}), - }; -} - -export async function resolveCollectorOutputModelError(params: { - cfg: OpenClawConfig; - targetAgentId: string; - targetAgentDir: string; - workspaceDir?: string; - resolvedModel?: string; -}): Promise { - const selected = splitModelRef(params.resolvedModel); - const fallback = resolveDefaultModelForAgent({ - cfg: params.cfg, - agentId: params.targetAgentId, - }); - const provider = selected.provider ?? fallback.provider; - const model = selected.model ?? fallback.model; - if (!provider || !model) { - return undefined; - } - let catalog: Awaited>; - try { - catalog = await getSubagentSpawnDeps().loadPreparedModelCatalog({ - config: params.cfg, - agentDir: params.targetAgentDir, - workspaceDir: params.workspaceDir, - }); - } catch (error) { - return `sessions_spawn could not verify outputSchema model capabilities: ${summarizeSpawnError(error)}`; - } - const entry = findModelCatalogEntry(catalog, { provider, modelId: model }); - if (!entry || supportsModelTools(entry)) { - return undefined; - } - return `sessions_spawn outputSchema requires a tool-capable target model; "${provider}/${model}" declares compat.supportsTools=false.`; -} diff --git a/src/agents/subagent-spawn-request.ts b/src/agents/subagent-spawn-request.ts index 0bbdaf59e1c1..bdbd216c1216 100644 --- a/src/agents/subagent-spawn-request.ts +++ b/src/agents/subagent-spawn-request.ts @@ -3,6 +3,7 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js"; import { isValidAgentId, normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js"; +import { listAgentIds } from "./agent-scope-config.js"; import { resolveSpawnAdmission, resolveSpawnMode } from "./spawn-plan.js"; import { listSwarmRunsForGroup } from "./subagent-registry.js"; import { resolveSubagentContextMode } from "./subagent-spawn-context.js"; @@ -15,13 +16,16 @@ import { getSubagentSpawnDeps } from "./subagent-spawn-deps.js"; import { resolveSubagentSpawnOwnership } from "./subagent-spawn-ownership.js"; import { resolveConfiguredSubagentRunTimeoutSeconds } from "./subagent-spawn-plan.js"; import { loadSubagentConfig } from "./subagent-spawn-session-patch.js"; -import { resolveConfiguredAgentIds } from "./subagent-spawn-validation.js"; import { resolveInternalSessionKey, resolveMainSessionAlias } from "./subagent-spawn.runtime.js"; import { normalizeSubagentTaskName } from "./subagent-task-name.js"; import { resolveSwarmConfig } from "./swarm-config.js"; import { validateStructuredOutputSchema } from "./swarm-output-schema.js"; import { reserveSwarmRun } from "./swarm-scheduler.js"; +function resolveConfiguredAgentIds(cfg: OpenClawConfig): string[] { + return listAgentIds(cfg); +} + type ResolvedSubagentSpawnRequest = { request: { taskName?: string; diff --git a/src/agents/subagent-spawn-validation.ts b/src/agents/subagent-spawn-validation.ts deleted file mode 100644 index 2684ec6a8a08..000000000000 --- a/src/agents/subagent-spawn-validation.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; -import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { listAgentIds } from "./agent-scope-config.js"; - -export function resolveConfiguredAgentIds(cfg: OpenClawConfig): string[] { - return listAgentIds(cfg); -} - -export function sanitizeMountPathHint(value?: string): string | undefined { - const trimmed = normalizeOptionalString(value); - if (!trimmed) { - return undefined; - } - if (hasPromptUnsafeControlCharacter(trimmed)) { - return undefined; - } - if (!/^[A-Za-z0-9._\-/:]+$/.test(trimmed)) { - return undefined; - } - return trimmed; -} - -function hasPromptUnsafeControlCharacter(value: string): boolean { - for (const char of value) { - const code = char.charCodeAt(0); - if (code <= 0x1f || code === 0x7f || code === 0x85 || code === 0x2028 || code === 0x2029) { - return true; - } - } - return false; -} diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index fdc11529de41..65a3acc3232a 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -4,6 +4,7 @@ * Validates spawn requests, prepares child sessions, stages attachments, binds delivery context, and registers runs. */ import { promises as fs } from "node:fs"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { isAcpRuntimeSpawnAvailable } from "../acp/runtime/availability.js"; import type { SubagentSpawnPreparation } from "../context-engine/types.js"; import { isFastTestRuntimeEnv } from "../infra/env.js"; @@ -57,7 +58,6 @@ import { bindThreadForSubagentSpawn, hasRoutableDeliveryOrigin, } from "./subagent-spawn-thread-binding.js"; -import { sanitizeMountPathHint } from "./subagent-spawn-validation.js"; import { buildSubagentSystemPrompt, emitSessionLifecycleEvent, @@ -69,6 +69,30 @@ export { SUBAGENT_SPAWN_CONTEXT_MODES, SUBAGENT_SPAWN_MODES } from "./subagent-s const SUBAGENT_CONTROL_GATEWAY_TIMEOUT_MS = 60_000; +function sanitizeMountPathHint(value?: string): string | undefined { + const trimmed = normalizeOptionalString(value); + if (!trimmed) { + return undefined; + } + if (hasPromptUnsafeControlCharacter(trimmed)) { + return undefined; + } + if (!/^[A-Za-z0-9._\-/:]+$/.test(trimmed)) { + return undefined; + } + return trimmed; +} + +function hasPromptUnsafeControlCharacter(value: string): boolean { + for (const char of value) { + const code = char.charCodeAt(0); + if (code <= 0x1f || code === 0x7f || code === 0x85 || code === 0x2028 || code === 0x2029) { + return true; + } + } + return false; +} + export async function spawnSubagentDirect( params: SpawnSubagentParams, ctx: SpawnSubagentContext,