@probelabs/probe 0.6.0-rc295 → 0.6.0-rc297

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.
Files changed (31) hide show
  1. package/README.md +7 -0
  2. package/bin/binaries/{probe-v0.6.0-rc295-aarch64-apple-darwin.tar.gz → probe-v0.6.0-rc297-aarch64-apple-darwin.tar.gz} +0 -0
  3. package/bin/binaries/{probe-v0.6.0-rc295-aarch64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc297-aarch64-unknown-linux-musl.tar.gz} +0 -0
  4. package/bin/binaries/{probe-v0.6.0-rc295-x86_64-apple-darwin.tar.gz → probe-v0.6.0-rc297-x86_64-apple-darwin.tar.gz} +0 -0
  5. package/bin/binaries/{probe-v0.6.0-rc295-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc297-x86_64-pc-windows-msvc.zip} +0 -0
  6. package/bin/binaries/{probe-v0.6.0-rc295-x86_64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc297-x86_64-unknown-linux-musl.tar.gz} +0 -0
  7. package/build/agent/ProbeAgent.d.ts +40 -2
  8. package/build/agent/ProbeAgent.js +703 -11
  9. package/build/agent/mcp/client.js +115 -4
  10. package/build/agent/mcp/xmlBridge.js +13 -1
  11. package/build/agent/otelLogBridge.js +184 -0
  12. package/build/agent/simpleTelemetry.js +8 -0
  13. package/build/delegate.js +75 -6
  14. package/build/index.js +6 -2
  15. package/build/tools/common.js +84 -11
  16. package/build/tools/vercel.js +78 -18
  17. package/cjs/agent/ProbeAgent.cjs +1095 -185
  18. package/cjs/agent/simpleTelemetry.cjs +112 -0
  19. package/cjs/index.cjs +1207 -185
  20. package/index.d.ts +26 -0
  21. package/package.json +2 -2
  22. package/src/agent/ProbeAgent.d.ts +40 -2
  23. package/src/agent/ProbeAgent.js +703 -11
  24. package/src/agent/mcp/client.js +115 -4
  25. package/src/agent/mcp/xmlBridge.js +13 -1
  26. package/src/agent/otelLogBridge.js +184 -0
  27. package/src/agent/simpleTelemetry.js +8 -0
  28. package/src/delegate.js +75 -6
  29. package/src/index.js +6 -2
  30. package/src/tools/common.js +84 -11
  31. package/src/tools/vercel.js +78 -18
@@ -58,6 +58,11 @@ export function isMethodAllowed(methodName, allowedMethods, blockedMethods) {
58
58
  export function createTransport(serverConfig) {
59
59
  const { transport, command, args, url, env } = serverConfig;
60
60
 
61
+ // Allow pre-created transport instances (e.g., InMemoryTransport for testing)
62
+ if (serverConfig.transportInstance) {
63
+ return serverConfig.transportInstance;
64
+ }
65
+
61
66
  switch (transport) {
62
67
  case 'stdio':
63
68
  return new StdioClientTransport({
@@ -159,6 +164,8 @@ export class MCPClientManager {
159
164
  this.debug = options.debug || process.env.DEBUG_MCP === '1';
160
165
  this.config = null;
161
166
  this.tracer = options.tracer || null;
167
+ // Optional event emitter for broadcasting tool call lifecycle to the agent (#522)
168
+ this.agentEvents = options.agentEvents || null;
162
169
  }
163
170
 
164
171
  /**
@@ -447,6 +454,7 @@ export class MCPClientManager {
447
454
  }
448
455
 
449
456
  const startTime = Date.now();
457
+ const toolCallId = `mcp-${toolName}-${startTime}`;
450
458
 
451
459
  // Record tool call start
452
460
  this.recordMcpEvent('tool.call_started', {
@@ -455,6 +463,17 @@ export class MCPClientManager {
455
463
  originalToolName: tool.originalName
456
464
  });
457
465
 
466
+ // Emit toolCall event so the agent's activeTools map tracks MCP tool calls (#522)
467
+ if (this.agentEvents) {
468
+ this.agentEvents.emit('toolCall', {
469
+ toolCallId,
470
+ name: toolName,
471
+ args,
472
+ status: 'started',
473
+ timestamp: new Date().toISOString(),
474
+ });
475
+ }
476
+
458
477
  try {
459
478
  if (this.debug) {
460
479
  console.error(`[MCP DEBUG] Calling ${toolName} with args:`, JSON.stringify(args, null, 2));
@@ -497,6 +516,16 @@ export class MCPClientManager {
497
516
  durationMs
498
517
  });
499
518
 
519
+ // Emit toolCall completion so agent's activeTools removes this entry (#522)
520
+ if (this.agentEvents) {
521
+ this.agentEvents.emit('toolCall', {
522
+ toolCallId,
523
+ name: toolName,
524
+ status: 'completed',
525
+ timestamp: new Date().toISOString(),
526
+ });
527
+ }
528
+
500
529
  return result;
501
530
  } catch (error) {
502
531
  const durationMs = Date.now() - startTime;
@@ -516,10 +545,71 @@ export class MCPClientManager {
516
545
  isTimeout: error.message.includes('timeout')
517
546
  });
518
547
 
548
+ // Emit toolCall error so agent's activeTools removes this entry (#522)
549
+ if (this.agentEvents) {
550
+ this.agentEvents.emit('toolCall', {
551
+ toolCallId,
552
+ name: toolName,
553
+ status: 'error',
554
+ timestamp: new Date().toISOString(),
555
+ });
556
+ }
557
+
519
558
  throw error;
520
559
  }
521
560
  }
522
561
 
562
+ /**
563
+ * Call graceful_stop on all MCP servers that expose it.
564
+ * This signals agent-type MCP servers to wrap up their work.
565
+ * @returns {Promise<Array<{server: string, success: boolean, error?: string}>>}
566
+ */
567
+ async callGracefulStopAll() {
568
+ const results = [];
569
+ for (const [serverName, clientInfo] of this.clients) {
570
+ // Look for a graceful_stop tool on this server (qualified name: serverName_graceful_stop)
571
+ const qualifiedName = `${serverName}_graceful_stop`;
572
+ if (this.tools.has(qualifiedName)) {
573
+ if (this.debug) {
574
+ console.log(`[DEBUG] MCP callGracefulStopAll: calling graceful_stop on server "${serverName}"`);
575
+ }
576
+ try {
577
+ // Short timeout — this is a signal, not a long operation
578
+ const timeoutMs = 5000;
579
+ const timeoutPromise = new Promise((_, reject) =>
580
+ setTimeout(() => reject(new Error('graceful_stop timeout')), timeoutMs)
581
+ );
582
+ await Promise.race([
583
+ clientInfo.client.callTool({ name: 'graceful_stop', arguments: {} }, undefined, { timeout: timeoutMs }),
584
+ timeoutPromise
585
+ ]);
586
+ results.push({ server: serverName, success: true });
587
+ if (this.debug) {
588
+ console.log(`[DEBUG] MCP callGracefulStopAll: server "${serverName}" acknowledged graceful_stop`);
589
+ }
590
+ } catch (e) {
591
+ results.push({ server: serverName, success: false, error: e.message });
592
+ if (this.debug) {
593
+ console.log(`[DEBUG] MCP callGracefulStopAll: server "${serverName}" graceful_stop failed: ${e.message}`);
594
+ }
595
+ }
596
+ }
597
+ }
598
+ if (this.debug) {
599
+ const withStop = results.length;
600
+ const total = this.clients.size;
601
+ console.log(`[DEBUG] MCP callGracefulStopAll: ${withStop}/${total} servers had graceful_stop tool`);
602
+ }
603
+ // Record telemetry event for the graceful_stop sweep
604
+ this.recordMcpEvent('graceful_stop.sweep_completed', {
605
+ servers_total: this.clients.size,
606
+ servers_with_graceful_stop: results.length,
607
+ servers_acknowledged: results.filter(r => r.success).length,
608
+ servers_failed: results.filter(r => !r.success).length,
609
+ });
610
+ return results;
611
+ }
612
+
523
613
  /**
524
614
  * Get all available tools with their schemas
525
615
  * @returns {Object} Map of tool name to tool definition
@@ -550,11 +640,32 @@ export class MCPClientManager {
550
640
  inputSchema: tool.inputSchema,
551
641
  execute: async (args) => {
552
642
  const result = await this.callTool(name, args);
553
- // Extract text content from MCP response
554
- if (result.content && result.content[0]) {
555
- return result.content[0].text;
643
+ if (!result.content || !result.content[0]) {
644
+ return JSON.stringify(result);
645
+ }
646
+ // Check if response contains image content blocks
647
+ const hasImage = result.content.some(block => block.type === 'image');
648
+ if (hasImage) {
649
+ // Return the full content array so toModelOutput can convert it
650
+ return { _mcpContent: result.content };
651
+ }
652
+ // Text-only: return just the text
653
+ return result.content[0].text;
654
+ },
655
+ // Convert MCP content blocks (including images) to Vercel AI SDK format
656
+ toModelOutput: ({ output }) => {
657
+ if (output && typeof output === 'object' && output._mcpContent) {
658
+ const parts = [];
659
+ for (const block of output._mcpContent) {
660
+ if (block.type === 'text') {
661
+ parts.push({ type: 'text', text: block.text });
662
+ } else if (block.type === 'image') {
663
+ parts.push({ type: 'image-data', data: block.data, mediaType: block.mimeType });
664
+ }
665
+ }
666
+ return { type: 'content', value: parts };
556
667
  }
557
- return JSON.stringify(result);
668
+ return { type: 'text', value: typeof output === 'string' ? output : JSON.stringify(output) };
558
669
  }
559
670
  };
560
671
  }
@@ -36,6 +36,7 @@ export class MCPXmlBridge {
36
36
  constructor(options = {}) {
37
37
  this.debug = options.debug || false;
38
38
  this.tracer = options.tracer || null;
39
+ this.agentEvents = options.agentEvents || null;
39
40
  this.mcpTools = {};
40
41
  this.mcpManager = null;
41
42
  this.toolDescriptions = {};
@@ -84,7 +85,7 @@ export class MCPXmlBridge {
84
85
  console.error('[MCP DEBUG] Initializing MCP client manager...');
85
86
  }
86
87
 
87
- this.mcpManager = new MCPClientManager({ debug: this.debug, tracer: this.tracer });
88
+ this.mcpManager = new MCPClientManager({ debug: this.debug, tracer: this.tracer, agentEvents: this.agentEvents });
88
89
  const result = await this.mcpManager.initialize(mcpConfigs);
89
90
 
90
91
  // Get tools from the manager (already in Vercel format)
@@ -145,6 +146,17 @@ export class MCPXmlBridge {
145
146
  return toolName in this.mcpTools;
146
147
  }
147
148
 
149
+ /**
150
+ * Call graceful_stop on all MCP servers that expose it.
151
+ * @returns {Promise<Array>}
152
+ */
153
+ async callGracefulStopAll() {
154
+ if (this.mcpManager) {
155
+ return this.mcpManager.callGracefulStopAll();
156
+ }
157
+ return [];
158
+ }
159
+
148
160
  /**
149
161
  * Clean up MCP connections
150
162
  */
@@ -0,0 +1,184 @@
1
+ /**
2
+ * OTEL Log Bridge — patches console.log/info/warn/error to:
3
+ * 1. Append trace context [trace_id=... span_id=...] to output (like visor2)
4
+ * 2. Emit each log as an OTEL Log Record via @opentelemetry/api-logs
5
+ *
6
+ * Lazy-loads @opentelemetry/api and @opentelemetry/api-logs.
7
+ * If packages are not installed, patching is a no-op.
8
+ *
9
+ * Usage:
10
+ * import { patchConsole, unpatchConsole } from './otelLogBridge.js';
11
+ * patchConsole(); // Call once at startup
12
+ */
13
+
14
+ import { createRequire } from 'module';
15
+
16
+ // createRequire for loading optional @opentelemetry packages in ESM context
17
+ const _require = createRequire(import.meta.url);
18
+
19
+ // OTel severity mapping (OpenTelemetry SeverityNumber values)
20
+ const OTEL_SEVERITY = {
21
+ log: 9, // INFO
22
+ info: 9, // INFO
23
+ warn: 13, // WARN
24
+ error: 17, // ERROR
25
+ debug: 5, // DEBUG
26
+ };
27
+
28
+ // Track patch state
29
+ let patched = false;
30
+ const originals = {};
31
+
32
+ // Lazy-loaded OTel references
33
+ let otelApi = null;
34
+ let otelApiAttempted = false;
35
+ let otelLogger = null;
36
+ let otelLoggerAttempted = false;
37
+
38
+ /**
39
+ * Try to load @opentelemetry/api lazily.
40
+ * Returns { trace, context } or null if not available.
41
+ */
42
+ function getOtelApi() {
43
+ if (otelApiAttempted) return otelApi;
44
+ otelApiAttempted = true;
45
+ try {
46
+ // Dynamic require wrapped in IIFE to prevent bundler from resolving
47
+ otelApi = (function(name) { return _require(name); })('@opentelemetry/api');
48
+ } catch {
49
+ // @opentelemetry/api not installed
50
+ }
51
+ return otelApi;
52
+ }
53
+
54
+ /**
55
+ * Try to get an OTEL Logger from @opentelemetry/api-logs lazily.
56
+ * Returns a logger instance or null if not available.
57
+ */
58
+ function getOtelLogger() {
59
+ if (otelLoggerAttempted) return otelLogger;
60
+ otelLoggerAttempted = true;
61
+ try {
62
+ const { logs } = (function(name) { return _require(name); })('@opentelemetry/api-logs');
63
+ otelLogger = logs.getLogger('probe-agent');
64
+ } catch {
65
+ // @opentelemetry/api-logs not installed
66
+ }
67
+ return otelLogger;
68
+ }
69
+
70
+ /**
71
+ * Extract trace context suffix from the active OTel span.
72
+ * Returns '' if no active span or OTel is not available.
73
+ */
74
+ function getTraceSuffix() {
75
+ try {
76
+ const api = getOtelApi();
77
+ if (!api) return '';
78
+ const span = api.trace.getSpan(api.context.active());
79
+ const ctx = span?.spanContext?.();
80
+ if (!ctx?.traceId) return '';
81
+ return ` [trace_id=${ctx.traceId} span_id=${ctx.spanId}]`;
82
+ } catch {
83
+ return '';
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Emit a log record to the OTEL Logs pipeline.
89
+ * Non-blocking, best-effort — errors are silently ignored.
90
+ */
91
+ function emitOtelLog(msg, level) {
92
+ try {
93
+ const logger = getOtelLogger();
94
+ if (!logger) return;
95
+
96
+ const api = getOtelApi();
97
+ let traceId, spanId;
98
+ if (api) {
99
+ const span = api.trace.getSpan(api.context.active());
100
+ const ctx = span?.spanContext?.();
101
+ if (ctx?.traceId) {
102
+ traceId = ctx.traceId;
103
+ spanId = ctx.spanId;
104
+ }
105
+ }
106
+
107
+ logger.emit({
108
+ severityNumber: OTEL_SEVERITY[level] || 9,
109
+ severityText: level.toUpperCase(),
110
+ body: msg,
111
+ attributes: {
112
+ 'probe.logger': true,
113
+ ...(traceId ? { trace_id: traceId, span_id: spanId } : {}),
114
+ },
115
+ });
116
+ } catch {
117
+ // OTel logs not available; ignore
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Patch console.log/info/warn/error to:
123
+ * - Append trace context to output
124
+ * - Emit OTEL log records
125
+ *
126
+ * Safe to call multiple times — only patches once.
127
+ */
128
+ export function patchConsole() {
129
+ if (patched) return;
130
+
131
+ const methods = ['log', 'info', 'warn', 'error'];
132
+ const c = globalThis.console;
133
+
134
+ for (const m of methods) {
135
+ const orig = c[m].bind(c);
136
+ originals[m] = orig;
137
+
138
+ c[m] = (...args) => {
139
+ // Build the message string for OTEL log emission
140
+ const msgParts = args.map(a =>
141
+ typeof a === 'string' ? a : (a instanceof Error ? a.message : JSON.stringify(a))
142
+ );
143
+ const msg = msgParts.join(' ');
144
+
145
+ // Emit to OTEL Logs pipeline (non-blocking, best-effort)
146
+ emitOtelLog(msg, m === 'log' ? 'log' : m);
147
+
148
+ // Append trace context suffix to console output
149
+ const suffix = getTraceSuffix();
150
+ if (suffix) {
151
+ if (typeof args[0] === 'string') {
152
+ args[0] = args[0] + suffix;
153
+ } else {
154
+ args.push(suffix);
155
+ }
156
+ }
157
+
158
+ return orig(...args);
159
+ };
160
+ }
161
+
162
+ patched = true;
163
+ }
164
+
165
+ /**
166
+ * Restore original console methods.
167
+ * Useful for testing or cleanup.
168
+ */
169
+ export function unpatchConsole() {
170
+ if (!patched) return;
171
+
172
+ const c = globalThis.console;
173
+ for (const [m, orig] of Object.entries(originals)) {
174
+ c[m] = orig;
175
+ }
176
+ patched = false;
177
+ }
178
+
179
+ /**
180
+ * Check if console is currently patched.
181
+ */
182
+ export function isConsolePatched() {
183
+ return patched;
184
+ }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, createWriteStream } from 'fs';
2
2
  import { dirname } from 'path';
3
+ import { patchConsole } from './otelLogBridge.js';
3
4
 
4
5
  /**
5
6
  * Simple telemetry implementation for probe-agent
@@ -512,5 +513,12 @@ export function initializeSimpleTelemetryFromOptions(options) {
512
513
  filePath: options.traceFile || './traces.jsonl'
513
514
  });
514
515
 
516
+ // Patch console methods to append trace context and emit OTEL log records.
517
+ // This bridges ALL console.log/info/warn/error calls to the OTEL Logs pipeline
518
+ // automatically — no code changes needed at existing call sites.
519
+ // Safe to call multiple times (idempotent). No-op if @opentelemetry/api-logs
520
+ // is not installed.
521
+ patchConsole();
522
+
515
523
  return telemetry;
516
524
  }
package/build/delegate.js CHANGED
@@ -397,7 +397,14 @@ export async function delegate({
397
397
  mcpConfigPath = null,
398
398
  delegationManager = null, // Optional per-instance manager, falls back to default singleton
399
399
  concurrencyLimiter = null, // Optional global AI concurrency limiter
400
- parentAbortSignal = null // Optional AbortSignal from parent to cancel this delegation
400
+ parentAbortSignal = null, // Optional AbortSignal from parent to cancel this delegation
401
+ // Timeout settings inherited from parent agent
402
+ timeoutBehavior = undefined,
403
+ requestTimeout = undefined,
404
+ gracefulTimeoutBonusSteps = undefined,
405
+ // Subagent lifecycle callbacks for graceful stop coordination
406
+ onSubagentCreated = null,
407
+ onSubagentCompleted = null,
401
408
  }) {
402
409
  if (!task || typeof task !== 'string') {
403
410
  throw new Error('Task parameter is required and must be a string');
@@ -489,12 +496,38 @@ export async function delegate({
489
496
  enableMcp, // Inherit from parent (subagent creates own MCPXmlBridge)
490
497
  mcpConfig, // Inherit from parent
491
498
  mcpConfigPath, // Inherit from parent
492
- concurrencyLimiter // Inherit global AI concurrency limiter
499
+ concurrencyLimiter, // Inherit global AI concurrency limiter
500
+ // Inherit timeout behavior from parent — subagent gets its own graceful wind-down
501
+ // so it can produce partial results instead of being hard-killed by the external timer.
502
+ // The external delegate timeout (capped to parent's remaining budget) is the hard limit;
503
+ // maxOperationTimeout on the subagent is set slightly shorter so its own wind-down
504
+ // fires before the external kill.
505
+ maxOperationTimeout: Math.max(10000, (timeout * 1000) - 15000), // 15s before external kill
506
+ timeoutBehavior: timeoutBehavior || 'graceful',
507
+ requestTimeout,
508
+ gracefulTimeoutBonusSteps: gracefulTimeoutBonusSteps ?? 2, // fewer steps for subagents
493
509
  });
494
510
 
511
+ // Register subagent with parent for graceful stop coordination
512
+ if (onSubagentCreated) {
513
+ onSubagentCreated(sessionId, subagent);
514
+ }
515
+
495
516
  if (debug) {
496
517
  console.error(`[DELEGATE] Created subagent with session ${sessionId}`);
497
518
  console.error(`[DELEGATE] Subagent config: promptType=${promptType}, enableDelegate=false, maxIterations=${remainingIterations}`);
519
+ console.error(`[DELEGATE] Timeout inheritance: externalTimeout=${timeout}s, maxOperationTimeout=${Math.max(10000, (timeout * 1000) - 15000)}ms, behavior=${timeoutBehavior || 'graceful'}, bonusSteps=${gracefulTimeoutBonusSteps ?? 2}`);
520
+ }
521
+ if (tracer) {
522
+ tracer.addEvent('delegation.subagent_created', {
523
+ 'delegation.session_id': sessionId,
524
+ 'delegation.parent_session_id': parentSessionId,
525
+ 'delegation.external_timeout_s': timeout,
526
+ 'delegation.internal_timeout_ms': Math.max(10000, (timeout * 1000) - 15000),
527
+ 'delegation.timeout_behavior': timeoutBehavior || 'graceful',
528
+ 'delegation.bonus_steps': gracefulTimeoutBonusSteps ?? 2,
529
+ 'delegation.max_iterations': remainingIterations,
530
+ });
498
531
  }
499
532
 
500
533
  // Set up timeout and parent abort handling.
@@ -507,8 +540,11 @@ export async function delegate({
507
540
  }, timeout * 1000);
508
541
  });
509
542
 
510
- // Listen for parent abort signal
543
+ // Listen for parent abort signal — use two-phase shutdown:
544
+ // Phase 1: Trigger graceful wind-down so subagent can summarize its work
545
+ // Phase 2: Hard cancel after deadline if subagent hasn't finished
511
546
  let parentAbortHandler;
547
+ let parentAbortHardCancelId = null;
512
548
  const parentAbortPromise = new Promise((_, reject) => {
513
549
  if (parentAbortSignal) {
514
550
  if (parentAbortSignal.aborted) {
@@ -517,8 +553,33 @@ export async function delegate({
517
553
  return;
518
554
  }
519
555
  parentAbortHandler = () => {
520
- subagent.cancel();
521
- reject(new Error('Delegation cancelled: parent operation was aborted'));
556
+ // Phase 1: graceful wind-down — let subagent finish its current step
557
+ subagent.triggerGracefulWindDown();
558
+ if (debug) {
559
+ console.error(`[DELEGATE] Parent abort signal received — triggered graceful wind-down on subagent ${sessionId}`);
560
+ }
561
+ if (tracer) {
562
+ tracer.addEvent('delegation.parent_abort_phase1', {
563
+ 'delegation.session_id': sessionId,
564
+ 'delegation.parent_session_id': parentSessionId,
565
+ 'delegation.action': 'graceful_wind_down',
566
+ });
567
+ }
568
+ // Phase 2: hard cancel after 30s if subagent hasn't finished
569
+ parentAbortHardCancelId = setTimeout(() => {
570
+ if (debug) {
571
+ console.error(`[DELEGATE] Graceful wind-down deadline expired — hard cancelling subagent ${sessionId}`);
572
+ }
573
+ if (tracer) {
574
+ tracer.addEvent('delegation.parent_abort_phase2', {
575
+ 'delegation.session_id': sessionId,
576
+ 'delegation.parent_session_id': parentSessionId,
577
+ 'delegation.action': 'hard_cancel',
578
+ });
579
+ }
580
+ subagent.cancel();
581
+ reject(new Error('Delegation cancelled: parent operation was aborted (graceful wind-down deadline expired)'));
582
+ }, 30000);
522
583
  };
523
584
  parentAbortSignal.addEventListener('abort', parentAbortHandler, { once: true });
524
585
  }
@@ -533,10 +594,18 @@ export async function delegate({
533
594
  try {
534
595
  response = await Promise.race(racers);
535
596
  } finally {
536
- // Clean up parent abort listener to prevent memory leaks
597
+ // Clean up parent abort listener and hard cancel timer to prevent memory leaks
537
598
  if (parentAbortHandler && parentAbortSignal) {
538
599
  parentAbortSignal.removeEventListener('abort', parentAbortHandler);
539
600
  }
601
+ if (parentAbortHardCancelId) {
602
+ clearTimeout(parentAbortHardCancelId);
603
+ parentAbortHardCancelId = null;
604
+ }
605
+ // Unregister subagent from parent
606
+ if (onSubagentCompleted) {
607
+ onSubagentCompleted(sessionId);
608
+ }
540
609
  }
541
610
 
542
611
  // Clear timeout immediately after race completes to prevent memory leak
package/build/index.js CHANGED
@@ -45,7 +45,7 @@ import { createExecutePlanTool, createCleanupExecutePlanTool } from './tools/exe
45
45
  import { bashTool } from './tools/bash.js';
46
46
  import { editTool, createTool, multiEditTool } from './tools/edit.js';
47
47
  import { FileTracker } from './tools/fileTracker.js';
48
- import { ProbeAgent } from './agent/ProbeAgent.js';
48
+ import { ProbeAgent, ENGINE_ACTIVITY_TIMEOUT_DEFAULT, ENGINE_ACTIVITY_TIMEOUT_MIN, ENGINE_ACTIVITY_TIMEOUT_MAX } from './agent/ProbeAgent.js';
49
49
  import { SimpleTelemetry, SimpleAppTracer, initializeSimpleTelemetryFromOptions } from './agent/simpleTelemetry.js';
50
50
  import { listFilesToolInstance, searchFilesToolInstance } from './agent/probeTool.js';
51
51
  import { StorageAdapter, InMemoryStorageAdapter } from './agent/storage/index.js';
@@ -68,8 +68,12 @@ export {
68
68
  listFilesByLevel,
69
69
  tools,
70
70
  DEFAULT_SYSTEM_MESSAGE,
71
- // Export AI Agent (NEW!)
71
+ // Export AI Agent
72
72
  ProbeAgent,
73
+ // Export timeout constants
74
+ ENGINE_ACTIVITY_TIMEOUT_DEFAULT,
75
+ ENGINE_ACTIVITY_TIMEOUT_MIN,
76
+ ENGINE_ACTIVITY_TIMEOUT_MAX,
73
77
  // Export storage adapters
74
78
  StorageAdapter,
75
79
  InMemoryStorageAdapter,