recursive-llm-ts 4.9.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,575 @@
1
+ package rlm
2
+
3
+ import (
4
+ "database/sql"
5
+ "encoding/json"
6
+ "fmt"
7
+ "regexp"
8
+ "strings"
9
+ "time"
10
+
11
+ _ "modernc.org/sqlite"
12
+ )
13
+
14
+ // SQLiteBackend is a SQLite-based StoreBackend implementation.
15
+ type SQLiteBackend struct {
16
+ db *sql.DB
17
+ }
18
+
19
+ // NewSQLiteBackend creates a new SQLite backend and runs schema migrations.
20
+ func NewSQLiteBackend(dbPath string) (*SQLiteBackend, error) {
21
+ db, err := sql.Open("sqlite", dbPath)
22
+ if err != nil {
23
+ return nil, fmt.Errorf("open sqlite database: %w", err)
24
+ }
25
+
26
+ backend := &SQLiteBackend{db: db}
27
+
28
+ if _, err := backend.db.Exec(`PRAGMA journal_mode = WAL;`); err != nil {
29
+ _ = backend.db.Close()
30
+ return nil, fmt.Errorf("enable WAL mode: %w", err)
31
+ }
32
+
33
+ if _, err := backend.db.Exec(`PRAGMA foreign_keys = ON;`); err != nil {
34
+ _ = backend.db.Close()
35
+ return nil, fmt.Errorf("enable foreign keys: %w", err)
36
+ }
37
+
38
+ if err := backend.migrate(); err != nil {
39
+ _ = backend.db.Close()
40
+ return nil, err
41
+ }
42
+
43
+ return backend, nil
44
+ }
45
+
46
+ func (b *SQLiteBackend) migrate() error {
47
+ if _, err := b.db.Exec(`
48
+ CREATE TABLE IF NOT EXISTS messages (
49
+ id TEXT PRIMARY KEY,
50
+ role TEXT,
51
+ content TEXT,
52
+ tokens INTEGER,
53
+ timestamp TEXT,
54
+ file_ids TEXT DEFAULT '[]',
55
+ metadata TEXT DEFAULT '{}'
56
+ );
57
+ `); err != nil {
58
+ return fmt.Errorf("create messages table: %w", err)
59
+ }
60
+
61
+ if _, err := b.db.Exec(`
62
+ CREATE INDEX IF NOT EXISTS idx_messages_role ON messages(role);
63
+ `); err != nil {
64
+ return fmt.Errorf("create messages role index: %w", err)
65
+ }
66
+
67
+ if _, err := b.db.Exec(`
68
+ CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
69
+ `); err != nil {
70
+ return fmt.Errorf("create messages timestamp index: %w", err)
71
+ }
72
+
73
+ if _, err := b.db.Exec(`
74
+ CREATE TABLE IF NOT EXISTS summaries (
75
+ id TEXT PRIMARY KEY,
76
+ kind TEXT,
77
+ content TEXT,
78
+ tokens INTEGER,
79
+ level INTEGER,
80
+ created_at TEXT,
81
+ message_ids TEXT DEFAULT '[]',
82
+ child_ids TEXT DEFAULT '[]',
83
+ parent_id TEXT DEFAULT '',
84
+ file_ids TEXT DEFAULT '[]'
85
+ );
86
+ `); err != nil {
87
+ return fmt.Errorf("create summaries table: %w", err)
88
+ }
89
+
90
+ if _, err := b.db.Exec(`
91
+ CREATE INDEX IF NOT EXISTS idx_summaries_kind ON summaries(kind);
92
+ `); err != nil {
93
+ return fmt.Errorf("create summaries kind index: %w", err)
94
+ }
95
+
96
+ if _, err := b.db.Exec(`
97
+ CREATE INDEX IF NOT EXISTS idx_summaries_parent_id ON summaries(parent_id);
98
+ `); err != nil {
99
+ return fmt.Errorf("create summaries parent_id index: %w", err)
100
+ }
101
+
102
+ if _, err := b.db.Exec(`
103
+ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
104
+ id,
105
+ content,
106
+ content='messages',
107
+ content_rowid='rowid'
108
+ );
109
+ `); err != nil {
110
+ return fmt.Errorf("create messages_fts table: %w", err)
111
+ }
112
+
113
+ if _, err := b.db.Exec(`
114
+ CREATE TRIGGER IF NOT EXISTS messages_ai
115
+ AFTER INSERT ON messages
116
+ BEGIN
117
+ INSERT INTO messages_fts(rowid, id, content)
118
+ VALUES (new.rowid, new.id, new.content);
119
+ END;
120
+ `); err != nil {
121
+ return fmt.Errorf("create messages_ai trigger: %w", err)
122
+ }
123
+
124
+ return nil
125
+ }
126
+
127
+ func (b *SQLiteBackend) PersistMessage(msg *StoreMessage) error {
128
+ if msg == nil {
129
+ return fmt.Errorf("message is nil")
130
+ }
131
+
132
+ fileIDsJSON, err := marshalJSON(msg.FileIDs, "[]")
133
+ if err != nil {
134
+ return fmt.Errorf("marshal message file_ids: %w", err)
135
+ }
136
+
137
+ metadataJSON, err := marshalJSON(msg.Metadata, "{}")
138
+ if err != nil {
139
+ return fmt.Errorf("marshal message metadata: %w", err)
140
+ }
141
+
142
+ timestamp := msg.Timestamp
143
+ if timestamp.IsZero() {
144
+ timestamp = time.Now()
145
+ }
146
+
147
+ _, err = b.db.Exec(`
148
+ INSERT OR REPLACE INTO messages (id, role, content, tokens, timestamp, file_ids, metadata)
149
+ VALUES (?, ?, ?, ?, ?, ?, ?);
150
+ `, msg.ID, string(msg.Role), msg.Content, msg.Tokens, timestamp.Format(time.RFC3339Nano), fileIDsJSON, metadataJSON)
151
+ if err != nil {
152
+ return fmt.Errorf("persist message %s: %w", msg.ID, err)
153
+ }
154
+
155
+ return nil
156
+ }
157
+
158
+ func (b *SQLiteBackend) GetMessage(id string) (*StoreMessage, error) {
159
+ row := b.db.QueryRow(`
160
+ SELECT id, role, content, tokens, timestamp, file_ids, metadata
161
+ FROM messages
162
+ WHERE id = ?;
163
+ `, id)
164
+
165
+ msg, err := scanMessage(row)
166
+ if err == sql.ErrNoRows {
167
+ return nil, nil
168
+ }
169
+ if err != nil {
170
+ return nil, fmt.Errorf("get message %s: %w", id, err)
171
+ }
172
+
173
+ return msg, nil
174
+ }
175
+
176
+ func (b *SQLiteBackend) GetAllMessages() ([]*StoreMessage, error) {
177
+ rows, err := b.db.Query(`
178
+ SELECT id, role, content, tokens, timestamp, file_ids, metadata
179
+ FROM messages
180
+ ORDER BY timestamp ASC;
181
+ `)
182
+ if err != nil {
183
+ return nil, fmt.Errorf("query all messages: %w", err)
184
+ }
185
+ defer rows.Close()
186
+
187
+ messages := make([]*StoreMessage, 0)
188
+ for rows.Next() {
189
+ msg, err := scanMessage(rows)
190
+ if err != nil {
191
+ return nil, fmt.Errorf("scan message: %w", err)
192
+ }
193
+ messages = append(messages, msg)
194
+ }
195
+
196
+ if err := rows.Err(); err != nil {
197
+ return nil, fmt.Errorf("iterate all messages: %w", err)
198
+ }
199
+
200
+ return messages, nil
201
+ }
202
+
203
+ func (b *SQLiteBackend) MessageCount() (int, error) {
204
+ var count int
205
+ if err := b.db.QueryRow(`SELECT COUNT(*) FROM messages;`).Scan(&count); err != nil {
206
+ return 0, fmt.Errorf("count messages: %w", err)
207
+ }
208
+ return count, nil
209
+ }
210
+
211
+ func (b *SQLiteBackend) PersistSummary(node *SummaryNode) error {
212
+ if node == nil {
213
+ return fmt.Errorf("summary node is nil")
214
+ }
215
+
216
+ messageIDsJSON, err := marshalJSON(node.MessageIDs, "[]")
217
+ if err != nil {
218
+ return fmt.Errorf("marshal summary message_ids: %w", err)
219
+ }
220
+
221
+ childIDsJSON, err := marshalJSON(node.ChildIDs, "[]")
222
+ if err != nil {
223
+ return fmt.Errorf("marshal summary child_ids: %w", err)
224
+ }
225
+
226
+ fileIDsJSON, err := marshalJSON(node.FileIDs, "[]")
227
+ if err != nil {
228
+ return fmt.Errorf("marshal summary file_ids: %w", err)
229
+ }
230
+
231
+ createdAt := node.CreatedAt
232
+ if createdAt.IsZero() {
233
+ createdAt = time.Now()
234
+ }
235
+
236
+ _, err = b.db.Exec(`
237
+ INSERT OR REPLACE INTO summaries (id, kind, content, tokens, level, created_at, message_ids, child_ids, parent_id, file_ids)
238
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
239
+ `, node.ID, string(node.Kind), node.Content, node.Tokens, node.Level, createdAt.Format(time.RFC3339Nano), messageIDsJSON, childIDsJSON, node.ParentID, fileIDsJSON)
240
+ if err != nil {
241
+ return fmt.Errorf("persist summary %s: %w", node.ID, err)
242
+ }
243
+
244
+ return nil
245
+ }
246
+
247
+ func (b *SQLiteBackend) GetSummary(id string) (*SummaryNode, error) {
248
+ row := b.db.QueryRow(`
249
+ SELECT id, kind, content, tokens, level, created_at, message_ids, child_ids, parent_id, file_ids
250
+ FROM summaries
251
+ WHERE id = ?;
252
+ `, id)
253
+
254
+ summary, err := scanSummary(row)
255
+ if err == sql.ErrNoRows {
256
+ return nil, nil
257
+ }
258
+ if err != nil {
259
+ return nil, fmt.Errorf("get summary %s: %w", id, err)
260
+ }
261
+
262
+ return summary, nil
263
+ }
264
+
265
+ func (b *SQLiteBackend) GetAllSummaries() ([]*SummaryNode, error) {
266
+ rows, err := b.db.Query(`
267
+ SELECT id, kind, content, tokens, level, created_at, message_ids, child_ids, parent_id, file_ids
268
+ FROM summaries
269
+ ORDER BY created_at ASC;
270
+ `)
271
+ if err != nil {
272
+ return nil, fmt.Errorf("query all summaries: %w", err)
273
+ }
274
+ defer rows.Close()
275
+
276
+ summaries := make([]*SummaryNode, 0)
277
+ for rows.Next() {
278
+ summary, err := scanSummary(rows)
279
+ if err != nil {
280
+ return nil, fmt.Errorf("scan summary: %w", err)
281
+ }
282
+ summaries = append(summaries, summary)
283
+ }
284
+
285
+ if err := rows.Err(); err != nil {
286
+ return nil, fmt.Errorf("iterate all summaries: %w", err)
287
+ }
288
+
289
+ return summaries, nil
290
+ }
291
+
292
+ func (b *SQLiteBackend) UpdateSummaryParent(summaryID, parentID string) error {
293
+ _, err := b.db.Exec(`
294
+ UPDATE summaries
295
+ SET parent_id = ?
296
+ WHERE id = ?;
297
+ `, parentID, summaryID)
298
+ if err != nil {
299
+ return fmt.Errorf("update summary parent for %s: %w", summaryID, err)
300
+ }
301
+ return nil
302
+ }
303
+
304
+ func (b *SQLiteBackend) GrepMessages(pattern string, summaryScope *string, maxResults int) ([]*StoreMessage, error) {
305
+ if maxResults <= 0 {
306
+ maxResults = 100
307
+ }
308
+
309
+ scopeSet, err := b.scopeMessageSet(summaryScope)
310
+ if err != nil {
311
+ return nil, err
312
+ }
313
+
314
+ if isSimpleFTSPattern(pattern) {
315
+ ftsQuery := "\"" + strings.ReplaceAll(pattern, "\"", "\"\"") + "\""
316
+ rows, ftsErr := b.db.Query(`
317
+ SELECT m.id, m.role, m.content, m.tokens, m.timestamp, m.file_ids, m.metadata
318
+ FROM messages_fts f
319
+ JOIN messages m ON m.rowid = f.rowid
320
+ WHERE messages_fts MATCH ?
321
+ ORDER BY m.timestamp ASC;
322
+ `, ftsQuery)
323
+ if ftsErr == nil {
324
+ defer rows.Close()
325
+
326
+ results := make([]*StoreMessage, 0, maxResults)
327
+ for rows.Next() {
328
+ msg, err := scanMessage(rows)
329
+ if err != nil {
330
+ return nil, fmt.Errorf("scan fts message: %w", err)
331
+ }
332
+ if scopeSet != nil {
333
+ if _, ok := scopeSet[msg.ID]; !ok {
334
+ continue
335
+ }
336
+ }
337
+ results = append(results, msg)
338
+ if len(results) >= maxResults {
339
+ break
340
+ }
341
+ }
342
+ if err := rows.Err(); err != nil {
343
+ return nil, fmt.Errorf("iterate fts messages: %w", err)
344
+ }
345
+ return results, nil
346
+ }
347
+ }
348
+
349
+ // Fallback: filter in Go with regex over stored messages.
350
+ messages, err := b.GetAllMessages()
351
+ if err != nil {
352
+ return nil, err
353
+ }
354
+
355
+ regexPattern := pattern
356
+ if isSimpleFTSPattern(pattern) {
357
+ regexPattern = regexp.QuoteMeta(pattern)
358
+ }
359
+
360
+ re, err := regexp.Compile(regexPattern)
361
+ if err != nil {
362
+ return nil, fmt.Errorf("compile grep regex %q: %w", pattern, err)
363
+ }
364
+
365
+ results := make([]*StoreMessage, 0, maxResults)
366
+ for _, msg := range messages {
367
+ if scopeSet != nil {
368
+ if _, ok := scopeSet[msg.ID]; !ok {
369
+ continue
370
+ }
371
+ }
372
+ if !re.MatchString(msg.Content) {
373
+ continue
374
+ }
375
+ results = append(results, msg)
376
+ if len(results) >= maxResults {
377
+ break
378
+ }
379
+ }
380
+
381
+ return results, nil
382
+ }
383
+
384
+ func (b *SQLiteBackend) Close() error {
385
+ if b == nil || b.db == nil {
386
+ return nil
387
+ }
388
+ return b.db.Close()
389
+ }
390
+
391
+ type rowScanner interface {
392
+ Scan(dest ...interface{}) error
393
+ }
394
+
395
+ func scanMessage(scanner rowScanner) (*StoreMessage, error) {
396
+ var (
397
+ msg StoreMessage
398
+ role string
399
+ timestamp string
400
+ fileIDsJSON string
401
+ metadataJSON string
402
+ )
403
+
404
+ if err := scanner.Scan(
405
+ &msg.ID,
406
+ &role,
407
+ &msg.Content,
408
+ &msg.Tokens,
409
+ &timestamp,
410
+ &fileIDsJSON,
411
+ &metadataJSON,
412
+ ); err != nil {
413
+ return nil, err
414
+ }
415
+
416
+ msg.Role = MessageRole(role)
417
+ parsedTime, err := parseTime(timestamp)
418
+ if err != nil {
419
+ return nil, fmt.Errorf("parse message timestamp: %w", err)
420
+ }
421
+ msg.Timestamp = parsedTime
422
+
423
+ if err := unmarshalStringSlice(fileIDsJSON, &msg.FileIDs); err != nil {
424
+ return nil, fmt.Errorf("unmarshal message file_ids: %w", err)
425
+ }
426
+ if err := unmarshalStringMap(metadataJSON, &msg.Metadata); err != nil {
427
+ return nil, fmt.Errorf("unmarshal message metadata: %w", err)
428
+ }
429
+
430
+ return &msg, nil
431
+ }
432
+
433
+ func scanSummary(scanner rowScanner) (*SummaryNode, error) {
434
+ var (
435
+ node SummaryNode
436
+ kind string
437
+ createdAt string
438
+ messageIDsJSON string
439
+ childIDsJSON string
440
+ fileIDsJSON string
441
+ )
442
+
443
+ if err := scanner.Scan(
444
+ &node.ID,
445
+ &kind,
446
+ &node.Content,
447
+ &node.Tokens,
448
+ &node.Level,
449
+ &createdAt,
450
+ &messageIDsJSON,
451
+ &childIDsJSON,
452
+ &node.ParentID,
453
+ &fileIDsJSON,
454
+ ); err != nil {
455
+ return nil, err
456
+ }
457
+
458
+ node.Kind = SummaryKind(kind)
459
+ parsedTime, err := parseTime(createdAt)
460
+ if err != nil {
461
+ return nil, fmt.Errorf("parse summary created_at: %w", err)
462
+ }
463
+ node.CreatedAt = parsedTime
464
+
465
+ if err := unmarshalStringSlice(messageIDsJSON, &node.MessageIDs); err != nil {
466
+ return nil, fmt.Errorf("unmarshal summary message_ids: %w", err)
467
+ }
468
+ if err := unmarshalStringSlice(childIDsJSON, &node.ChildIDs); err != nil {
469
+ return nil, fmt.Errorf("unmarshal summary child_ids: %w", err)
470
+ }
471
+ if err := unmarshalStringSlice(fileIDsJSON, &node.FileIDs); err != nil {
472
+ return nil, fmt.Errorf("unmarshal summary file_ids: %w", err)
473
+ }
474
+
475
+ return &node, nil
476
+ }
477
+
478
+ func marshalJSON(v interface{}, defaultJSON string) (string, error) {
479
+ data, err := json.Marshal(v)
480
+ if err != nil {
481
+ return "", err
482
+ }
483
+ if string(data) == "null" {
484
+ return defaultJSON, nil
485
+ }
486
+ return string(data), nil
487
+ }
488
+
489
+ func unmarshalStringSlice(raw string, out *[]string) error {
490
+ if strings.TrimSpace(raw) == "" {
491
+ *out = []string{}
492
+ return nil
493
+ }
494
+ if err := json.Unmarshal([]byte(raw), out); err != nil {
495
+ return err
496
+ }
497
+ if *out == nil {
498
+ *out = []string{}
499
+ }
500
+ return nil
501
+ }
502
+
503
+ func unmarshalStringMap(raw string, out *map[string]string) error {
504
+ if strings.TrimSpace(raw) == "" {
505
+ *out = map[string]string{}
506
+ return nil
507
+ }
508
+ if err := json.Unmarshal([]byte(raw), out); err != nil {
509
+ return err
510
+ }
511
+ if *out == nil {
512
+ *out = map[string]string{}
513
+ }
514
+ return nil
515
+ }
516
+
517
+ func parseTime(value string) (time.Time, error) {
518
+ if value == "" {
519
+ return time.Time{}, nil
520
+ }
521
+ if ts, err := time.Parse(time.RFC3339Nano, value); err == nil {
522
+ return ts, nil
523
+ }
524
+ return time.Parse(time.RFC3339, value)
525
+ }
526
+
527
+ func isSimpleFTSPattern(pattern string) bool {
528
+ if strings.TrimSpace(pattern) == "" {
529
+ return false
530
+ }
531
+ // If regex metacharacters appear, skip FTS and use Go regex fallback.
532
+ meta := regexp.MustCompile(`[\\.\^\$\|\?\*\+\(\)\{\}\[\]]`)
533
+ return !meta.MatchString(pattern)
534
+ }
535
+
536
+ func (b *SQLiteBackend) scopeMessageSet(summaryScope *string) (map[string]struct{}, error) {
537
+ if summaryScope == nil || strings.TrimSpace(*summaryScope) == "" {
538
+ return nil, nil
539
+ }
540
+
541
+ visited := map[string]struct{}{}
542
+ messageSet := map[string]struct{}{}
543
+
544
+ if err := b.collectScopedMessages(*summaryScope, visited, messageSet); err != nil {
545
+ return nil, err
546
+ }
547
+
548
+ return messageSet, nil
549
+ }
550
+
551
+ func (b *SQLiteBackend) collectScopedMessages(summaryID string, visited map[string]struct{}, messageSet map[string]struct{}) error {
552
+ if _, ok := visited[summaryID]; ok {
553
+ return nil
554
+ }
555
+ visited[summaryID] = struct{}{}
556
+
557
+ summary, err := b.GetSummary(summaryID)
558
+ if err != nil {
559
+ return fmt.Errorf("collect scoped messages for %s: %w", summaryID, err)
560
+ }
561
+ if summary == nil {
562
+ return nil
563
+ }
564
+
565
+ for _, id := range summary.MessageIDs {
566
+ messageSet[id] = struct{}{}
567
+ }
568
+ for _, childID := range summary.ChildIDs {
569
+ if err := b.collectScopedMessages(childID, visited, messageSet); err != nil {
570
+ return err
571
+ }
572
+ }
573
+
574
+ return nil
575
+ }
@@ -514,17 +514,8 @@ func generateFieldQuery(fieldName string, schema *JSONSchema) string {
514
514
 
515
515
  // parseAndValidateJSON extracts JSON from response and validates against schema
516
516
  func parseAndValidateJSON(result string, schema *JSONSchema) (map[string]interface{}, error) {
517
- // Remove markdown code blocks if present
518
- result = strings.TrimSpace(result)
519
- if strings.HasPrefix(result, "```") {
520
- // Extract content between ``` markers
521
- lines := strings.Split(result, "\n")
522
- if len(lines) > 2 {
523
- // Remove first line (```json or ```) and last line (```)
524
- result = strings.Join(lines[1:len(lines)-1], "\n")
525
- result = strings.TrimSpace(result)
526
- }
527
- }
517
+ // Remove markdown code blocks if present (shared utility)
518
+ result = StripMarkdownCodeBlock(result)
528
519
 
529
520
  // For non-object schemas (arrays, primitives), handle special cases
530
521
  if schema.Type != "object" {
@@ -602,7 +593,7 @@ func parseAndValidateJSON(result string, schema *JSONSchema) (map[string]interfa
602
593
  }
603
594
 
604
595
  // If full-string parse failed, use balanced-brace extraction to find JSON objects
605
- jsonCandidates := extractBalancedJSON(result)
596
+ jsonCandidates := ExtractAllBalancedJSON(result)
606
597
 
607
598
  if len(jsonCandidates) == 0 {
608
599
  return nil, fmt.Errorf("no JSON object found in response: %s", truncateForError(result))
@@ -621,77 +612,9 @@ func parseAndValidateJSON(result string, schema *JSONSchema) (map[string]interfa
621
612
  return nil, fmt.Errorf("no valid JSON object matching schema found in response")
622
613
  }
623
614
 
624
- // extractBalancedJSON finds all top-level JSON objects in a string by tracking
625
- // balanced braces. This handles arbitrary nesting depth, unlike the previous
626
- // regex approach that could only match 1 level of nesting.
627
- func extractBalancedJSON(s string) []string {
628
- var results []string
629
- inString := false
630
- escaped := false
631
-
632
- for i := 0; i < len(s); i++ {
633
- c := s[i]
634
-
635
- if c == '{' && !inString {
636
- // Found start of a potential JSON object; track balanced braces
637
- depth := 0
638
- inStr := false
639
- esc := false
640
- j := i
641
-
642
- for j < len(s) {
643
- ch := s[j]
644
-
645
- if esc {
646
- esc = false
647
- j++
648
- continue
649
- }
650
-
651
- if ch == '\\' && inStr {
652
- esc = true
653
- j++
654
- continue
655
- }
656
-
657
- if ch == '"' {
658
- inStr = !inStr
659
- }
660
-
661
- if !inStr {
662
- if ch == '{' {
663
- depth++
664
- } else if ch == '}' {
665
- depth--
666
- if depth == 0 {
667
- candidate := s[i : j+1]
668
- results = append(results, candidate)
669
- i = j // outer loop will increment past this
670
- break
671
- }
672
- }
673
- }
674
-
675
- j++
676
- }
677
- }
678
-
679
- // Track string state in the outer scan (for skipping { inside strings)
680
- if escaped {
681
- escaped = false
682
- continue
683
- }
684
- if c == '\\' && inString {
685
- escaped = true
686
- continue
687
- }
688
- if c == '"' {
689
- inString = !inString
690
- }
691
- }
692
-
693
- return results
694
- }
615
+ // extractBalancedJSON is an alias for ExtractAllBalancedJSON (shared in json_extraction.go).
616
+ // Kept as package-level reference for backward compatibility with tests.
617
+ var extractBalancedJSON = ExtractAllBalancedJSON
695
618
 
696
619
  // truncateForError truncates a string for use in error messages
697
620
  func truncateForError(s string) string {