Download src/utils/swarm/spawnInProcess.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 10.2 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/utils/swarm/spawnInProcess.ts
- Command line
-
hf download hf://chenbhao/codev/src/utils/swarm/spawnInProcess.ts
-
curl -L -o spawnInProcess.ts https://huggingface.co/chenbhao/codev/resolve/main/src/utils/swarm/spawnInProcess.ts
10.2 kB
| /** | |
| * 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 | |
| } | |