@probelabs/probe 0.6.0-rc125 → 0.6.0-rc126

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.
@@ -10,6 +10,8 @@ import { existsSync } from 'fs';
10
10
  import { readFile, stat } from 'fs/promises';
11
11
  import { resolve, isAbsolute, dirname } from 'path';
12
12
  import { TokenCounter } from './tokenCounter.js';
13
+ import { InMemoryStorageAdapter } from './storage/InMemoryStorageAdapter.js';
14
+ import { HookManager, HOOK_TYPES } from './hooks/HookManager.js';
13
15
  import {
14
16
  createTools,
15
17
  searchToolDefinition,
@@ -80,6 +82,8 @@ export class ProbeAgent {
80
82
  * @param {string} [options.mcpConfigPath] - Path to MCP configuration file
81
83
  * @param {Object} [options.mcpConfig] - MCP configuration object (overrides mcpConfigPath)
82
84
  * @param {Array} [options.mcpServers] - Deprecated, use mcpConfig instead
85
+ * @param {Object} [options.storageAdapter] - Custom storage adapter for history management
86
+ * @param {Object} [options.hooks] - Hook callbacks for events (e.g., {'tool:start': callback})
83
87
  */
84
88
  constructor(options = {}) {
85
89
  // Basic configuration
@@ -95,6 +99,19 @@ export class ProbeAgent {
95
99
  this.maxIterations = options.maxIterations || null;
96
100
  this.disableMermaidValidation = !!options.disableMermaidValidation;
97
101
 
102
+ // Storage adapter (defaults to in-memory)
103
+ this.storageAdapter = options.storageAdapter || new InMemoryStorageAdapter();
104
+
105
+ // Hook manager
106
+ this.hooks = new HookManager();
107
+
108
+ // Register hooks from options
109
+ if (options.hooks) {
110
+ for (const [hookName, callback] of Object.entries(options.hooks)) {
111
+ this.hooks.on(hookName, callback);
112
+ }
113
+ }
114
+
98
115
  // Bash configuration
99
116
  this.enableBash = !!options.enableBash;
100
117
  this.bashConfig = options.bashConfig || {};
@@ -152,9 +169,29 @@ export class ProbeAgent {
152
169
 
153
170
  /**
154
171
  * Initialize the agent asynchronously (must be called after constructor)
155
- * This method initializes MCP and merges MCP tools into the tool list
172
+ * This method initializes MCP and merges MCP tools into the tool list, and loads history from storage
156
173
  */
157
174
  async initialize() {
175
+ // Load history from storage adapter
176
+ try {
177
+ const history = await this.storageAdapter.loadHistory(this.sessionId);
178
+ this.history = history;
179
+
180
+ if (this.debug && history.length > 0) {
181
+ console.log(`[DEBUG] Loaded ${history.length} messages from storage for session ${this.sessionId}`);
182
+ }
183
+
184
+ // Emit storage load hook
185
+ await this.hooks.emit(HOOK_TYPES.STORAGE_LOAD, {
186
+ sessionId: this.sessionId,
187
+ messages: history
188
+ });
189
+ } catch (error) {
190
+ console.error(`[ERROR] Failed to load history from storage:`, error);
191
+ // Continue with empty history if storage fails
192
+ this.history = [];
193
+ }
194
+
158
195
  // Initialize MCP if enabled and not already initialized
159
196
  if (this.enableMcp && !this._mcpInitialized) {
160
197
  this._mcpInitialized = true; // Prevent multiple initialization attempts
@@ -193,6 +230,12 @@ export class ProbeAgent {
193
230
  this.mcpBridge = null;
194
231
  }
195
232
  }
233
+
234
+ // Emit agent initialized hook
235
+ await this.hooks.emit(HOOK_TYPES.AGENT_INITIALIZED, {
236
+ sessionId: this.sessionId,
237
+ agent: this
238
+ });
196
239
  }
197
240
 
198
241
  /**
@@ -261,7 +304,8 @@ export class ProbeAgent {
261
304
  // Get API keys from environment variables
262
305
  const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
263
306
  const openaiApiKey = process.env.OPENAI_API_KEY;
264
- const googleApiKey = process.env.GOOGLE_API_KEY;
307
+ // Support both GOOGLE_GENERATIVE_AI_API_KEY (official) and GOOGLE_API_KEY (legacy)
308
+ const googleApiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GOOGLE_API_KEY;
265
309
  const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID;
266
310
  const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
267
311
  const awsRegion = process.env.AWS_REGION;
@@ -320,7 +364,7 @@ export class ProbeAgent {
320
364
  } else if ((awsAccessKeyId && awsSecretAccessKey && awsRegion) || awsApiKey) {
321
365
  this.initializeBedrockModel(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken, awsApiKey, awsBedrockBaseUrl, modelName);
322
366
  } else {
323
- throw new Error('No API key provided. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION), or AWS_BEDROCK_API_KEY environment variables.');
367
+ throw new Error('No API key provided. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY (or GOOGLE_API_KEY), AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION), or AWS_BEDROCK_API_KEY environment variables.');
324
368
  }
325
369
  }
326
370
 
@@ -1080,6 +1124,16 @@ When troubleshooting:
1080
1124
  }
1081
1125
 
1082
1126
  try {
1127
+ // Track initial history length for storage
1128
+ const oldHistoryLength = this.history.length;
1129
+
1130
+ // Emit user message hook
1131
+ await this.hooks.emit(HOOK_TYPES.MESSAGE_USER, {
1132
+ sessionId: this.sessionId,
1133
+ message,
1134
+ images
1135
+ });
1136
+
1083
1137
  // Generate system message
1084
1138
  const systemMessage = await this.getSystemMessage();
1085
1139
 
@@ -1196,9 +1250,13 @@ When troubleshooting:
1196
1250
  temperature: 0.3,
1197
1251
  });
1198
1252
 
1199
- // Collect the streamed response
1253
+ // Collect the streamed response - stream all content for now
1200
1254
  for await (const delta of result.textStream) {
1201
1255
  assistantResponseContent += delta;
1256
+ // For now, stream everything - we'll handle segmentation after tools execute
1257
+ if (options.onStream) {
1258
+ options.onStream(delta);
1259
+ }
1202
1260
  }
1203
1261
 
1204
1262
  // Record token usage
@@ -1283,6 +1341,16 @@ When troubleshooting:
1283
1341
  const validation = attemptCompletionSchema.safeParse(params);
1284
1342
  if (validation.success) {
1285
1343
  finalResult = validation.data.result;
1344
+
1345
+ // Stream the final result if callback is provided
1346
+ if (options.onStream && finalResult) {
1347
+ const chunkSize = 50; // Characters per chunk for smoother streaming
1348
+ for (let i = 0; i < finalResult.length; i += chunkSize) {
1349
+ const chunk = finalResult.slice(i, Math.min(i + chunkSize, finalResult.length));
1350
+ options.onStream(chunk);
1351
+ }
1352
+ }
1353
+
1286
1354
  if (this.debug) console.log(`[DEBUG] Task completed successfully with result: ${finalResult.substring(0, 100)}...`);
1287
1355
  } else {
1288
1356
  console.error(`[ERROR] Invalid attempt_completion parameters:`, validation.error);
@@ -1365,12 +1433,13 @@ When troubleshooting:
1365
1433
  console.error(`[DEBUG] ========================================\n`);
1366
1434
  }
1367
1435
 
1368
- // Emit tool start event
1436
+ // Emit tool start event with stream pause signal
1369
1437
  this.events.emit('toolCall', {
1370
1438
  timestamp: new Date().toISOString(),
1371
1439
  name: toolName,
1372
1440
  args: toolParams,
1373
- status: 'started'
1441
+ status: 'started',
1442
+ pauseStream: true // Signal to pause text streaming
1374
1443
  });
1375
1444
 
1376
1445
  // Execute tool with tracing if available
@@ -1597,6 +1666,21 @@ IMPORTANT: When using <attempt_complete>, this must be the ONLY content in your
1597
1666
  // Update token counter with final history
1598
1667
  this.tokenCounter.updateHistory(this.history);
1599
1668
 
1669
+ // Save new messages to storage (save only the new ones added in this turn)
1670
+ try {
1671
+ const messagesToSave = currentMessages.slice(oldHistoryLength);
1672
+ for (const message of messagesToSave) {
1673
+ await this.storageAdapter.saveMessage(this.sessionId, message);
1674
+ await this.hooks.emit(HOOK_TYPES.STORAGE_SAVE, {
1675
+ sessionId: this.sessionId,
1676
+ message
1677
+ });
1678
+ }
1679
+ } catch (error) {
1680
+ console.error(`[ERROR] Failed to save messages to storage:`, error);
1681
+ // Continue even if storage fails
1682
+ }
1683
+
1600
1684
  // Schema handling - format response according to provided schema
1601
1685
  // Skip schema processing if result came from attempt_completion tool
1602
1686
  // Don't apply schema formatting if we failed due to max iterations
@@ -2040,11 +2124,24 @@ Convert your previous response content into actual JSON data that follows this s
2040
2124
  /**
2041
2125
  * Clear conversation history and reset counters
2042
2126
  */
2043
- clearHistory() {
2127
+ async clearHistory() {
2128
+ // Clear in storage
2129
+ try {
2130
+ await this.storageAdapter.clearHistory(this.sessionId);
2131
+ } catch (error) {
2132
+ console.error(`[ERROR] Failed to clear history in storage:`, error);
2133
+ }
2134
+
2135
+ // Clear in-memory
2044
2136
  this.history = [];
2045
2137
  this.tokenCounter.clear();
2046
2138
  clearToolExecutionData(this.sessionId);
2047
-
2139
+
2140
+ // Emit hook
2141
+ await this.hooks.emit(HOOK_TYPES.STORAGE_CLEAR, {
2142
+ sessionId: this.sessionId
2143
+ });
2144
+
2048
2145
  if (this.debug) {
2049
2146
  console.log(`[DEBUG] Cleared conversation history and reset counters for session ${this.sessionId}`);
2050
2147
  }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Hook manager for ProbeAgent
3
+ * Enables event-driven integration with external systems
4
+ */
5
+ export class HookManager {
6
+ constructor() {
7
+ this.hooks = new Map(); // hookName -> Set<callback>
8
+ }
9
+
10
+ /**
11
+ * Register a hook callback
12
+ * @param {string} hookName - Name of the hook
13
+ * @param {Function} callback - Callback function
14
+ * @returns {Function} Unregister function
15
+ */
16
+ on(hookName, callback) {
17
+ if (!this.hooks.has(hookName)) {
18
+ this.hooks.set(hookName, new Set());
19
+ }
20
+ this.hooks.get(hookName).add(callback);
21
+
22
+ // Return unregister function
23
+ return () => this.off(hookName, callback);
24
+ }
25
+
26
+ /**
27
+ * Register a one-time hook callback
28
+ * @param {string} hookName - Name of the hook
29
+ * @param {Function} callback - Callback function
30
+ * @returns {Function} Unregister function
31
+ */
32
+ once(hookName, callback) {
33
+ const wrappedCallback = async (data) => {
34
+ this.off(hookName, wrappedCallback);
35
+ await callback(data);
36
+ };
37
+ return this.on(hookName, wrappedCallback);
38
+ }
39
+
40
+ /**
41
+ * Unregister a hook callback
42
+ * @param {string} hookName - Name of the hook
43
+ * @param {Function} callback - Callback function
44
+ */
45
+ off(hookName, callback) {
46
+ const callbacks = this.hooks.get(hookName);
47
+ if (callbacks) {
48
+ callbacks.delete(callback);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Emit a hook event
54
+ * @param {string} hookName - Name of the hook
55
+ * @param {any} data - Data to pass to callbacks
56
+ * @returns {Promise<void>}
57
+ */
58
+ async emit(hookName, data) {
59
+ const callbacks = this.hooks.get(hookName);
60
+ if (!callbacks || callbacks.size === 0) return;
61
+
62
+ // Execute all callbacks in parallel using Promise.allSettled
63
+ // This ensures one failing hook doesn't break others
64
+ const promises = Array.from(callbacks).map(callback => {
65
+ try {
66
+ return Promise.resolve(callback(data));
67
+ } catch (error) {
68
+ // Catch synchronous errors
69
+ return Promise.reject(error);
70
+ }
71
+ });
72
+
73
+ const results = await Promise.allSettled(promises);
74
+
75
+ // Log any rejected promises
76
+ results.forEach((result, index) => {
77
+ if (result.status === 'rejected') {
78
+ console.error(`[HookManager] Error in hook "${hookName}" (callback ${index + 1}):`, result.reason);
79
+ }
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Clear all hooks or hooks for a specific event
85
+ * @param {string} [hookName] - Optional hook name to clear
86
+ */
87
+ clear(hookName) {
88
+ if (hookName) {
89
+ this.hooks.delete(hookName);
90
+ } else {
91
+ this.hooks.clear();
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get list of registered hook names
97
+ * @returns {string[]} Array of hook names
98
+ */
99
+ getHookNames() {
100
+ return Array.from(this.hooks.keys());
101
+ }
102
+
103
+ /**
104
+ * Get number of callbacks for a hook
105
+ * @param {string} hookName - Name of the hook
106
+ * @returns {number} Number of callbacks
107
+ */
108
+ getCallbackCount(hookName) {
109
+ const callbacks = this.hooks.get(hookName);
110
+ return callbacks ? callbacks.size : 0;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Available hook types
116
+ * @type {Object<string, string>}
117
+ */
118
+ export const HOOK_TYPES = {
119
+ // Lifecycle hooks
120
+ AGENT_INITIALIZED: 'agent:initialized',
121
+ AGENT_CLEANUP: 'agent:cleanup',
122
+
123
+ // Message hooks
124
+ MESSAGE_USER: 'message:user',
125
+ MESSAGE_ASSISTANT: 'message:assistant',
126
+ MESSAGE_SYSTEM: 'message:system',
127
+
128
+ // Tool execution hooks
129
+ TOOL_START: 'tool:start',
130
+ TOOL_END: 'tool:end',
131
+ TOOL_ERROR: 'tool:error',
132
+
133
+ // AI streaming hooks
134
+ AI_STREAM_START: 'ai:stream:start',
135
+ AI_STREAM_DELTA: 'ai:stream:delta',
136
+ AI_STREAM_END: 'ai:stream:end',
137
+
138
+ // Storage hooks
139
+ STORAGE_LOAD: 'storage:load',
140
+ STORAGE_SAVE: 'storage:save',
141
+ STORAGE_CLEAR: 'storage:clear',
142
+
143
+ // Iteration hooks
144
+ ITERATION_START: 'iteration:start',
145
+ ITERATION_END: 'iteration:end',
146
+ };
@@ -0,0 +1 @@
1
+ export { HookManager, HOOK_TYPES } from './HookManager.js';