@mcpmesh/sdk 2.4.0 → 2.5.0
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/dist/__tests__/agent-single-instance.spec.d.ts +2 -0
- package/dist/__tests__/agent-single-instance.spec.d.ts.map +1 -0
- package/dist/__tests__/agent-single-instance.spec.js +80 -0
- package/dist/__tests__/agent-single-instance.spec.js.map +1 -0
- package/dist/__tests__/event-loop-resilience.spec.d.ts +2 -0
- package/dist/__tests__/event-loop-resilience.spec.d.ts.map +1 -0
- package/dist/__tests__/event-loop-resilience.spec.js +321 -0
- package/dist/__tests__/event-loop-resilience.spec.js.map +1 -0
- package/dist/__tests__/llm-max-iterations.test.js +10 -8
- package/dist/__tests__/llm-max-iterations.test.js.map +1 -1
- package/dist/__tests__/llm-provider-multistep.test.d.ts +20 -0
- package/dist/__tests__/llm-provider-multistep.test.d.ts.map +1 -0
- package/dist/__tests__/llm-provider-multistep.test.js +138 -0
- package/dist/__tests__/llm-provider-multistep.test.js.map +1 -0
- package/dist/__tests__/llm-provider-output-mode.test.js +1 -0
- package/dist/__tests__/llm-provider-output-mode.test.js.map +1 -1
- package/dist/__tests__/llm-provider-stopwhen.test.d.ts +22 -0
- package/dist/__tests__/llm-provider-stopwhen.test.d.ts.map +1 -0
- package/dist/__tests__/llm-provider-stopwhen.test.js +127 -0
- package/dist/__tests__/llm-provider-stopwhen.test.js.map +1 -0
- package/dist/__tests__/llm-provider-system-synthesis.test.js +1 -0
- package/dist/__tests__/llm-provider-system-synthesis.test.js.map +1 -1
- package/dist/__tests__/llm-provider-vertex-settings.test.d.ts +18 -0
- package/dist/__tests__/llm-provider-vertex-settings.test.d.ts.map +1 -0
- package/dist/__tests__/llm-provider-vertex-settings.test.js +128 -0
- package/dist/__tests__/llm-provider-vertex-settings.test.js.map +1 -0
- package/dist/__tests__/port-conflict-fallback.spec.d.ts +2 -0
- package/dist/__tests__/port-conflict-fallback.spec.d.ts.map +1 -0
- package/dist/__tests__/port-conflict-fallback.spec.js +123 -0
- package/dist/__tests__/port-conflict-fallback.spec.js.map +1 -0
- package/dist/__tests__/port-probe-errors.spec.d.ts +2 -0
- package/dist/__tests__/port-probe-errors.spec.d.ts.map +1 -0
- package/dist/__tests__/port-probe-errors.spec.js +100 -0
- package/dist/__tests__/port-probe-errors.spec.js.map +1 -0
- package/dist/__tests__/provider-handler-registry.test.d.ts +0 -1
- package/dist/__tests__/provider-handler-registry.test.d.ts.map +1 -1
- package/dist/__tests__/provider-handler-registry.test.js +23 -1
- package/dist/__tests__/provider-handler-registry.test.js.map +1 -1
- package/dist/__tests__/proxy-sse-no-data.test.d.ts +13 -0
- package/dist/__tests__/proxy-sse-no-data.test.d.ts.map +1 -0
- package/dist/__tests__/proxy-sse-no-data.test.js +147 -0
- package/dist/__tests__/proxy-sse-no-data.test.js.map +1 -0
- package/dist/__tests__/proxy-stream.test.js +26 -0
- package/dist/__tests__/proxy-stream.test.js.map +1 -1
- package/dist/__tests__/proxy-timer-leak.test.d.ts +16 -0
- package/dist/__tests__/proxy-timer-leak.test.d.ts.map +1 -0
- package/dist/__tests__/proxy-timer-leak.test.js +97 -0
- package/dist/__tests__/proxy-timer-leak.test.js.map +1 -0
- package/dist/__tests__/proxy-tool-error.test.d.ts +13 -0
- package/dist/__tests__/proxy-tool-error.test.d.ts.map +1 -0
- package/dist/__tests__/proxy-tool-error.test.js +313 -0
- package/dist/__tests__/proxy-tool-error.test.js.map +1 -0
- package/dist/__tests__/route.test.js +21 -1
- package/dist/__tests__/route.test.js.map +1 -1
- package/dist/__tests__/settle-window.spec.d.ts +2 -0
- package/dist/__tests__/settle-window.spec.d.ts.map +1 -0
- package/dist/__tests__/settle-window.spec.js +324 -0
- package/dist/__tests__/settle-window.spec.js.map +1 -0
- package/dist/__tests__/sse.test.js +12 -0
- package/dist/__tests__/sse.test.js.map +1 -1
- package/dist/__tests__/stop-dispatchers.spec.d.ts +2 -0
- package/dist/__tests__/stop-dispatchers.spec.d.ts.map +1 -0
- package/dist/__tests__/stop-dispatchers.spec.js +227 -0
- package/dist/__tests__/stop-dispatchers.spec.js.map +1 -0
- package/dist/agent.d.ts +65 -5
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +313 -78
- package/dist/agent.js.map +1 -1
- package/dist/api-runtime.d.ts +33 -3
- package/dist/api-runtime.d.ts.map +1 -1
- package/dist/api-runtime.js +125 -32
- package/dist/api-runtime.js.map +1 -1
- package/dist/claim-dispatcher.d.ts +25 -0
- package/dist/claim-dispatcher.d.ts.map +1 -1
- package/dist/claim-dispatcher.js +59 -1
- package/dist/claim-dispatcher.js.map +1 -1
- package/dist/config.d.ts +73 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +108 -2
- package/dist/config.js.map +1 -1
- package/dist/debug.d.ts +1 -1
- package/dist/debug.d.ts.map +1 -1
- package/dist/express.d.ts +33 -0
- package/dist/express.d.ts.map +1 -1
- package/dist/express.js +149 -31
- package/dist/express.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/llm-provider.d.ts +18 -0
- package/dist/llm-provider.d.ts.map +1 -1
- package/dist/llm-provider.js +86 -34
- package/dist/llm-provider.js.map +1 -1
- package/dist/provider-handlers/gemini-handler.js +6 -0
- package/dist/provider-handlers/gemini-handler.js.map +1 -1
- package/dist/provider-handlers/provider-handler-registry.d.ts +10 -1
- package/dist/provider-handlers/provider-handler-registry.d.ts.map +1 -1
- package/dist/provider-handlers/provider-handler-registry.js +4 -1
- package/dist/provider-handlers/provider-handler-registry.js.map +1 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +178 -40
- package/dist/proxy.js.map +1 -1
- package/dist/route.d.ts.map +1 -1
- package/dist/route.js +38 -0
- package/dist/route.js.map +1 -1
- package/dist/settle.d.ts +129 -0
- package/dist/settle.d.ts.map +1 -0
- package/dist/settle.js +284 -0
- package/dist/settle.js.map +1 -0
- package/dist/sse.d.ts.map +1 -1
- package/dist/sse.js +5 -2
- package/dist/sse.js.map +1 -1
- package/dist/tracing.d.ts +1 -0
- package/dist/tracing.d.ts.map +1 -1
- package/dist/tracing.js +3 -0
- package/dist/tracing.js.map +1 -1
- package/package.json +2 -2
package/dist/agent.d.ts
CHANGED
|
@@ -48,6 +48,13 @@ export declare function __getWorkerToolMap(): Map<string, (...args: unknown[]) =
|
|
|
48
48
|
* - Dependency injection for tool functions
|
|
49
49
|
*/
|
|
50
50
|
export declare class MeshAgent {
|
|
51
|
+
/**
|
|
52
|
+
* Hard cap on the signal-path shutdown sequence. Must exceed the
|
|
53
|
+
* dispatcher drain hard-cap (30s drain + 10s grace, see
|
|
54
|
+
* `stopDispatchers`) so a clean drain isn't cut short; the extra 5s
|
|
55
|
+
* covers pool/A2A teardown and the registry unregister.
|
|
56
|
+
*/
|
|
57
|
+
private static readonly SIGNAL_SHUTDOWN_TIMEOUT_MS;
|
|
51
58
|
private server;
|
|
52
59
|
private config;
|
|
53
60
|
private agentId;
|
|
@@ -63,6 +70,23 @@ export declare class MeshAgent {
|
|
|
63
70
|
private started;
|
|
64
71
|
private tracingEnabled;
|
|
65
72
|
private shutdownRequested;
|
|
73
|
+
/**
|
|
74
|
+
* Memoized in-flight (or completed) teardown. `shutdown()` is
|
|
75
|
+
* idempotent: the first caller creates this promise and every later
|
|
76
|
+
* caller — user code, the signal handler, a second signal — awaits
|
|
77
|
+
* the SAME teardown instead of racing a concurrent one (double
|
|
78
|
+
* dispatcher drain, double napi `handle.shutdown()`).
|
|
79
|
+
*/
|
|
80
|
+
private shutdownPromise;
|
|
81
|
+
/**
|
|
82
|
+
* This agent's own SIGINT/SIGTERM handler references, kept so
|
|
83
|
+
* shutdown() can `process.off` them. Without removal, sequential
|
|
84
|
+
* MeshAgent instances in one process (legal across async chunk
|
|
85
|
+
* boundaries) accumulate stale handlers whose memoized — already
|
|
86
|
+
* resolved — shutdown() would `process.exit(0)` immediately on the
|
|
87
|
+
* next signal, cutting short the LIVE agent's drain.
|
|
88
|
+
*/
|
|
89
|
+
private signalHandlers;
|
|
66
90
|
/**
|
|
67
91
|
* Resolved dependencies: composite key -> proxy
|
|
68
92
|
* Key format: "${toolName}:dep_${depIndex}" (e.g., "myTool:dep_0")
|
|
@@ -190,10 +214,15 @@ export declare class MeshAgent {
|
|
|
190
214
|
* Install signal handlers for graceful shutdown.
|
|
191
215
|
* Ensures agent unregisters from registry on SIGINT/SIGTERM.
|
|
192
216
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
217
|
+
* Runs the FULL `shutdown()` sequence (issue #1163 MED-2): claim
|
|
218
|
+
* dispatchers drain their in-flight jobs (concurrently, under one
|
|
219
|
+
* shared budget — see `stopDispatchers`), A2A clients / HTTP pool /
|
|
220
|
+
* tool-worker pool close, and `handle.shutdown()` unregisters from
|
|
221
|
+
* the registry (which also resolves the event loop's `nextEvent()`
|
|
222
|
+
* with a "shutdown" event, ending it cleanly).
|
|
223
|
+
*
|
|
224
|
+
* The whole sequence is bounded by a force-exit timer so a hang in
|
|
225
|
+
* any cleanup step cannot wedge the process past its SIGTERM grace.
|
|
197
226
|
*/
|
|
198
227
|
private installSignalHandlers;
|
|
199
228
|
/**
|
|
@@ -202,6 +231,15 @@ export declare class MeshAgent {
|
|
|
202
231
|
private startHeartbeat;
|
|
203
232
|
/**
|
|
204
233
|
* Run the event loop to handle mesh events.
|
|
234
|
+
*
|
|
235
|
+
* Resilience (issue #1163 MED-1): the loop must outlive individual
|
|
236
|
+
* failures. A throw from an event handler (e.g. a malformed event
|
|
237
|
+
* hitting a non-null assertion) is logged and the loop continues; a
|
|
238
|
+
* `nextEvent()` rejection (e.g. a transient napi failure) backs off
|
|
239
|
+
* exponentially (capped) and retries. Only the "shutdown" event — or
|
|
240
|
+
* the handle being torn down — exits the loop. Previously a single
|
|
241
|
+
* throw broke the loop permanently, freezing dependency-topology
|
|
242
|
+
* updates for the process lifetime while the agent kept serving.
|
|
205
243
|
*/
|
|
206
244
|
private runEventLoop;
|
|
207
245
|
/**
|
|
@@ -237,6 +275,16 @@ export declare class MeshAgent {
|
|
|
237
275
|
* Get all resolved dependencies.
|
|
238
276
|
*/
|
|
239
277
|
getAllDependencies(): Map<string, McpMeshTool>;
|
|
278
|
+
/**
|
|
279
|
+
* Inject a mock/fake proxy for a capability (the documented mock
|
|
280
|
+
* contract — see `meshctl man testing --typescript`).
|
|
281
|
+
*
|
|
282
|
+
* Fills the dependency slot of every registered tool that declares the
|
|
283
|
+
* capability and marks those slots resolved with the settle state, so
|
|
284
|
+
* the settling-window grace (#1193) never waits on a caller-supplied
|
|
285
|
+
* dependency.
|
|
286
|
+
*/
|
|
287
|
+
setMockDependency(capability: string, mock: McpMeshTool): void;
|
|
240
288
|
/**
|
|
241
289
|
* Get the agent handle for advanced operations.
|
|
242
290
|
*/
|
|
@@ -251,8 +299,20 @@ export declare class MeshAgent {
|
|
|
251
299
|
getAgentId(): string;
|
|
252
300
|
/**
|
|
253
301
|
* Shutdown the agent gracefully.
|
|
302
|
+
*
|
|
303
|
+
* Idempotent and re-entrant: the first call runs the teardown; every
|
|
304
|
+
* later call (user code, the signal handler, a double signal) returns
|
|
305
|
+
* the SAME promise — later calls' `opts` are ignored. The memo is
|
|
306
|
+
* never cleared: shutdown is terminal, and re-running a half-torn-down
|
|
307
|
+
* cleanup after a failure would be worse than surfacing the original
|
|
308
|
+
* rejection to every caller.
|
|
254
309
|
*/
|
|
255
|
-
shutdown(
|
|
310
|
+
shutdown(opts?: {
|
|
311
|
+
/** Shared in-flight-handler drain window for claim dispatchers. */
|
|
312
|
+
drainTimeoutMs?: number;
|
|
313
|
+
/** Headroom on top of the drain window before drains are abandoned. */
|
|
314
|
+
drainGraceMs?: number;
|
|
315
|
+
}): Promise<void>;
|
|
256
316
|
}
|
|
257
317
|
/**
|
|
258
318
|
* Create a MeshAgent wrapping a FastMCP server.
|
package/dist/agent.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B,OAAO,EAGL,KAAK,aAAa,EAInB,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,WAAW,EAEX,WAAW,EAEX,iBAAiB,EAClB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
|
package/dist/agent.js
CHANGED
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
12
12
|
import { isMainThread } from "node:worker_threads";
|
|
13
13
|
import { startAgent, } from "@mcpmesh/core";
|
|
14
|
-
import { resolveConfig, generateAgentIdSuffix, findAvailablePort } from "./config.js";
|
|
14
|
+
import { resolveConfig, generateAgentIdSuffix, findAvailablePort, resolveStartupBindPort, MAX_CONSECUTIVE_NEXT_EVENT_FAILURES, NEXT_EVENT_BACKOFF_CAP_MS, } from "./config.js";
|
|
15
15
|
import { enrichSchemaWithMediaTypes } from "./media-param.js";
|
|
16
16
|
import { createProxy, normalizeDependency, runWithTraceContext, runWithPropagatedHeaders, PROXY_DISPATCH_META } from "./proxy.js";
|
|
17
17
|
import { readJobHeaders, runWithJobContext, makeJobController, spliceJobController, } from "./inbound-job-dispatch.js";
|
|
18
18
|
import { MeshJobSubmitter } from "./mesh-job-submitter.js";
|
|
19
|
-
import {
|
|
19
|
+
import { getSettleState } from "./settle.js";
|
|
20
|
+
import { ClaimDispatcher, stopDispatchers, } from "./claim-dispatcher.js";
|
|
20
21
|
import { registerJobHelperTools } from "./jobs-helper-tools.js";
|
|
21
22
|
import { registerCancelRoute } from "./jobs-cancel-route.js";
|
|
22
23
|
import { clusterStrictEnabled, normalizeSchemaWithPolicy, } from "./schema-normalize.js";
|
|
@@ -68,17 +69,35 @@ const _workerToolMap = new Map();
|
|
|
68
69
|
export function __getWorkerToolMap() {
|
|
69
70
|
return _workerToolMap;
|
|
70
71
|
}
|
|
71
|
-
// Internal: pending agent for auto-start
|
|
72
|
+
// Internal: pending agent for auto-start. The slot holds at most one
|
|
73
|
+
// agent awaiting its auto-start tick; it is consumed (cleared) when the
|
|
74
|
+
// scheduled nextTick fires.
|
|
72
75
|
let pendingAgent = null;
|
|
73
76
|
let autoStartScheduled = false;
|
|
77
|
+
// Issue #1163 LOW-6 guard: name of the agent constructed in the CURRENT
|
|
78
|
+
// synchronous chunk. Two constructions in one chunk meant the second
|
|
79
|
+
// silently overwrote `pendingAgent` before the auto-start tick fired —
|
|
80
|
+
// the first agent never started and nobody noticed. The constructor now
|
|
81
|
+
// throws while this guard is set. The guard auto-releases on the first
|
|
82
|
+
// microtask: in real Node the auto-start nextTick drains BEFORE any
|
|
83
|
+
// microtask continuation, so the guard window fully covers the
|
|
84
|
+
// pre-consumption danger zone, while sequential constructions across
|
|
85
|
+
// async boundaries (e.g. one agent per test in a harness) stay allowed.
|
|
86
|
+
let constructionGuardName = null;
|
|
74
87
|
// Schedule auto-start after module loading completes
|
|
75
88
|
function scheduleAutoStart() {
|
|
76
89
|
if (autoStartScheduled)
|
|
77
90
|
return;
|
|
78
91
|
autoStartScheduled = true;
|
|
79
92
|
process.nextTick(() => {
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
// Consume the slot and re-arm the scheduler BEFORE starting, so a
|
|
94
|
+
// later (post-start) construction gets its own auto-start tick
|
|
95
|
+
// instead of being silently dropped.
|
|
96
|
+
const agent = pendingAgent;
|
|
97
|
+
pendingAgent = null;
|
|
98
|
+
autoStartScheduled = false;
|
|
99
|
+
if (agent) {
|
|
100
|
+
agent._autoStart().catch((err) => {
|
|
82
101
|
console.error("MCP Mesh auto-start failed:", err);
|
|
83
102
|
process.exit(1);
|
|
84
103
|
});
|
|
@@ -95,6 +114,13 @@ function scheduleAutoStart() {
|
|
|
95
114
|
* - Dependency injection for tool functions
|
|
96
115
|
*/
|
|
97
116
|
export class MeshAgent {
|
|
117
|
+
/**
|
|
118
|
+
* Hard cap on the signal-path shutdown sequence. Must exceed the
|
|
119
|
+
* dispatcher drain hard-cap (30s drain + 10s grace, see
|
|
120
|
+
* `stopDispatchers`) so a clean drain isn't cut short; the extra 5s
|
|
121
|
+
* covers pool/A2A teardown and the registry unregister.
|
|
122
|
+
*/
|
|
123
|
+
static SIGNAL_SHUTDOWN_TIMEOUT_MS = 45_000;
|
|
98
124
|
server;
|
|
99
125
|
config;
|
|
100
126
|
agentId;
|
|
@@ -110,6 +136,23 @@ export class MeshAgent {
|
|
|
110
136
|
started = false;
|
|
111
137
|
tracingEnabled = false;
|
|
112
138
|
shutdownRequested = false;
|
|
139
|
+
/**
|
|
140
|
+
* Memoized in-flight (or completed) teardown. `shutdown()` is
|
|
141
|
+
* idempotent: the first caller creates this promise and every later
|
|
142
|
+
* caller — user code, the signal handler, a second signal — awaits
|
|
143
|
+
* the SAME teardown instead of racing a concurrent one (double
|
|
144
|
+
* dispatcher drain, double napi `handle.shutdown()`).
|
|
145
|
+
*/
|
|
146
|
+
shutdownPromise = null;
|
|
147
|
+
/**
|
|
148
|
+
* This agent's own SIGINT/SIGTERM handler references, kept so
|
|
149
|
+
* shutdown() can `process.off` them. Without removal, sequential
|
|
150
|
+
* MeshAgent instances in one process (legal across async chunk
|
|
151
|
+
* boundaries) accumulate stale handlers whose memoized — already
|
|
152
|
+
* resolved — shutdown() would `process.exit(0)` immediately on the
|
|
153
|
+
* next signal, cutting short the LIVE agent's drain.
|
|
154
|
+
*/
|
|
155
|
+
signalHandlers = null;
|
|
113
156
|
/**
|
|
114
157
|
* Resolved dependencies: composite key -> proxy
|
|
115
158
|
* Key format: "${toolName}:dep_${depIndex}" (e.g., "myTool:dep_0")
|
|
@@ -186,7 +229,22 @@ export class MeshAgent {
|
|
|
186
229
|
this.config = resolveConfig(config);
|
|
187
230
|
// Generate unique agent ID with suffix (e.g., "calculator-a1b2c3d4")
|
|
188
231
|
this.agentId = `${this.config.name}-${generateAgentIdSuffix()}`;
|
|
189
|
-
// Register as pending agent for auto-start
|
|
232
|
+
// Register as pending agent for auto-start. Throw if another agent
|
|
233
|
+
// was already constructed in this synchronous chunk — the previous
|
|
234
|
+
// behavior overwrote the slot and the earlier agent silently never
|
|
235
|
+
// started (issue #1163 LOW-6). Only one MeshAgent per process is
|
|
236
|
+
// supported (it owns the FastMCP HTTP server, registry heartbeat,
|
|
237
|
+
// and process-wide signal handlers).
|
|
238
|
+
if (constructionGuardName !== null) {
|
|
239
|
+
throw new Error(`Only one MeshAgent may be constructed per process: agent ` +
|
|
240
|
+
`'${constructionGuardName}' is already pending auto-start. ` +
|
|
241
|
+
`Register all tools on a single MeshAgent instead of constructing ` +
|
|
242
|
+
`'${this.config.name}' as a second agent.`);
|
|
243
|
+
}
|
|
244
|
+
constructionGuardName = this.config.name;
|
|
245
|
+
queueMicrotask(() => {
|
|
246
|
+
constructionGuardName = null;
|
|
247
|
+
});
|
|
190
248
|
pendingAgent = this;
|
|
191
249
|
scheduleAutoStart();
|
|
192
250
|
}
|
|
@@ -373,6 +431,16 @@ export class MeshAgent {
|
|
|
373
431
|
// Normalize dependencies
|
|
374
432
|
const normalizedDeps = (def.dependencies ?? []).map(normalizeDependency);
|
|
375
433
|
const depEndpoints = normalizedDeps.map((d) => d.capability);
|
|
434
|
+
// Settling-window grace (#1193): declare this tool's proxy deps with the
|
|
435
|
+
// process-wide settle state so the agent-level "all declared deps
|
|
436
|
+
// resolved" latch can flip eagerly. The MeshJob slot is excluded — its
|
|
437
|
+
// submitter is constructed locally, not resolved by an event.
|
|
438
|
+
const settleState = getSettleState();
|
|
439
|
+
normalizedDeps.forEach((_dep, depIndex) => {
|
|
440
|
+
if (depIndex !== def.meshJobDepIndex) {
|
|
441
|
+
settleState.registerDeclared(`${toolName}:dep_${depIndex}`);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
376
444
|
// Capture for closures — these reads must be live at invocation
|
|
377
445
|
// time (e.g. registryUrl/agentId aren't set yet at addTool time).
|
|
378
446
|
const isTaskTool = def.task === true;
|
|
@@ -403,6 +471,24 @@ export class MeshAgent {
|
|
|
403
471
|
}
|
|
404
472
|
// Create wrapper that injects dependencies positionally and handles tracing
|
|
405
473
|
const wrappedExecute = async (args) => {
|
|
474
|
+
// Settling-window grace (#1193): while the agent is still settling,
|
|
475
|
+
// wait — bounded by the remaining settle budget — for any declared
|
|
476
|
+
// dep this call would inject that is still unresolved. No-op (single
|
|
477
|
+
// latch check) once settled; the deps array below is built AFTER the
|
|
478
|
+
// wait so it re-reads the resolution state.
|
|
479
|
+
if (normalizedDeps.length > 0 && !settleState.isSettled()) {
|
|
480
|
+
const pendingSettle = [];
|
|
481
|
+
normalizedDeps.forEach((dep, depIndex) => {
|
|
482
|
+
const depKey = `${toolName}:dep_${depIndex}`;
|
|
483
|
+
if (depIndex !== meshJobDepIndex &&
|
|
484
|
+
!this.resolvedDeps.has(depKey)) {
|
|
485
|
+
pendingSettle.push({ depKey, capability: dep.capability });
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
if (pendingSettle.length > 0) {
|
|
489
|
+
await settleState.awaitPending(pendingSettle);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
406
492
|
// Build positional deps array using composite keys (toolName:dep_index)
|
|
407
493
|
// Phase 1 MeshJob substrate (consumer-side): if meshJobDepIndex is
|
|
408
494
|
// set, swap the McpMeshTool proxy at that slot for a
|
|
@@ -651,6 +737,22 @@ export class MeshAgent {
|
|
|
651
737
|
if (isTaskTool) {
|
|
652
738
|
const capability = def.capability ?? toolName;
|
|
653
739
|
const handler = async (payload, controller) => {
|
|
740
|
+
// Settling-window grace (#1193): claim dispatch gets the same
|
|
741
|
+
// bounded wait as the inbound HTTP path — a claim arriving during
|
|
742
|
+
// the settling window would otherwise see null deps.
|
|
743
|
+
if (normalizedDeps.length > 0 && !settleState.isSettled()) {
|
|
744
|
+
const pendingSettle = [];
|
|
745
|
+
normalizedDeps.forEach((dep, depIndex) => {
|
|
746
|
+
const depKey = `${toolName}:dep_${depIndex}`;
|
|
747
|
+
if (depIndex !== meshJobDepIndex &&
|
|
748
|
+
!this.resolvedDeps.has(depKey)) {
|
|
749
|
+
pendingSettle.push({ depKey, capability: dep.capability });
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
if (pendingSettle.length > 0) {
|
|
753
|
+
await settleState.awaitPending(pendingSettle);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
654
756
|
const liveDeps = normalizedDeps.map((dep, depIndex) => {
|
|
655
757
|
if (depIndex === meshJobDepIndex) {
|
|
656
758
|
return new MeshJobSubmitter(dep.capability, this.agentId, this.config.registryUrl);
|
|
@@ -840,11 +942,17 @@ export class MeshAgent {
|
|
|
840
942
|
// Auto-detect template base path from agent's package.json location
|
|
841
943
|
// This ensures file:// templates resolve correctly regardless of cwd
|
|
842
944
|
findAndSetBasePath();
|
|
843
|
-
//
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
945
|
+
// Resolve the bind port BEFORE tracing/server/heartbeat so every
|
|
946
|
+
// downstream consumer sees the port we will actually bind (issue
|
|
947
|
+
// #1194: a conflict falls back to an OS-assigned port with a
|
|
948
|
+
// prominent warning — see resolveStartupBindPort). The heartbeat
|
|
949
|
+
// reads `this.config.httpPort`, so registration carries the ACTUAL
|
|
950
|
+
// port instead of a phantom endpoint.
|
|
951
|
+
{
|
|
952
|
+
const resolvedPort = await resolveStartupBindPort(this.config.httpPort, "agent");
|
|
953
|
+
if (resolvedPort !== this.config.httpPort) {
|
|
954
|
+
this.config = { ...this.config, httpPort: resolvedPort };
|
|
955
|
+
}
|
|
848
956
|
}
|
|
849
957
|
console.log(`Starting MCP Mesh agent: ${this.agentId}`);
|
|
850
958
|
// Prepare TLS credentials (fetches from Vault if configured)
|
|
@@ -1035,38 +1143,59 @@ export class MeshAgent {
|
|
|
1035
1143
|
* Install signal handlers for graceful shutdown.
|
|
1036
1144
|
* Ensures agent unregisters from registry on SIGINT/SIGTERM.
|
|
1037
1145
|
*
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1146
|
+
* Runs the FULL `shutdown()` sequence (issue #1163 MED-2): claim
|
|
1147
|
+
* dispatchers drain their in-flight jobs (concurrently, under one
|
|
1148
|
+
* shared budget — see `stopDispatchers`), A2A clients / HTTP pool /
|
|
1149
|
+
* tool-worker pool close, and `handle.shutdown()` unregisters from
|
|
1150
|
+
* the registry (which also resolves the event loop's `nextEvent()`
|
|
1151
|
+
* with a "shutdown" event, ending it cleanly).
|
|
1152
|
+
*
|
|
1153
|
+
* The whole sequence is bounded by a force-exit timer so a hang in
|
|
1154
|
+
* any cleanup step cannot wedge the process past its SIGTERM grace.
|
|
1042
1155
|
*/
|
|
1043
1156
|
installSignalHandlers() {
|
|
1157
|
+
// Dedupe repeated signals locally. This must NOT key off
|
|
1158
|
+
// `shutdownRequested` (set by shutdown() itself): a signal arriving
|
|
1159
|
+
// while a user-initiated shutdown() is draining must still arm the
|
|
1160
|
+
// force-exit timer and exit the process when that same (memoized)
|
|
1161
|
+
// shutdown completes.
|
|
1162
|
+
let signalHandled = false;
|
|
1044
1163
|
const shutdownHandler = (signal) => {
|
|
1045
|
-
if (
|
|
1164
|
+
if (signalHandled)
|
|
1046
1165
|
return;
|
|
1047
|
-
|
|
1166
|
+
signalHandled = true;
|
|
1048
1167
|
console.log(`\nReceived ${signal}, shutting down agent ${this.agentId}...`);
|
|
1049
|
-
//
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1168
|
+
// Bounded overall shutdown: the dispatcher drain phase is already
|
|
1169
|
+
// hard-capped (30s drain + 10s grace shared across dispatchers);
|
|
1170
|
+
// this timer covers everything else (pool/A2A close, registry
|
|
1171
|
+
// unregister) so a hang anywhere cannot block exit forever.
|
|
1172
|
+
//
|
|
1173
|
+
// Deliberately ref'd (no unref): both completion paths below
|
|
1174
|
+
// clearTimeout and process.exit synchronously, so the timer never
|
|
1175
|
+
// delays a successful exit — but it must keep a wedged shutdown's
|
|
1176
|
+
// otherwise-empty event loop alive long enough to emit the loud
|
|
1177
|
+
// exit(1) diagnostic instead of silently exiting 0.
|
|
1178
|
+
const forceExitTimer = setTimeout(() => {
|
|
1179
|
+
console.error(`Shutdown did not complete within ${MeshAgent.SIGNAL_SHUTDOWN_TIMEOUT_MS}ms; forcing exit`);
|
|
1180
|
+
process.exit(1);
|
|
1181
|
+
}, MeshAgent.SIGNAL_SHUTDOWN_TIMEOUT_MS);
|
|
1182
|
+
this.shutdown().then(() => {
|
|
1183
|
+
clearTimeout(forceExitTimer);
|
|
1184
|
+
console.log(`Agent ${this.agentId} shut down cleanly`);
|
|
1065
1185
|
process.exit(0);
|
|
1066
|
-
}
|
|
1186
|
+
}).catch((err) => {
|
|
1187
|
+
clearTimeout(forceExitTimer);
|
|
1188
|
+
console.error("Error during shutdown:", err);
|
|
1189
|
+
process.exit(1);
|
|
1190
|
+
});
|
|
1067
1191
|
};
|
|
1068
|
-
|
|
1069
|
-
|
|
1192
|
+
// Keep named references so shutdown() can remove exactly these
|
|
1193
|
+
// listeners (anonymous arrows can't be process.off'd).
|
|
1194
|
+
const sigint = () => shutdownHandler("SIGINT");
|
|
1195
|
+
const sigterm = () => shutdownHandler("SIGTERM");
|
|
1196
|
+
this.signalHandlers = { sigint, sigterm };
|
|
1197
|
+
process.on("SIGINT", sigint);
|
|
1198
|
+
process.on("SIGTERM", sigterm);
|
|
1070
1199
|
}
|
|
1071
1200
|
/**
|
|
1072
1201
|
* Start the Rust core heartbeat loop.
|
|
@@ -1242,13 +1371,56 @@ export class MeshAgent {
|
|
|
1242
1371
|
}
|
|
1243
1372
|
/**
|
|
1244
1373
|
* Run the event loop to handle mesh events.
|
|
1374
|
+
*
|
|
1375
|
+
* Resilience (issue #1163 MED-1): the loop must outlive individual
|
|
1376
|
+
* failures. A throw from an event handler (e.g. a malformed event
|
|
1377
|
+
* hitting a non-null assertion) is logged and the loop continues; a
|
|
1378
|
+
* `nextEvent()` rejection (e.g. a transient napi failure) backs off
|
|
1379
|
+
* exponentially (capped) and retries. Only the "shutdown" event — or
|
|
1380
|
+
* the handle being torn down — exits the loop. Previously a single
|
|
1381
|
+
* throw broke the loop permanently, freezing dependency-topology
|
|
1382
|
+
* updates for the process lifetime while the agent kept serving.
|
|
1245
1383
|
*/
|
|
1246
1384
|
async runEventLoop() {
|
|
1247
1385
|
if (!this.handle)
|
|
1248
1386
|
return;
|
|
1387
|
+
let consecutiveNextEventFailures = 0;
|
|
1249
1388
|
while (true) {
|
|
1389
|
+
// Handle is nulled by shutdown(); exit cleanly instead of
|
|
1390
|
+
// spinning on a dead reference.
|
|
1391
|
+
if (!this.handle) {
|
|
1392
|
+
console.log("Event loop: handle closed, exiting");
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
let event;
|
|
1396
|
+
try {
|
|
1397
|
+
event = await this.handle.nextEvent();
|
|
1398
|
+
consecutiveNextEventFailures = 0;
|
|
1399
|
+
}
|
|
1400
|
+
catch (err) {
|
|
1401
|
+
// Explicit shutdown() racing a failing nextEvent(): exit
|
|
1402
|
+
// promptly instead of burning more backoff cycles.
|
|
1403
|
+
if (!this.handle || this.shutdownRequested) {
|
|
1404
|
+
console.log("Event loop: shutdown requested, exiting");
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
consecutiveNextEventFailures++;
|
|
1408
|
+
// Ceiling (~60s of continuous failure — see the constant's doc
|
|
1409
|
+
// in config.ts): a permanently broken handle must not retry
|
|
1410
|
+
// forever, keeping the process alive via the backoff timer.
|
|
1411
|
+
if (consecutiveNextEventFailures >= MAX_CONSECUTIVE_NEXT_EVENT_FAILURES) {
|
|
1412
|
+
console.error(`Event loop: terminating after ${consecutiveNextEventFailures} ` +
|
|
1413
|
+
`consecutive nextEvent() failures; dependency topology is ` +
|
|
1414
|
+
`frozen for the remainder of the process:`, err);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const backoffMs = Math.min(100 * 2 ** (consecutiveNextEventFailures - 1), NEXT_EVENT_BACKOFF_CAP_MS);
|
|
1418
|
+
console.error(`Event loop: nextEvent() failed (consecutive=${consecutiveNextEventFailures}), ` +
|
|
1419
|
+
`retrying in ${backoffMs}ms:`, err);
|
|
1420
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1250
1423
|
try {
|
|
1251
|
-
const event = await this.handle.nextEvent();
|
|
1252
1424
|
switch (event.eventType) {
|
|
1253
1425
|
case "agent_registered":
|
|
1254
1426
|
console.log(`Agent registered with ID: ${event.agentId}`);
|
|
@@ -1314,8 +1486,10 @@ export class MeshAgent {
|
|
|
1314
1486
|
}
|
|
1315
1487
|
}
|
|
1316
1488
|
catch (err) {
|
|
1317
|
-
|
|
1318
|
-
|
|
1489
|
+
// Per-event isolation: a bad event (or a bug in one handler)
|
|
1490
|
+
// must not kill dependency-event processing for the process
|
|
1491
|
+
// lifetime. Log and keep consuming events.
|
|
1492
|
+
console.error(`Event loop: error handling event '${event.eventType}':`, err);
|
|
1319
1493
|
}
|
|
1320
1494
|
}
|
|
1321
1495
|
}
|
|
@@ -1335,6 +1509,10 @@ export class MeshAgent {
|
|
|
1335
1509
|
const depKey = `${requestingFunction}:dep_${depIndex}`;
|
|
1336
1510
|
const proxy = createProxy(endpoint, capability, functionName, kwargs);
|
|
1337
1511
|
this.resolvedDeps.set(depKey, proxy);
|
|
1512
|
+
// Settling-window grace (#1193): wake any settling call waiting on
|
|
1513
|
+
// this dependency AFTER the proxy is stored so the woken call
|
|
1514
|
+
// re-reads a real proxy.
|
|
1515
|
+
getSettleState().markResolved(depKey);
|
|
1338
1516
|
console.log(`Dependency available: ${capability} at ${endpoint} (tool: ${requestingFunction}, index: ${depIndex}, agent: ${agentId})`);
|
|
1339
1517
|
return;
|
|
1340
1518
|
}
|
|
@@ -1350,6 +1528,7 @@ export class MeshAgent {
|
|
|
1350
1528
|
const depKey = `${toolName}:dep_${idx}`;
|
|
1351
1529
|
const proxy = createProxy(endpoint, capability, functionName, kwargs);
|
|
1352
1530
|
this.resolvedDeps.set(depKey, proxy);
|
|
1531
|
+
getSettleState().markResolved(depKey);
|
|
1353
1532
|
matchCount++;
|
|
1354
1533
|
}
|
|
1355
1534
|
});
|
|
@@ -1417,6 +1596,30 @@ export class MeshAgent {
|
|
|
1417
1596
|
getAllDependencies() {
|
|
1418
1597
|
return new Map(this.resolvedDeps);
|
|
1419
1598
|
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Inject a mock/fake proxy for a capability (the documented mock
|
|
1601
|
+
* contract — see `meshctl man testing --typescript`).
|
|
1602
|
+
*
|
|
1603
|
+
* Fills the dependency slot of every registered tool that declares the
|
|
1604
|
+
* capability and marks those slots resolved with the settle state, so
|
|
1605
|
+
* the settling-window grace (#1193) never waits on a caller-supplied
|
|
1606
|
+
* dependency.
|
|
1607
|
+
*/
|
|
1608
|
+
setMockDependency(capability, mock) {
|
|
1609
|
+
for (const [toolName, meta] of this.tools.entries()) {
|
|
1610
|
+
if (!meta.dependencies)
|
|
1611
|
+
continue;
|
|
1612
|
+
meta.dependencies.forEach((dep, depIndex) => {
|
|
1613
|
+
if (dep.capability === capability) {
|
|
1614
|
+
const depKey = `${toolName}:dep_${depIndex}`;
|
|
1615
|
+
this.resolvedDeps.set(depKey, mock);
|
|
1616
|
+
// A caller-supplied slot needs no resolution event — count it
|
|
1617
|
+
// as resolved so settling calls never wait on it.
|
|
1618
|
+
getSettleState().markResolved(depKey);
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1420
1623
|
/**
|
|
1421
1624
|
* Get the agent handle for advanced operations.
|
|
1422
1625
|
*/
|
|
@@ -1437,50 +1640,82 @@ export class MeshAgent {
|
|
|
1437
1640
|
}
|
|
1438
1641
|
/**
|
|
1439
1642
|
* Shutdown the agent gracefully.
|
|
1643
|
+
*
|
|
1644
|
+
* Idempotent and re-entrant: the first call runs the teardown; every
|
|
1645
|
+
* later call (user code, the signal handler, a double signal) returns
|
|
1646
|
+
* the SAME promise — later calls' `opts` are ignored. The memo is
|
|
1647
|
+
* never cleared: shutdown is terminal, and re-running a half-torn-down
|
|
1648
|
+
* cleanup after a failure would be worse than surfacing the original
|
|
1649
|
+
* rejection to every caller.
|
|
1440
1650
|
*/
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1651
|
+
shutdown(opts) {
|
|
1652
|
+
if (this.shutdownPromise)
|
|
1653
|
+
return this.shutdownPromise;
|
|
1654
|
+
this.shutdownRequested = true;
|
|
1655
|
+
this.shutdownPromise = (async () => {
|
|
1656
|
+
// Phase 1 MeshJob substrate: stop claim dispatchers first so they
|
|
1657
|
+
// don't pull a fresh job mid-shutdown. Issue #1173: all dispatchers
|
|
1658
|
+
// drain CONCURRENTLY under one shared, hard-capped budget — never
|
|
1659
|
+
// N×30s sequential — and a hanging drain is abandoned with a
|
|
1660
|
+
// warning so the registry unregister below always runs.
|
|
1661
|
+
await stopDispatchers(this._claimDispatchers, opts?.drainTimeoutMs, opts?.drainGraceMs);
|
|
1662
|
+
this._claimDispatchers = [];
|
|
1663
|
+
// Issue #917: mark all cached A2AClients closed so any in-flight
|
|
1664
|
+
// user code raises cleanly instead of reusing a torn-down instance.
|
|
1665
|
+
// Close in parallel so one slow client doesn't block the others —
|
|
1666
|
+
// the undici Agent pool is shared via closeHttpPool() below.
|
|
1667
|
+
const closePromises = Array.from(this._a2aClients.values()).map((client) => client.close().catch((err) => {
|
|
1668
|
+
console.warn("[mesh-a2a] Error closing A2AClient:", err);
|
|
1669
|
+
return null;
|
|
1670
|
+
}));
|
|
1671
|
+
await Promise.allSettled(closePromises);
|
|
1672
|
+
this._a2aClients.clear();
|
|
1445
1673
|
try {
|
|
1446
|
-
await
|
|
1674
|
+
await closeHttpPool();
|
|
1447
1675
|
}
|
|
1448
1676
|
catch (err) {
|
|
1449
|
-
console.warn(
|
|
1677
|
+
console.warn("Error closing HTTP pool:", err);
|
|
1450
1678
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
this
|
|
1482
|
-
|
|
1483
|
-
|
|
1679
|
+
try {
|
|
1680
|
+
await closePool();
|
|
1681
|
+
}
|
|
1682
|
+
catch (err) {
|
|
1683
|
+
console.warn("Error closing tool worker pool:", err);
|
|
1684
|
+
}
|
|
1685
|
+
if (this.httpsProxy) {
|
|
1686
|
+
// Await the close callback so the TLS listener's port is
|
|
1687
|
+
// actually released by the time shutdown() resolves (mirrors
|
|
1688
|
+
// MeshExpress.shutdown()'s server.close handling).
|
|
1689
|
+
const proxy = this.httpsProxy;
|
|
1690
|
+
this.httpsProxy = undefined;
|
|
1691
|
+
await new Promise((resolve) => {
|
|
1692
|
+
proxy.close((err) => {
|
|
1693
|
+
if (err)
|
|
1694
|
+
console.warn("Error closing HTTPS proxy:", err);
|
|
1695
|
+
resolve();
|
|
1696
|
+
});
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
// Registry unregister runs regardless of how the cleanup steps above
|
|
1700
|
+
// fared — every prior step is guarded so a drain/close failure can't
|
|
1701
|
+
// leave a stale registration behind.
|
|
1702
|
+
if (this.handle) {
|
|
1703
|
+
await this.handle.shutdown();
|
|
1704
|
+
this.handle = null;
|
|
1705
|
+
}
|
|
1706
|
+
cleanupTls();
|
|
1707
|
+
// Remove this agent's own signal listeners LAST: during the drain
|
|
1708
|
+
// above a signal must still reach the handler (it arms the
|
|
1709
|
+
// force-exit timer and exits when this memoized teardown settles).
|
|
1710
|
+
// Leaving them installed would let a LATER agent's signal hit this
|
|
1711
|
+
// already-resolved shutdown() and process.exit(0) prematurely.
|
|
1712
|
+
if (this.signalHandlers) {
|
|
1713
|
+
process.off("SIGINT", this.signalHandlers.sigint);
|
|
1714
|
+
process.off("SIGTERM", this.signalHandlers.sigterm);
|
|
1715
|
+
this.signalHandlers = null;
|
|
1716
|
+
}
|
|
1717
|
+
})();
|
|
1718
|
+
return this.shutdownPromise;
|
|
1484
1719
|
}
|
|
1485
1720
|
}
|
|
1486
1721
|
/**
|