@agentionai/agents 0.10.0 → 0.10.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.
@@ -81,6 +81,9 @@ 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();
84
87
  try {
85
88
  const messages = transformers_1.anthropicTransformer.toProvider(this.history.getEntries());
86
89
  const systemMessage = this.history.getSystemMessage();
@@ -181,6 +181,9 @@ 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();
184
187
  try {
185
188
  const contents = transformers_1.geminiTransformer.toProvider(this.history.getEntries());
186
189
  const systemMessage = this.history.getSystemMessage();
@@ -91,6 +91,9 @@ 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();
94
97
  try {
95
98
  const messages = transformers_1.mistralTransformer.toProvider(this.history.getEntries());
96
99
  const response = await this.client.chat.complete({
@@ -102,6 +102,9 @@ 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();
105
108
  try {
106
109
  const inputMessages = transformers_1.openAiTransformer.toProvider(this.history.getEntries());
107
110
  const response = await this.client.responses.create({
@@ -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,7 @@ export declare class History extends EventEmitter {
123
123
  transient: boolean;
124
124
  private _plugins;
125
125
  private _reducing;
126
+ private _sessionAnchor;
126
127
  constructor(entries?: HistoryEntry[], options?: HistoryOptions);
127
128
  /**
128
129
  * Register a plugin with this history instance.
@@ -137,6 +138,20 @@ export declare class History extends EventEmitter {
137
138
  * ```
138
139
  */
139
140
  use(plugin: HistoryPlugin): this;
141
+ /**
142
+ * Mark the current entry count as the session boundary.
143
+ * Call this at the start of each agent `execute()` after adding the user
144
+ * message. Transform plugins (e.g. toolResultMaskingPlugin) will not mask
145
+ * any entries added at or after this position, preventing tool results from
146
+ * the current execution loop from being masked mid-session.
147
+ */
148
+ setSessionAnchor(): void;
149
+ /**
150
+ * The entry index set by the last call to `setSessionAnchor()`, or `null`
151
+ * if no anchor has been set. Entries at this index or beyond belong to the
152
+ * current session and should not be masked by transform plugins.
153
+ */
154
+ get sessionAnchor(): number | null;
140
155
  /**
141
156
  * Add a complete history entry
142
157
  */
@@ -232,11 +247,24 @@ export declare class History extends EventEmitter {
232
247
  * Create a copy of this history
233
248
  */
234
249
  clone(options?: HistoryOptions): History;
250
+ /**
251
+ * Apply maxLength and maxTokens trimming to the current entry list.
252
+ * Safe to call after bulk-loading entries (e.g. RedisHistory.load()).
253
+ * Subclasses may call this after directly manipulating _entries.
254
+ */
255
+ protected applyTrimming(): void;
235
256
  /**
236
257
  * Drop oldest non-system entries until totalEstimatedTokens fits within budget.
237
258
  * Called synchronously from addEntry() as a safety net.
238
259
  * The system message is always preserved.
239
260
  */
240
261
  private trimToTokenBudget;
262
+ /**
263
+ * After any trim, remove tool_result blocks whose paired tool_use was dropped.
264
+ * Entries that become empty after filtering are also removed.
265
+ * This prevents 400 errors from providers that require tool_result blocks to
266
+ * have a corresponding tool_use in the conversation history.
267
+ */
268
+ private sanitizeToolPairs;
241
269
  }
242
270
  //# sourceMappingURL=History.d.ts.map
@@ -127,6 +127,7 @@ class History extends events_1.default {
127
127
  this.transient = false;
128
128
  this._plugins = [];
129
129
  this._reducing = false;
130
+ this._sessionAnchor = null;
130
131
  this.options = options;
131
132
  this.transient = Boolean(options?.transient);
132
133
  // Convert initial entries to internal format with metadata
@@ -154,6 +155,24 @@ class History extends events_1.default {
154
155
  plugin.onRegistered?.(this);
155
156
  return this;
156
157
  }
158
+ /**
159
+ * Mark the current entry count as the session boundary.
160
+ * Call this at the start of each agent `execute()` after adding the user
161
+ * message. Transform plugins (e.g. toolResultMaskingPlugin) will not mask
162
+ * any entries added at or after this position, preventing tool results from
163
+ * the current execution loop from being masked mid-session.
164
+ */
165
+ setSessionAnchor() {
166
+ this._sessionAnchor = this._entries.length;
167
+ }
168
+ /**
169
+ * The entry index set by the last call to `setSessionAnchor()`, or `null`
170
+ * if no anchor has been set. Entries at this index or beyond belong to the
171
+ * current session and should not be masked by transform plugins.
172
+ */
173
+ get sessionAnchor() {
174
+ return this._sessionAnchor;
175
+ }
157
176
  // ===========================================================================
158
177
  // Core write operations
159
178
  // ===========================================================================
@@ -172,12 +191,7 @@ class History extends events_1.default {
172
191
  ...entry,
173
192
  __metadata,
174
193
  });
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
- }
194
+ this.applyTrimming();
181
195
  this.emit("entry", entry);
182
196
  // Fire plugin afterAdd hooks. Skipped during reduce() to avoid recursion
183
197
  // when compression plugins add summary entries to the history.
@@ -385,6 +399,20 @@ class History extends events_1.default {
385
399
  // ===========================================================================
386
400
  // Private helpers
387
401
  // ===========================================================================
402
+ /**
403
+ * Apply maxLength and maxTokens trimming to the current entry list.
404
+ * Safe to call after bulk-loading entries (e.g. RedisHistory.load()).
405
+ * Subclasses may call this after directly manipulating _entries.
406
+ */
407
+ applyTrimming() {
408
+ if (this.options.maxLength && this._entries.length > this.options.maxLength) {
409
+ this._entries = this._entries.slice(this._entries.length - this.options.maxLength);
410
+ this.sanitizeToolPairs();
411
+ }
412
+ if (this.options.maxTokens) {
413
+ this.trimToTokenBudget();
414
+ }
415
+ }
388
416
  /**
389
417
  * Drop oldest non-system entries until totalEstimatedTokens fits within budget.
390
418
  * Called synchronously from addEntry() as a safety net.
@@ -400,6 +428,32 @@ class History extends events_1.default {
400
428
  break;
401
429
  this._entries.splice(firstNonSystem, 1);
402
430
  }
431
+ this.sanitizeToolPairs();
432
+ }
433
+ /**
434
+ * After any trim, remove tool_result blocks whose paired tool_use was dropped.
435
+ * Entries that become empty after filtering are also removed.
436
+ * This prevents 400 errors from providers that require tool_result blocks to
437
+ * have a corresponding tool_use in the conversation history.
438
+ */
439
+ sanitizeToolPairs() {
440
+ // Collect all tool_use IDs still present in the history
441
+ const toolUseIds = new Set();
442
+ for (const entry of this._entries) {
443
+ for (const block of entry.content) {
444
+ if ((0, types_1.isToolUseContent)(block)) {
445
+ toolUseIds.add(block.id);
446
+ }
447
+ }
448
+ }
449
+ // Filter out orphaned tool_result blocks; drop entries that become empty
450
+ this._entries = this._entries.filter((entry) => {
451
+ const filtered = entry.content.filter((block) => !(0, types_1.isToolResultContent)(block) || toolUseIds.has(block.tool_use_id));
452
+ if (filtered.length === 0)
453
+ return false;
454
+ entry.content = filtered;
455
+ return true;
456
+ });
403
457
  }
404
458
  }
405
459
  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.1",
5
5
  "description": "Agent Library",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",