Setting the file. One moment.
Batching · Swarm · langchain-ai/langchain-skills · Skills Docs
ContentsBack to the top of the page
Size
9 KB scripts/ batching.ts
TypeScript · 312 lines · 9 KB
* Group an array of items into batches of a given size.
12 *
13 * The last batch may be smaller than `batchSize` if the total count
14 * is not evenly divisible.
15 *
16 * @param items - Array of items to batch.
17 * @param batchSize - Maximum number of items per batch.
18 * @returns Array of batches (each batch is an array of items).
19 */
20 export function createBatches < T >( items : T [], batchSize : number ) : T [][] {
21 const batches : T [][] = [];
22 for ( let i = 0 ; i < items. length ; i += batchSize) {
23 batches. push (items. slice (i, i + batchSize));
24 }
25 return batches;
26 }
27
28 /**
29 * Clamp a batch size to [1, MAX_BATCH_SIZE].
30 */
31 function clampBatchSize ( n : number ) : number {
32 return Math. max ( 1 , Math. min (Math. round (n), MAX_BATCH_SIZE ));
33 }
34
35 /**
36 * Resolve batch sizes and group rows into dispatch-ready batches.
37 *
38 * Handles all three modes:
39 * - **Auto** (`batchSize` undefined): computes a uniform size from row
40 * count and `maxSubagents` to stay within the concurrency budget.
41 * - **Uniform** (`batchSize` is a number): all rows use that size.
42 * - **Per-row** (`batchSize` is a function): evaluates per row, groups
43 * rows sharing the same batch size, then chunks each group.
44 *
45 * Every batch size is clamped to [1, MAX_BATCH_SIZE].
46 *
47 * @param rows - Matched rows to dispatch.
48 * @param batchSize - Batch strategy: undefined (auto), number, or function.
49 * @param maxSubagents - Concurrency cap used for auto-batch calculation.
50 * @returns Array of row batches, each ready for dispatch as a single task.
51 */
52 export function resolveBatchGroups (
53 rows : Record < string , unknown >[],
54 maxSubagents : number ,
55 batchSize ?: number | BatchFn ,
56 ) : Record < string , unknown >[][] {
57 if (rows. length === 0 ) {
58 return [];
59 }
60
61 if (batchSize === undefined ) {
62 // Auto: keep total dispatches under maxSubagents
63 const auto =
64 rows. length > maxSubagents
65 ? Math. min (Math. ceil (rows. length / maxSubagents), MAX_BATCH_SIZE )
66 : 1 ;
67 return createBatches (rows, auto);
68 }
69
70 if ( typeof batchSize === "number" ) {
71 return createBatches (rows, clampBatchSize (batchSize));
72 }
73
74 const groups = new Map < number , Record < string , unknown >[]>();
75 for ( const row of rows) {
76 const size = clampBatchSize ( batchSize (row, rows. length ));
77 let group = groups. get (size);
78 if ( ! group) {
79 group = [];
80 groups. set (size, group);
81 }
82 group. push (row);
83 }
84
85 const batches : Record < string , unknown >[][] = [];
86 for ( const [ size , group ] of groups) {
87 for ( const batch of createBatches (group, size)) {
88 batches. push (batch);
89 }
90 }
91
92 return batches;
93 }
94
95 /**
96 * Wrap a per-item JSON Schema into a batch-level response schema.
97 *
98 * Produces a schema of the form:
99 * ```json
100 * { "results": [{ "id": "...", ...itemProps }] }
101 * ```
102 *
103 * The item schema's properties are merged with an `id` field so each
104 * batch entry can be matched back to its row.
105 *
106 * @param itemSchema - Per-item JSON Schema.
107 * @returns Batch-level JSON Schema wrapping items in a `results` array.
108 */
109 export function wrapSchema (
110 itemSchema : Record < string , unknown >,
111 count ?: number ,
112 ) : Record < string , unknown > {
113 const props = (itemSchema.properties as Record < string , unknown >) ?? {};
114 const req = (itemSchema.required as string []) ?? [];
115 const itemProperties : Record < string , unknown > = {
116 id: { type: "string" },
117 ... props,
118 };
119 const itemRequired : string [] = [ "id" , ... req];
120
121 const resultsArray : Record < string , unknown > = {
122 type: "array" ,
123 items: {
124 type: "object" ,
125 additionalProperties: false ,
126 properties: itemProperties,
127 required: itemRequired,
128 },
129 };
130
131 if (count != null ) {
132 resultsArray.minItems = count;
133 resultsArray.maxItems = count;
134 }
135
136 return {
137 type: "object" ,
138 additionalProperties: false ,
139 properties: {
140 results: resultsArray,
141 },
142 required: [ "results" ],
143 };
144 }
145
146 /**
147 * Format a single column value for inclusion in a batch prompt.
148 *
149 * Strings are inserted verbatim; numbers/booleans are stringified;
150 * objects/arrays are JSON-serialized; `undefined` and `null` become
151 * the empty string so the row still renders with its id.
152 */
153 function formatValue ( value : unknown ) : string {
154 if (value === undefined || value === null ) {
155 return "" ;
156 }
157 if ( typeof value === "string" ) {
158 return value;
159 }
160 if ( typeof value === "number" || typeof value === "boolean" ) {
161 return String (value);
162 }
163 return JSON . stringify (value);
164 }
165
166 /**
167 * Rewrite `{col}` placeholders in the author's instruction to
168 * `` `col` `` (backtick-quoted column name, no braces). The model
169 * sees a column name as a name, never as template syntax.
170 */
171 function renderTaskBlock ( instruction : string ) : string {
172 return instruction. replace (
173 / \{ ( [ ^ }] + ) \} / g ,
174 ( _m , raw ) => ` \` ${ String ( raw ). trim () } \` ` ,
175 );
176 }
177
178 /**
179 * Render the items section.
180 *
181 * - 0 placeholders → `[id]` per row (no values, degenerate).
182 * - 1 placeholder → `[id] <value>` per row (flat).
183 * - 2+ placeholders → labeled block:
184 * [id]
185 * col1: <value>
186 * col2: <value>
187 */
188 function renderItemsBlock (
189 rows : Array < Record < string , unknown >>,
190 placeholders : string [],
191 ) : string {
192 const lines : string [] = [];
193
194 for ( const row of rows) {
195 const id = String (row.id);
196
197 if (placeholders. length === 0 ) {
198 lines. push ( `[${ id }]` );
199 continue ;
200 }
201
202 if (placeholders. length === 1 ) {
203 const value = readColumn (row, placeholders[ 0 ]);
204 lines. push ( `[${ id }] ${ formatValue ( value ) }` );
205 continue ;
206 }
207
208 lines. push ( `[${ id }]` );
209 for ( const col of placeholders) {
210 const value = readColumn (row, col);
211 lines. push ( ` ${ col }: ${ formatValue ( value ) }` );
212 }
213 }
214
215 return lines. join ( " \n " );
216 }
217
218 /**
219 * Build a single prompt for a batch of rows.
220 *
221 * The instruction is rewritten to drop template-syntax braces — every
222 * `{col}` becomes `` `col` `` so the model sees column names as names,
223 * not as slots it must fill in. Items are rendered as either a flat
224 * list (single-column case) or a labeled per-column block, so the
225 * binding from row id to column value is structural and explicit.
226 *
227 * @param instruction - Instruction template with `{column}` placeholders.
228 * @param rows - Array of row objects to include in the batch.
229 * @param context - Optional context prose prepended to the prompt.
230 * @returns A single prompt string covering all rows in the batch.
231 */
232 export function buildBatchPrompt (
233 instruction : string ,
234 rows : Array < Record < string , unknown >>,
235 context ?: string ,
236 ) : string {
237 const placeholders = extractPlaceholders (instruction);
238 const taskBlock = renderTaskBlock (instruction);
239 const itemsBlock = renderItemsBlock (rows, placeholders);
240
241 const parts : string [] = [];
242
243 if (context) {
244 parts. push (context);
245 parts. push ( "" );
246 }
247
248 parts. push ( "# Task" );
249 parts. push (taskBlock);
250 parts. push ( "" );
251
252 parts. push ( `# Items (${ rows . length })` );
253 if (placeholders. length === 1 ) {
254 parts. push ( `Each item below is the value of \` ${ placeholders [ 0 ] } \` .` );
255 parts. push ( "" );
256 } else if (placeholders. length > 1 ) {
257 const cols = placeholders. map (( p ) => ` \` ${ p } \` ` ). join ( ", " );
258 parts. push ( `Each item below provides ${ cols }.` );
259 parts. push ( "" );
260 }
261 parts. push (itemsBlock);
262 parts. push ( "" );
263
264 parts. push (
265 `Return a JSON object with a 'results' array of exactly ${ rows . length } ` +
266 "entries, each including the item's 'id' exactly as shown above." ,
267 );
268
269 return parts. join ( " \n " );
270 }
271
272 /**
273 * Unpack a batch response string into per-row results.
274 *
275 * Parses the JSON response expecting `{ results: [{ id, ...fields }] }`.
276 * Maps each item's `id` to its remaining fields. IDs present in
277 * `expectedIds` but absent from the response are returned in `missing`.
278 *
279 * @param response - Raw JSON string from the subagent.
280 * @param expectedIds - List of row IDs the batch was supposed to cover.
281 * @returns Map of ID → result fields, plus a list of IDs missing from
282 * the response.
283 */
284 export function unpackBatchResults (
285 response : string ,
286 expectedIds : string [],
287 ) : { results : Map < string , unknown >; missing : string [] } {
288 const resultsMap = new Map < string , unknown >();
289 const missing : string [] = [];
290
291 try {
292 const parsed = JSON . parse (response);
293 const items : Array < Record < string , unknown >> = parsed?.results ?? [];
294
295 for ( const item of items) {
296 if (item && typeof item.id === "string" ) {
297 const { id , ... fields } = item;
298 resultsMap. set (id, fields);
299 }
300 }
301 } catch {
302 // Parse failure — all IDs are missing
303 }
304
305 for ( const id of expectedIds) {
306 if ( ! resultsMap. has (id)) {
307 missing. push (id);
308 }
309 }
310
311 return { results: resultsMap, missing };
312 }