llmist 1.2.0 → 1.3.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/dist/cli.cjs CHANGED
@@ -1113,6 +1113,417 @@ var init_output_viewer = __esm({
1113
1113
  }
1114
1114
  });
1115
1115
 
1116
+ // src/agent/compaction/config.ts
1117
+ function resolveCompactionConfig(config = {}) {
1118
+ const trigger = config.triggerThresholdPercent ?? DEFAULT_COMPACTION_CONFIG.triggerThresholdPercent;
1119
+ const target = config.targetPercent ?? DEFAULT_COMPACTION_CONFIG.targetPercent;
1120
+ if (target >= trigger) {
1121
+ console.warn(
1122
+ `[llmist/compaction] targetPercent (${target}) should be less than triggerThresholdPercent (${trigger}) to be effective.`
1123
+ );
1124
+ }
1125
+ const strategy = config.strategy ?? DEFAULT_COMPACTION_CONFIG.strategy;
1126
+ const strategyName = typeof strategy === "object" && "name" in strategy ? strategy.name : strategy;
1127
+ return {
1128
+ enabled: config.enabled ?? DEFAULT_COMPACTION_CONFIG.enabled,
1129
+ strategy: strategyName,
1130
+ triggerThresholdPercent: trigger,
1131
+ targetPercent: target,
1132
+ preserveRecentTurns: config.preserveRecentTurns ?? DEFAULT_COMPACTION_CONFIG.preserveRecentTurns,
1133
+ summarizationModel: config.summarizationModel,
1134
+ summarizationPrompt: config.summarizationPrompt ?? DEFAULT_SUMMARIZATION_PROMPT,
1135
+ onCompaction: config.onCompaction
1136
+ };
1137
+ }
1138
+ var DEFAULT_COMPACTION_CONFIG, DEFAULT_SUMMARIZATION_PROMPT;
1139
+ var init_config = __esm({
1140
+ "src/agent/compaction/config.ts"() {
1141
+ "use strict";
1142
+ DEFAULT_COMPACTION_CONFIG = {
1143
+ enabled: true,
1144
+ strategy: "hybrid",
1145
+ triggerThresholdPercent: 80,
1146
+ targetPercent: 50,
1147
+ preserveRecentTurns: 5
1148
+ };
1149
+ DEFAULT_SUMMARIZATION_PROMPT = `Summarize this conversation history concisely, preserving:
1150
+ 1. Key decisions made and their rationale
1151
+ 2. Important facts and data discovered
1152
+ 3. Errors encountered and how they were resolved
1153
+ 4. Current task context and goals
1154
+
1155
+ Format as a brief narrative paragraph, not bullet points.
1156
+ Previous conversation:`;
1157
+ }
1158
+ });
1159
+
1160
+ // src/agent/compaction/strategy.ts
1161
+ function groupIntoTurns(messages) {
1162
+ const turns = [];
1163
+ let currentTurn = [];
1164
+ for (const msg of messages) {
1165
+ if (msg.role === "user" && currentTurn.length > 0) {
1166
+ turns.push({
1167
+ messages: currentTurn,
1168
+ tokenEstimate: estimateTurnTokens(currentTurn)
1169
+ });
1170
+ currentTurn = [msg];
1171
+ } else {
1172
+ currentTurn.push(msg);
1173
+ }
1174
+ }
1175
+ if (currentTurn.length > 0) {
1176
+ turns.push({
1177
+ messages: currentTurn,
1178
+ tokenEstimate: estimateTurnTokens(currentTurn)
1179
+ });
1180
+ }
1181
+ return turns;
1182
+ }
1183
+ function estimateTurnTokens(messages) {
1184
+ return Math.ceil(messages.reduce((sum, msg) => sum + (msg.content?.length ?? 0), 0) / 4);
1185
+ }
1186
+ function flattenTurns(turns) {
1187
+ return turns.flatMap((turn) => turn.messages);
1188
+ }
1189
+ var init_strategy = __esm({
1190
+ "src/agent/compaction/strategy.ts"() {
1191
+ "use strict";
1192
+ }
1193
+ });
1194
+
1195
+ // src/agent/compaction/strategies/sliding-window.ts
1196
+ var TRUNCATION_MARKER_TEMPLATE, SlidingWindowStrategy;
1197
+ var init_sliding_window = __esm({
1198
+ "src/agent/compaction/strategies/sliding-window.ts"() {
1199
+ "use strict";
1200
+ init_strategy();
1201
+ TRUNCATION_MARKER_TEMPLATE = "[Previous conversation truncated. Removed {count} turn(s) to fit context window.]";
1202
+ SlidingWindowStrategy = class {
1203
+ name = "sliding-window";
1204
+ async compact(messages, config, context) {
1205
+ const turns = groupIntoTurns(messages);
1206
+ const preserveCount = Math.min(config.preserveRecentTurns, turns.length);
1207
+ if (turns.length <= preserveCount) {
1208
+ return {
1209
+ messages,
1210
+ strategyName: this.name,
1211
+ metadata: {
1212
+ originalCount: messages.length,
1213
+ compactedCount: messages.length,
1214
+ tokensBefore: context.currentTokens,
1215
+ tokensAfter: context.currentTokens
1216
+ }
1217
+ };
1218
+ }
1219
+ const turnsToKeep = turns.slice(-preserveCount);
1220
+ const turnsRemoved = turns.length - preserveCount;
1221
+ const truncationMarker = {
1222
+ role: "user",
1223
+ content: TRUNCATION_MARKER_TEMPLATE.replace("{count}", turnsRemoved.toString())
1224
+ };
1225
+ const compactedMessages = [truncationMarker, ...flattenTurns(turnsToKeep)];
1226
+ const tokensAfter = Math.ceil(
1227
+ compactedMessages.reduce((sum, msg) => sum + (msg.content?.length ?? 0), 0) / 4
1228
+ );
1229
+ return {
1230
+ messages: compactedMessages,
1231
+ strategyName: this.name,
1232
+ metadata: {
1233
+ originalCount: messages.length,
1234
+ compactedCount: compactedMessages.length,
1235
+ tokensBefore: context.currentTokens,
1236
+ tokensAfter
1237
+ }
1238
+ };
1239
+ }
1240
+ };
1241
+ }
1242
+ });
1243
+
1244
+ // src/agent/compaction/strategies/summarization.ts
1245
+ var SummarizationStrategy;
1246
+ var init_summarization = __esm({
1247
+ "src/agent/compaction/strategies/summarization.ts"() {
1248
+ "use strict";
1249
+ init_strategy();
1250
+ SummarizationStrategy = class {
1251
+ name = "summarization";
1252
+ async compact(messages, config, context) {
1253
+ const turns = groupIntoTurns(messages);
1254
+ const preserveCount = Math.min(config.preserveRecentTurns, turns.length);
1255
+ if (turns.length <= preserveCount) {
1256
+ return {
1257
+ messages,
1258
+ strategyName: this.name,
1259
+ metadata: {
1260
+ originalCount: messages.length,
1261
+ compactedCount: messages.length,
1262
+ tokensBefore: context.currentTokens,
1263
+ tokensAfter: context.currentTokens
1264
+ }
1265
+ };
1266
+ }
1267
+ const turnsToSummarize = turns.slice(0, -preserveCount);
1268
+ const turnsToKeep = turns.slice(-preserveCount);
1269
+ const conversationToSummarize = this.formatTurnsForSummary(flattenTurns(turnsToSummarize));
1270
+ const summary = await this.generateSummary(conversationToSummarize, config, context);
1271
+ const summaryMessage = {
1272
+ role: "user",
1273
+ content: `[Previous conversation summary]
1274
+ ${summary}
1275
+ [End of summary - conversation continues below]`
1276
+ };
1277
+ const compactedMessages = [summaryMessage, ...flattenTurns(turnsToKeep)];
1278
+ const tokensAfter = Math.ceil(
1279
+ compactedMessages.reduce((sum, msg) => sum + (msg.content?.length ?? 0), 0) / 4
1280
+ );
1281
+ return {
1282
+ messages: compactedMessages,
1283
+ summary,
1284
+ strategyName: this.name,
1285
+ metadata: {
1286
+ originalCount: messages.length,
1287
+ compactedCount: compactedMessages.length,
1288
+ tokensBefore: context.currentTokens,
1289
+ tokensAfter
1290
+ }
1291
+ };
1292
+ }
1293
+ /**
1294
+ * Formats messages into a readable conversation format for summarization.
1295
+ */
1296
+ formatTurnsForSummary(messages) {
1297
+ return messages.map((msg) => {
1298
+ const role = msg.role.charAt(0).toUpperCase() + msg.role.slice(1);
1299
+ return `${role}: ${msg.content}`;
1300
+ }).join("\n\n");
1301
+ }
1302
+ /**
1303
+ * Generates a summary using the configured LLM.
1304
+ */
1305
+ async generateSummary(conversation, config, context) {
1306
+ const model = config.summarizationModel ?? context.model;
1307
+ const prompt = `${config.summarizationPrompt}
1308
+
1309
+ ${conversation}`;
1310
+ const response = await context.client.complete(prompt, {
1311
+ model,
1312
+ temperature: 0.3
1313
+ // Low temperature for factual summarization
1314
+ });
1315
+ return response.trim();
1316
+ }
1317
+ };
1318
+ }
1319
+ });
1320
+
1321
+ // src/agent/compaction/strategies/hybrid.ts
1322
+ var MIN_TURNS_FOR_SUMMARIZATION, HybridStrategy;
1323
+ var init_hybrid = __esm({
1324
+ "src/agent/compaction/strategies/hybrid.ts"() {
1325
+ "use strict";
1326
+ init_strategy();
1327
+ init_sliding_window();
1328
+ init_summarization();
1329
+ MIN_TURNS_FOR_SUMMARIZATION = 3;
1330
+ HybridStrategy = class {
1331
+ name = "hybrid";
1332
+ slidingWindow = new SlidingWindowStrategy();
1333
+ summarization = new SummarizationStrategy();
1334
+ async compact(messages, config, context) {
1335
+ const turns = groupIntoTurns(messages);
1336
+ const preserveCount = Math.min(config.preserveRecentTurns, turns.length);
1337
+ if (turns.length <= preserveCount) {
1338
+ return {
1339
+ messages,
1340
+ strategyName: this.name,
1341
+ metadata: {
1342
+ originalCount: messages.length,
1343
+ compactedCount: messages.length,
1344
+ tokensBefore: context.currentTokens,
1345
+ tokensAfter: context.currentTokens
1346
+ }
1347
+ };
1348
+ }
1349
+ const turnsToSummarize = turns.length - preserveCount;
1350
+ if (turnsToSummarize < MIN_TURNS_FOR_SUMMARIZATION) {
1351
+ return this.slidingWindow.compact(messages, config, context);
1352
+ }
1353
+ return this.summarization.compact(messages, config, context);
1354
+ }
1355
+ };
1356
+ }
1357
+ });
1358
+
1359
+ // src/agent/compaction/strategies/index.ts
1360
+ var init_strategies = __esm({
1361
+ "src/agent/compaction/strategies/index.ts"() {
1362
+ "use strict";
1363
+ init_sliding_window();
1364
+ init_summarization();
1365
+ init_hybrid();
1366
+ }
1367
+ });
1368
+
1369
+ // src/agent/compaction/manager.ts
1370
+ function createStrategy(name) {
1371
+ switch (name) {
1372
+ case "sliding-window":
1373
+ return new SlidingWindowStrategy();
1374
+ case "summarization":
1375
+ return new SummarizationStrategy();
1376
+ case "hybrid":
1377
+ return new HybridStrategy();
1378
+ default:
1379
+ throw new Error(`Unknown compaction strategy: ${name}`);
1380
+ }
1381
+ }
1382
+ var CompactionManager;
1383
+ var init_manager = __esm({
1384
+ "src/agent/compaction/manager.ts"() {
1385
+ "use strict";
1386
+ init_config();
1387
+ init_strategies();
1388
+ CompactionManager = class {
1389
+ client;
1390
+ model;
1391
+ config;
1392
+ strategy;
1393
+ modelLimits;
1394
+ // Statistics
1395
+ totalCompactions = 0;
1396
+ totalTokensSaved = 0;
1397
+ lastTokenCount = 0;
1398
+ constructor(client, model, config = {}) {
1399
+ this.client = client;
1400
+ this.model = model;
1401
+ this.config = resolveCompactionConfig(config);
1402
+ if (typeof config.strategy === "object" && "compact" in config.strategy) {
1403
+ this.strategy = config.strategy;
1404
+ } else {
1405
+ this.strategy = createStrategy(this.config.strategy);
1406
+ }
1407
+ }
1408
+ /**
1409
+ * Check if compaction is needed and perform it if so.
1410
+ *
1411
+ * @param conversation - The conversation manager to compact
1412
+ * @param iteration - Current agent iteration (for event metadata)
1413
+ * @returns CompactionEvent if compaction was performed, null otherwise
1414
+ */
1415
+ async checkAndCompact(conversation, iteration) {
1416
+ if (!this.config.enabled) {
1417
+ return null;
1418
+ }
1419
+ if (!this.modelLimits) {
1420
+ this.modelLimits = this.client.modelRegistry.getModelLimits(this.model);
1421
+ if (!this.modelLimits) {
1422
+ return null;
1423
+ }
1424
+ }
1425
+ if (!this.client.countTokens) {
1426
+ return null;
1427
+ }
1428
+ const messages = conversation.getMessages();
1429
+ const currentTokens = await this.client.countTokens(this.model, messages);
1430
+ this.lastTokenCount = currentTokens;
1431
+ const usagePercent = currentTokens / this.modelLimits.contextWindow * 100;
1432
+ if (usagePercent < this.config.triggerThresholdPercent) {
1433
+ return null;
1434
+ }
1435
+ const historyMessages = conversation.getHistoryMessages();
1436
+ const baseMessages = conversation.getBaseMessages();
1437
+ const historyTokens = await this.client.countTokens(this.model, historyMessages);
1438
+ const baseTokens = await this.client.countTokens(this.model, baseMessages);
1439
+ return this.compact(conversation, iteration, {
1440
+ historyMessages,
1441
+ baseMessages,
1442
+ historyTokens,
1443
+ baseTokens,
1444
+ currentTokens: historyTokens + baseTokens
1445
+ });
1446
+ }
1447
+ /**
1448
+ * Force compaction regardless of threshold.
1449
+ *
1450
+ * @param conversation - The conversation manager to compact
1451
+ * @param iteration - Current agent iteration (for event metadata). Use -1 for manual compaction.
1452
+ * @param precomputed - Optional pre-computed token counts (passed from checkAndCompact for efficiency)
1453
+ * @returns CompactionEvent with compaction details
1454
+ */
1455
+ async compact(conversation, iteration, precomputed) {
1456
+ if (!this.modelLimits) {
1457
+ this.modelLimits = this.client.modelRegistry.getModelLimits(this.model);
1458
+ if (!this.modelLimits) {
1459
+ return null;
1460
+ }
1461
+ }
1462
+ const historyMessages = precomputed?.historyMessages ?? conversation.getHistoryMessages();
1463
+ const baseMessages = precomputed?.baseMessages ?? conversation.getBaseMessages();
1464
+ const historyTokens = precomputed?.historyTokens ?? await this.client.countTokens(this.model, historyMessages);
1465
+ const baseTokens = precomputed?.baseTokens ?? await this.client.countTokens(this.model, baseMessages);
1466
+ const currentTokens = precomputed?.currentTokens ?? historyTokens + baseTokens;
1467
+ const targetTotalTokens = Math.floor(
1468
+ this.modelLimits.contextWindow * this.config.targetPercent / 100
1469
+ );
1470
+ const targetHistoryTokens = Math.max(0, targetTotalTokens - baseTokens);
1471
+ const result = await this.strategy.compact(historyMessages, this.config, {
1472
+ currentTokens: historyTokens,
1473
+ targetTokens: targetHistoryTokens,
1474
+ modelLimits: this.modelLimits,
1475
+ client: this.client,
1476
+ model: this.config.summarizationModel ?? this.model
1477
+ });
1478
+ conversation.replaceHistory(result.messages);
1479
+ const afterTokens = await this.client.countTokens(this.model, conversation.getMessages());
1480
+ const tokensSaved = currentTokens - afterTokens;
1481
+ this.totalCompactions++;
1482
+ this.totalTokensSaved += tokensSaved;
1483
+ this.lastTokenCount = afterTokens;
1484
+ const event = {
1485
+ strategy: result.strategyName,
1486
+ tokensBefore: currentTokens,
1487
+ tokensAfter: afterTokens,
1488
+ messagesBefore: historyMessages.length + baseMessages.length,
1489
+ messagesAfter: result.messages.length + baseMessages.length,
1490
+ summary: result.summary,
1491
+ iteration
1492
+ };
1493
+ if (this.config.onCompaction) {
1494
+ try {
1495
+ this.config.onCompaction(event);
1496
+ } catch (err) {
1497
+ console.warn("[llmist/compaction] onCompaction callback error:", err);
1498
+ }
1499
+ }
1500
+ return event;
1501
+ }
1502
+ /**
1503
+ * Get compaction statistics.
1504
+ */
1505
+ getStats() {
1506
+ const contextWindow = this.modelLimits?.contextWindow ?? 0;
1507
+ return {
1508
+ totalCompactions: this.totalCompactions,
1509
+ totalTokensSaved: this.totalTokensSaved,
1510
+ currentUsage: {
1511
+ tokens: this.lastTokenCount,
1512
+ percent: contextWindow > 0 ? this.lastTokenCount / contextWindow * 100 : 0
1513
+ },
1514
+ contextWindow
1515
+ };
1516
+ }
1517
+ /**
1518
+ * Check if compaction is enabled.
1519
+ */
1520
+ isEnabled() {
1521
+ return this.config.enabled;
1522
+ }
1523
+ };
1524
+ }
1525
+ });
1526
+
1116
1527
  // src/agent/gadget-output-store.ts
1117
1528
  var import_node_crypto, GadgetOutputStore;
1118
1529
  var init_gadget_output_store = __esm({
@@ -1215,10 +1626,16 @@ var init_conversation_manager = __esm({
1215
1626
  baseMessages;
1216
1627
  initialMessages;
1217
1628
  historyBuilder;
1629
+ startPrefix;
1630
+ endPrefix;
1631
+ argPrefix;
1218
1632
  constructor(baseMessages, initialMessages, options = {}) {
1219
1633
  this.baseMessages = baseMessages;
1220
1634
  this.initialMessages = initialMessages;
1221
1635
  this.historyBuilder = new LLMMessageBuilder();
1636
+ this.startPrefix = options.startPrefix;
1637
+ this.endPrefix = options.endPrefix;
1638
+ this.argPrefix = options.argPrefix;
1222
1639
  if (options.startPrefix && options.endPrefix) {
1223
1640
  this.historyBuilder.withPrefixes(options.startPrefix, options.endPrefix, options.argPrefix);
1224
1641
  }
@@ -1235,6 +1652,25 @@ var init_conversation_manager = __esm({
1235
1652
  getMessages() {
1236
1653
  return [...this.baseMessages, ...this.initialMessages, ...this.historyBuilder.build()];
1237
1654
  }
1655
+ getHistoryMessages() {
1656
+ return this.historyBuilder.build();
1657
+ }
1658
+ getBaseMessages() {
1659
+ return [...this.baseMessages, ...this.initialMessages];
1660
+ }
1661
+ replaceHistory(newHistory) {
1662
+ this.historyBuilder = new LLMMessageBuilder();
1663
+ if (this.startPrefix && this.endPrefix) {
1664
+ this.historyBuilder.withPrefixes(this.startPrefix, this.endPrefix, this.argPrefix);
1665
+ }
1666
+ for (const msg of newHistory) {
1667
+ if (msg.role === "user") {
1668
+ this.historyBuilder.addUser(msg.content);
1669
+ } else if (msg.role === "assistant") {
1670
+ this.historyBuilder.addAssistant(msg.content);
1671
+ }
1672
+ }
1673
+ }
1238
1674
  };
1239
1675
  }
1240
1676
  });
@@ -2781,6 +3217,7 @@ var init_agent = __esm({
2781
3217
  init_model_shortcuts();
2782
3218
  init_output_viewer();
2783
3219
  init_logger();
3220
+ init_manager();
2784
3221
  init_gadget_output_store();
2785
3222
  init_agent_internal_key();
2786
3223
  init_conversation_manager();
@@ -2811,6 +3248,8 @@ var init_agent = __esm({
2811
3248
  outputStore;
2812
3249
  outputLimitEnabled;
2813
3250
  outputLimitCharLimit;
3251
+ // Context compaction
3252
+ compactionManager;
2814
3253
  /**
2815
3254
  * Creates a new Agent instance.
2816
3255
  * @internal This constructor is private. Use LLMist.createAgent() or AgentBuilder instead.
@@ -2870,6 +3309,14 @@ var init_agent = __esm({
2870
3309
  if (options.userPrompt) {
2871
3310
  this.conversation.addUserMessage(options.userPrompt);
2872
3311
  }
3312
+ const compactionEnabled = options.compactionConfig?.enabled ?? true;
3313
+ if (compactionEnabled) {
3314
+ this.compactionManager = new CompactionManager(
3315
+ this.client,
3316
+ this.model,
3317
+ options.compactionConfig
3318
+ );
3319
+ }
2873
3320
  }
2874
3321
  /**
2875
3322
  * Get the gadget registry for this agent.
@@ -2892,6 +3339,53 @@ var init_agent = __esm({
2892
3339
  getRegistry() {
2893
3340
  return this.registry;
2894
3341
  }
3342
+ /**
3343
+ * Manually trigger context compaction.
3344
+ *
3345
+ * Forces compaction regardless of threshold. Useful for:
3346
+ * - Pre-emptive context management before expected long operations
3347
+ * - Testing compaction behavior
3348
+ *
3349
+ * @returns CompactionEvent if compaction was performed, null if not configured or no history
3350
+ *
3351
+ * @example
3352
+ * ```typescript
3353
+ * const agent = await LLMist.createAgent()
3354
+ * .withModel('sonnet')
3355
+ * .withCompaction()
3356
+ * .ask('...');
3357
+ *
3358
+ * // Manually compact before a long operation
3359
+ * const event = await agent.compact();
3360
+ * if (event) {
3361
+ * console.log(`Saved ${event.tokensBefore - event.tokensAfter} tokens`);
3362
+ * }
3363
+ * ```
3364
+ */
3365
+ async compact() {
3366
+ if (!this.compactionManager) {
3367
+ return null;
3368
+ }
3369
+ return this.compactionManager.compact(this.conversation, -1);
3370
+ }
3371
+ /**
3372
+ * Get compaction statistics.
3373
+ *
3374
+ * @returns CompactionStats if compaction is enabled, null otherwise
3375
+ *
3376
+ * @example
3377
+ * ```typescript
3378
+ * const stats = agent.getCompactionStats();
3379
+ * if (stats) {
3380
+ * console.log(`Total compactions: ${stats.totalCompactions}`);
3381
+ * console.log(`Tokens saved: ${stats.totalTokensSaved}`);
3382
+ * console.log(`Current usage: ${stats.currentUsage.percent.toFixed(1)}%`);
3383
+ * }
3384
+ * ```
3385
+ */
3386
+ getCompactionStats() {
3387
+ return this.compactionManager?.getStats() ?? null;
3388
+ }
2895
3389
  /**
2896
3390
  * Run the agent loop.
2897
3391
  * Clean, simple orchestration - all complexity is in StreamProcessor.
@@ -2912,6 +3406,30 @@ var init_agent = __esm({
2912
3406
  while (currentIteration < this.maxIterations) {
2913
3407
  this.logger.debug("Starting iteration", { iteration: currentIteration });
2914
3408
  try {
3409
+ if (this.compactionManager) {
3410
+ const compactionEvent = await this.compactionManager.checkAndCompact(
3411
+ this.conversation,
3412
+ currentIteration
3413
+ );
3414
+ if (compactionEvent) {
3415
+ this.logger.info("Context compacted", {
3416
+ strategy: compactionEvent.strategy,
3417
+ tokensBefore: compactionEvent.tokensBefore,
3418
+ tokensAfter: compactionEvent.tokensAfter
3419
+ });
3420
+ yield { type: "compaction", event: compactionEvent };
3421
+ await this.safeObserve(async () => {
3422
+ if (this.hooks.observers?.onCompaction) {
3423
+ await this.hooks.observers.onCompaction({
3424
+ iteration: currentIteration,
3425
+ event: compactionEvent,
3426
+ stats: this.compactionManager.getStats(),
3427
+ logger: this.logger
3428
+ });
3429
+ }
3430
+ });
3431
+ }
3432
+ }
2915
3433
  let llmOptions = {
2916
3434
  model: this.model,
2917
3435
  messages: this.conversation.getMessages(),
@@ -2931,6 +3449,7 @@ var init_agent = __esm({
2931
3449
  if (this.hooks.controllers?.beforeLLMCall) {
2932
3450
  const context = {
2933
3451
  iteration: currentIteration,
3452
+ maxIterations: this.maxIterations,
2934
3453
  options: llmOptions,
2935
3454
  logger: this.logger
2936
3455
  };
@@ -2995,12 +3514,17 @@ var init_agent = __esm({
2995
3514
  });
2996
3515
  let finalMessage = result.finalMessage;
2997
3516
  if (this.hooks.controllers?.afterLLMCall) {
3517
+ const gadgetCallCount = result.outputs.filter(
3518
+ (output) => output.type === "gadget_result"
3519
+ ).length;
2998
3520
  const context = {
2999
3521
  iteration: currentIteration,
3522
+ maxIterations: this.maxIterations,
3000
3523
  options: llmOptions,
3001
3524
  finishReason: result.finishReason,
3002
3525
  usage: result.usage,
3003
3526
  finalMessage: result.finalMessage,
3527
+ gadgetCallCount,
3004
3528
  logger: this.logger
3005
3529
  };
3006
3530
  const action = await this.hooks.controllers.afterLLMCall(context);
@@ -5235,6 +5759,7 @@ var init_builder = __esm({
5235
5759
  defaultGadgetTimeoutMs;
5236
5760
  gadgetOutputLimit;
5237
5761
  gadgetOutputLimitPercent;
5762
+ compactionConfig;
5238
5763
  constructor(client) {
5239
5764
  this.client = client;
5240
5765
  }
@@ -5630,6 +6155,57 @@ var init_builder = __esm({
5630
6155
  this.gadgetOutputLimitPercent = percent;
5631
6156
  return this;
5632
6157
  }
6158
+ /**
6159
+ * Configure context compaction.
6160
+ *
6161
+ * Context compaction automatically manages conversation history to prevent
6162
+ * context window overflow in long-running agent conversations.
6163
+ *
6164
+ * @param config - Compaction configuration options
6165
+ * @returns This builder for chaining
6166
+ *
6167
+ * @example
6168
+ * ```typescript
6169
+ * // Custom thresholds
6170
+ * .withCompaction({
6171
+ * triggerThresholdPercent: 70,
6172
+ * targetPercent: 40,
6173
+ * preserveRecentTurns: 10,
6174
+ * })
6175
+ *
6176
+ * // Different strategy
6177
+ * .withCompaction({
6178
+ * strategy: 'sliding-window',
6179
+ * })
6180
+ *
6181
+ * // With callback
6182
+ * .withCompaction({
6183
+ * onCompaction: (event) => {
6184
+ * console.log(`Saved ${event.tokensBefore - event.tokensAfter} tokens`);
6185
+ * }
6186
+ * })
6187
+ * ```
6188
+ */
6189
+ withCompaction(config) {
6190
+ this.compactionConfig = { ...config, enabled: config.enabled ?? true };
6191
+ return this;
6192
+ }
6193
+ /**
6194
+ * Disable context compaction.
6195
+ *
6196
+ * By default, compaction is enabled. Use this method to explicitly disable it.
6197
+ *
6198
+ * @returns This builder for chaining
6199
+ *
6200
+ * @example
6201
+ * ```typescript
6202
+ * .withoutCompaction() // Disable automatic compaction
6203
+ * ```
6204
+ */
6205
+ withoutCompaction() {
6206
+ this.compactionConfig = { enabled: false };
6207
+ return this;
6208
+ }
5633
6209
  /**
5634
6210
  * Add a synthetic gadget call to the conversation history.
5635
6211
  *
@@ -5745,7 +6321,8 @@ ${endPrefix}`
5745
6321
  shouldContinueAfterError: this.shouldContinueAfterError,
5746
6322
  defaultGadgetTimeoutMs: this.defaultGadgetTimeoutMs,
5747
6323
  gadgetOutputLimit: this.gadgetOutputLimit,
5748
- gadgetOutputLimitPercent: this.gadgetOutputLimitPercent
6324
+ gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
6325
+ compactionConfig: this.compactionConfig
5749
6326
  };
5750
6327
  return new Agent(AGENT_INTERNAL_KEY, options);
5751
6328
  }
@@ -5847,7 +6424,8 @@ ${endPrefix}`
5847
6424
  shouldContinueAfterError: this.shouldContinueAfterError,
5848
6425
  defaultGadgetTimeoutMs: this.defaultGadgetTimeoutMs,
5849
6426
  gadgetOutputLimit: this.gadgetOutputLimit,
5850
- gadgetOutputLimitPercent: this.gadgetOutputLimitPercent
6427
+ gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
6428
+ compactionConfig: this.compactionConfig
5851
6429
  };
5852
6430
  return new Agent(AGENT_INTERNAL_KEY, options);
5853
6431
  }
@@ -5906,7 +6484,7 @@ var import_commander2 = require("commander");
5906
6484
  // package.json
5907
6485
  var package_default = {
5908
6486
  name: "llmist",
5909
- version: "1.1.0",
6487
+ version: "1.3.0",
5910
6488
  description: "Universal TypeScript LLM client with streaming-first agent framework. Works with any model - no structured outputs or native tool calling required. Implements its own flexible grammar for function calling.",
5911
6489
  type: "module",
5912
6490
  main: "dist/index.cjs",
@@ -6053,26 +6631,16 @@ var askUser = createGadget({
6053
6631
  });
6054
6632
  var tellUser = createGadget({
6055
6633
  name: "TellUser",
6056
- description: "Tell the user something important. Set done=true when your work is complete and you want to end the conversation.",
6634
+ description: "Tell the user something important.",
6057
6635
  schema: import_zod2.z.object({
6058
6636
  message: import_zod2.z.string().optional().describe("The message to display to the user in Markdown"),
6059
- done: import_zod2.z.boolean().default(false).describe("Set to true to end the conversation, false to continue"),
6060
6637
  type: import_zod2.z.enum(["info", "success", "warning", "error"]).default("info").describe("Message type: info, success, warning, or error")
6061
6638
  }),
6062
6639
  examples: [
6063
6640
  {
6064
- comment: "Report successful completion and end the conversation",
6065
- params: {
6066
- message: "I've completed the refactoring. All tests pass.",
6067
- done: true,
6068
- type: "success"
6069
- }
6070
- },
6071
- {
6072
- comment: "Warn the user about something without ending",
6641
+ comment: "Warn the user about something",
6073
6642
  params: {
6074
6643
  message: "Found 3 files with potential issues. Continuing analysis...",
6075
- done: false,
6076
6644
  type: "warning"
6077
6645
  }
6078
6646
  },
@@ -6080,12 +6648,11 @@ var tellUser = createGadget({
6080
6648
  comment: "Share detailed analysis with bullet points (use heredoc for multiline)",
6081
6649
  params: {
6082
6650
  message: "Here's what I found in the codebase:\n\n1. **Main entry point**: `src/index.ts` exports all public APIs\n2. **Core logic**: Located in `src/core/` with 5 modules\n3. **Tests**: Good coverage in `src/__tests__/`\n\nI'll continue exploring the core modules.",
6083
- done: false,
6084
6651
  type: "info"
6085
6652
  }
6086
6653
  }
6087
6654
  ],
6088
- execute: ({ message, done, type }) => {
6655
+ execute: ({ message, type }) => {
6089
6656
  if (!message || message.trim() === "") {
6090
6657
  return "\u26A0\uFE0F TellUser was called without a message. Please provide content in the 'message' field.";
6091
6658
  }
@@ -6095,14 +6662,24 @@ var tellUser = createGadget({
6095
6662
  warning: "\u26A0\uFE0F ",
6096
6663
  error: "\u274C "
6097
6664
  };
6098
- const plainResult = prefixes[type] + message;
6099
- if (done) {
6100
- throw new BreakLoopException(plainResult);
6665
+ return prefixes[type] + message;
6666
+ }
6667
+ });
6668
+ var finish = createGadget({
6669
+ name: "Finish",
6670
+ description: "Signal that you have completed your task. Call this when your work is done.",
6671
+ schema: import_zod2.z.object({}),
6672
+ examples: [
6673
+ {
6674
+ comment: "Signal task completion",
6675
+ params: {}
6101
6676
  }
6102
- return plainResult;
6677
+ ],
6678
+ execute: () => {
6679
+ throw new BreakLoopException("Task completed");
6103
6680
  }
6104
6681
  });
6105
- var builtinGadgets = [askUser, tellUser];
6682
+ var builtinGadgets = [askUser, tellUser, finish];
6106
6683
 
6107
6684
  // src/cli/gadgets.ts
6108
6685
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -6658,6 +7235,17 @@ var StreamProgress = class {
6658
7235
  } else {
6659
7236
  parts.push(iterPart);
6660
7237
  }
7238
+ const usagePercent = this.getContextUsagePercent();
7239
+ if (usagePercent !== null) {
7240
+ const formatted = `${Math.round(usagePercent)}%`;
7241
+ if (usagePercent >= 80) {
7242
+ parts.push(import_chalk2.default.red(formatted));
7243
+ } else if (usagePercent >= 50) {
7244
+ parts.push(import_chalk2.default.yellow(formatted));
7245
+ } else {
7246
+ parts.push(import_chalk2.default.green(formatted));
7247
+ }
7248
+ }
6661
7249
  if (this.callInputTokens > 0) {
6662
7250
  const prefix = this.callInputTokensEstimated ? "~" : "";
6663
7251
  parts.push(import_chalk2.default.dim("\u2191") + import_chalk2.default.yellow(` ${prefix}${formatTokens(this.callInputTokens)}`));
@@ -6693,6 +7281,21 @@ var StreamProgress = class {
6693
7281
  return 0;
6694
7282
  }
6695
7283
  }
7284
+ /**
7285
+ * Calculates context window usage percentage.
7286
+ * Returns null if model is unknown or context window unavailable.
7287
+ */
7288
+ getContextUsagePercent() {
7289
+ if (!this.modelRegistry || !this.model || this.callInputTokens === 0) {
7290
+ return null;
7291
+ }
7292
+ const modelName = this.model.includes(":") ? this.model.split(":")[1] : this.model;
7293
+ const limits = this.modelRegistry.getModelLimits(modelName);
7294
+ if (!limits?.contextWindow) {
7295
+ return null;
7296
+ }
7297
+ return this.callInputTokens / limits.contextWindow * 100;
7298
+ }
6696
7299
  renderCumulativeMode(spinner) {
6697
7300
  const elapsed = ((Date.now() - this.totalStartTime) / 1e3).toFixed(1);
6698
7301
  const parts = [];
@@ -7320,7 +7923,9 @@ function resolveTemplate(eta, template, context = {}, configPath) {
7320
7923
  try {
7321
7924
  const fullContext = {
7322
7925
  ...context,
7323
- env: process.env
7926
+ env: process.env,
7927
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
7928
+ // "2025-12-01"
7324
7929
  };
7325
7930
  return eta.renderString(template, fullContext);
7326
7931
  } catch (error) {