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 // 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( 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] }