{"version":3,"file":"fanout-child.d.ts","sourceRoot":"","sources":["../../../src/extension/fanout-child.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,2BAA2B,CAAC;AAgL9E,MAAM,CAAC,OAAO,UAAU,oCAAoC,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAiEnF","sourcesContent":["import * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { ExtensionAPI, ToolDefinition } from \"@lpb-work/pi-coding-agent\";\nimport { discoverAgents } from \"../agents/agents.ts\";\nimport { resolveSubagentIntercomTarget } from \"../intercom/intercom-bridge.ts\";\nimport { deliverSubagentIntercomMessageEvent } from \"../intercom/result-intercom.ts\";\nimport { resolveWaitToolConfig } from \"../runs/background/wait-config.ts\";\nimport { createSubagentExecutor, type SubagentParamsLike } from \"../runs/foreground/subagent-executor.ts\";\nimport {\n\ttype NestedRoute,\n\treadNestedControlRequests,\n\tresolveNestedRouteFromEnv,\n\twriteNestedControlResult,\n} from \"../runs/shared/nested-events.ts\";\nimport { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from \"../runs/shared/pi-args.ts\";\nimport { getArtifactsDir } from \"../shared/artifacts.ts\";\nimport type { Details, SubagentState } from \"../shared/types.ts\";\nimport { loadConfig, resolveAsyncByDefault } from \"./config.ts\";\nimport { SubagentParams } from \"./schemas.ts\";\n\nfunction getSubagentSessionRoot(parentSessionFile: string | null): string {\n\tif (parentSessionFile) {\n\t\tconst baseName = path.basename(parentSessionFile, \".jsonl\");\n\t\tconst sessionsDir = path.dirname(parentSessionFile);\n\t\treturn path.join(sessionsDir, baseName);\n\t}\n\treturn fs.mkdtempSync(path.join(os.tmpdir(), \"pi-subagent-session-\"));\n}\n\nfunction expandTilde(p: string): string {\n\treturn p.startsWith(\"~/\") ? path.join(os.homedir(), p.slice(2)) : p;\n}\n\nfunction createChildSafeState(): SubagentState {\n\treturn {\n\t\tbaseCwd: \"\",\n\t\tcurrentSessionId: null,\n\t\tsubagentInProgress: false,\n\t\tsubagentSpawns: { sessionId: null, count: 0 },\n\t\tasyncJobs: new Map(),\n\t\tforegroundRuns: new Map(),\n\t\tforegroundControls: new Map(),\n\t\tlastForegroundControlId: null,\n\t\tcleanupTimers: new Map(),\n\t\tlastUiContext: null,\n\t\tpoller: null,\n\t\tcompletionSeen: new Map(),\n\t\twatcher: null,\n\t\twatcherRestartTimer: null,\n\t\tresultFileCoalescer: {\n\t\t\tschedule: () => false,\n\t\t\tclear: () => {},\n\t\t},\n\t};\n}\n\nfunction resolveNestedControlRoute(): NestedRoute | undefined {\n\ttry {\n\t\treturn resolveNestedRouteFromEnv();\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction nestedControlRouteKey(route: NestedRoute): string {\n\treturn route.controlInbox;\n}\n\ninterface NestedControlInboxState {\n\tseen: Set<string>;\n\tinFlight: Set<string>;\n\tpendingResults: Map<string, Parameters<typeof writeNestedControlResult>[1]>;\n}\n\ninterface NestedControlListenerEntry {\n\tcleanup: () => void;\n\tstate: NestedControlInboxState;\n}\n\nfunction createNestedControlInboxState(): NestedControlInboxState {\n\treturn { seen: new Set(), inFlight: new Set(), pendingResults: new Map() };\n}\n\nfunction startNestedControlInboxListener(\n\tpi: ExtensionAPI,\n\tstate: SubagentState,\n\troute: NestedRoute,\n\tinboxState: NestedControlInboxState,\n): () => void {\n\tconst timer = setInterval(() => {\n\t\ttry {\n\t\t\tfor (const request of readNestedControlRequests(route)) {\n\t\t\t\tif (inboxState.seen.has(request.requestId) || inboxState.inFlight.has(request.requestId)) continue;\n\t\t\t\tinboxState.inFlight.add(request.requestId);\n\t\t\t\tvoid (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tlet result = inboxState.pendingResults.get(request.requestId);\n\t\t\t\t\t\tif (!result) {\n\t\t\t\t\t\t\tlet ok = false;\n\t\t\t\t\t\t\tlet message = \"Control request failed.\";\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst control = state.foregroundControls.get(request.targetRunId);\n\t\t\t\t\t\t\t\tif (!control) {\n\t\t\t\t\t\t\t\t\tmessage = `Nested run ${request.targetRunId} is not active in this fanout child.`;\n\t\t\t\t\t\t\t\t} else if (request.action === \"interrupt\") {\n\t\t\t\t\t\t\t\t\tok = control.interrupt?.() === true;\n\t\t\t\t\t\t\t\t\tmessage = ok\n\t\t\t\t\t\t\t\t\t\t? `Interrupt requested for nested run ${request.targetRunId}.`\n\t\t\t\t\t\t\t\t\t\t: `Nested run ${request.targetRunId} has no active child step to interrupt.`;\n\t\t\t\t\t\t\t\t} else if (!request.message?.trim()) {\n\t\t\t\t\t\t\t\t\tmessage = \"Nested resume requires message.\";\n\t\t\t\t\t\t\t\t} else if (!control.currentAgent) {\n\t\t\t\t\t\t\t\t\tmessage = `Nested run ${request.targetRunId} has no active child message route.`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tconst index = control.currentIndex ?? 0;\n\t\t\t\t\t\t\t\t\tconst target = resolveSubagentIntercomTarget(\n\t\t\t\t\t\t\t\t\t\trequest.targetRunId,\n\t\t\t\t\t\t\t\t\t\tcontrol.currentAgent,\n\t\t\t\t\t\t\t\t\t\tindex,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tok = await deliverSubagentIntercomMessageEvent(\n\t\t\t\t\t\t\t\t\t\tpi.events,\n\t\t\t\t\t\t\t\t\t\ttarget,\n\t\t\t\t\t\t\t\t\t\t`Follow-up for nested run ${request.targetRunId} (${control.currentAgent}):\\n\\n${request.message.trim()}`,\n\t\t\t\t\t\t\t\t\t\t500,\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tsource: \"nested-resume\",\n\t\t\t\t\t\t\t\t\t\t\trunId: request.targetRunId,\n\t\t\t\t\t\t\t\t\t\t\tagent: control.currentAgent,\n\t\t\t\t\t\t\t\t\t\t\tindex,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tmessage = ok\n\t\t\t\t\t\t\t\t\t\t? `Delivered follow-up to live nested run ${request.targetRunId}.`\n\t\t\t\t\t\t\t\t\t\t: `Nested child intercom target is not registered: ${target}`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\tmessage = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tresult = {\n\t\t\t\t\t\t\t\tts: Date.now(),\n\t\t\t\t\t\t\t\trequestId: request.requestId,\n\t\t\t\t\t\t\t\ttargetRunId: request.targetRunId,\n\t\t\t\t\t\t\t\tok,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\twriteNestedControlResult(route, result);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tinboxState.pendingResults.set(request.requestId, result);\n\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t`Failed to write nested control result for request '${request.requestId}' targeting '${request.targetRunId}' via inbox '${route.controlInbox}'; keeping request for retry:`,\n\t\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinboxState.pendingResults.delete(request.requestId);\n\t\t\t\t\t\tinboxState.seen.add(request.requestId);\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tfs.unlinkSync(request.filePath);\n\t\t\t\t\t\t} catch {}\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tinboxState.inFlight.delete(request.requestId);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t`Failed to poll nested control inbox '${route.controlInbox}' for root '${route.rootRunId}':`,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}, 200);\n\ttimer.unref?.();\n\treturn () => clearInterval(timer);\n}\n\nexport default function registerFanoutChildSubagentExtension(pi: ExtensionAPI): void {\n\tif (process.env[SUBAGENT_CHILD_ENV] !== \"1\" || process.env[SUBAGENT_FANOUT_CHILD_ENV] !== \"1\") return;\n\n\tconst globalStore = globalThis as Record<string, unknown>;\n\tconst registeredKey = \"__piSubagentFanoutChildRegisteredApis\";\n\tconst registeredApis =\n\t\tglobalStore[registeredKey] instanceof WeakSet\n\t\t\t? (globalStore[registeredKey] as WeakSet<ExtensionAPI>)\n\t\t\t: new WeakSet<ExtensionAPI>();\n\tglobalStore[registeredKey] = registeredApis;\n\tif (registeredApis.has(pi)) return;\n\tregisteredApis.add(pi);\n\n\tconst config = loadConfig();\n\tconst state = createChildSafeState();\n\tconst executor = createSubagentExecutor({\n\t\tpi,\n\t\tstate,\n\t\tconfig,\n\t\tasyncByDefault: resolveAsyncByDefault(config),\n\t\twaitToolEnabled: resolveWaitToolConfig(config.waitTool).enabled,\n\t\ttempArtifactsDir: getArtifactsDir(null),\n\t\tgetSubagentSessionRoot,\n\t\texpandTilde,\n\t\tdiscoverAgents,\n\t\tallowMutatingManagementActions: false,\n\t});\n\n\tconst tool: ToolDefinition<typeof SubagentParams, Details> = {\n\t\tname: \"subagent\",\n\t\tlabel: \"Subagent\",\n\t\tdescription: [\n\t\t\t\"Delegate to subagents from child-safe fanout mode.\",\n\t\t\t\"Allowed management/control actions: list, get, status, interrupt, resume, steer, append-step, doctor.\",\n\t\t\t\"Mutating management actions (create, update, delete, eject, disable, enable, reset, grant-spawn-budget) are blocked in this mode.\",\n\t\t].join(\"\\n\"),\n\t\tparameters: SubagentParams,\n\t\texecute(id, params, signal, onUpdate, ctx) {\n\t\t\treturn executor.executePublic(\n\t\t\t\tid,\n\t\t\t\tparams as SubagentParamsLike,\n\t\t\t\tsignal ?? new AbortController().signal,\n\t\t\t\tonUpdate,\n\t\t\t\tctx,\n\t\t\t);\n\t\t},\n\t};\n\n\tpi.registerTool(tool);\n\tconst route = resolveNestedControlRoute();\n\tif (!route) return;\n\tconst listenerCleanupKey = \"__piSubagentFanoutChildNestedControlInboxCleanups\";\n\tconst listenerCleanups =\n\t\tglobalStore[listenerCleanupKey] instanceof Map\n\t\t\t? (globalStore[listenerCleanupKey] as Map<string, NestedControlListenerEntry>)\n\t\t\t: new Map<string, NestedControlListenerEntry>();\n\tglobalStore[listenerCleanupKey] = listenerCleanups;\n\tconst routeKey = nestedControlRouteKey(route);\n\tconst previous = listenerCleanups.get(routeKey);\n\tprevious?.cleanup();\n\tconst inboxState = previous?.state ?? createNestedControlInboxState();\n\tlistenerCleanups.set(routeKey, {\n\t\tstate: inboxState,\n\t\tcleanup: startNestedControlInboxListener(pi, state, route, inboxState),\n\t});\n}\n"]}