@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.
- package/README.md +7 -0
- 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
- 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
- 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
- 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
- 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
- package/build/agent/ProbeAgent.d.ts +40 -2
- package/build/agent/ProbeAgent.js +703 -11
- package/build/agent/mcp/client.js +115 -4
- package/build/agent/mcp/xmlBridge.js +13 -1
- package/build/agent/otelLogBridge.js +184 -0
- package/build/agent/simpleTelemetry.js +8 -0
- package/build/delegate.js +75 -6
- package/build/index.js +6 -2
- package/build/tools/common.js +84 -11
- package/build/tools/vercel.js +78 -18
- package/cjs/agent/ProbeAgent.cjs +1095 -185
- package/cjs/agent/simpleTelemetry.cjs +112 -0
- package/cjs/index.cjs +1207 -185
- package/index.d.ts +26 -0
- package/package.json +2 -2
- package/src/agent/ProbeAgent.d.ts +40 -2
- package/src/agent/ProbeAgent.js +703 -11
- package/src/agent/mcp/client.js +115 -4
- package/src/agent/mcp/xmlBridge.js +13 -1
- package/src/agent/otelLogBridge.js +184 -0
- package/src/agent/simpleTelemetry.js +8 -0
- package/src/delegate.js +75 -6
- package/src/index.js +6 -2
- package/src/tools/common.js +84 -11
- 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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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(
|
|
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
|
|
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
|
|
521
|
-
|
|
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
|
|
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,
|