Setting the file. One moment.
Table · Swarm · langchain-ai/langchain-skills · Skills Docs
ContentsBack to the top of the page This file
Number 22.6
Position 6 of 8
Type TypeScript
Size 16 KB
Lines 561 scripts/ table.ts
TypeScript · 561 lines · 16 KB
})
=>
Promise
<
string
>;
11 readFile ?: ( args : { file_path : string }) => Promise < string >;
12 writeFile ?: ( args : { file_path : string ; content : string }) => Promise < string >;
13 editFile ?: ( args : {
14 file_path : string ;
15 old_string : string ;
16 new_string : string ;
17 }) => Promise < string >;
18 };
19
20 /**
21 * Session ID injected by the QuickJS middleware as a global.
22 * Scopes table files to the current conversation thread.
23 */
24 declare const __sessionId__ : string | undefined ;
25
26 /**
27 * Sanitize a session ID for use as a directory name component.
28 * Replaces any character that isn't alphanumeric, hyphen, or underscore
29 * with an underscore, and caps length to prevent excessively long paths.
30 */
31 function sanitizeSessionId ( id : string ) : string {
32 return id. replace ( / [ ^ a-zA-Z0-9_-] / g , "_" ). slice ( 0 , 64 );
33 }
34
35 /**
36 * Directory prefix for all table JSONL files, scoped to the session.
37 */
38 function getTableDir () : string {
39 const id = typeof __sessionId__ !== "undefined" ? __sessionId__ : "default" ;
40 return `/tmp/.swarm/${ sanitizeSessionId ( id ) }` ;
41 }
42
43 /**
44 * Maximum number of tables before oldest are evicted.
45 */
46 const MAX_TABLES = 5 ;
47
48 /**
49 * A table's rows and backend file path, cached in memory to avoid
50 * redundant PTC reads within the same session.
51 */
52 interface CachedTable {
53 /**
54 * The table's row data. Mutated in place during `run()`.
55 */
56 rows : Record < string , unknown >[];
57
58 /**
59 * Backend file path (e.g. `".swarm/003-t_a1b2c3.jsonl"`).
60 */
61 path : string ;
62
63 /**
64 * The JSONL content from the most recent successful write.
65 * Used as `old_string` when falling back to editFile for overwrites.
66 */
67 lastWritten : string ;
68 }
69
70 /**
71 * In-memory table cache keyed by table ID.
72 */
73 const cache = new Map < string , CachedTable >();
74
75 /**
76 * Monotonic counter for table file sequence numbers.
77 */
78 let sequenceCounter = 0 ;
79
80 /**
81 * Reset all module-level state for testing.
82 *
83 * Clears the in-memory cache and resets the sequence counter.
84 */
85 export function _resetForTesting () : void {
86 cache. clear ();
87 sequenceCounter = 0 ;
88 }
89
90 /**
91 * Generate a random 6-hex-char table ID prefixed with `t_`.
92 *
93 * @returns A string like `"t_a1b2c3"`.
94 */
95 export function generateId () : string {
96 const hex = Math. floor (Math. random () * 0xffffff )
97 . toString ( 16 )
98 . padStart ( 6 , "0" );
99 return `t_${ hex }` ;
100 }
101
102 /**
103 * Build the backend file path for a table.
104 *
105 * @param sequence - Zero-padded monotonic sequence number.
106 * @param id - Table ID (e.g. `"t_a1b2c3"`).
107 * @returns Path like `".swarm/003-t_a1b2c3.jsonl"`.
108 */
109 export function tablePath ( sequence : number , id : string ) : string {
110 const padded = String (sequence). padStart ( 3 , "0" );
111 return `${ getTableDir () }/${ padded }-${ id }.jsonl` ;
112 }
113
114 /**
115 * Serialize an array of row objects to JSONL format.
116 * One JSON object per line, no trailing newline.
117 *
118 * @param rows - Array of row objects to serialize.
119 * @returns JSONL string.
120 */
121 export function serializeJsonl ( rows : Record < string , unknown >[]) : string {
122 return rows. map (( r ) => JSON . stringify (r)). join ( " \n " );
123 }
124
125 /**
126 * Parse a JSONL string into an array of row objects.
127 * Validates that each line parses to a non-null, non-array object.
128 *
129 * @param content - Raw JSONL content from the backend.
130 * @returns Array of parsed row objects.
131 * @throws Error with line number if any line is malformed.
132 */
133 export function parseJsonl ( content : string ) : Record < string , unknown >[] {
134 if ( ! content. trim ()) {
135 return [];
136 }
137
138 const parseLine = ( line : string , idx : number ) : Record < string , unknown > => {
139 try {
140 const parsed = JSON . parse (line);
141 if (
142 typeof parsed !== "object" ||
143 parsed === null ||
144 Array. isArray (parsed)
145 ) {
146 throw new Error ( `expected object` );
147 }
148 return parsed as Record < string , unknown >;
149 } catch (e) {
150 throw new Error (
151 `JSONL parse error at line ${ idx + 1 }: ${ ( e as Error ). message }` ,
152 { cause: e },
153 );
154 }
155 };
156
157 return content
158 . split ( " \n " )
159 . filter (( line ) => line. trim () !== "" )
160 . map (parseLine);
161 }
162
163 /**
164 * Extract a table ID from a `.swarm/NNN-t_XXXXXX.jsonl` filename.
165 *
166 * @param filePath - Full path to a table JSONL file.
167 * @returns The table ID (e.g. `"t_a1b2c3"`), or `undefined` if the
168 * filename doesn't match the expected pattern.
169 */
170 export function extractIdFromPath ( filePath : string ) : string | undefined {
171 const filename = filePath. split ( "/" ). pop () || "" ;
172 const match = filename. match ( / ^ \d + -(t_ [a-f0-9] + ) \. jsonl $ / );
173 return match ? match[ 1 ] : undefined ;
174 }
175
176 /**
177 * Extract the sequence number from a `.swarm/NNN-t_XXXXXX.jsonl` filename.
178 *
179 * @param filePath - Full path to a table JSONL file.
180 * @returns The sequence number, or `0` if the filename doesn't match.
181 */
182 export function extractSeqFromPath ( filePath : string ) : number {
183 const filename = filePath. split ( "/" ). pop () || "" ;
184 const match = filename. match ( / ^ ( \d + )-/ );
185 return match ? parseInt (match[ 1 ], 10 ) : 0 ;
186 }
187
188 /**
189 * Build `{ id, file }` rows from a list of file paths.
190 *
191 * Uses the basename (last path segment) as the row ID. When multiple
192 * paths share the same basename, disambiguates by prepending the
193 * parent directory name (e.g. `"routes-index.ts"` vs `"handlers-index.ts"`).
194 *
195 * @param paths - List of file paths.
196 * @returns Array of `{ id, file }` row objects.
197 */
198 export function pathsToRows (
199 paths : string [],
200 ) : Array <{ id : string ; file : string }> {
201 const basenames = paths. map (( p ) => {
202 const parts = p. split ( "/" );
203 return parts[parts. length - 1 ] || p;
204 });
205
206 const counts = new Map < string , number >();
207 for ( const basename of basenames) {
208 counts. set (basename, (counts. get (basename) ?? 0 ) + 1 );
209 }
210
211 return paths. map (( filePath , idx ) => {
212 let id = basenames[idx];
213 if ((counts. get (id) ?? 0 ) > 1 ) {
214 const parts = filePath. split ( "/" );
215 if (parts. length >= 2 ) {
216 id = `${ parts [ parts . length - 2 ] }-${ id }` ;
217 }
218 }
219 return { id, file: filePath };
220 });
221 }
222
223 /**
224 * Find duplicate `id` values in a row array.
225 *
226 * @param rows - Row objects to scan.
227 * @returns Array of duplicate ids, in first-seen order, deduplicated.
228 */
229 function findDuplicateIds ( rows : Record < string , unknown >[]) : string [] {
230 const seen = new Set < string >();
231 const dupes = new Set < string >();
232 for ( const row of rows) {
233 const id = String (row.id);
234 if (seen. has (id)) {
235 dupes. add (id);
236 } else {
237 seen. add (id);
238 }
239 }
240 return [ ... dupes];
241 }
242
243 /**
244 * Resolve a glob pattern to a list of file paths via the PTC `glob` tool.
245 *
246 * Handles both `string[]` and `{ path: string }[]` return formats
247 * from different glob tool implementations.
248 *
249 * @param pattern - Glob pattern to resolve.
250 * @returns Array of matching file paths.
251 * @throws Error if the `glob` PTC tool is not configured.
252 *
253 * @internal
254 */
255 export async function globFiles ( pattern : string ) : Promise < string []> {
256 if ( typeof tools.glob !== "function" ) {
257 throw new Error ( `Swarm requires a 'glob' tool in the PTC configuration` );
258 }
259
260 const raw = await tools. glob ({ pattern });
261 const parsed = JSON . parse (raw);
262 if ( ! Array. isArray (parsed)) {
263 return [];
264 }
265
266 const paths : string [] = [];
267 for ( const item of parsed) {
268 if ( typeof item === "string" ) {
269 paths. push (item);
270 } else if (item && typeof item.path === "string" ) {
271 paths. push (item.path);
272 }
273 }
274
275 return paths;
276 }
277
278 /**
279 * Read a file's content from the backend via the PTC `readFile` tool.
280 *
281 * @param path - Backend file path.
282 * @returns The file content as a string.
283 * @throws Error if the `readFile` PTC tool is not configured.
284 *
285 * @internal
286 */
287 export async function readFile ( path : string ) : Promise < string > {
288 if ( typeof tools.readFile !== "function" ) {
289 throw new Error (
290 `Swarm requires a 'readFile' tool in the PTC configuration` ,
291 );
292 }
293 return tools. readFile ({ file_path: path });
294 }
295
296 /**
297 * Write string content to a backend file via the PTC `writeFile` tool.
298 *
299 * If the file already exists, falls back to `editFile` for a full
300 * replacement — the backend's `write` rejects overwrites by design.
301 * When `previousContent` is provided it is used as the `old_string`
302 * for the edit, avoiding an unreliable round-trip through readFile.
303 *
304 * @param path - Backend file path. Created if it doesn't exist.
305 * @param content - String content to write.
306 * @param previousContent - The last-known content of the file, used
307 * as `old_string` for the editFile fallback.
308 * @throws Error if the `writeFile` PTC tool is not configured.
309 *
310 * @internal
311 */
312 export async function writeFile (
313 path : string ,
314 content : string ,
315 previousContent ?: string ,
316 ) : Promise < void > {
317 if ( typeof tools.writeFile !== "function" ) {
318 throw new Error (
319 `Swarm requires a 'writeFile' tool in the PTC configuration` ,
320 );
321 }
322 const result = await tools. writeFile ({ file_path: path, content });
323 if ( typeof result === "string" && result. includes ( "already exists" )) {
324 if ( typeof tools.editFile !== "function" ) {
325 throw new Error (
326 "Swarm requires an 'edit_file' PTC tool to update existing tables" ,
327 );
328 }
329 if (previousContent == null ) {
330 throw new Error (
331 `Cannot overwrite ${ path }: file already exists and no previous content available` ,
332 );
333 }
334 await tools. editFile ({
335 file_path: path,
336 old_string: previousContent,
337 new_string: content,
338 });
339 }
340 }
341
342 /**
343 * List all table JSONL files in the `.swarm/` directory, sorted by
344 * filename (which encodes creation order via the sequence prefix).
345 *
346 * @returns Sorted array of file paths, or empty array on failure.
347 */
348 async function listTableFiles () : Promise < string []> {
349 try {
350 const files = await globFiles ( `${ getTableDir () }/*.jsonl` );
351 return files. sort ();
352 } catch {
353 return [];
354 }
355 }
356
357 /**
358 * Evict the oldest tables when the count meets or exceeds `MAX_TABLES`.
359 *
360 * Clears evicted entries from the in-memory cache and overwrites
361 * backend files with empty content (no delete_file tool available).
362 * Empty files are treated as evicted by `loadTable`.
363 */
364 async function evict () : Promise < void > {
365 const files = await listTableFiles ();
366 if (files. length < MAX_TABLES ) {
367 return ;
368 }
369
370 const toEvict = files. slice ( 0 , files. length - MAX_TABLES + 1 );
371 for ( const filePath of toEvict) {
372 const id = extractIdFromPath (filePath);
373 const prev = id ? cache. get (id)?.lastWritten : undefined ;
374 if (id) {
375 cache. delete (id);
376 }
377 try {
378 await writeFile (filePath, "" , prev);
379 } catch {
380 // Best-effort eviction — non-fatal if overwrite fails
381 }
382 }
383 }
384
385 /**
386 * Determine the next sequence number for a new table file.
387 *
388 * Reads existing files on the backend to avoid sequence collisions
389 * across runs (same thread, new session). The counter only advances
390 * forward — it never reuses a sequence number.
391 *
392 * @returns The next available sequence number.
393 */
394 async function nextSequence () : Promise < number > {
395 const files = await listTableFiles ();
396 if (files. length > 0 ) {
397 const lastSequence = extractSeqFromPath (files[files. length - 1 ]);
398 if (lastSequence >= sequenceCounter) {
399 sequenceCounter = lastSequence + 1 ;
400 }
401 }
402 return sequenceCounter ++ ;
403 }
404
405 /**
406 * Resolve one or more glob patterns into a deduplicated, sorted list
407 * of file paths.
408 *
409 * @param pattern - A single glob string or array of glob strings.
410 * @returns Sorted, deduplicated array of matching file paths.
411 * @throws Error if no files match any of the provided patterns.
412 */
413 async function resolveGlob ( pattern : string | string []) : Promise < string []> {
414 const patterns = Array. isArray (pattern) ? pattern : [pattern];
415 const allPaths : string [] = [];
416 for ( const p of patterns) {
417 const paths = await globFiles (p);
418 allPaths. push ( ... paths);
419 }
420
421 const unique = [ ...new Set (allPaths)]. sort ();
422 if (unique. length === 0 ) {
423 throw new Error ( `No files matched pattern: ${ JSON . stringify ( pattern ) }` );
424 }
425
426 return unique;
427 }
428
429 /**
430 * Create a table from a source spec.
431 *
432 * Validates the source, builds rows, runs eviction if the table count
433 * is at capacity, persists the new table to the backend as JSONL, and
434 * returns a lightweight handle.
435 *
436 * @param source - Exactly one of `glob`, `filePaths`, or `tasks`.
437 * @returns A handle with the table's ID, row count, and column names.
438 * @throws Error if the source is invalid, empty, or missing required PTC tools.
439 */
440 export async function createTable ( source : CreateSource ) : Promise < SwarmHandle > {
441 const sourceCount = [source.glob, source.filePaths, source.tasks]. filter (
442 ( s ) => s != null ,
443 ). length ;
444
445 if (sourceCount === 0 ) {
446 throw new Error (
447 "create() requires exactly one source: glob, filePaths, or tasks" ,
448 );
449 }
450
451 if (sourceCount > 1 ) {
452 throw new Error ( "create() accepts only one source type at a time" );
453 }
454
455 let rows : Record < string , unknown >[];
456
457 if (source.glob != null ) {
458 const paths = await resolveGlob (source.glob);
459 rows = pathsToRows (paths);
460 } else if (source.filePaths != null ) {
461 if (source.filePaths. length === 0 ) {
462 throw new Error ( "filePaths array is empty" );
463 }
464 rows = pathsToRows (source.filePaths);
465 } else {
466 const tasks = source.tasks ?? [];
467 if (tasks. length === 0 ) {
468 throw new Error ( "tasks array is empty" );
469 }
470
471 for ( let idx = 0 ; idx < tasks. length ; idx ++ ) {
472 if ( typeof tasks[idx].id !== "string" ) {
473 throw new Error ( `tasks[${ idx }] is missing string 'id' field` );
474 }
475 }
476
477 rows = tasks;
478 }
479
480 const dupes = findDuplicateIds (rows);
481 if (dupes. length > 0 ) {
482 throw new Error ( `create() received duplicate row ids: ${ dupes . join ( ", " ) }` );
483 }
484
485 await evict ();
486
487 const id = generateId ();
488 const seq = await nextSequence ();
489 const path = tablePath (seq, id);
490
491 const content = serializeJsonl (rows);
492 await writeFile (path, content);
493 cache. set (id, { rows, path, lastWritten: content });
494
495 return {
496 id,
497 count: rows. length ,
498 columns: Object. keys (rows[ 0 ] ?? {}),
499 };
500 }
501
502 /**
503 * Load a table's rows by ID.
504 *
505 * Checks the in-memory cache first. On a cache miss (e.g. cross-run
506 * resume), globs the backend to locate the JSONL file, reads and
507 * parses it, and populates the cache.
508 *
509 * @param id - The table ID from a `SwarmHandle`.
510 * @returns The table's row array (by reference — mutations are visible).
511 * @throws Error if the table is not found (evicted or never created).
512 */
513 export async function loadTable (
514 id : string ,
515 ) : Promise < Record < string , unknown >[]> {
516 const cached = cache. get (id);
517 if (cached) {
518 return cached.rows;
519 }
520
521 const files = await listTableFiles ();
522 const match = files. find (( f ) => f. endsWith ( `-${ id }.jsonl` ));
523 if ( ! match) {
524 throw new Error ( `Table "${ id }" not found. It may have been evicted` );
525 }
526
527 const content = await readFile (match);
528 if ( ! content. trim ()) {
529 throw new Error ( `Table "${ id }" not found. It may have been evicted` );
530 }
531
532 const rows = parseJsonl (content);
533 cache. set (id, { rows, path: match, lastWritten: serializeJsonl (rows) });
534
535 return rows;
536 }
537
538 /**
539 * Persist a table's current rows to the backend.
540 *
541 * Updates both the in-memory cache and the backend JSONL file.
542 * The table must have been previously loaded via `loadTable` so
543 * that its backend file path is known.
544 *
545 * @param id - The table ID from a `SwarmHandle`.
546 * @param rows - The updated row array to persist.
547 * @throws Error if the table has not been loaded into cache.
548 */
549 export async function saveTable (
550 id : string ,
551 rows : Record < string , unknown >[],
552 ) : Promise < void > {
553 const cached = cache. get (id);
554 if ( ! cached) {
555 throw new Error ( `Table "${ id }" is not loaded - call loadTable first` );
556 }
557 cached.rows = rows;
558 const content = serializeJsonl (rows);
559 await writeFile (cached.path, content, cached.lastWritten);
560 cached.lastWritten = content;
561 }