@@ -33,6 +33,10 @@ import {
3333 resolveThreadBindingSpawnPolicy ,
3434} from "../channels/thread-bindings-policy.js" ;
3535import { parseDurationMs } from "../cli/parse-duration.js" ;
36+ import {
37+ DEFAULT_SUBAGENT_MAX_CHILDREN_PER_AGENT ,
38+ DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH ,
39+ } from "../config/agent-limits.js" ;
3640import { loadConfig } from "../config/config.js" ;
3741import { resolveStorePath } from "../config/sessions/paths.js" ;
3842import { loadSessionStore } from "../config/sessions/store.js" ;
@@ -60,6 +64,7 @@ import {
6064 normalizeOptionalString ,
6165} from "../shared/string-coerce.js" ;
6266import { createRunningTaskRun } from "../tasks/detached-task-runtime.js" ;
67+ import { listTasksForOwnerKey } from "../tasks/runtime-internal.js" ;
6368import {
6469 deliveryContextFromSession ,
6570 formatConversationTarget ,
@@ -75,6 +80,14 @@ import { resolveAgentConfig, resolveDefaultAgentId } from "./agent-scope.js";
7580import { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js" ;
7681import { resolveRequesterOriginForChild } from "./spawn-requester-origin.js" ;
7782import { resolveSpawnedWorkspaceInheritance } from "./spawned-context.js" ;
83+ import {
84+ isSubagentEnvelopeSession ,
85+ resolveSubagentCapabilities ,
86+ resolveSubagentCapabilityStore ,
87+ type SessionCapabilityStore ,
88+ } from "./subagent-capabilities.js" ;
89+ import { getSubagentDepthFromSessionStore } from "./subagent-depth.js" ;
90+ import { countActiveRunsForSession , getSubagentRunByChildSessionKey } from "./subagent-registry.js" ;
7891import { resolveInternalSessionKey , resolveMainSessionAlias } from "./tools/sessions-helpers.js" ;
7992
8093const log = createSubsystemLogger ( "agents/acp-spawn" ) ;
@@ -117,6 +130,7 @@ export const ACP_SPAWN_ERROR_CODES = [
117130 "acp_disabled" ,
118131 "requester_session_required" ,
119132 "runtime_policy" ,
133+ "subagent_policy" ,
120134 "thread_required" ,
121135 "target_agent_required" ,
122136 "agent_forbidden" ,
@@ -216,6 +230,52 @@ type AcpSpawnStreamPlan = {
216230 effectiveStreamToParent : boolean ;
217231} ;
218232
233+ type AcpSubagentEnvelopeState = {
234+ childSessionPatch ?: {
235+ spawnDepth : number ;
236+ subagentRole : "orchestrator" | "leaf" | null ;
237+ subagentControlScope : "children" | "none" ;
238+ } ;
239+ error ?: string ;
240+ } ;
241+
242+ function isActiveTaskStatus ( status : string | undefined ) : boolean {
243+ return status === "queued" || status === "running" ;
244+ }
245+
246+ function countUntrackedActiveAcpRunsForOwner ( ownerKey : string | undefined ) : number {
247+ const normalizedOwnerKey = normalizeOptionalString ( ownerKey ) ;
248+ if ( ! normalizedOwnerKey ) {
249+ return 0 ;
250+ }
251+ const tasks = listTasksForOwnerKey ( normalizedOwnerKey ) ;
252+ const trackedChildSessionKeys = new Set (
253+ tasks
254+ . filter (
255+ ( task ) =>
256+ task . runtime === "subagent" &&
257+ isActiveTaskStatus ( task . status ) &&
258+ normalizeOptionalString ( task . childSessionKey ) ,
259+ )
260+ . map ( ( task ) => normalizeOptionalString ( task . childSessionKey ) as string ) ,
261+ ) ;
262+ const activeAcpChildSessionKeys = new Set (
263+ tasks . flatMap ( ( task ) => {
264+ const childSessionKey = normalizeOptionalString ( task . childSessionKey ) ;
265+ const trackedRun = childSessionKey ? getSubagentRunByChildSessionKey ( childSessionKey ) : null ;
266+ const hasActiveRegistryRun = Boolean ( trackedRun && typeof trackedRun . endedAt !== "number" ) ;
267+ return task . runtime === "acp" &&
268+ isActiveTaskStatus ( task . status ) &&
269+ childSessionKey !== undefined &&
270+ ! hasActiveRegistryRun &&
271+ ! trackedChildSessionKeys . has ( childSessionKey )
272+ ? [ childSessionKey ]
273+ : [ ] ;
274+ } ) ,
275+ ) ;
276+ return activeAcpChildSessionKeys . size ;
277+ }
278+
219279type AcpSpawnBootstrapDeliveryPlan = {
220280 useInlineDelivery : boolean ;
221281 channel ?: string ;
@@ -658,6 +718,7 @@ function resolveAcpSpawnRequesterState(params: {
658718 parentSessionKey ?: string ;
659719 targetAgentId : string ;
660720 ctx : SpawnAcpContext ;
721+ subagentStore ?: SessionCapabilityStore ;
661722} ) : AcpSpawnRequesterState {
662723 const bindingService = getSessionBindingService ( ) ;
663724 const requesterParsedSession = parseAgentSessionKey ( params . parentSessionKey ) ;
@@ -706,6 +767,94 @@ function resolveAcpSpawnRequesterState(params: {
706767 } ;
707768}
708769
770+ function resolveAcpSubagentEnvelopeState ( params : {
771+ cfg : OpenClawConfig ;
772+ requesterSessionKey ?: string ;
773+ targetAgentId : string ;
774+ requestedAgentId ?: string ;
775+ subagentStore ?: SessionCapabilityStore ;
776+ } ) : AcpSubagentEnvelopeState {
777+ const requesterSessionKey = normalizeOptionalString ( params . requesterSessionKey ) ;
778+ if ( ! requesterSessionKey ) {
779+ return { } ;
780+ }
781+ if (
782+ ! isSubagentEnvelopeSession ( requesterSessionKey , {
783+ cfg : params . cfg ,
784+ store : params . subagentStore ,
785+ } )
786+ ) {
787+ return { } ;
788+ }
789+
790+ const callerDepth = getSubagentDepthFromSessionStore ( requesterSessionKey , {
791+ cfg : params . cfg ,
792+ } ) ;
793+ const maxSpawnDepth =
794+ params . cfg . agents ?. defaults ?. subagents ?. maxSpawnDepth ?? DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH ;
795+ if ( callerDepth >= maxSpawnDepth ) {
796+ return {
797+ error : `sessions_spawn is not allowed at this depth (current depth: ${ callerDepth } , max: ${ maxSpawnDepth } )` ,
798+ } ;
799+ }
800+
801+ const maxChildren =
802+ params . cfg . agents ?. defaults ?. subagents ?. maxChildrenPerAgent ??
803+ DEFAULT_SUBAGENT_MAX_CHILDREN_PER_AGENT ;
804+ const activeChildren =
805+ countActiveRunsForSession ( requesterSessionKey ) +
806+ countUntrackedActiveAcpRunsForOwner ( requesterSessionKey ) ;
807+ if ( activeChildren >= maxChildren ) {
808+ return {
809+ error : `sessions_spawn has reached max active children for this session (${ activeChildren } /${ maxChildren } )` ,
810+ } ;
811+ }
812+
813+ const requesterAgentId = normalizeAgentId ( parseAgentSessionKey ( requesterSessionKey ) ?. agentId ) ;
814+ const requireAgentId =
815+ resolveAgentConfig ( params . cfg , requesterAgentId ) ?. subagents ?. requireAgentId ??
816+ params . cfg . agents ?. defaults ?. subagents ?. requireAgentId ??
817+ false ;
818+ if ( requireAgentId && ! params . requestedAgentId ?. trim ( ) ) {
819+ return {
820+ error :
821+ "sessions_spawn requires explicit agentId when requireAgentId is configured. Use agents_list to see allowed agent ids." ,
822+ } ;
823+ }
824+
825+ if ( params . targetAgentId !== requesterAgentId ) {
826+ const allowAgents =
827+ resolveAgentConfig ( params . cfg , requesterAgentId ) ?. subagents ?. allowAgents ??
828+ params . cfg . agents ?. defaults ?. subagents ?. allowAgents ??
829+ [ ] ;
830+ const allowAny = allowAgents . some ( ( value ) => value . trim ( ) === "*" ) ;
831+ const normalizedTargetId = normalizeOptionalLowercaseString ( params . targetAgentId ) ?? "" ;
832+ const allowSet = new Set (
833+ allowAgents
834+ . filter ( ( value ) => value . trim ( ) && value . trim ( ) !== "*" )
835+ . map ( ( value ) => normalizeOptionalLowercaseString ( normalizeAgentId ( value ) ) ?? "" ) ,
836+ ) ;
837+ if ( ! allowAny && ! allowSet . has ( normalizedTargetId ) ) {
838+ const allowedText = allowSet . size > 0 ? Array . from ( allowSet ) . join ( ", " ) : "none" ;
839+ return {
840+ error : `agentId is not allowed for sessions_spawn (allowed: ${ allowedText } )` ,
841+ } ;
842+ }
843+ }
844+
845+ const childCapabilities = resolveSubagentCapabilities ( {
846+ depth : callerDepth + 1 ,
847+ maxSpawnDepth,
848+ } ) ;
849+ return {
850+ childSessionPatch : {
851+ spawnDepth : childCapabilities . depth ,
852+ subagentRole : childCapabilities . role === "main" ? null : childCapabilities . role ,
853+ subagentControlScope : childCapabilities . controlScope ,
854+ } ,
855+ } ;
856+ }
857+
709858function resolveAcpSpawnStreamPlan ( params : {
710859 spawnMode : SpawnAcpMode ;
711860 requestThreadBinding : boolean ;
@@ -1006,12 +1155,30 @@ export async function spawnAcpDirect(
10061155 error : agentPolicyError . message ,
10071156 } ) ;
10081157 }
1158+ const subagentStore = resolveSubagentCapabilityStore ( parentSessionKey , {
1159+ cfg,
1160+ } ) ;
10091161 const requesterState = resolveAcpSpawnRequesterState ( {
10101162 cfg,
10111163 parentSessionKey,
10121164 targetAgentId,
10131165 ctx,
1166+ subagentStore,
1167+ } ) ;
1168+ const subagentEnvelopeState = resolveAcpSubagentEnvelopeState ( {
1169+ cfg,
1170+ requesterSessionKey : requesterInternalKey ,
1171+ targetAgentId,
1172+ requestedAgentId : params . agentId ,
1173+ subagentStore,
10141174 } ) ;
1175+ if ( subagentEnvelopeState . error ) {
1176+ return createAcpSpawnFailure ( {
1177+ status : "forbidden" ,
1178+ errorCode : "subagent_policy" ,
1179+ error : subagentEnvelopeState . error ,
1180+ } ) ;
1181+ }
10151182 const { effectiveStreamToParent } = resolveAcpSpawnStreamPlan ( {
10161183 spawnMode,
10171184 requestThreadBinding,
@@ -1070,6 +1237,7 @@ export async function spawnAcpDirect(
10701237 params : {
10711238 key : sessionKey ,
10721239 spawnedBy : requesterInternalKey ,
1240+ ...subagentEnvelopeState . childSessionPatch ,
10731241 ...( params . label ? { label : params . label } : { } ) ,
10741242 } ,
10751243 timeoutMs : 10_000 ,
0 commit comments