File size: 4,322 Bytes
064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | 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]
}
|