33 *
44 * Creates an isolated in-memory child AgentSession for focused subtask execution.
55 * Children inherit the parent's model, thinking level, cwd, and ledger access.
6- * Max nesting depth: 1 edge (parent → child only ).
6+ * Children do not inherit the spawn tool (recursion prevention ).
77 *
88 * Spawn is context isolation, not a security boundary. Child agents are trusted
99 * extensions of the parent and inherit parent authority by design.
@@ -39,7 +39,6 @@ import {
3939
4040// ── Constants ─────────────────────────────────────────────────────────
4141
42- const MAX_SPAWN_DEPTH = 1 ;
4342const CHILD_MAX_LINES = 2000 ;
4443const CHILD_MAX_BYTES = 50 * 1024 ;
4544
@@ -113,8 +112,7 @@ function truncateResult(text: string): { text: string; truncated: boolean } {
113112 * child custom tools defined here. Parent-only custom tools are intentionally
114113 * excluded so the child never advertises a tool it cannot execute.
115114 *
116- * handoff never carries into children, and spawn is only re-added from
117- * childTools when the current depth still allows nesting.
115+ * handoff and spawn never carry into children.
118116 */
119117function getInheritableParentToolNames ( parentToolNames : string [ ] , availableTools : Pick < ToolInfo , "name" | "sourceInfo" > [ ] ) : string [ ] {
120118 const activeToolNames = new Set ( parentToolNames ) ;
@@ -135,11 +133,11 @@ export function buildChildToolNames(
135133 return [ ...new Set ( [ ...inheritedTools , ...childTools . map ( ( tool ) => tool . name ) ] ) ] ;
136134}
137135
138- // ── Shared spawn tool metadata (used by both parent and child tool definitions) ──
136+ // ── Spawn tool metadata ──
139137
140138const SPAWN_DESCRIPTION =
141139 "Spawn an isolated child agent for a focused subtask. " +
142- "Child inherits parent model, thinking level, cwd, supported built-in tools, and shared ledger tools; spawn is only exposed when depth allows . " +
140+ "Child inherits parent model, thinking level, cwd, supported built-in tools, and shared ledger tools; children cannot spawn further children . " +
143141 "Reference ledger entries by name — child will ledger_get them on demand." ;
144142
145143const SPAWN_PROMPT_SNIPPET = "Spawn a focused subtask agent" ;
@@ -168,85 +166,35 @@ const SPAWN_PARAMETERS = Type.Object({
168166/**
169167 * Build the custom tool set for child agent sessions.
170168 *
171- * Produces ledger tools (add/get/list) and conditionally includes the spawn
172- * tool when currentDepth is below MAX_SPAWN_DEPTH. The spawn tool is omitted
173- * at max depth to prevent the LLM from attempting illegal recursion.
169+ * Produces ledger tools (add/get/list). Children do not receive the spawn
170+ * tool to prevent the LLM from attempting recursion.
174171 *
175172 * All tools read/write the shared parent state so ledger entries are visible
176173 * across parent and child contexts.
177- *
178- * @param sessionFactory - Test seam for dependency-injecting createAgentSession.
179174 */
180175export function createChildTools (
181176 pi : ExtensionAPI ,
182177 state : AgenticodingState ,
183- defaultThinking : ThinkingValue ,
184- currentDepth : number ,
185- sessionFactory : typeof createAgentSession = createAgentSession ,
186178 options ?: { isStale ?: ( ) => boolean } ,
187179) : ToolDefinition [ ] {
188- // Child sessions inherit only executable parent tools via
189- // buildChildToolNames(). Only built-in parent tools are carried through.
190- // handoff never carries into children, and spawn is only re-added here
191- // while depth allows it.
192-
193- const childSpawnTool : ToolDefinition = {
194- name : "spawn" ,
195- label : "Spawn" ,
196- description : SPAWN_DESCRIPTION ,
197- promptSnippet : SPAWN_PROMPT_SNIPPET ,
198- promptGuidelines : SPAWN_PROMPT_GUIDELINES ,
199- parameters : SPAWN_PARAMETERS ,
200- async execute (
201- toolCallId : string ,
202- params : { prompt : string ; thinking ?: ThinkingValue } ,
203- signal : AbortSignal | undefined ,
204- onUpdate :
205- | ( ( result : {
206- content : { type : string ; text : string } [ ] ;
207- details ?: unknown ;
208- } ) => void )
209- | undefined ,
210- ctx : ExtensionContext ,
211- ) {
212- return executeSpawn ( toolCallId , pi , ctx , state , params , signal , onUpdate , defaultThinking , currentDepth , sessionFactory ) ;
213- } ,
214- renderCall : renderSpawnCall ,
215- renderResult ( result , { expanded } , theme , context ) {
216- return renderSpawnResult ( result , expanded , theme , context , state ) ;
217- } ,
218- } ;
219-
220- const childLedgerTools = createLedgerToolDefinitions ( pi , state , { isStale : options ?. isStale } ) ;
221-
222- return [
223- ...( currentDepth < MAX_SPAWN_DEPTH ? [ childSpawnTool ] : [ ] ) ,
224- ...childLedgerTools ,
225- ] ;
180+ return createLedgerToolDefinitions ( pi , state , { isStale : options ?. isStale } ) ;
226181}
227182
228183
229184
230185// ── Shared spawn execution logic ──────────────────────────────────────
231- // Used by both the parent-registered spawn tool and child custom spawn tools.
232186
233187/**
234188 * Creates an isolated child agent session, runs the given prompt, and returns
235189 * the result with usage stats.
236190 *
237- * Errors (all thrown, not returned):
238- * - "Max spawn depth reached" → currentDepth >= MAX_SPAWN_DEPTH
239- * - "No model configured..." → ctx.model is undefined
240- * - "Child agent produced no output." → no assistant text after prompt
191+ * Error: "No model configured..." → ctx.model is undefined
241192 *
242193 * Side effects on state:
243194 * - state.childSessions.set(toolCallId, session) on creation
244195 * - state.liveChildSessions.set(toolCallId, session) on creation
245196 * - both registries delete(toolCallId) on error and completion paths
246197 *
247- * @param onUpdate - Callback that fires once after session creation with
248- * empty content + initial details (depth, model, thinking). Pi uses this
249- * to render the component before the child produces output.
250198 * @param sessionFactory - Test seam for mocking createAgentSession.
251199 */
252200export async function executeSpawn (
@@ -263,20 +211,15 @@ export async function executeSpawn(
263211 } ) => void )
264212 | undefined ,
265213 defaultThinking : ThinkingValue ,
266- currentDepth : number ,
267214 sessionFactory : typeof createAgentSession = createAgentSession ,
268215) {
269- if ( currentDepth >= MAX_SPAWN_DEPTH ) {
270- throw new Error ( `Max spawn depth (${ MAX_SPAWN_DEPTH } ) reached. Cannot spawn further children.` ) ;
271- }
272216
273217 const childModel = ctx . model ;
274218 if ( ! childModel ) {
275219 throw new Error ( "No model configured. Cannot spawn child agent." ) ;
276220 }
277221
278222 const childThinking : ThinkingValue = params . thinking ?? defaultThinking ;
279- const depth = currentDepth + 1 ;
280223
281224 const listing = formatEntryList ( state ) ;
282225 const ledgerListing = listing
@@ -285,7 +228,7 @@ export async function executeSpawn(
285228 const fullPrompt =
286229 `You are a focused child agent spawned by a parent agent. ` +
287230 `You have the same authority as the parent. ` +
288- `You inherit the parent's supported built-in tools plus shared ledger tools, and spawn is only exposed when depth allows it . ` +
231+ `Children cannot spawn further children . ` +
289232 `Your result will be read by the parent, so be concise and complete.\n\n` +
290233 `${ ledgerListing } \n\n` +
291234 `## Task\n\n${ params . prompt } \n\n` +
@@ -296,7 +239,7 @@ export async function executeSpawn(
296239 const modelRegistry = ModelRegistry . create ( authStorage ) ;
297240 const childSessionEpoch = state . childSessionEpoch ;
298241 const isStale = ( ) => state . childSessionEpoch !== childSessionEpoch ;
299- const childTools = createChildTools ( pi , state , childThinking , depth , sessionFactory , { isStale } ) ;
242+ const childTools = createChildTools ( pi , state , { isStale } ) ;
300243 const parentToolNames = pi . getActiveTools ( ) ;
301244 const childToolNames = buildChildToolNames ( parentToolNames , childTools , pi . getAllTools ( ) ) ;
302245
@@ -356,7 +299,6 @@ export async function executeSpawn(
356299 onUpdate ?.( {
357300 content : [ ] ,
358301 details : {
359- depth,
360302 model : childModel . id ,
361303 thinking : childThinking ,
362304 truncated : false ,
@@ -420,7 +362,6 @@ export async function executeSpawn(
420362 }
421363
422364 const details : SpawnResultDetails = {
423- depth,
424365 model : childModel . id ,
425366 thinking : childThinking ,
426367 truncated,
@@ -475,7 +416,7 @@ export function registerSpawnTool(
475416 ctx : ExtensionContext ,
476417 ) {
477418 const parentThinking : ThinkingValue = pi . getThinkingLevel ( ) ;
478- return executeSpawn ( _toolCallId , pi , ctx , state , params , signal , onUpdate , parentThinking , 0 , sessionFactory ) ;
419+ return executeSpawn ( _toolCallId , pi , ctx , state , params , signal , onUpdate , parentThinking , sessionFactory ) ;
479420 } ,
480421
481422 renderCall : renderSpawnCall ,
0 commit comments