π File detail
utils/swarm/spawnInProcess.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes SpawnContext, InProcessSpawnConfig, InProcessSpawnOutput, spawnInProcessTeammate, and killInProcessTeammate β mainly types, interfaces, or factory objects. Dependencies touch lodash-es. It composes internal code from bootstrap, constants, state, Task, and tasks (relative imports). What the file header says: In-process teammate spawning Creates and registers an in-process teammate task. Unlike process-based teammates (tmux/iTerm2), in-process teammates run in the same Node.js process using AsyncLocalStorage for context isolation. The actual agent execution loop is handled by InProces.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
In-process teammate spawning Creates and registers an in-process teammate task. Unlike process-based teammates (tmux/iTerm2), in-process teammates run in the same Node.js process using AsyncLocalStorage for context isolation. The actual agent execution loop is handled by InProcessTeammateTask component (Task #14). This module handles: 1. Creating TeammateContext 2. Creating linked AbortController 3. Registering InProcessTeammateTaskState in AppState 4. Returning spawn result for backend
π€ Exports (heuristic)
SpawnContextInProcessSpawnConfigInProcessSpawnOutputspawnInProcessTeammatekillInProcessTeammate
π External import roots
Package roots from from "β¦" (relative paths omitted).
lodash-es
π₯οΈ Source preview
/**
* In-process teammate spawning
*
* Creates and registers an in-process teammate task. Unlike process-based
* teammates (tmux/iTerm2), in-process teammates run in the same Node.js
* process using AsyncLocalStorage for context isolation.
*
* The actual agent execution loop is handled by InProcessTeammateTask
* component (Task #14). This module handles:
* 1. Creating TeammateContext
* 2. Creating linked AbortController
* 3. Registering InProcessTeammateTaskState in AppState
* 4. Returning spawn result for backend
*/
import sample from 'lodash-es/sample.js'
import { getSessionId } from '../../bootstrap/state.js'
import { getSpinnerVerbs } from '../../constants/spinnerVerbs.js'
import { TURN_COMPLETION_VERBS } from '../../constants/turnCompletionVerbs.js'
import type { AppState } from '../../state/AppState.js'
import { createTaskStateBase, generateTaskId } from '../../Task.js'
import type {
InProcessTeammateTaskState,
TeammateIdentity,
} from '../../tasks/InProcessTeammateTask/types.js'
import { createAbortController } from '../abortController.js'
import { formatAgentId } from '../agentId.js'
import { registerCleanup } from '../cleanupRegistry.js'
import { logForDebugging } from '../debug.js'
import { emitTaskTerminatedSdk } from '../sdkEventQueue.js'
import { evictTaskOutput } from '../task/diskOutput.js'
import {
evictTerminalTask,
registerTask,
STOPPED_DISPLAY_MS,
} from '../task/framework.js'
import { createTeammateContext } from '../teammateContext.js'
import {
isPerfettoTracingEnabled,
registerAgent as registerPerfettoAgent,
unregisterAgent as unregisterPerfettoAgent,
} from '../telemetry/perfettoTracing.js'
import { removeMemberByAgentId } from './teamHelpers.js'
type SetAppStateFn = (updater: (prev: AppState) => AppState) => void
/**
* Minimal context required for spawning an in-process teammate.
* This is a subset of ToolUseContext - only what spawnInProcessTeammate actually uses.
*/
export type SpawnContext = {
setAppState: SetAppStateFn
toolUseId?: string
}
/**
* Configuration for spawning an in-process teammate.
*/
export type InProcessSpawnConfig = {
/** Display name for the teammate, e.g., "researcher" */
name: string
/** Team this teammate belongs to */
teamName: string
/** Initial prompt/task for the teammate */
prompt: string
/** Optional UI color for the teammate */
color?: string
/** Whether teammate must enter plan mode before implementing */
planModeRequired: boolean
/** Optional model override for this teammate */
model?: string
}
/**
* Result from spawning an in-process teammate.
*/
export type InProcessSpawnOutput = {
/** Whether spawn was successful */
success: boolean
/** Full agent ID (format: "name@team") */
agentId: string
/** Task ID for tracking in AppState */
taskId?: string
/** AbortController for this teammate (linked to parent) */
abortController?: AbortController
/** Teammate context for AsyncLocalStorage */
teammateContext?: ReturnType<typeof createTeammateContext>
/** Error message if spawn failed */
error?: string
}
/**
* Spawns an in-process teammate.
*
* Creates the teammate's context, registers the task in AppState, and returns
* the spawn result. The actual agent execution is driven by the
* InProcessTeammateTask component which uses runWithTeammateContext() to
* execute the agent loop with proper identity isolation.
*
* @param config - Spawn configuration
* @param context - Context with setAppState for registering task
* @returns Spawn result with teammate info
*/
export async function spawnInProcessTeammate(
config: InProcessSpawnConfig,
context: SpawnContext,
): Promise<InProcessSpawnOutput> {
const { name, teamName, prompt, color, planModeRequired, model } = config
const { setAppState } = context
// Generate deterministic agent ID
const agentId = formatAgentId(name, teamName)
const taskId = generateTaskId('in_process_teammate')
logForDebugging(
`[spawnInProcessTeammate] Spawning ${agentId} (taskId: ${taskId})`,
)
try {
// Create independent AbortController for this teammate
// Teammates should not be aborted when the leader's query is interrupted
const abortController = createAbortController()
// Get parent session ID for transcript correlation
const parentSessionId = getSessionId()
// Create teammate identity (stored as plain data in AppState)
const identity: TeammateIdentity = {
agentId,
agentName: name,
teamName,
color,
planModeRequired,
parentSessionId,
}
// Create teammate context for AsyncLocalStorage
// This will be used by runWithTeammateContext() during agent execution
const teammateContext = createTeammateContext({
agentId,
agentName: name,
teamName,
color,
planModeRequired,
parentSessionId,
abortController,
})
// Register agent in Perfetto trace for hierarchy visualization
if (isPerfettoTracingEnabled()) {
registerPerfettoAgent(agentId, name, parentSessionId)
}
// Create task state
const description = `${name}: ${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}`
const taskState: InProcessTeammateTaskState = {
...createTaskStateBase(
taskId,
'in_process_teammate',
description,
context.toolUseId,
),
type: 'in_process_teammate',
status: 'running',
identity,
prompt,
model,
abortController,
awaitingPlanApproval: false,
spinnerVerb: sample(getSpinnerVerbs()),
pastTenseVerb: sample(TURN_COMPLETION_VERBS),
permissionMode: planModeRequired ? 'plan' : 'default',
isIdle: false,
shutdownRequested: false,
lastReportedToolCount: 0,
lastReportedTokenCount: 0,
pendingUserMessages: [],
messages: [], // Initialize to empty array so getDisplayedMessages works immediately
}
// Register cleanup handler for graceful shutdown
const unregisterCleanup = registerCleanup(async () => {
logForDebugging(`[spawnInProcessTeammate] Cleanup called for ${agentId}`)
abortController.abort()
// Task state will be updated by the execution loop when it detects abort
})
taskState.unregisterCleanup = unregisterCleanup
// Register task in AppState
registerTask(taskState, setAppState)
logForDebugging(
`[spawnInProcessTeammate] Registered ${agentId} in AppState`,
)
return {
success: true,
agentId,
taskId,
abortController,
teammateContext,
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error during spawn'
logForDebugging(
`[spawnInProcessTeammate] Failed to spawn ${agentId}: ${errorMessage}`,
)
return {
success: false,
agentId,
error: errorMessage,
}
}
}
/**
* Kills an in-process teammate by aborting its controller.
*
* Note: This is the implementation called by InProcessBackend.kill().
*
* @param taskId - Task ID of the teammate to kill
* @param setAppState - AppState setter
* @returns true if killed successfully
*/
export function killInProcessTeammate(
taskId: string,
setAppState: SetAppStateFn,
): boolean {
let killed = false
let teamName: string | null = null
let agentId: string | null = null
let toolUseId: string | undefined
let description: string | undefined
setAppState((prev: AppState) => {
const task = prev.tasks[taskId]
if (!task || task.type !== 'in_process_teammate') {
return prev
}
const teammateTask = task as InProcessTeammateTaskState
if (teammateTask.status !== 'running') {
return prev
}
// Capture identity for cleanup after state update
teamName = teammateTask.identity.teamName
agentId = teammateTask.identity.agentId
toolUseId = teammateTask.toolUseId
description = teammateTask.description
// Abort the controller to stop execution
teammateTask.abortController?.abort()
// Call cleanup handler
teammateTask.unregisterCleanup?.()
// Update task state and remove from teamContext.teammates
killed = true
// Call pending idle callbacks to unblock any waiters (e.g., engine.waitForIdle)
teammateTask.onIdleCallbacks?.forEach(cb => cb())
// Remove from teamContext.teammates using the agentId
let updatedTeamContext = prev.teamContext
if (prev.teamContext && prev.teamContext.teammates && agentId) {
const { [agentId]: _, ...remainingTeammates } = prev.teamContext.teammates
updatedTeamContext = {
...prev.teamContext,
teammates: remainingTeammates,
}
}
return {
...prev,
teamContext: updatedTeamContext,
tasks: {
...prev.tasks,
[taskId]: {
...teammateTask,
status: 'killed' as const,
notified: true,
endTime: Date.now(),
onIdleCallbacks: [], // Clear callbacks to prevent stale references
messages: teammateTask.messages?.length
? [teammateTask.messages[teammateTask.messages.length - 1]!]
: undefined,
pendingUserMessages: [],
inProgressToolUseIDs: undefined,
abortController: undefined,
unregisterCleanup: undefined,
currentWorkAbortController: undefined,
},
},
}
})
// Remove from team file (outside state updater to avoid file I/O in callback)
if (teamName && agentId) {
removeMemberByAgentId(teamName, agentId)
}
if (killed) {
void evictTaskOutput(taskId)
// notified:true was pre-set so no XML notification fires; close the SDK
// task_started bookend directly. The in-process runner's own
// completion/failure emit guards on status==='running' so it won't
// double-emit after seeing status:killed.
emitTaskTerminatedSdk(taskId, 'stopped', {
toolUseId,
summary: description,
})
setTimeout(
evictTerminalTask.bind(null, taskId, setAppState),
STOPPED_DISPLAY_MS,
)
}
// Release perfetto agent registry entry
if (agentId) {
unregisterPerfettoAgent(agentId)
}
return killed
}