@iinm/plain-agent 1.13.1 → 1.14.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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Plain Agent
2
2
 
3
3
  [![CodeQL](https://github.com/iinm/plain-agent/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/iinm/plain-agent/actions/workflows/github-code-scanning/codeql)
4
- [![Socket Badge](https://badge.socket.dev/npm/package/@iinm/plain-agent/1.13.1)](https://socket.dev/npm/package/@iinm/plain-agent)
4
+ [![Socket Badge](https://badge.socket.dev/npm/package/@iinm/plain-agent/1.14.0)](https://socket.dev/npm/package/@iinm/plain-agent)
5
5
  [![install size](https://packagephobia.com/badge?p=@iinm/plain-agent)](https://packagephobia.com/result?p=@iinm/plain-agent)
6
6
 
7
7
  A lightweight terminal-based coding agent focused on safety and low token cost
@@ -229,7 +229,7 @@ A few design choices keep token usage low:
229
229
 
230
230
  - Minimal system prompt — the [system prompt](https://github.com/iinm/plain-agent/blob/main/src/prompt.mjs) contains only what the agent needs to function.
231
231
  - Output truncation — when a command or MCP tool produces large output, it is truncated and saved to a file. The agent can then read only the relevant parts.
232
- - Context compaction — when the context grows large, you can run `/compact` to discard old messages and reload task state from a memory file.
232
+ - Context compaction — run `/compact` to discard old messages and reload task state from a memory file. This also happens automatically when input tokens exceed a configurable soft limit.
233
233
  - MCP tool filtering — MCP servers often expose many tools. Use `enabledTools` in the server config to enable only the ones you need, which reduces the number of tool definitions sent to the model.
234
234
 
235
235
  ### Claude Code Compatibility
@@ -745,6 +745,17 @@ Files are loaded in the following order. Settings in later files override earlie
745
745
  }
746
746
  },
747
747
 
748
+ // Auto-compact: when input tokens exceed the soft limit after a tool execution,
749
+ // the agent is prompted to update the memory file and call compact_context.
750
+ // Reduces noise and token costs before hitting the model's hard limit.
751
+ "autoCompact": {
752
+ "softLimit": 120000,
753
+ // Optional: override per model (prefix match on name+variant)
754
+ "softLimitPerModelPrefix": {
755
+ "gemini-2.5-pro": 500000
756
+ }
757
+ },
758
+
748
759
  // Override the default notification command
749
760
  "notifyCmd": { "command": "plain-notify-desktop", "args": [] }
750
761
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "autoCompact": { "softLimit": 120000 },
2
3
  "autoApproval": {
3
4
  "defaultAction": "ask",
4
5
  "maxApprovals": 50,
@@ -154,12 +155,18 @@
154
155
  "tests": [
155
156
  {
156
157
  "desc": "find should be denied",
157
- "toolUse": { "toolName": "exec_command", "input": { "command": "find" } },
158
+ "toolUse": {
159
+ "toolName": "exec_command",
160
+ "input": { "command": "find" }
161
+ },
158
162
  "expectedAction": "deny"
159
163
  },
160
164
  {
161
165
  "desc": "grep should be denied",
162
- "toolUse": { "toolName": "exec_command", "input": { "command": "grep" } },
166
+ "toolUse": {
167
+ "toolName": "exec_command",
168
+ "input": { "command": "grep" }
169
+ },
163
170
  "expectedAction": "deny"
164
171
  },
165
172
  {
@@ -362,7 +369,12 @@
362
369
  "toolName": "exec_command",
363
370
  "input": {
364
371
  "command": "gh",
365
- "args": ["api", "--method", "GET", "repos/owner/repo/pulls/123/comments"]
372
+ "args": [
373
+ "api",
374
+ "--method",
375
+ "GET",
376
+ "repos/owner/repo/pulls/123/comments"
377
+ ]
366
378
  }
367
379
  },
368
380
  "expectedAction": "allow"
@@ -371,7 +383,10 @@
371
383
  "desc": "gh api without --method should be denied",
372
384
  "toolUse": {
373
385
  "toolName": "exec_command",
374
- "input": { "command": "gh", "args": ["api", "repos/owner/repo/pulls"] }
386
+ "input": {
387
+ "command": "gh",
388
+ "args": ["api", "repos/owner/repo/pulls"]
389
+ }
375
390
  },
376
391
  "expectedAction": "deny"
377
392
  },
@@ -398,7 +413,10 @@
398
413
  "desc": "bash -c with pipe should not match deny pattern",
399
414
  "toolUse": {
400
415
  "toolName": "exec_command",
401
- "input": { "command": "bash", "args": ["-c", "echo hello | grep hello"] }
416
+ "input": {
417
+ "command": "bash",
418
+ "args": ["-c", "echo hello | grep hello"]
419
+ }
402
420
  },
403
421
  "expectedAction": null
404
422
  },
@@ -406,7 +424,10 @@
406
424
  "desc": "bash -c with redirect should not match deny pattern",
407
425
  "toolUse": {
408
426
  "toolName": "exec_command",
409
- "input": { "command": "bash", "args": ["-c", "echo hello > file.txt"] }
427
+ "input": {
428
+ "command": "bash",
429
+ "args": ["-c", "echo hello > file.txt"]
430
+ }
410
431
  },
411
432
  "expectedAction": null
412
433
  },
@@ -422,7 +443,10 @@
422
443
  "desc": "bash -c with semicolon should not match deny pattern",
423
444
  "toolUse": {
424
445
  "toolName": "exec_command",
425
- "input": { "command": "bash", "args": ["-c", "for i in *.txt; do echo $i; done"] }
446
+ "input": {
447
+ "command": "bash",
448
+ "args": ["-c", "for i in *.txt; do echo $i; done"]
449
+ }
426
450
  },
427
451
  "expectedAction": null
428
452
  },
@@ -470,6 +494,9 @@
470
494
  "cache_read_input_tokens": 0.1,
471
495
  "cache_creation_input_tokens": 1.25
472
496
  }
497
+ },
498
+ "autoCompact": {
499
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
473
500
  }
474
501
  },
475
502
  {
@@ -497,6 +524,9 @@
497
524
  "cache_read_input_tokens": 0.1,
498
525
  "cache_creation_input_tokens": 1.25
499
526
  }
527
+ },
528
+ "autoCompact": {
529
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
500
530
  }
501
531
  },
502
532
  {
@@ -525,6 +555,9 @@
525
555
  "cache_read_input_tokens": 0.3,
526
556
  "cache_creation_input_tokens": 3.75
527
557
  }
558
+ },
559
+ "autoCompact": {
560
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
528
561
  }
529
562
  },
530
563
  {
@@ -553,6 +586,9 @@
553
586
  "cache_read_input_tokens": 0.3,
554
587
  "cache_creation_input_tokens": 3.75
555
588
  }
589
+ },
590
+ "autoCompact": {
591
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
556
592
  }
557
593
  },
558
594
  {
@@ -581,6 +617,9 @@
581
617
  "cache_read_input_tokens": 0.5,
582
618
  "cache_creation_input_tokens": 6.25
583
619
  }
620
+ },
621
+ "autoCompact": {
622
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
584
623
  }
585
624
  },
586
625
  {
@@ -609,6 +648,9 @@
609
648
  "cache_read_input_tokens": 0.5,
610
649
  "cache_creation_input_tokens": 6.25
611
650
  }
651
+ },
652
+ "autoCompact": {
653
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
612
654
  }
613
655
  },
614
656
  {
@@ -635,8 +677,11 @@
635
677
  "input_tokens": 10,
636
678
  "output_tokens": 50,
637
679
  "cache_read_input_tokens": 1,
638
- "cache_creation_input_tokens": 12.50
680
+ "cache_creation_input_tokens": 12.5
639
681
  }
682
+ },
683
+ "autoCompact": {
684
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
640
685
  }
641
686
  },
642
687
  {
@@ -663,8 +708,11 @@
663
708
  "input_tokens": 10,
664
709
  "output_tokens": 50,
665
710
  "cache_read_input_tokens": 1,
666
- "cache_creation_input_tokens": 12.50
711
+ "cache_creation_input_tokens": 12.5
667
712
  }
713
+ },
714
+ "autoCompact": {
715
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
668
716
  }
669
717
  },
670
718
 
@@ -692,6 +740,9 @@
692
740
  "cache_read_input_tokens": 0.1,
693
741
  "cache_creation_input_tokens": 1.25
694
742
  }
743
+ },
744
+ "autoCompact": {
745
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
695
746
  }
696
747
  },
697
748
  {
@@ -718,6 +769,9 @@
718
769
  "cache_read_input_tokens": 0.1,
719
770
  "cache_creation_input_tokens": 1.25
720
771
  }
772
+ },
773
+ "autoCompact": {
774
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
721
775
  }
722
776
  },
723
777
  {
@@ -745,6 +799,9 @@
745
799
  "cache_read_input_tokens": 0.3,
746
800
  "cache_creation_input_tokens": 3.75
747
801
  }
802
+ },
803
+ "autoCompact": {
804
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
748
805
  }
749
806
  },
750
807
  {
@@ -772,6 +829,9 @@
772
829
  "cache_read_input_tokens": 0.3,
773
830
  "cache_creation_input_tokens": 3.75
774
831
  }
832
+ },
833
+ "autoCompact": {
834
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
775
835
  }
776
836
  },
777
837
  {
@@ -799,6 +859,9 @@
799
859
  "cache_read_input_tokens": 0.5,
800
860
  "cache_creation_input_tokens": 6.25
801
861
  }
862
+ },
863
+ "autoCompact": {
864
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
802
865
  }
803
866
  },
804
867
  {
@@ -826,6 +889,9 @@
826
889
  "cache_read_input_tokens": 0.5,
827
890
  "cache_creation_input_tokens": 6.25
828
891
  }
892
+ },
893
+ "autoCompact": {
894
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
829
895
  }
830
896
  },
831
897
  {
@@ -851,8 +917,11 @@
851
917
  "input_tokens": 10,
852
918
  "output_tokens": 50,
853
919
  "cache_read_input_tokens": 1,
854
- "cache_creation_input_tokens": 12.50
920
+ "cache_creation_input_tokens": 12.5
855
921
  }
922
+ },
923
+ "autoCompact": {
924
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
856
925
  }
857
926
  },
858
927
  {
@@ -878,8 +947,11 @@
878
947
  "input_tokens": 10,
879
948
  "output_tokens": 50,
880
949
  "cache_read_input_tokens": 1,
881
- "cache_creation_input_tokens": 12.50
950
+ "cache_creation_input_tokens": 12.5
882
951
  }
952
+ },
953
+ "autoCompact": {
954
+ "inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
883
955
  }
884
956
  },
885
957
 
@@ -912,7 +984,8 @@
912
984
  "cachedContentTokenCount": -1.35,
913
985
  "candidatesTokenCount": 9
914
986
  }
915
- }
987
+ },
988
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
916
989
  },
917
990
  {
918
991
  "name": "gemini-3.5-flash",
@@ -943,7 +1016,8 @@
943
1016
  "cachedContentTokenCount": -1.35,
944
1017
  "candidatesTokenCount": 9
945
1018
  }
946
- }
1019
+ },
1020
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
947
1021
  },
948
1022
  {
949
1023
  "name": "gemini-3.1-pro-preview",
@@ -974,7 +1048,8 @@
974
1048
  "cachedContentTokenCount": -1.8,
975
1049
  "candidatesTokenCount": 12
976
1050
  }
977
- }
1051
+ },
1052
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
978
1053
  },
979
1054
  {
980
1055
  "name": "gemini-3.1-pro-preview",
@@ -1005,7 +1080,8 @@
1005
1080
  "cachedContentTokenCount": -1.8,
1006
1081
  "candidatesTokenCount": 12
1007
1082
  }
1008
- }
1083
+ },
1084
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
1009
1085
  },
1010
1086
 
1011
1087
  {
@@ -1036,7 +1112,8 @@
1036
1112
  "cachedContentTokenCount": -1.35,
1037
1113
  "candidatesTokenCount": 9
1038
1114
  }
1039
- }
1115
+ },
1116
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
1040
1117
  },
1041
1118
  {
1042
1119
  "name": "gemini-3.5-flash",
@@ -1066,7 +1143,8 @@
1066
1143
  "cachedContentTokenCount": -1.35,
1067
1144
  "candidatesTokenCount": 9
1068
1145
  }
1069
- }
1146
+ },
1147
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
1070
1148
  },
1071
1149
  {
1072
1150
  "name": "gemini-3.1-pro-preview",
@@ -1096,7 +1174,8 @@
1096
1174
  "cachedContentTokenCount": -1.8,
1097
1175
  "candidatesTokenCount": 12
1098
1176
  }
1099
- }
1177
+ },
1178
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
1100
1179
  },
1101
1180
  {
1102
1181
  "name": "gemini-3.1-pro-preview",
@@ -1126,7 +1205,8 @@
1126
1205
  "cachedContentTokenCount": -1.8,
1127
1206
  "candidatesTokenCount": 12
1128
1207
  }
1129
- }
1208
+ },
1209
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
1130
1210
  },
1131
1211
 
1132
1212
  {
@@ -1154,7 +1234,8 @@
1154
1234
  "input_tokens_details.cached_tokens": -0.675,
1155
1235
  "output_tokens": 4.5
1156
1236
  }
1157
- }
1237
+ },
1238
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1158
1239
  },
1159
1240
  {
1160
1241
  "name": "gpt-5.4-mini",
@@ -1181,7 +1262,8 @@
1181
1262
  "input_tokens_details.cached_tokens": -0.675,
1182
1263
  "output_tokens": 4.5
1183
1264
  }
1184
- }
1265
+ },
1266
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1185
1267
  },
1186
1268
  {
1187
1269
  "name": "gpt-5.4-mini",
@@ -1208,7 +1290,8 @@
1208
1290
  "input_tokens_details.cached_tokens": -0.675,
1209
1291
  "output_tokens": 4.5
1210
1292
  }
1211
- }
1293
+ },
1294
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1212
1295
  },
1213
1296
  {
1214
1297
  "name": "gpt-5.5",
@@ -1235,7 +1318,8 @@
1235
1318
  "input_tokens_details.cached_tokens": -4.5,
1236
1319
  "output_tokens": 30
1237
1320
  }
1238
- }
1321
+ },
1322
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1239
1323
  },
1240
1324
  {
1241
1325
  "name": "gpt-5.5",
@@ -1262,7 +1346,8 @@
1262
1346
  "input_tokens_details.cached_tokens": -4.5,
1263
1347
  "output_tokens": 30
1264
1348
  }
1265
- }
1349
+ },
1350
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1266
1351
  },
1267
1352
  {
1268
1353
  "name": "gpt-5.5",
@@ -1289,7 +1374,8 @@
1289
1374
  "input_tokens_details.cached_tokens": -4.5,
1290
1375
  "output_tokens": 30
1291
1376
  }
1292
- }
1377
+ },
1378
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1293
1379
  },
1294
1380
 
1295
1381
  {
@@ -1316,7 +1402,8 @@
1316
1402
  "input_tokens_details.cached_tokens": -0.675,
1317
1403
  "output_tokens": 4.5
1318
1404
  }
1319
- }
1405
+ },
1406
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1320
1407
  },
1321
1408
  {
1322
1409
  "name": "gpt-5.4-mini",
@@ -1342,7 +1429,8 @@
1342
1429
  "input_tokens_details.cached_tokens": -0.675,
1343
1430
  "output_tokens": 4.5
1344
1431
  }
1345
- }
1432
+ },
1433
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1346
1434
  },
1347
1435
  {
1348
1436
  "name": "gpt-5.4-mini",
@@ -1368,7 +1456,8 @@
1368
1456
  "input_tokens_details.cached_tokens": -0.675,
1369
1457
  "output_tokens": 4.5
1370
1458
  }
1371
- }
1459
+ },
1460
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1372
1461
  },
1373
1462
  {
1374
1463
  "name": "gpt-5.5",
@@ -1394,7 +1483,8 @@
1394
1483
  "input_tokens_details.cached_tokens": -4.5,
1395
1484
  "output_tokens": 30
1396
1485
  }
1397
- }
1486
+ },
1487
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1398
1488
  },
1399
1489
  {
1400
1490
  "name": "gpt-5.5",
@@ -1420,7 +1510,8 @@
1420
1510
  "input_tokens_details.cached_tokens": -4.5,
1421
1511
  "output_tokens": 30
1422
1512
  }
1423
- }
1513
+ },
1514
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1424
1515
  },
1425
1516
  {
1426
1517
  "name": "gpt-5.5",
@@ -1446,7 +1537,8 @@
1446
1537
  "input_tokens_details.cached_tokens": -4.5,
1447
1538
  "output_tokens": 30
1448
1539
  }
1449
- }
1540
+ },
1541
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1450
1542
  },
1451
1543
 
1452
1544
  {
@@ -1468,9 +1560,10 @@
1468
1560
  "prices": {
1469
1561
  "prompt_tokens": 0.15,
1470
1562
  "prompt_tokens_details.cached_tokens": -0.14,
1471
- "completion_tokens": 0.6
1563
+ "completion_tokens": 0.6
1472
1564
  }
1473
- }
1565
+ },
1566
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1474
1567
  },
1475
1568
 
1476
1569
  {
@@ -1494,7 +1587,8 @@
1494
1587
  "completion_tokens": 3.2,
1495
1588
  "prompt_tokens_details.cached_tokens": -0.9
1496
1589
  }
1497
- }
1590
+ },
1591
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1498
1592
  },
1499
1593
  {
1500
1594
  "name": "glm-5",
@@ -1517,7 +1611,8 @@
1517
1611
  "prompt_tokens": 1,
1518
1612
  "completion_tokens": 3.2
1519
1613
  }
1520
- }
1614
+ },
1615
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1521
1616
  },
1522
1617
 
1523
1618
  {
@@ -1541,7 +1636,8 @@
1541
1636
  "prompt_tokens_details.cached_tokens": -1.14,
1542
1637
  "completion_tokens": 4.4
1543
1638
  }
1544
- }
1639
+ },
1640
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1545
1641
  },
1546
1642
 
1547
1643
  {
@@ -1557,7 +1653,8 @@
1557
1653
  "model": "Kimi-K2.6",
1558
1654
  "reasoning_effort": "high"
1559
1655
  }
1560
- }
1656
+ },
1657
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1561
1658
  },
1562
1659
  {
1563
1660
  "name": "kimi-k2.7-code",
@@ -1580,7 +1677,8 @@
1580
1677
  "prompt_tokens_details.cached_tokens": -0.76,
1581
1678
  "completion_tokens": 4
1582
1679
  }
1583
- }
1680
+ },
1681
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1584
1682
  },
1585
1683
 
1586
1684
  {
@@ -1604,7 +1702,8 @@
1604
1702
  "prompt_tokens_details.cached_tokens": -1.6,
1605
1703
  "completion_tokens": 3.48
1606
1704
  }
1607
- }
1705
+ },
1706
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1608
1707
  },
1609
1708
  {
1610
1709
  "name": "deepseek-v4-pro",
@@ -1619,7 +1718,8 @@
1619
1718
  "model": "DeepSeek-V4-Pro",
1620
1719
  "reasoning_effort": "high"
1621
1720
  }
1622
- }
1721
+ },
1722
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1623
1723
  },
1624
1724
 
1625
1725
  {
@@ -1643,7 +1743,8 @@
1643
1743
  "prompt_tokens_details.cached_tokens": -0.24,
1644
1744
  "completion_tokens": 1.2
1645
1745
  }
1646
- }
1746
+ },
1747
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1647
1748
  },
1648
1749
 
1649
1750
  {
@@ -1667,7 +1768,8 @@
1667
1768
  "prompt_tokens_details.cached_tokens": -0.32,
1668
1769
  "completion_tokens": 1.6
1669
1770
  }
1670
- }
1771
+ },
1772
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1671
1773
  },
1672
1774
 
1673
1775
  {
@@ -1689,6 +1791,9 @@
1689
1791
  }
1690
1792
  }
1691
1793
  }
1794
+ },
1795
+ "autoCompact": {
1796
+ "inputTokensKeys": ["inputTokens", "cacheReadInputTokens"]
1692
1797
  }
1693
1798
  },
1694
1799
  {
@@ -1713,6 +1818,9 @@
1713
1818
  }
1714
1819
  }
1715
1820
  }
1821
+ },
1822
+ "autoCompact": {
1823
+ "inputTokensKeys": ["inputTokens", "cacheReadInputTokens"]
1716
1824
  }
1717
1825
  }
1718
1826
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.13.1",
3
+ "version": "1.14.0",
4
4
  "description": "A lightweight terminal-based coding agent focused on safety and low token cost",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/agent.d.ts CHANGED
@@ -61,4 +61,8 @@ export type AgentConfig = {
61
61
  };
62
62
  /** When provided, the agent restores its state from this snapshot. */
63
63
  initialState?: SessionState | null;
64
+ /** Soft limit on input tokens; triggers auto-compact prompt when exceeded. */
65
+ contextSoftLimit?: number;
66
+ /** Keys in providerTokenUsage to sum for input token count. */
67
+ inputTokensKeys?: string[];
64
68
  };
package/src/agent.mjs CHANGED
@@ -34,6 +34,8 @@ export function createAgent({
34
34
  modelCostConfig,
35
35
  sessionMetadata,
36
36
  initialState,
37
+ contextSoftLimit,
38
+ inputTokensKeys,
37
39
  }) {
38
40
  /** @type {UserEventEmitter} */
39
41
  const userEventEmitter = new EventEmitter();
@@ -195,6 +197,8 @@ export function createAgent({
195
197
  toolUseApprover,
196
198
  subagentManager,
197
199
  pauseSignal,
200
+ contextSoftLimit,
201
+ inputTokensKeys,
198
202
  });
199
203
 
200
204
  userEventEmitter.on("userInput", agentLoop.handleUserInput);
package/src/agentLoop.mjs CHANGED
@@ -7,6 +7,8 @@
7
7
  */
8
8
 
9
9
  import { styleText } from "node:util";
10
+ import { buildCompactPrompt } from "./prompt.mjs";
11
+ import { extractInputTokenCount } from "./tokenUsage.mjs";
10
12
  import { compactContextToolName } from "./tools/compactContext.mjs";
11
13
 
12
14
  /**
@@ -55,6 +57,8 @@ function applyCompactContextIfCalled(stateManager, toolUseParts, toolResults) {
55
57
  * @property {ToolUseApprover} toolUseApprover - Tool use approval checker
56
58
  * @property {SubagentManager} subagentManager - Subagent manager instance
57
59
  * @property {PauseSignal} pauseSignal - Signal to pause auto-approve after current tool completes
60
+ * @property {number} [contextSoftLimit] - Soft limit on input tokens for auto-compact
61
+ * @property {string[]} [inputTokensKeys] - Keys in providerTokenUsage to sum for input token count
58
62
  */
59
63
 
60
64
  /**
@@ -74,6 +78,8 @@ export function createAgentLoop({
74
78
  toolUseApprover,
75
79
  subagentManager,
76
80
  pauseSignal,
81
+ contextSoftLimit,
82
+ inputTokensKeys,
77
83
  }) {
78
84
  const inputHandler = createInputHandler({
79
85
  stateManager,
@@ -102,7 +108,7 @@ export function createAgentLoop({
102
108
  async function runTurnLoop() {
103
109
  let thinkingLoops = 0;
104
110
  const maxThinkingLoops = 5;
105
-
111
+ let turnsAfterCompactPrompt = -1;
106
112
  while (true) {
107
113
  // Check if auto-approve was paused by Ctrl-C during tool execution
108
114
  if (pauseSignal.isPaused()) {
@@ -240,6 +246,7 @@ export function createAgentLoop({
240
246
  if (
241
247
  applyCompactContextIfCalled(stateManager, toolUseParts, toolResults)
242
248
  ) {
249
+ turnsAfterCompactPrompt = -1;
243
250
  continue;
244
251
  }
245
252
 
@@ -254,6 +261,40 @@ export function createAgentLoop({
254
261
  } else {
255
262
  stateManager.appendMessages([{ role: "user", content: toolResults }]);
256
263
  }
264
+
265
+ // Auto-compact: insert prompt if context exceeds soft limit
266
+ if (contextSoftLimit && inputTokensKeys && providerTokenUsage) {
267
+ const inputTokens = extractInputTokenCount(
268
+ providerTokenUsage,
269
+ inputTokensKeys,
270
+ );
271
+ if (inputTokens !== undefined && inputTokens > contextSoftLimit) {
272
+ if (0 <= turnsAfterCompactPrompt && turnsAfterCompactPrompt < 3) {
273
+ turnsAfterCompactPrompt += 1;
274
+ } else {
275
+ stateManager.appendMessages([
276
+ {
277
+ role: "user",
278
+ content: [
279
+ {
280
+ type: "text",
281
+ text: buildCompactPrompt({
282
+ isSubagent: subagentManager.isSubagentActive(),
283
+ }),
284
+ },
285
+ ],
286
+ },
287
+ ]);
288
+ turnsAfterCompactPrompt = 0;
289
+ console.error(
290
+ styleText(
291
+ "yellow",
292
+ `\nContext exceeded soft limit (${inputTokens.toLocaleString()} / ${contextSoftLimit.toLocaleString()} tokens). Auto-compact prompt inserted.`,
293
+ ),
294
+ );
295
+ }
296
+ }
297
+ }
257
298
  }
258
299
  }
259
300
 
@@ -8,7 +8,10 @@ import { styleText } from "node:util";
8
8
  import { loadAgentRoles } from "../context/loadAgentRoles.mjs";
9
9
  import { loadPrompts } from "../context/loadPrompts.mjs";
10
10
  import { loadUserMessageContext } from "../context/loadUserMessageContext.mjs";
11
- import { CLAUDE_CODE_COMPATIBILITY_NOTES } from "../prompt.mjs";
11
+ import {
12
+ buildCompactPrompt,
13
+ CLAUDE_CODE_COMPATIBILITY_NOTES,
14
+ } from "../prompt.mjs";
12
15
  import { parseFileRange } from "../utils/parseFileRange.mjs";
13
16
  import { readFileRange } from "../utils/readFileRange.mjs";
14
17
  import { toOneLine } from "../utils/toOneLine.mjs";
@@ -140,14 +143,10 @@ export function createCommandHandler({
140
143
 
141
144
  // /compact [reason]
142
145
  if (/^\/compact( |$)/i.test(inputTrimmed)) {
143
- const invocation = inputTrimmed;
144
- const message = [
145
- `System: This prompt was invoked as "${invocation}".`,
146
- "",
147
- "Compact the conversation context:",
148
- "1. Update the memory file for the current task so it fully captures the task overview, progress, decisions, and next steps in a self-contained way.",
149
- '2. Then call the "compact_context" tool alone with that memory file path and a brief reason.',
150
- ].join("\n");
146
+ const message = buildCompactPrompt({
147
+ invocation: inputTrimmed,
148
+ isSubagent: agentCommands.getActiveSubagent() !== null,
149
+ });
151
150
  userEventEmitter.emit("userInput", [{ type: "text", text: message }]);
152
151
  return "continue";
153
152
  }
package/src/config.d.ts CHANGED
@@ -87,6 +87,10 @@ export type AppConfig = {
87
87
  };
88
88
  mcpServers?: Record<string, MCPServerConfig>;
89
89
  notifyCmd?: { command: string; args?: string[] };
90
+ autoCompact?: {
91
+ softLimit?: number;
92
+ softLimitPerModelPrefix?: Record<string, number>;
93
+ };
90
94
  claudeCodePlugins?: ClaudeCodePluginRepo[];
91
95
  };
92
96
 
package/src/config.mjs CHANGED
@@ -124,6 +124,16 @@ export async function loadAppConfig(options = {}) {
124
124
  ...(merged.mcpServers ?? {}),
125
125
  ...(config.mcpServers ?? {}),
126
126
  },
127
+ autoCompact: config.autoCompact
128
+ ? {
129
+ softLimit:
130
+ config.autoCompact.softLimit ?? merged.autoCompact?.softLimit,
131
+ softLimitPerModelPrefix: {
132
+ ...(merged.autoCompact?.softLimitPerModelPrefix ?? {}),
133
+ ...(config.autoCompact.softLimitPerModelPrefix ?? {}),
134
+ },
135
+ }
136
+ : merged.autoCompact,
127
137
  notifyCmd: config.notifyCmd ?? merged.notifyCmd,
128
138
  claudeCodePlugins: [
129
139
  ...(merged.claudeCodePlugins ?? []),
@@ -223,3 +233,21 @@ async function trustConfigHash(hash) {
223
233
  await fs.mkdir(TRUSTED_CONFIG_HASHES_DIR, { recursive: true });
224
234
  await fs.writeFile(path.join(TRUSTED_CONFIG_HASHES_DIR, hash), "");
225
235
  }
236
+
237
+ /**
238
+ * Resolve the effective context soft limit for the given model.
239
+ *
240
+ * @param {AppConfig["autoCompact"]} autoCompact
241
+ * @param {string} modelNameWithVariant - e.g. "claude-sonnet-4-6+thinking-high"
242
+ * @returns {number | undefined}
243
+ */
244
+ export function resolveContextSoftLimit(autoCompact, modelNameWithVariant) {
245
+ if (!autoCompact) return undefined;
246
+ const perPrefix = autoCompact.softLimitPerModelPrefix;
247
+ const matched = perPrefix
248
+ ? Object.entries(perPrefix).find(([prefix]) =>
249
+ modelNameWithVariant.startsWith(prefix),
250
+ )?.[1]
251
+ : undefined;
252
+ return matched ?? autoCompact.softLimit ?? undefined;
253
+ }
package/src/main.mjs CHANGED
@@ -16,7 +16,7 @@ import { startBatchSession } from "./cli/batch.mjs";
16
16
  import { runCostCommand } from "./cli/cost.mjs";
17
17
  import { startInteractiveSession } from "./cli/interactive.mjs";
18
18
  import { runTestApprovalCommand } from "./cli/testApproval.mjs";
19
- import { loadAppConfig } from "./config.mjs";
19
+ import { loadAppConfig, resolveContextSoftLimit } from "./config.mjs";
20
20
  import { loadAgentRoles } from "./context/loadAgentRoles.mjs";
21
21
  import { loadPrompts } from "./context/loadPrompts.mjs";
22
22
  import { AGENT_PROJECT_METADATA_DIR, USER_NAME } from "./env.mjs";
@@ -428,6 +428,12 @@ export async function main(argv = process.argv) {
428
428
  },
429
429
  });
430
430
 
431
+ // Resolve context soft limit from autoCompact config
432
+ const contextSoftLimit = resolveContextSoftLimit(
433
+ appConfig.autoCompact,
434
+ modelNameWithVariant,
435
+ );
436
+
431
437
  const { userEventEmitter, agentEventEmitter, agentCommands } = createAgent({
432
438
  callModel: agentCallModel,
433
439
  prompt,
@@ -442,6 +448,8 @@ export async function main(argv = process.argv) {
442
448
  startTime,
443
449
  },
444
450
  initialState: resumedState,
451
+ contextSoftLimit,
452
+ inputTokensKeys: modelDef.autoCompact?.inputTokensKeys,
445
453
  });
446
454
 
447
455
  const sessionOptions = {
@@ -10,6 +10,12 @@ export type ModelDefinition = {
10
10
  platform: PlatformConfig;
11
11
  model: ModelConfig;
12
12
  cost?: CostConfig;
13
+ autoCompact?: ModelAutoCompactConfig;
14
+ };
15
+
16
+ export type ModelAutoCompactConfig = {
17
+ /** Keys in providerTokenUsage whose values are summed to get the input token count. */
18
+ inputTokensKeys: string[];
13
19
  };
14
20
 
15
21
  export type PlatformConfig =
package/src/prompt.mjs CHANGED
@@ -105,3 +105,42 @@ export const CLAUDE_CODE_COMPATIBILITY_NOTES = `# Environment Constraints
105
105
  - Subagents cannot run in parallel. Switch to them one at a time.
106
106
  - Use AGENTS.md instead when CLAUDE.md is absent.
107
107
  - If instructed to use "haiku agent", "sonnet agent", or "opus agent", use "worker" instead.`;
108
+
109
+ /**
110
+ * Build the compact context prompt message text.
111
+ *
112
+ * Shared between the interactive `/compact` command and the automatic
113
+ * soft-limit prompt insertion in the agent loop.
114
+ *
115
+ * @param {object} [options]
116
+ * @param {string} [options.invocation] - The invocation string (e.g., "/compact reason").
117
+ * When omitted, a generic auto-compact preamble is used instead.
118
+ * @param {boolean} [options.isSubagent] - When true, instructs the model to
119
+ * return to the main agent via `switch_to_main_agent` before compacting.
120
+ * @returns {string}
121
+ */
122
+ export function buildCompactPrompt(options) {
123
+ const { invocation, isSubagent } = options ?? {};
124
+
125
+ const preamble = invocation
126
+ ? `System: This prompt was invoked as "${invocation}".`
127
+ : "System: Context is approaching the soft limit.";
128
+
129
+ if (isSubagent) {
130
+ return [
131
+ preamble,
132
+ "",
133
+ "The context is growing large. Wrap up your current work:",
134
+ "1. Summarize your progress and findings so far in the memory file.",
135
+ '2. Call "switch_to_main_agent" to hand back control to the main agent.',
136
+ ].join("\n");
137
+ }
138
+
139
+ return [
140
+ preamble,
141
+ "",
142
+ "Compact the conversation context:",
143
+ "1. Update the memory file for the current task so it fully captures the task overview, progress, decisions, and next steps in a self-contained way.",
144
+ '2. Then call the "compact_context" tool alone with that memory file path and a brief reason.',
145
+ ].join("\n");
146
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @import { ProviderTokenUsage } from "./model"
3
+ */
4
+
5
+ /**
6
+ * Extract the input (prompt/context) token count from a single turn's
7
+ * provider token usage object by summing the values of the specified keys.
8
+ *
9
+ * Returns `undefined` when no specified key yields a positive number.
10
+ *
11
+ * @param {ProviderTokenUsage} usage
12
+ * @param {string[]} inputTokensKeys - Keys whose numeric values are summed.
13
+ * @returns {number | undefined}
14
+ */
15
+ export function extractInputTokenCount(usage, inputTokensKeys) {
16
+ let total = 0;
17
+ let found = false;
18
+
19
+ for (const key of inputTokensKeys) {
20
+ const value = usage[key];
21
+ if (typeof value === "number" && value > 0) {
22
+ total += value;
23
+ found = true;
24
+ }
25
+ }
26
+
27
+ return found ? total : undefined;
28
+ }