@iinm/plain-agent 1.13.1 → 1.14.1

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.1)](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,13 @@
470
494
  "cache_read_input_tokens": 0.1,
471
495
  "cache_creation_input_tokens": 1.25
472
496
  }
497
+ },
498
+ "autoCompact": {
499
+ "inputTokensKeys": [
500
+ "input_tokens",
501
+ "cache_read_input_tokens",
502
+ "cache_creation_input_tokens"
503
+ ]
473
504
  }
474
505
  },
475
506
  {
@@ -497,6 +528,13 @@
497
528
  "cache_read_input_tokens": 0.1,
498
529
  "cache_creation_input_tokens": 1.25
499
530
  }
531
+ },
532
+ "autoCompact": {
533
+ "inputTokensKeys": [
534
+ "input_tokens",
535
+ "cache_read_input_tokens",
536
+ "cache_creation_input_tokens"
537
+ ]
500
538
  }
501
539
  },
502
540
  {
@@ -525,6 +563,13 @@
525
563
  "cache_read_input_tokens": 0.3,
526
564
  "cache_creation_input_tokens": 3.75
527
565
  }
566
+ },
567
+ "autoCompact": {
568
+ "inputTokensKeys": [
569
+ "input_tokens",
570
+ "cache_read_input_tokens",
571
+ "cache_creation_input_tokens"
572
+ ]
528
573
  }
529
574
  },
530
575
  {
@@ -553,6 +598,13 @@
553
598
  "cache_read_input_tokens": 0.3,
554
599
  "cache_creation_input_tokens": 3.75
555
600
  }
601
+ },
602
+ "autoCompact": {
603
+ "inputTokensKeys": [
604
+ "input_tokens",
605
+ "cache_read_input_tokens",
606
+ "cache_creation_input_tokens"
607
+ ]
556
608
  }
557
609
  },
558
610
  {
@@ -581,6 +633,13 @@
581
633
  "cache_read_input_tokens": 0.5,
582
634
  "cache_creation_input_tokens": 6.25
583
635
  }
636
+ },
637
+ "autoCompact": {
638
+ "inputTokensKeys": [
639
+ "input_tokens",
640
+ "cache_read_input_tokens",
641
+ "cache_creation_input_tokens"
642
+ ]
584
643
  }
585
644
  },
586
645
  {
@@ -609,6 +668,13 @@
609
668
  "cache_read_input_tokens": 0.5,
610
669
  "cache_creation_input_tokens": 6.25
611
670
  }
671
+ },
672
+ "autoCompact": {
673
+ "inputTokensKeys": [
674
+ "input_tokens",
675
+ "cache_read_input_tokens",
676
+ "cache_creation_input_tokens"
677
+ ]
612
678
  }
613
679
  },
614
680
  {
@@ -635,8 +701,15 @@
635
701
  "input_tokens": 10,
636
702
  "output_tokens": 50,
637
703
  "cache_read_input_tokens": 1,
638
- "cache_creation_input_tokens": 12.50
704
+ "cache_creation_input_tokens": 12.5
639
705
  }
706
+ },
707
+ "autoCompact": {
708
+ "inputTokensKeys": [
709
+ "input_tokens",
710
+ "cache_read_input_tokens",
711
+ "cache_creation_input_tokens"
712
+ ]
640
713
  }
641
714
  },
642
715
  {
@@ -663,8 +736,15 @@
663
736
  "input_tokens": 10,
664
737
  "output_tokens": 50,
665
738
  "cache_read_input_tokens": 1,
666
- "cache_creation_input_tokens": 12.50
739
+ "cache_creation_input_tokens": 12.5
667
740
  }
741
+ },
742
+ "autoCompact": {
743
+ "inputTokensKeys": [
744
+ "input_tokens",
745
+ "cache_read_input_tokens",
746
+ "cache_creation_input_tokens"
747
+ ]
668
748
  }
669
749
  },
670
750
 
@@ -692,6 +772,13 @@
692
772
  "cache_read_input_tokens": 0.1,
693
773
  "cache_creation_input_tokens": 1.25
694
774
  }
775
+ },
776
+ "autoCompact": {
777
+ "inputTokensKeys": [
778
+ "input_tokens",
779
+ "cache_read_input_tokens",
780
+ "cache_creation_input_tokens"
781
+ ]
695
782
  }
696
783
  },
697
784
  {
@@ -718,6 +805,13 @@
718
805
  "cache_read_input_tokens": 0.1,
719
806
  "cache_creation_input_tokens": 1.25
720
807
  }
808
+ },
809
+ "autoCompact": {
810
+ "inputTokensKeys": [
811
+ "input_tokens",
812
+ "cache_read_input_tokens",
813
+ "cache_creation_input_tokens"
814
+ ]
721
815
  }
722
816
  },
723
817
  {
@@ -745,6 +839,13 @@
745
839
  "cache_read_input_tokens": 0.3,
746
840
  "cache_creation_input_tokens": 3.75
747
841
  }
842
+ },
843
+ "autoCompact": {
844
+ "inputTokensKeys": [
845
+ "input_tokens",
846
+ "cache_read_input_tokens",
847
+ "cache_creation_input_tokens"
848
+ ]
748
849
  }
749
850
  },
750
851
  {
@@ -772,6 +873,13 @@
772
873
  "cache_read_input_tokens": 0.3,
773
874
  "cache_creation_input_tokens": 3.75
774
875
  }
876
+ },
877
+ "autoCompact": {
878
+ "inputTokensKeys": [
879
+ "input_tokens",
880
+ "cache_read_input_tokens",
881
+ "cache_creation_input_tokens"
882
+ ]
775
883
  }
776
884
  },
777
885
  {
@@ -799,6 +907,13 @@
799
907
  "cache_read_input_tokens": 0.5,
800
908
  "cache_creation_input_tokens": 6.25
801
909
  }
910
+ },
911
+ "autoCompact": {
912
+ "inputTokensKeys": [
913
+ "input_tokens",
914
+ "cache_read_input_tokens",
915
+ "cache_creation_input_tokens"
916
+ ]
802
917
  }
803
918
  },
804
919
  {
@@ -826,6 +941,13 @@
826
941
  "cache_read_input_tokens": 0.5,
827
942
  "cache_creation_input_tokens": 6.25
828
943
  }
944
+ },
945
+ "autoCompact": {
946
+ "inputTokensKeys": [
947
+ "input_tokens",
948
+ "cache_read_input_tokens",
949
+ "cache_creation_input_tokens"
950
+ ]
829
951
  }
830
952
  },
831
953
  {
@@ -851,8 +973,15 @@
851
973
  "input_tokens": 10,
852
974
  "output_tokens": 50,
853
975
  "cache_read_input_tokens": 1,
854
- "cache_creation_input_tokens": 12.50
976
+ "cache_creation_input_tokens": 12.5
855
977
  }
978
+ },
979
+ "autoCompact": {
980
+ "inputTokensKeys": [
981
+ "input_tokens",
982
+ "cache_read_input_tokens",
983
+ "cache_creation_input_tokens"
984
+ ]
856
985
  }
857
986
  },
858
987
  {
@@ -878,8 +1007,15 @@
878
1007
  "input_tokens": 10,
879
1008
  "output_tokens": 50,
880
1009
  "cache_read_input_tokens": 1,
881
- "cache_creation_input_tokens": 12.50
1010
+ "cache_creation_input_tokens": 12.5
882
1011
  }
1012
+ },
1013
+ "autoCompact": {
1014
+ "inputTokensKeys": [
1015
+ "input_tokens",
1016
+ "cache_read_input_tokens",
1017
+ "cache_creation_input_tokens"
1018
+ ]
883
1019
  }
884
1020
  },
885
1021
 
@@ -912,7 +1048,8 @@
912
1048
  "cachedContentTokenCount": -1.35,
913
1049
  "candidatesTokenCount": 9
914
1050
  }
915
- }
1051
+ },
1052
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
916
1053
  },
917
1054
  {
918
1055
  "name": "gemini-3.5-flash",
@@ -943,7 +1080,8 @@
943
1080
  "cachedContentTokenCount": -1.35,
944
1081
  "candidatesTokenCount": 9
945
1082
  }
946
- }
1083
+ },
1084
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
947
1085
  },
948
1086
  {
949
1087
  "name": "gemini-3.1-pro-preview",
@@ -974,7 +1112,8 @@
974
1112
  "cachedContentTokenCount": -1.8,
975
1113
  "candidatesTokenCount": 12
976
1114
  }
977
- }
1115
+ },
1116
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
978
1117
  },
979
1118
  {
980
1119
  "name": "gemini-3.1-pro-preview",
@@ -1005,7 +1144,8 @@
1005
1144
  "cachedContentTokenCount": -1.8,
1006
1145
  "candidatesTokenCount": 12
1007
1146
  }
1008
- }
1147
+ },
1148
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
1009
1149
  },
1010
1150
 
1011
1151
  {
@@ -1036,7 +1176,8 @@
1036
1176
  "cachedContentTokenCount": -1.35,
1037
1177
  "candidatesTokenCount": 9
1038
1178
  }
1039
- }
1179
+ },
1180
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
1040
1181
  },
1041
1182
  {
1042
1183
  "name": "gemini-3.5-flash",
@@ -1066,7 +1207,8 @@
1066
1207
  "cachedContentTokenCount": -1.35,
1067
1208
  "candidatesTokenCount": 9
1068
1209
  }
1069
- }
1210
+ },
1211
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
1070
1212
  },
1071
1213
  {
1072
1214
  "name": "gemini-3.1-pro-preview",
@@ -1096,7 +1238,8 @@
1096
1238
  "cachedContentTokenCount": -1.8,
1097
1239
  "candidatesTokenCount": 12
1098
1240
  }
1099
- }
1241
+ },
1242
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
1100
1243
  },
1101
1244
  {
1102
1245
  "name": "gemini-3.1-pro-preview",
@@ -1126,7 +1269,8 @@
1126
1269
  "cachedContentTokenCount": -1.8,
1127
1270
  "candidatesTokenCount": 12
1128
1271
  }
1129
- }
1272
+ },
1273
+ "autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
1130
1274
  },
1131
1275
 
1132
1276
  {
@@ -1154,7 +1298,8 @@
1154
1298
  "input_tokens_details.cached_tokens": -0.675,
1155
1299
  "output_tokens": 4.5
1156
1300
  }
1157
- }
1301
+ },
1302
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1158
1303
  },
1159
1304
  {
1160
1305
  "name": "gpt-5.4-mini",
@@ -1181,7 +1326,8 @@
1181
1326
  "input_tokens_details.cached_tokens": -0.675,
1182
1327
  "output_tokens": 4.5
1183
1328
  }
1184
- }
1329
+ },
1330
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1185
1331
  },
1186
1332
  {
1187
1333
  "name": "gpt-5.4-mini",
@@ -1208,7 +1354,8 @@
1208
1354
  "input_tokens_details.cached_tokens": -0.675,
1209
1355
  "output_tokens": 4.5
1210
1356
  }
1211
- }
1357
+ },
1358
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1212
1359
  },
1213
1360
  {
1214
1361
  "name": "gpt-5.5",
@@ -1235,7 +1382,8 @@
1235
1382
  "input_tokens_details.cached_tokens": -4.5,
1236
1383
  "output_tokens": 30
1237
1384
  }
1238
- }
1385
+ },
1386
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1239
1387
  },
1240
1388
  {
1241
1389
  "name": "gpt-5.5",
@@ -1262,7 +1410,8 @@
1262
1410
  "input_tokens_details.cached_tokens": -4.5,
1263
1411
  "output_tokens": 30
1264
1412
  }
1265
- }
1413
+ },
1414
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1266
1415
  },
1267
1416
  {
1268
1417
  "name": "gpt-5.5",
@@ -1289,7 +1438,8 @@
1289
1438
  "input_tokens_details.cached_tokens": -4.5,
1290
1439
  "output_tokens": 30
1291
1440
  }
1292
- }
1441
+ },
1442
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1293
1443
  },
1294
1444
 
1295
1445
  {
@@ -1316,7 +1466,8 @@
1316
1466
  "input_tokens_details.cached_tokens": -0.675,
1317
1467
  "output_tokens": 4.5
1318
1468
  }
1319
- }
1469
+ },
1470
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1320
1471
  },
1321
1472
  {
1322
1473
  "name": "gpt-5.4-mini",
@@ -1342,7 +1493,8 @@
1342
1493
  "input_tokens_details.cached_tokens": -0.675,
1343
1494
  "output_tokens": 4.5
1344
1495
  }
1345
- }
1496
+ },
1497
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1346
1498
  },
1347
1499
  {
1348
1500
  "name": "gpt-5.4-mini",
@@ -1368,7 +1520,8 @@
1368
1520
  "input_tokens_details.cached_tokens": -0.675,
1369
1521
  "output_tokens": 4.5
1370
1522
  }
1371
- }
1523
+ },
1524
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1372
1525
  },
1373
1526
  {
1374
1527
  "name": "gpt-5.5",
@@ -1394,7 +1547,8 @@
1394
1547
  "input_tokens_details.cached_tokens": -4.5,
1395
1548
  "output_tokens": 30
1396
1549
  }
1397
- }
1550
+ },
1551
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1398
1552
  },
1399
1553
  {
1400
1554
  "name": "gpt-5.5",
@@ -1420,7 +1574,8 @@
1420
1574
  "input_tokens_details.cached_tokens": -4.5,
1421
1575
  "output_tokens": 30
1422
1576
  }
1423
- }
1577
+ },
1578
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1424
1579
  },
1425
1580
  {
1426
1581
  "name": "gpt-5.5",
@@ -1446,7 +1601,8 @@
1446
1601
  "input_tokens_details.cached_tokens": -4.5,
1447
1602
  "output_tokens": 30
1448
1603
  }
1449
- }
1604
+ },
1605
+ "autoCompact": { "inputTokensKeys": ["input_tokens"] }
1450
1606
  },
1451
1607
 
1452
1608
  {
@@ -1468,9 +1624,10 @@
1468
1624
  "prices": {
1469
1625
  "prompt_tokens": 0.15,
1470
1626
  "prompt_tokens_details.cached_tokens": -0.14,
1471
- "completion_tokens": 0.6
1627
+ "completion_tokens": 0.6
1472
1628
  }
1473
- }
1629
+ },
1630
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1474
1631
  },
1475
1632
 
1476
1633
  {
@@ -1494,7 +1651,8 @@
1494
1651
  "completion_tokens": 3.2,
1495
1652
  "prompt_tokens_details.cached_tokens": -0.9
1496
1653
  }
1497
- }
1654
+ },
1655
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1498
1656
  },
1499
1657
  {
1500
1658
  "name": "glm-5",
@@ -1517,7 +1675,8 @@
1517
1675
  "prompt_tokens": 1,
1518
1676
  "completion_tokens": 3.2
1519
1677
  }
1520
- }
1678
+ },
1679
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1521
1680
  },
1522
1681
 
1523
1682
  {
@@ -1541,7 +1700,8 @@
1541
1700
  "prompt_tokens_details.cached_tokens": -1.14,
1542
1701
  "completion_tokens": 4.4
1543
1702
  }
1544
- }
1703
+ },
1704
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1545
1705
  },
1546
1706
 
1547
1707
  {
@@ -1557,7 +1717,8 @@
1557
1717
  "model": "Kimi-K2.6",
1558
1718
  "reasoning_effort": "high"
1559
1719
  }
1560
- }
1720
+ },
1721
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1561
1722
  },
1562
1723
  {
1563
1724
  "name": "kimi-k2.7-code",
@@ -1580,7 +1741,8 @@
1580
1741
  "prompt_tokens_details.cached_tokens": -0.76,
1581
1742
  "completion_tokens": 4
1582
1743
  }
1583
- }
1744
+ },
1745
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1584
1746
  },
1585
1747
 
1586
1748
  {
@@ -1604,7 +1766,8 @@
1604
1766
  "prompt_tokens_details.cached_tokens": -1.6,
1605
1767
  "completion_tokens": 3.48
1606
1768
  }
1607
- }
1769
+ },
1770
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1608
1771
  },
1609
1772
  {
1610
1773
  "name": "deepseek-v4-pro",
@@ -1619,7 +1782,8 @@
1619
1782
  "model": "DeepSeek-V4-Pro",
1620
1783
  "reasoning_effort": "high"
1621
1784
  }
1622
- }
1785
+ },
1786
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1623
1787
  },
1624
1788
 
1625
1789
  {
@@ -1643,7 +1807,8 @@
1643
1807
  "prompt_tokens_details.cached_tokens": -0.24,
1644
1808
  "completion_tokens": 1.2
1645
1809
  }
1646
- }
1810
+ },
1811
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1647
1812
  },
1648
1813
 
1649
1814
  {
@@ -1667,7 +1832,8 @@
1667
1832
  "prompt_tokens_details.cached_tokens": -0.32,
1668
1833
  "completion_tokens": 1.6
1669
1834
  }
1670
- }
1835
+ },
1836
+ "autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
1671
1837
  },
1672
1838
 
1673
1839
  {
@@ -1689,6 +1855,9 @@
1689
1855
  }
1690
1856
  }
1691
1857
  }
1858
+ },
1859
+ "autoCompact": {
1860
+ "inputTokensKeys": ["inputTokens", "cacheReadInputTokens"]
1692
1861
  }
1693
1862
  },
1694
1863
  {
@@ -1713,6 +1882,13 @@
1713
1882
  }
1714
1883
  }
1715
1884
  }
1885
+ },
1886
+ "autoCompact": {
1887
+ "inputTokensKeys": [
1888
+ "inputTokens",
1889
+ "cacheReadInputTokens",
1890
+ "cacheWriteInputTokens"
1891
+ ]
1716
1892
  }
1717
1893
  }
1718
1894
  ]
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.1",
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
+ }