@agentionai/agents 0.10.0 → 0.10.2

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.
@@ -81,6 +81,12 @@ class ClaudeAgent extends BaseAgent_1.BaseAgent {
81
81
  else {
82
82
  this.addMessageToHistory("user", input);
83
83
  }
84
+ // Mark session boundary so transform plugins (e.g. toolResultMaskingPlugin)
85
+ // don't mask tool results produced within this execute() loop.
86
+ this.history.setSessionAnchor();
87
+ // Suspend auto-trimming so tool_use / tool_result pairs are never split
88
+ // mid-loop. endExecution() in the finally block enforces limits once.
89
+ this.history.beginExecution();
84
90
  try {
85
91
  const messages = transformers_1.anthropicTransformer.toProvider(this.history.getEntries());
86
92
  const systemMessage = this.history.getSystemMessage();
@@ -122,6 +128,9 @@ class ClaudeAgent extends BaseAgent_1.BaseAgent {
122
128
  throw executionError;
123
129
  }
124
130
  }
131
+ finally {
132
+ this.history.endExecution();
133
+ }
125
134
  }
126
135
  async handleResponse(response) {
127
136
  const usage = this.parseUsage(response.usage);
@@ -181,6 +181,12 @@ class GeminiAgent extends BaseAgent_1.BaseAgent {
181
181
  else {
182
182
  this.addMessageToHistory("user", input);
183
183
  }
184
+ // Mark session boundary so transform plugins (e.g. toolResultMaskingPlugin)
185
+ // don't mask tool results produced within this execute() loop.
186
+ this.history.setSessionAnchor();
187
+ // Suspend auto-trimming so tool_use / tool_result pairs are never split
188
+ // mid-loop. endExecution() in the finally block enforces limits once.
189
+ this.history.beginExecution();
184
190
  try {
185
191
  const contents = transformers_1.geminiTransformer.toProvider(this.history.getEntries());
186
192
  const systemMessage = this.history.getSystemMessage();
@@ -226,6 +232,9 @@ class GeminiAgent extends BaseAgent_1.BaseAgent {
226
232
  throw executionError;
227
233
  }
228
234
  }
235
+ finally {
236
+ this.history.endExecution();
237
+ }
229
238
  }
230
239
  async handleResponse(response) {
231
240
  const result = response.response;
@@ -91,6 +91,12 @@ class MistralAgent extends BaseAgent_1.BaseAgent {
91
91
  else {
92
92
  this.addMessageToHistory("user", input);
93
93
  }
94
+ // Mark session boundary so transform plugins (e.g. toolResultMaskingPlugin)
95
+ // don't mask tool results produced within this execute() loop.
96
+ this.history.setSessionAnchor();
97
+ // Suspend auto-trimming so tool_use / tool_result pairs are never split
98
+ // mid-loop. endExecution() in the finally block enforces limits once.
99
+ this.history.beginExecution();
94
100
  try {
95
101
  const messages = transformers_1.mistralTransformer.toProvider(this.history.getEntries());
96
102
  const response = await this.client.chat.complete({
@@ -130,6 +136,9 @@ class MistralAgent extends BaseAgent_1.BaseAgent {
130
136
  throw executionError;
131
137
  }
132
138
  }
139
+ finally {
140
+ this.history.endExecution();
141
+ }
133
142
  }
134
143
  async handleResponse(response) {
135
144
  if (!response.choices || response.choices.length === 0) {
@@ -102,6 +102,12 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
102
102
  else {
103
103
  this.addMessageToHistory("user", input);
104
104
  }
105
+ // Mark session boundary so transform plugins (e.g. toolResultMaskingPlugin)
106
+ // don't mask tool results produced within this execute() loop.
107
+ this.history.setSessionAnchor();
108
+ // Suspend auto-trimming so tool_use / tool_result pairs are never split
109
+ // mid-loop. endExecution() in the finally block enforces limits once.
110
+ this.history.beginExecution();
105
111
  try {
106
112
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
107
113
  const response = await this.client.responses.create({
@@ -147,6 +153,9 @@ class OpenAiAgent extends BaseAgent_1.BaseAgent {
147
153
  throw executionError;
148
154
  }
149
155
  }
156
+ finally {
157
+ this.history.endExecution();
158
+ }
150
159
  }
151
160
  async handleResponse(response) {
152
161
  if (!response.output || !response.output.length) {
@@ -61,7 +61,7 @@ type EntryWithMetadata = ReducibleEntry;
61
61
  /**
62
62
  * History configuration options
63
63
  */
64
- type HistoryOptions = {
64
+ export type HistoryOptions = {
65
65
  maxLength?: number;
66
66
  /**
67
67
  * Maximum estimated tokens to retain in history. When exceeded, oldest
@@ -123,6 +123,8 @@ export declare class History extends EventEmitter {
123
123
  transient: boolean;
124
124
  private _plugins;
125
125
  private _reducing;
126
+ private _executing;
127
+ private _sessionAnchor;
126
128
  constructor(entries?: HistoryEntry[], options?: HistoryOptions);
127
129
  /**
128
130
  * Register a plugin with this history instance.
@@ -137,6 +139,20 @@ export declare class History extends EventEmitter {
137
139
  * ```
138
140
  */
139
141
  use(plugin: HistoryPlugin): this;
142
+ /**
143
+ * Mark the current entry count as the session boundary.
144
+ * Call this at the start of each agent `execute()` after adding the user
145
+ * message. Transform plugins (e.g. toolResultMaskingPlugin) will not mask
146
+ * any entries added at or after this position, preventing tool results from
147
+ * the current execution loop from being masked mid-session.
148
+ */
149
+ setSessionAnchor(): void;
150
+ /**
151
+ * The entry index set by the last call to `setSessionAnchor()`, or `null`
152
+ * if no anchor has been set. Entries at this index or beyond belong to the
153
+ * current session and should not be masked by transform plugins.
154
+ */
155
+ get sessionAnchor(): number | null;
140
156
  /**
141
157
  * Add a complete history entry
142
158
  */
@@ -232,11 +248,40 @@ export declare class History extends EventEmitter {
232
248
  * Create a copy of this history
233
249
  */
234
250
  clone(options?: HistoryOptions): History;
251
+ /**
252
+ * Signal the start of an agent execute() loop. While executing, automatic
253
+ * trimming on addEntry() is suspended so tool_use / tool_result pairs are
254
+ * never split mid-loop. Call endExecution() in a finally block to resume.
255
+ */
256
+ beginExecution(): void;
257
+ /**
258
+ * Signal the end of an agent execute() loop. Resumes automatic trimming and
259
+ * immediately enforces maxLength / maxTokens limits on the accumulated history.
260
+ */
261
+ endExecution(): void;
262
+ /**
263
+ * Explicitly enforce maxLength and maxTokens limits. Useful when using
264
+ * History standalone, outside of an agent execute() loop.
265
+ */
266
+ trim(): void;
267
+ /**
268
+ * Apply maxLength and maxTokens trimming to the current entry list.
269
+ * Safe to call after bulk-loading entries (e.g. RedisHistory.load()).
270
+ * Subclasses may call this after directly manipulating _entries.
271
+ */
272
+ protected applyTrimming(): void;
235
273
  /**
236
274
  * Drop oldest non-system entries until totalEstimatedTokens fits within budget.
237
275
  * Called synchronously from addEntry() as a safety net.
238
276
  * The system message is always preserved.
239
277
  */
240
278
  private trimToTokenBudget;
279
+ /**
280
+ * After any trim, remove tool_result blocks whose paired tool_use was dropped.
281
+ * Entries that become empty after filtering are also removed.
282
+ * This prevents 400 errors from providers that require tool_result blocks to
283
+ * have a corresponding tool_use in the conversation history.
284
+ */
285
+ private sanitizeToolPairs;
241
286
  }
242
287
  //# sourceMappingURL=History.d.ts.map
@@ -127,6 +127,8 @@ class History extends events_1.default {
127
127
  this.transient = false;
128
128
  this._plugins = [];
129
129
  this._reducing = false;
130
+ this._executing = false;
131
+ this._sessionAnchor = null;
130
132
  this.options = options;
131
133
  this.transient = Boolean(options?.transient);
132
134
  // Convert initial entries to internal format with metadata
@@ -154,6 +156,24 @@ class History extends events_1.default {
154
156
  plugin.onRegistered?.(this);
155
157
  return this;
156
158
  }
159
+ /**
160
+ * Mark the current entry count as the session boundary.
161
+ * Call this at the start of each agent `execute()` after adding the user
162
+ * message. Transform plugins (e.g. toolResultMaskingPlugin) will not mask
163
+ * any entries added at or after this position, preventing tool results from
164
+ * the current execution loop from being masked mid-session.
165
+ */
166
+ setSessionAnchor() {
167
+ this._sessionAnchor = this._entries.length;
168
+ }
169
+ /**
170
+ * The entry index set by the last call to `setSessionAnchor()`, or `null`
171
+ * if no anchor has been set. Entries at this index or beyond belong to the
172
+ * current session and should not be masked by transform plugins.
173
+ */
174
+ get sessionAnchor() {
175
+ return this._sessionAnchor;
176
+ }
157
177
  // ===========================================================================
158
178
  // Core write operations
159
179
  // ===========================================================================
@@ -172,12 +192,7 @@ class History extends events_1.default {
172
192
  ...entry,
173
193
  __metadata,
174
194
  });
175
- if (this.options.maxLength && this._entries.length > this.options.maxLength) {
176
- this._entries = this._entries.slice(this._entries.length - this.options.maxLength);
177
- }
178
- if (this.options.maxTokens) {
179
- this.trimToTokenBudget();
180
- }
195
+ this.applyTrimming();
181
196
  this.emit("entry", entry);
182
197
  // Fire plugin afterAdd hooks. Skipped during reduce() to avoid recursion
183
198
  // when compression plugins add summary entries to the history.
@@ -385,6 +400,45 @@ class History extends events_1.default {
385
400
  // ===========================================================================
386
401
  // Private helpers
387
402
  // ===========================================================================
403
+ /**
404
+ * Signal the start of an agent execute() loop. While executing, automatic
405
+ * trimming on addEntry() is suspended so tool_use / tool_result pairs are
406
+ * never split mid-loop. Call endExecution() in a finally block to resume.
407
+ */
408
+ beginExecution() {
409
+ this._executing = true;
410
+ }
411
+ /**
412
+ * Signal the end of an agent execute() loop. Resumes automatic trimming and
413
+ * immediately enforces maxLength / maxTokens limits on the accumulated history.
414
+ */
415
+ endExecution() {
416
+ this._executing = false;
417
+ this.applyTrimming();
418
+ }
419
+ /**
420
+ * Explicitly enforce maxLength and maxTokens limits. Useful when using
421
+ * History standalone, outside of an agent execute() loop.
422
+ */
423
+ trim() {
424
+ this.applyTrimming();
425
+ }
426
+ /**
427
+ * Apply maxLength and maxTokens trimming to the current entry list.
428
+ * Safe to call after bulk-loading entries (e.g. RedisHistory.load()).
429
+ * Subclasses may call this after directly manipulating _entries.
430
+ */
431
+ applyTrimming() {
432
+ if (this._executing)
433
+ return;
434
+ if (this.options.maxLength && this._entries.length > this.options.maxLength) {
435
+ this._entries = this._entries.slice(this._entries.length - this.options.maxLength);
436
+ this.sanitizeToolPairs();
437
+ }
438
+ if (this.options.maxTokens) {
439
+ this.trimToTokenBudget();
440
+ }
441
+ }
388
442
  /**
389
443
  * Drop oldest non-system entries until totalEstimatedTokens fits within budget.
390
444
  * Called synchronously from addEntry() as a safety net.
@@ -400,6 +454,32 @@ class History extends events_1.default {
400
454
  break;
401
455
  this._entries.splice(firstNonSystem, 1);
402
456
  }
457
+ this.sanitizeToolPairs();
458
+ }
459
+ /**
460
+ * After any trim, remove tool_result blocks whose paired tool_use was dropped.
461
+ * Entries that become empty after filtering are also removed.
462
+ * This prevents 400 errors from providers that require tool_result blocks to
463
+ * have a corresponding tool_use in the conversation history.
464
+ */
465
+ sanitizeToolPairs() {
466
+ // Collect all tool_use IDs still present in the history
467
+ const toolUseIds = new Set();
468
+ for (const entry of this._entries) {
469
+ for (const block of entry.content) {
470
+ if ((0, types_1.isToolUseContent)(block)) {
471
+ toolUseIds.add(block.id);
472
+ }
473
+ }
474
+ }
475
+ // Filter out orphaned tool_result blocks; drop entries that become empty
476
+ this._entries = this._entries.filter((entry) => {
477
+ const filtered = entry.content.filter((block) => !(0, types_1.isToolResultContent)(block) || toolUseIds.has(block.tool_use_id));
478
+ if (filtered.length === 0)
479
+ return false;
480
+ entry.content = filtered;
481
+ return true;
482
+ });
403
483
  }
404
484
  }
405
485
  exports.History = History;
@@ -1,13 +1,15 @@
1
- import { History } from "./History";
1
+ import { History, HistoryOptions } from "./History";
2
2
  interface RedisInstance {
3
3
  get(key: string): Promise<string | null>;
4
4
  set(key: string, content: string): Promise<"OK">;
5
5
  }
6
6
  export declare class RedisHistory extends History {
7
7
  private redisInstance;
8
- constructor(redisInstance: RedisInstance);
8
+ constructor(redisInstance: RedisInstance, options?: HistoryOptions);
9
9
  /**
10
- * Loads history entries from Redis using the specified key
10
+ * Loads history entries from Redis using the specified key.
11
+ * Entries are re-added via addEntry() so metadata is computed correctly.
12
+ * Trimming (maxLength / maxTokens) is applied after load.
11
13
  *
12
14
  * @param {string} key - The Redis key to retrieve history entries from
13
15
  * @returns {Promise<void>} A promise that resolves when history is loaded
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RedisHistory = void 0;
4
4
  const History_1 = require("./History");
5
5
  class RedisHistory extends History_1.History {
6
- constructor(redisInstance) {
7
- super([], { transient: false });
6
+ constructor(redisInstance, options = {}) {
7
+ super([], options);
8
8
  this.redisInstance = redisInstance;
9
9
  }
10
10
  /**
11
- * Loads history entries from Redis using the specified key
11
+ * Loads history entries from Redis using the specified key.
12
+ * Entries are re-added via addEntry() so metadata is computed correctly.
13
+ * Trimming (maxLength / maxTokens) is applied after load.
12
14
  *
13
15
  * @param {string} key - The Redis key to retrieve history entries from
14
16
  * @returns {Promise<void>} A promise that resolves when history is loaded
@@ -16,15 +18,26 @@ class RedisHistory extends History_1.History {
16
18
  */
17
19
  async load(key) {
18
20
  try {
19
- // Retrieve the serialized history from Redis
20
21
  const serializedHistory = await this.redisInstance.get(key);
21
- // If no history exists for the key, return early
22
- if (!serializedHistory) {
22
+ if (!serializedHistory)
23
23
  return;
24
- }
25
- // Parse the serialized history and create a new History instance
26
24
  const entries = JSON.parse(serializedHistory);
27
- this._entries = entries;
25
+ this._entries = [];
26
+ // Re-add via addEntry to compute metadata; suppress plugins during bulk load
27
+ // by temporarily bypassing plugin afterAdd hooks (handled by _reducing flag
28
+ // which is private — so we push entries directly and call applyTrimming once).
29
+ for (const entry of entries) {
30
+ const serialized = JSON.stringify(entry.content);
31
+ this._entries.push({
32
+ ...entry,
33
+ __metadata: {
34
+ date: new Date().toISOString(),
35
+ contentLength: serialized.length,
36
+ estimatedTokens: Math.ceil(serialized.length / 4),
37
+ },
38
+ });
39
+ }
40
+ this.applyTrimming();
28
41
  }
29
42
  catch (error) {
30
43
  console.error(`Error loading history from Redis key "${key}":`, error);
@@ -40,9 +53,7 @@ class RedisHistory extends History_1.History {
40
53
  */
41
54
  async save(key) {
42
55
  try {
43
- // Serialize the current history entries
44
56
  const serializedHistory = this.toJSON();
45
- // Save the serialized history to Redis
46
57
  await this.redisInstance.set(key, serializedHistory);
47
58
  }
48
59
  catch (error) {
@@ -88,7 +88,13 @@ function toolResultMaskingPlugin(options) {
88
88
  }
89
89
  }
90
90
  const maskable = [];
91
+ const sessionAnchor = _history?.sessionAnchor ?? null;
91
92
  for (let ei = 0; ei < entries.length; ei++) {
93
+ // Never mask entries from the current execute() session — doing so
94
+ // would cause the model to call retrieve_tool_result mid-loop, whose
95
+ // result would itself be masked, creating an infinite retrieval loop.
96
+ if (sessionAnchor !== null && ei >= sessionAnchor)
97
+ continue;
92
98
  const entry = entries[ei];
93
99
  for (let bi = 0; bi < entry.content.length; bi++) {
94
100
  const block = entry.content[bi];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentionai/agents",
3
3
  "author": "Laurent Zuijdwijk",
4
- "version": "0.10.0",
4
+ "version": "0.10.2",
5
5
  "description": "Agent Library",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",