@benkhz/context-manager 2.0.0 → 2.0.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
@@ -223,6 +223,11 @@ The manager tracks two parallel message lists: the full **history** (everything
223
223
  received, exposed via `getMessages()`) and the **active window** (`getActiveMessages()`) — the
224
224
  slice actually sent to the LLM, which compaction and truncation shrink. History is never pruned.
225
225
 
226
+ This check runs at the start of every `send()` call, and also between tool-call iterations
227
+ *within* a single turn — a request that triggers several tool calls in a row can grow the active
228
+ window past `contextLimit` well before the turn finishes, so compaction can kick in mid-turn
229
+ rather than waiting for the next `send()`.
230
+
226
231
  When the character count of the active window exceeds `contextLimit`:
227
232
 
228
233
  1. `onContextLimit` hook is called — returns `'compact'` (default), `'truncate'`, or `'error'`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benkhz/context-manager",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Provider-agnostic LLM context manager with tool execution, auto-compaction, reactive state, and an event bus.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -73,13 +73,7 @@ export class AIContextManager {
73
73
  }
74
74
 
75
75
  // Auto-compact when approaching the context limit
76
- const chars = this._charCount()
77
- if (chars > this._config.contextLimit) {
78
- const policy = this._hooks.onContextLimit?.(chars, this._config.contextLimit) ?? 'compact'
79
- if (policy === 'compact') await this.compact()
80
- else if (policy === 'truncate') this._truncate()
81
- else throw new Error(`AIContextManager: context limit exceeded (${chars} chars)`)
82
- }
76
+ await this._enforceContextLimit()
83
77
 
84
78
  try {
85
79
  const assistantMsg = await this._runLoop(opts.system)
@@ -256,9 +250,24 @@ export class AIContextManager {
256
250
  this._pushMessage({ role: 'tool', content: JSON.stringify(result), toolCallId: tc.id })
257
251
  }
258
252
 
253
+ // Re-check the context limit between tool-call iterations, not just at the
254
+ // top of send() — a single turn can run several iterations and blow past
255
+ // the limit long before the next send() call ever re-evaluates it.
256
+ await this._enforceContextLimit()
257
+
259
258
  return this._runLoop(system, depth + 1)
260
259
  }
261
260
 
261
+ /** Check contextLimit and apply the configured policy (compact/truncate/error). */
262
+ async _enforceContextLimit() {
263
+ const chars = this._charCount()
264
+ if (chars <= this._config.contextLimit) return
265
+ const policy = this._hooks.onContextLimit?.(chars, this._config.contextLimit) ?? 'compact'
266
+ if (policy === 'compact') await this.compact()
267
+ else if (policy === 'truncate') this._truncate()
268
+ else throw new Error(`AIContextManager: context limit exceeded (${chars} chars)`)
269
+ }
270
+
262
271
  /** Append a message to both the full history and the active LLM-facing window. */
263
272
  _pushMessage(msg) {
264
273
  this._history.push(msg)