types.ts
  1  import type { TaskStateBase } from '../../Task.js'
  2  import type { AgentToolResult } from '../../tools/AgentTool/agentToolUtils.js'
  3  import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js'
  4  import type { Message } from '../../types/message.js'
  5  import type { PermissionMode } from '../../utils/permissions/PermissionMode.js'
  6  import type { AgentProgress } from '../LocalAgentTask/LocalAgentTask.js'
  7  
  8  /**
  9   * Teammate identity stored in task state.
 10   * Same shape as TeammateContext (runtime) but stored as plain data.
 11   * TeammateContext is for AsyncLocalStorage; this is for AppState persistence.
 12   */
 13  export type TeammateIdentity = {
 14    agentId: string // e.g., "researcher@my-team"
 15    agentName: string // e.g., "researcher"
 16    teamName: string
 17    color?: string
 18    planModeRequired: boolean
 19    parentSessionId: string // Leader's session ID
 20  }
 21  
 22  export type InProcessTeammateTaskState = TaskStateBase & {
 23    type: 'in_process_teammate'
 24  
 25    // Identity as sub-object (matches TeammateContext shape for consistency)
 26    // Stored as plain data in AppState, NOT a reference to AsyncLocalStorage
 27    identity: TeammateIdentity
 28  
 29    // Execution
 30    prompt: string
 31    // Optional model override for this teammate
 32    model?: string
 33    // Optional: Only set if teammate uses a specific agent definition
 34    // Many teammates run as general-purpose agents without a predefined definition
 35    selectedAgent?: AgentDefinition
 36    abortController?: AbortController // Runtime only, not serialized to disk - kills WHOLE teammate
 37    currentWorkAbortController?: AbortController // Runtime only - aborts current turn without killing teammate
 38    unregisterCleanup?: () => void // Runtime only
 39  
 40    // Plan mode approval tracking (planModeRequired is in identity)
 41    awaitingPlanApproval: boolean
 42  
 43    // Permission mode for this teammate (cycled independently via Shift+Tab when viewing)
 44    permissionMode: PermissionMode
 45  
 46    // State
 47    error?: string
 48    result?: AgentToolResult // Reuse existing type since teammates run via runAgent()
 49    progress?: AgentProgress
 50  
 51    // Conversation history for zoomed view (NOT mailbox messages)
 52    // Mailbox messages are stored separately in teamContext.inProcessMailboxes
 53    messages?: Message[]
 54  
 55    // Tool use IDs currently being executed (for animation in transcript view)
 56    inProgressToolUseIDs?: Set<string>
 57  
 58    // Queue of user messages to deliver when viewing teammate transcript
 59    pendingUserMessages: string[]
 60  
 61    // UI: random spinner verbs (stable across re-renders, shared between components)
 62    spinnerVerb?: string
 63    pastTenseVerb?: string
 64  
 65    // Lifecycle
 66    isIdle: boolean
 67    shutdownRequested: boolean
 68  
 69    // Callbacks to notify when teammate becomes idle (runtime only)
 70    // Used by leader to efficiently wait without polling
 71    onIdleCallbacks?: Array<() => void>
 72  
 73    // Progress tracking (for computing deltas in notifications)
 74    lastReportedToolCount: number
 75    lastReportedTokenCount: number
 76  }
 77  
 78  export function isInProcessTeammateTask(
 79    task: unknown,
 80  ): task is InProcessTeammateTaskState {
 81    return (
 82      typeof task === 'object' &&
 83      task !== null &&
 84      'type' in task &&
 85      task.type === 'in_process_teammate'
 86    )
 87  }
 88  
 89  /**
 90   * Cap on the number of messages kept in task.messages (the AppState UI mirror).
 91   *
 92   * task.messages exists purely for the zoomed transcript dialog, which only
 93   * needs recent context. The full conversation lives in the local allMessages
 94   * array (inProcessRunner) and on disk at the agent transcript path.
 95   *
 96   * BQ analysis (round 9, 2026-03-20) showed ~20MB RSS per agent at 500+ turn
 97   * sessions and ~125MB per concurrent agent in swarm bursts. Whale session
 98   * 9a990de8 launched 292 agents in 2 minutes and reached 36.8GB. The dominant
 99   * cost is this array holding a second full copy of every message.
100   */
101  export const TEAMMATE_MESSAGES_UI_CAP = 50
102  
103  /**
104   * Append an item to a message array, capping the result at
105   * TEAMMATE_MESSAGES_UI_CAP entries by dropping the oldest. Always returns
106   * a new array (AppState immutability).
107   */
108  export function appendCappedMessage<T>(
109    prev: readonly T[] | undefined,
110    item: T,
111  ): T[] {
112    if (prev === undefined || prev.length === 0) {
113      return [item]
114    }
115    if (prev.length >= TEAMMATE_MESSAGES_UI_CAP) {
116      const next = prev.slice(-(TEAMMATE_MESSAGES_UI_CAP - 1))
117      next.push(item)
118      return next
119    }
120    return [...prev, item]
121  }