| import type { TaskStateBase } from '../../Task.js' | |
| import type { AgentToolResult } from '../../tools/AgentTool/agentToolUtils.js' | |
| import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js' | |
| import type { Message } from '../../types/message.js' | |
| import type { PermissionMode } from '../../utils/permissions/PermissionMode.js' | |
| import type { AgentProgress } from '../LocalAgentTask/LocalAgentTask.js' | |
| /** | |
| * Teammate identity stored in task state. | |
| * Same shape as TeammateContext (runtime) but stored as plain data. | |
| * TeammateContext is for AsyncLocalStorage; this is for AppState persistence. | |
| */ | |
| export type TeammateIdentity = { | |
| agentId: string // e.g., "researcher@my-team" | |
| agentName: string // e.g., "researcher" | |
| teamName: string | |
| color?: string | |
| planModeRequired: boolean | |
| parentSessionId: string // Leader's session ID | |
| } | |
| export type InProcessTeammateTaskState = TaskStateBase & { | |
| type: 'in_process_teammate' | |
| // Identity as sub-object (matches TeammateContext shape for consistency) | |
| // Stored as plain data in AppState, NOT a reference to AsyncLocalStorage | |
| identity: TeammateIdentity | |
| // Execution | |
| prompt: string | |
| // Optional model override for this teammate | |
| model?: string | |
| // Optional: Only set if teammate uses a specific agent definition | |
| // Many teammates run as general-purpose agents without a predefined definition | |
| selectedAgent?: AgentDefinition | |
| abortController?: AbortController // Runtime only, not serialized to disk - kills WHOLE teammate | |
| currentWorkAbortController?: AbortController // Runtime only - aborts current turn without killing teammate | |
| unregisterCleanup?: () => void // Runtime only | |
| // Plan mode approval tracking (planModeRequired is in identity) | |
| awaitingPlanApproval: boolean | |
| // Permission mode for this teammate (cycled independently via Shift+Tab when viewing) | |
| permissionMode: PermissionMode | |
| // State | |
| error?: string | |
| result?: AgentToolResult // Reuse existing type since teammates run via runAgent() | |
| progress?: AgentProgress | |
| // Conversation history for zoomed view (NOT mailbox messages) | |
| // Mailbox messages are stored separately in teamContext.inProcessMailboxes | |
| messages?: Message[] | |
| // Tool use IDs currently being executed (for animation in transcript view) | |
| inProgressToolUseIDs?: Set<string> | |
| // Queue of user messages to deliver when viewing teammate transcript | |
| pendingUserMessages: string[] | |
| // UI: random spinner verbs (stable across re-renders, shared between components) | |
| spinnerVerb?: string | |
| pastTenseVerb?: string | |
| // Lifecycle | |
| isIdle: boolean | |
| shutdownRequested: boolean | |
| // Callbacks to notify when teammate becomes idle (runtime only) | |
| // Used by leader to efficiently wait without polling | |
| onIdleCallbacks?: Array<() => void> | |
| // Progress tracking (for computing deltas in notifications) | |
| lastReportedToolCount: number | |
| lastReportedTokenCount: number | |
| } | |
| export function isInProcessTeammateTask( | |
| task: unknown, | |
| ): task is InProcessTeammateTaskState { | |
| return ( | |
| typeof task === 'object' && | |
| task !== null && | |
| 'type' in task && | |
| task.type === 'in_process_teammate' | |
| ) | |
| } | |
| /** | |
| * Cap on the number of messages kept in task.messages (the AppState UI mirror). | |
| * | |
| * task.messages exists purely for the zoomed transcript dialog, which only | |
| * needs recent context. The full conversation lives in the local allMessages | |
| * array (inProcessRunner) and on disk at the agent transcript path. | |
| * | |
| * BQ analysis (round 9, 2026-03-20) showed ~20MB RSS per agent at 500+ turn | |
| * sessions and ~125MB per concurrent agent in swarm bursts. Whale session | |
| * 9a990de8 launched 292 agents in 2 minutes and reached 36.8GB. The dominant | |
| * cost is this array holding a second full copy of every message. | |
| */ | |
| export const TEAMMATE_MESSAGES_UI_CAP = 50 | |
| /** | |
| * Append an item to a message array, capping the result at | |
| * TEAMMATE_MESSAGES_UI_CAP entries by dropping the oldest. Always returns | |
| * a new array (AppState immutability). | |
| */ | |
| export function appendCappedMessage<T>( | |
| prev: readonly T[] | undefined, | |
| item: T, | |
| ): T[] { | |
| if (prev === undefined || prev.length === 0) { | |
| return [item] | |
| } | |
| if (prev.length >= TEAMMATE_MESSAGES_UI_CAP) { | |
| const next = prev.slice(-(TEAMMATE_MESSAGES_UI_CAP - 1)) | |
| next.push(item) | |
| return next | |
| } | |
| return [...prev, item] | |
| } | |