| 126 | }; |
| 127 | |
| 128 | export class SubagentBatch<T> { |
| 129 | private readonly states: Array<TaskState<T>>; |
| 130 | private readonly pending: Array<TaskState<T>>; |
| 131 | private readonly results: Array<SubagentResult<T> | undefined>; |
| 132 | private readonly active = new Set<ActiveAttempt<T>>(); |
| 133 | private readonly controller = new AbortController(); |
| 134 | private readonly batchSignal: AbortSignal | undefined; |
| 135 | private readonly batchAbortListener: () => void; |
| 136 | private readonly maxConcurrency: number | undefined; |
| 137 | private normalLaunchCount = 0; |
| 138 | private normalLaunchTimer: ReturnType<typeof setTimeout> | undefined; |
| 139 | private rateLimitLaunchTimer: ReturnType<typeof setTimeout> | undefined; |
| 140 | private resolve: ((results: Array<SubagentResult<T>>) => void) | undefined; |
| 141 | private reject: ((error: unknown) => void) | undefined; |
| 142 | private finished = false; |
| 143 | private started = false; |
| 144 | private rateLimitMode = false; |
| 145 | private startedSuccessCount = 0; |
| 146 | private rateLimitCapacity = 1; |
| 147 | private lastRateLimitAt: number | undefined; |
| 148 | private lastCapacityShrinkAt: number | undefined; |
| 149 | private lastCapacityRecoveryAt: number | undefined; |
| 150 | private globalRetryIntervalMs = RATE_LIMIT_RETRY_BASE_MS; |
| 151 | private nextRateLimitLaunchAt = 0; |
| 152 | |
| 153 | constructor( |
| 154 | private readonly launcher: SubagentBatchLauncher, |
| 155 | tasks: readonly QueuedSubagentTask<T>[], |
| 156 | options: SubagentBatchOptions = {}, |
| 157 | ) { |
| 158 | this.maxConcurrency = options.maxConcurrency; |
| 159 | this.states = tasks.map((task, index) => ({ |
| 160 | index, |
| 161 | task, |
| 162 | retryCount: 0, |
| 163 | retryReadyAt: 0, |
| 164 | started: false, |
| 165 | })); |
| 166 | this.pending = [...this.states]; |
| 167 | this.results = Array.from({ length: tasks.length }); |
| 168 | this.batchSignal = tasks.find((task) => task.signal !== undefined)?.signal; |
| 169 | this.batchAbortListener = () => { |
| 170 | this.controller.abort(this.batchSignal?.reason); |
| 171 | if (isUserCancellation(this.batchSignal?.reason)) { |
| 172 | this.finishWithUserCancellation(); |
| 173 | } else { |
| 174 | this.fail(this.batchSignal?.reason ?? new Error('Aborted')); |
| 175 | } |
| 176 | }; |
| 177 | } |
| 178 | |
| 179 | run(): Promise<Array<SubagentResult<T>>> { |
| 180 | if (this.started) { |
| 181 | throw new Error('SubagentBatch.run() can only be called once.'); |
| 182 | } |
| 183 | this.started = true; |
| 184 | |
| 185 | return new Promise((resolve, reject) => { |
nothing calls this directly
no outgoing calls
no test coverage detected