@probelabs/probe 0.6.0-rc124 → 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.
package/index.d.ts CHANGED
@@ -31,6 +31,10 @@ export interface ProbeAgentOptions {
31
31
  mcpConfig?: any;
32
32
  /** @deprecated Use mcpConfig instead */
33
33
  mcpServers?: any[];
34
+ /** Pluggable storage adapter for conversation history */
35
+ storageAdapter?: StorageAdapter;
36
+ /** Hook callbacks for event-driven integration */
37
+ hooks?: Record<string, (data: any) => void | Promise<void>>;
34
38
  }
35
39
 
36
40
  /**
@@ -99,6 +103,97 @@ export interface ChatMessage {
99
103
  metadata?: any;
100
104
  }
101
105
 
106
+ /**
107
+ * Storage adapter base class for pluggable history storage
108
+ */
109
+ export declare class StorageAdapter {
110
+ /**
111
+ * Load conversation history for a session
112
+ */
113
+ loadHistory(sessionId: string): Promise<ChatMessage[]>;
114
+
115
+ /**
116
+ * Save a message to storage
117
+ */
118
+ saveMessage(sessionId: string, message: ChatMessage): Promise<void>;
119
+
120
+ /**
121
+ * Clear history for a session
122
+ */
123
+ clearHistory(sessionId: string): Promise<void>;
124
+
125
+ /**
126
+ * Optional: Get session metadata
127
+ */
128
+ getSessionMetadata(sessionId: string): Promise<any | null>;
129
+
130
+ /**
131
+ * Optional: Update session activity timestamp
132
+ */
133
+ updateSessionActivity(sessionId: string): Promise<void>;
134
+ }
135
+
136
+ /**
137
+ * Default in-memory storage adapter
138
+ */
139
+ export declare class InMemoryStorageAdapter extends StorageAdapter {
140
+ constructor();
141
+ }
142
+
143
+ /**
144
+ * Hook types for ProbeAgent event system
145
+ */
146
+ export declare const HOOK_TYPES: {
147
+ readonly AGENT_INITIALIZED: 'agent:initialized';
148
+ readonly AGENT_CLEANUP: 'agent:cleanup';
149
+ readonly MESSAGE_USER: 'message:user';
150
+ readonly MESSAGE_ASSISTANT: 'message:assistant';
151
+ readonly MESSAGE_SYSTEM: 'message:system';
152
+ readonly TOOL_START: 'tool:start';
153
+ readonly TOOL_END: 'tool:end';
154
+ readonly TOOL_ERROR: 'tool:error';
155
+ readonly AI_STREAM_START: 'ai:stream:start';
156
+ readonly AI_STREAM_DELTA: 'ai:stream:delta';
157
+ readonly AI_STREAM_END: 'ai:stream:end';
158
+ readonly STORAGE_LOAD: 'storage:load';
159
+ readonly STORAGE_SAVE: 'storage:save';
160
+ readonly STORAGE_CLEAR: 'storage:clear';
161
+ readonly ITERATION_START: 'iteration:start';
162
+ readonly ITERATION_END: 'iteration:end';
163
+ };
164
+
165
+ /**
166
+ * Hook manager for event-driven integration
167
+ */
168
+ export declare class HookManager {
169
+ constructor();
170
+
171
+ /**
172
+ * Register a hook callback
173
+ */
174
+ on(hookName: string, callback: (data: any) => void | Promise<void>): () => void;
175
+
176
+ /**
177
+ * Register a one-time hook callback
178
+ */
179
+ once(hookName: string, callback: (data: any) => void | Promise<void>): void;
180
+
181
+ /**
182
+ * Unregister a hook callback
183
+ */
184
+ off(hookName: string, callback: (data: any) => void | Promise<void>): void;
185
+
186
+ /**
187
+ * Emit a hook event
188
+ */
189
+ emit(hookName: string, data: any): Promise<void>;
190
+
191
+ /**
192
+ * Clear all hooks or hooks for a specific event
193
+ */
194
+ clear(hookName?: string): void;
195
+ }
196
+
102
197
  /**
103
198
  * Answer options
104
199
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc124",
3
+ "version": "0.6.0-rc126",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -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';
@@ -224,19 +224,27 @@ export const listFilesTool = {
224
224
  // Security: Validate path to prevent traversal attacks
225
225
  const secureBaseDir = path.resolve(baseCwd);
226
226
 
227
+ // Check if this is a dependency path that should bypass workspace restrictions
228
+ const isDependencyPath = directory.startsWith('/dep/') ||
229
+ directory.startsWith('go:') ||
230
+ directory.startsWith('js:') ||
231
+ directory.startsWith('rust:');
232
+
227
233
  // If directory is absolute, check if it's within the secure base directory
228
234
  // If it's relative, resolve it against the secure base directory
229
235
  let targetDir;
230
236
  if (path.isAbsolute(directory)) {
231
237
  targetDir = path.resolve(directory);
232
238
  // Check if the absolute path is within the secure base directory
233
- if (!targetDir.startsWith(secureBaseDir + path.sep) && targetDir !== secureBaseDir) {
239
+ // Allow dependency paths to bypass this restriction
240
+ if (!isDependencyPath && !targetDir.startsWith(secureBaseDir + path.sep) && targetDir !== secureBaseDir) {
234
241
  throw new Error(`Path traversal attempt detected. Cannot access directory outside workspace: ${directory}`);
235
242
  }
236
243
  } else {
237
244
  targetDir = path.resolve(secureBaseDir, directory);
238
245
  // Double-check the resolved path is still within the secure base directory
239
- if (!targetDir.startsWith(secureBaseDir + path.sep) && targetDir !== secureBaseDir) {
246
+ // Allow dependency paths to bypass this restriction
247
+ if (!isDependencyPath && !targetDir.startsWith(secureBaseDir + path.sep) && targetDir !== secureBaseDir) {
240
248
  throw new Error(`Path traversal attempt detected. Access denied: ${directory}`);
241
249
  }
242
250
  }
@@ -323,19 +331,27 @@ export const searchFilesTool = {
323
331
  const baseCwd = workingDirectory || process.cwd();
324
332
  const secureBaseDir = path.resolve(baseCwd);
325
333
 
334
+ // Check if this is a dependency path that should bypass workspace restrictions
335
+ const isDependencyPath = directory.startsWith('/dep/') ||
336
+ directory.startsWith('go:') ||
337
+ directory.startsWith('js:') ||
338
+ directory.startsWith('rust:');
339
+
326
340
  // If directory is absolute, check if it's within the secure base directory
327
341
  // If it's relative, resolve it against the secure base directory
328
342
  let targetDir;
329
343
  if (path.isAbsolute(directory)) {
330
344
  targetDir = path.resolve(directory);
331
345
  // Check if the absolute path is within the secure base directory
332
- if (!targetDir.startsWith(secureBaseDir + path.sep) && targetDir !== secureBaseDir) {
346
+ // Allow dependency paths to bypass this restriction
347
+ if (!isDependencyPath && !targetDir.startsWith(secureBaseDir + path.sep) && targetDir !== secureBaseDir) {
333
348
  throw new Error(`Path traversal attempt detected. Cannot access directory outside workspace: ${directory}`);
334
349
  }
335
350
  } else {
336
351
  targetDir = path.resolve(secureBaseDir, directory);
337
352
  // Double-check the resolved path is still within the secure base directory
338
- if (!targetDir.startsWith(secureBaseDir + path.sep) && targetDir !== secureBaseDir) {
353
+ // Allow dependency paths to bypass this restriction
354
+ if (!isDependencyPath && !targetDir.startsWith(secureBaseDir + path.sep) && targetDir !== secureBaseDir) {
339
355
  throw new Error(`Path traversal attempt detected. Access denied: ${directory}`);
340
356
  }
341
357
  }
@@ -0,0 +1,49 @@
1
+ import { StorageAdapter } from './StorageAdapter.js';
2
+
3
+ /**
4
+ * Default in-memory storage adapter
5
+ * This is the default behavior - stores history in a Map in memory
6
+ */
7
+ export class InMemoryStorageAdapter extends StorageAdapter {
8
+ constructor() {
9
+ super();
10
+ this.sessions = new Map(); // sessionId -> {messages: [], metadata: {}}
11
+ }
12
+
13
+ async loadHistory(sessionId) {
14
+ const session = this.sessions.get(sessionId);
15
+ return session ? session.messages : [];
16
+ }
17
+
18
+ async saveMessage(sessionId, message) {
19
+ if (!this.sessions.has(sessionId)) {
20
+ this.sessions.set(sessionId, {
21
+ messages: [],
22
+ metadata: {
23
+ createdAt: new Date().toISOString(),
24
+ lastActivity: new Date().toISOString()
25
+ }
26
+ });
27
+ }
28
+
29
+ const session = this.sessions.get(sessionId);
30
+ session.messages.push(message);
31
+ session.metadata.lastActivity = new Date().toISOString();
32
+ }
33
+
34
+ async clearHistory(sessionId) {
35
+ this.sessions.delete(sessionId);
36
+ }
37
+
38
+ async getSessionMetadata(sessionId) {
39
+ const session = this.sessions.get(sessionId);
40
+ return session ? session.metadata : null;
41
+ }
42
+
43
+ async updateSessionActivity(sessionId) {
44
+ const session = this.sessions.get(sessionId);
45
+ if (session) {
46
+ session.metadata.lastActivity = new Date().toISOString();
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Base class for storage adapters
3
+ * Implement this interface to provide custom storage backends for ProbeAgent history
4
+ */
5
+ export class StorageAdapter {
6
+ /**
7
+ * Load conversation history for a session
8
+ * @param {string} sessionId - Session identifier
9
+ * @returns {Promise<Array<Object>>} Array of message objects with {role, content, ...}
10
+ */
11
+ async loadHistory(sessionId) {
12
+ throw new Error('StorageAdapter.loadHistory() must be implemented by subclass');
13
+ }
14
+
15
+ /**
16
+ * Save a message to storage
17
+ * @param {string} sessionId - Session identifier
18
+ * @param {Object} message - Message object { role, content, ... }
19
+ * @returns {Promise<void>}
20
+ */
21
+ async saveMessage(sessionId, message) {
22
+ throw new Error('StorageAdapter.saveMessage() must be implemented by subclass');
23
+ }
24
+
25
+ /**
26
+ * Clear history for a session
27
+ * @param {string} sessionId - Session identifier
28
+ * @returns {Promise<void>}
29
+ */
30
+ async clearHistory(sessionId) {
31
+ throw new Error('StorageAdapter.clearHistory() must be implemented by subclass');
32
+ }
33
+
34
+ /**
35
+ * Get session metadata (optional)
36
+ * @param {string} sessionId - Session identifier
37
+ * @returns {Promise<Object|null>} Session metadata or null
38
+ */
39
+ async getSessionMetadata(sessionId) {
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Update session activity timestamp (optional)
45
+ * @param {string} sessionId - Session identifier
46
+ * @returns {Promise<void>}
47
+ */
48
+ async updateSessionActivity(sessionId) {
49
+ // Optional - implement if you want to track session activity
50
+ }
51
+ }
@@ -0,0 +1,2 @@
1
+ export { StorageAdapter } from './StorageAdapter.js';
2
+ export { InMemoryStorageAdapter } from './InMemoryStorageAdapter.js';
package/src/index.js CHANGED
@@ -36,6 +36,8 @@ import { bashTool } from './tools/bash.js';
36
36
  import { ProbeAgent } from './agent/ProbeAgent.js';
37
37
  import { SimpleTelemetry, SimpleAppTracer, initializeSimpleTelemetryFromOptions } from './agent/simpleTelemetry.js';
38
38
  import { listFilesToolInstance, searchFilesToolInstance } from './agent/probeTool.js';
39
+ import { StorageAdapter, InMemoryStorageAdapter } from './agent/storage/index.js';
40
+ import { HookManager, HOOK_TYPES } from './agent/hooks/index.js';
39
41
 
40
42
  export {
41
43
  search,
@@ -50,6 +52,12 @@ export {
50
52
  DEFAULT_SYSTEM_MESSAGE,
51
53
  // Export AI Agent (NEW!)
52
54
  ProbeAgent,
55
+ // Export storage adapters
56
+ StorageAdapter,
57
+ InMemoryStorageAdapter,
58
+ // Export hooks
59
+ HookManager,
60
+ HOOK_TYPES,
53
61
  // Export telemetry classes
54
62
  SimpleTelemetry,
55
63
  SimpleAppTracer,