@mcpmesh/sdk 2.8.2 → 3.0.1
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-add-tool.spec.js +106 -0
- package/dist/__tests__/agent-add-tool.spec.js.map +1 -1
- package/dist/__tests__/api-runtime-apply-idempotency.spec.d.ts +2 -0
- package/dist/__tests__/api-runtime-apply-idempotency.spec.d.ts.map +1 -0
- package/dist/__tests__/api-runtime-apply-idempotency.spec.js +89 -0
- package/dist/__tests__/api-runtime-apply-idempotency.spec.js.map +1 -0
- package/dist/__tests__/claim-dispatcher-resume-cursor.test.d.ts +2 -0
- package/dist/__tests__/claim-dispatcher-resume-cursor.test.d.ts.map +1 -0
- package/dist/__tests__/claim-dispatcher-resume-cursor.test.js +145 -0
- package/dist/__tests__/claim-dispatcher-resume-cursor.test.js.map +1 -0
- package/dist/__tests__/dependency-apply-idempotency.spec.d.ts +2 -0
- package/dist/__tests__/dependency-apply-idempotency.spec.d.ts.map +1 -0
- package/dist/__tests__/dependency-apply-idempotency.spec.js +114 -0
- package/dist/__tests__/dependency-apply-idempotency.spec.js.map +1 -0
- package/dist/__tests__/service-view-types.spec.d.ts +2 -0
- package/dist/__tests__/service-view-types.spec.d.ts.map +1 -0
- package/dist/__tests__/service-view-types.spec.js +66 -0
- package/dist/__tests__/service-view-types.spec.js.map +1 -0
- package/dist/__tests__/service-view.spec.d.ts +2 -0
- package/dist/__tests__/service-view.spec.d.ts.map +1 -0
- package/dist/__tests__/service-view.spec.js +902 -0
- package/dist/__tests__/service-view.spec.js.map +1 -0
- package/dist/__tests__/superseded-signal.spec.d.ts +19 -0
- package/dist/__tests__/superseded-signal.spec.d.ts.map +1 -0
- package/dist/__tests__/superseded-signal.spec.js +222 -0
- package/dist/__tests__/superseded-signal.spec.js.map +1 -0
- package/dist/__tests__/worker-error-serde.spec.d.ts +2 -0
- package/dist/__tests__/worker-error-serde.spec.d.ts.map +1 -0
- package/dist/__tests__/worker-error-serde.spec.js +122 -0
- package/dist/__tests__/worker-error-serde.spec.js.map +1 -0
- package/dist/a2a/producer/mount.d.ts.map +1 -1
- package/dist/a2a/producer/mount.js +4 -0
- package/dist/a2a/producer/mount.js.map +1 -1
- package/dist/agent.d.ts +53 -3
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +406 -33
- package/dist/agent.js.map +1 -1
- package/dist/api-runtime.d.ts +9 -0
- package/dist/api-runtime.d.ts.map +1 -1
- package/dist/api-runtime.js +29 -0
- package/dist/api-runtime.js.map +1 -1
- package/dist/claim-dispatcher.d.ts +25 -1
- package/dist/claim-dispatcher.d.ts.map +1 -1
- package/dist/claim-dispatcher.js +51 -2
- package/dist/claim-dispatcher.js.map +1 -1
- package/dist/inbound-job-dispatch.d.ts +1 -1
- package/dist/inbound-job-dispatch.d.ts.map +1 -1
- package/dist/inbound-job-dispatch.js +8 -2
- package/dist/inbound-job-dispatch.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/llm-provider.d.ts +14 -16
- package/dist/llm-provider.d.ts.map +1 -1
- package/dist/llm-provider.js +2 -4
- package/dist/llm-provider.js.map +1 -1
- package/dist/llm.d.ts +1 -2
- package/dist/llm.d.ts.map +1 -1
- package/dist/llm.js +1 -2
- package/dist/llm.js.map +1 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +37 -0
- package/dist/proxy.js.map +1 -1
- package/dist/route.d.ts.map +1 -1
- package/dist/route.js +5 -0
- package/dist/route.js.map +1 -1
- package/dist/service-view.d.ts +245 -0
- package/dist/service-view.d.ts.map +1 -0
- package/dist/service-view.js +210 -0
- package/dist/service-view.js.map +1 -0
- package/dist/settle.d.ts +22 -0
- package/dist/settle.d.ts.map +1 -1
- package/dist/settle.js +51 -22
- package/dist/settle.js.map +1 -1
- package/dist/superseded.d.ts +70 -0
- package/dist/superseded.d.ts.map +1 -0
- package/dist/superseded.js +99 -0
- package/dist/superseded.js.map +1 -0
- package/dist/tool-worker-entry.js +1 -12
- package/dist/tool-worker-entry.js.map +1 -1
- package/dist/tool-worker-pool.d.ts.map +1 -1
- package/dist/tool-worker-pool.js +1 -12
- package/dist/tool-worker-pool.js.map +1 -1
- package/dist/types.d.ts +31 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/worker-error-serde.d.ts +40 -0
- package/dist/worker-error-serde.d.ts.map +1 -0
- package/dist/worker-error-serde.js +86 -0
- package/dist/worker-error-serde.js.map +1 -0
- package/package.json +2 -2
package/dist/agent.js
CHANGED
|
@@ -12,9 +12,10 @@ import { UserError } from "fastmcp";
|
|
|
12
12
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
13
13
|
import { isMainThread } from "node:worker_threads";
|
|
14
14
|
import { startAgent, } from "@mcpmesh/core";
|
|
15
|
+
import { expandDependencies, isServiceView, MeshServiceUnavailableError, CAPABILITY_NAME_PATTERN, defaultProducerParams, } from "./service-view.js";
|
|
15
16
|
import { resolveConfig, generateAgentIdSuffix, findAvailablePort, resolveStartupBindPort, MAX_CONSECUTIVE_NEXT_EVENT_FAILURES, NEXT_EVENT_BACKOFF_CAP_MS, } from "./config.js";
|
|
16
17
|
import { enrichSchemaWithMediaTypes } from "./media-param.js";
|
|
17
|
-
import { createProxy,
|
|
18
|
+
import { createProxy, runWithTraceContext, runWithPropagatedHeaders, PROXY_DISPATCH_META } from "./proxy.js";
|
|
18
19
|
import { readJobHeaders, runWithJobContext, makeJobController, spliceJobController, } from "./inbound-job-dispatch.js";
|
|
19
20
|
import { MeshJobSubmitter } from "./mesh-job-submitter.js";
|
|
20
21
|
import { getSettleState, } from "./settle.js";
|
|
@@ -115,6 +116,30 @@ function scheduleAutoStart() {
|
|
|
115
116
|
}
|
|
116
117
|
});
|
|
117
118
|
}
|
|
119
|
+
// #1314: canonical JSON so that value-equal-but-reference-distinct objects
|
|
120
|
+
// (e.g. re-parsed kwargs on a reconcile re-emit) hash identically. Object keys
|
|
121
|
+
// are emitted in sorted order recursively; arrays keep their order.
|
|
122
|
+
function stableStringify(value) {
|
|
123
|
+
if (value === null || typeof value !== "object") {
|
|
124
|
+
return JSON.stringify(value) ?? "null";
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
128
|
+
}
|
|
129
|
+
const obj = value;
|
|
130
|
+
const keys = Object.keys(obj).sort();
|
|
131
|
+
return `{${keys
|
|
132
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)
|
|
133
|
+
.join(",")}}`;
|
|
134
|
+
}
|
|
135
|
+
// #1314: the resolution signature actually wired for a dependency edge. Two
|
|
136
|
+
// applies with the same signature produce an identical proxy, so the second is
|
|
137
|
+
// a no-op we can skip. `agentId` is included so this composes with #1315: an
|
|
138
|
+
// agent_id-only change still rebuilds, while an unchanged reconcile re-emit
|
|
139
|
+
// (same agentId) is skipped.
|
|
140
|
+
export function depSignature(endpoint, functionName, kwargs, agentId) {
|
|
141
|
+
return stableStringify([endpoint, functionName, kwargs, agentId]);
|
|
142
|
+
}
|
|
118
143
|
/**
|
|
119
144
|
* MeshAgent wraps a FastMCP server with MCP Mesh capabilities.
|
|
120
145
|
*
|
|
@@ -173,6 +198,14 @@ export class MeshAgent {
|
|
|
173
198
|
* different tags/settings without overwriting each other.
|
|
174
199
|
*/
|
|
175
200
|
resolvedDeps = new Map();
|
|
201
|
+
// #1314: last-applied resolution signature per depKey. The Rust core
|
|
202
|
+
// re-emits `dependency_available` for every believed-delivered edge on an
|
|
203
|
+
// independent ~10s wall-clock tick (self-heals dropped applies). Without a
|
|
204
|
+
// guard the SDK would rebuild every proxy on each tick. We record the
|
|
205
|
+
// signature (endpoint, functionName, kwargs, agentId) actually wired for a
|
|
206
|
+
// depKey and skip the rebuild when an incoming apply carries the same one.
|
|
207
|
+
// Cleared on the unavailable/removal path so a later re-add rebuilds.
|
|
208
|
+
appliedDepSignatures = new Map();
|
|
176
209
|
// True when this MeshAgent is constructed inside a worker_threads Worker.
|
|
177
210
|
// In worker mode addTool() only stashes execute fns and skips all FastMCP /
|
|
178
211
|
// registry / Rust core wiring (no Express port conflict, no double-register).
|
|
@@ -276,6 +309,22 @@ export class MeshAgent {
|
|
|
276
309
|
addTool(def) {
|
|
277
310
|
const toolName = def.name;
|
|
278
311
|
const execute = def.execute;
|
|
312
|
+
// Issue #1293 item 6: validate the PRODUCED capability name — the value
|
|
313
|
+
// published to the registry (def.capability ?? toolName) — against the
|
|
314
|
+
// segment-wise dotted grammar, closing the parity gap with the Go registry
|
|
315
|
+
// (src/core/registry/validation.go) and the Python runtime
|
|
316
|
+
// (support_types.py validate_capability_name). Only the produced capability
|
|
317
|
+
// is checked; dependency capability names are deliberately left unvalidated
|
|
318
|
+
// (they are unvalidated in the Go registry and every runtime), so checking
|
|
319
|
+
// them here would be stricter than the contract, not parity. Reuses the
|
|
320
|
+
// same CAPABILITY_NAME_PATTERN that addService applies to synthesized names.
|
|
321
|
+
const producedCapability = def.capability ?? toolName;
|
|
322
|
+
if (!CAPABILITY_NAME_PATTERN.test(producedCapability)) {
|
|
323
|
+
throw new Error(`addTool: capability '${producedCapability}' is not a valid capability ` +
|
|
324
|
+
`name — each dot-separated segment must start with a letter and ` +
|
|
325
|
+
`contain only letters, digits, underscores, and hyphens ` +
|
|
326
|
+
`(cross-ref src/core/registry/validation.go).`);
|
|
327
|
+
}
|
|
279
328
|
// Phase 1 MeshJob substrate: validate `task: true` requires an
|
|
280
329
|
// async function. Long-running tools need a Promise-based control
|
|
281
330
|
// flow so the dispatch wrapper (Phase B) can await
|
|
@@ -388,6 +437,15 @@ export class MeshAgent {
|
|
|
388
437
|
}
|
|
389
438
|
}
|
|
390
439
|
}
|
|
440
|
+
// Issue #1277: validate resumeCursor at registration so misuse fails loud
|
|
441
|
+
// before the agent talks to the registry. Mirrors the retryOn check:
|
|
442
|
+
// resumeCursor requires task: true — only job-bound handlers have a
|
|
443
|
+
// controller (and a per-filter recvEvent cursor) to resume, so the flag
|
|
444
|
+
// is meaningless for synchronous tools.
|
|
445
|
+
if (def.resumeCursor === true && def.task !== true) {
|
|
446
|
+
throw new Error(`addTool({ resumeCursor }) for tool '${toolName}': resumeCursor is ` +
|
|
447
|
+
`only valid with task: true; remove resumeCursor or set task: true.`);
|
|
448
|
+
}
|
|
391
449
|
// Issue #917: validate a2aConfig at registration time so misuse fails
|
|
392
450
|
// loud BEFORE the agent talks to the registry. Match the Python
|
|
393
451
|
// `mesh.a2a_consumer` and Java `@A2AConsumer` startup-time checks.
|
|
@@ -446,16 +504,42 @@ export class MeshAgent {
|
|
|
446
504
|
_workerToolMap.set(toolName, execute);
|
|
447
505
|
return this;
|
|
448
506
|
}
|
|
449
|
-
// Normalize dependencies
|
|
450
|
-
|
|
507
|
+
// Normalize dependencies. RFC #1280: a `mesh.serviceView(...)` entry
|
|
508
|
+
// occupies ONE positional slot but expands into N ordinary edges (name-
|
|
509
|
+
// sorted, in-place). `normalizedDeps` is the FLAT edge array — the wire
|
|
510
|
+
// payload, settle keys, resolution events, and resolvedDeps all index it
|
|
511
|
+
// exactly as before; `depSlots` maps each positional execute slot to its
|
|
512
|
+
// edge (a `dep`) or edge-range (a `view`). For a view-free tool the edges
|
|
513
|
+
// are byte-identical to `deps.map(normalizeDependency)` and
|
|
514
|
+
// `depSlots[i].edgeIndex === i` — zero behavior change.
|
|
515
|
+
const { edges: normalizedDeps, slots: depSlots } = expandDependencies(def.dependencies ?? [], toolName);
|
|
451
516
|
const depEndpoints = normalizedDeps.map((d) => d.capability);
|
|
517
|
+
// Phase 1 MeshJob substrate: meshJobDepIndex is an index into the AUTHORED
|
|
518
|
+
// dependencies array (i.e. a positional slot). It must point at an ordinary
|
|
519
|
+
// dependency slot, never a service-view slot (a view has no single edge to
|
|
520
|
+
// swap for a submitter).
|
|
521
|
+
if (def.meshJobDepIndex !== undefined) {
|
|
522
|
+
const slot = depSlots[def.meshJobDepIndex];
|
|
523
|
+
if (slot && slot.kind === "view") {
|
|
524
|
+
throw new Error(`addTool({ meshJobDepIndex: ${def.meshJobDepIndex} }) for tool ` +
|
|
525
|
+
`'${toolName}' points at a mesh.serviceView slot — a MeshJob ` +
|
|
526
|
+
`submitter can only replace an ordinary dependency slot.`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// The flat edge index of the MeshJob submitter slot (if any). Used by the
|
|
530
|
+
// edge-indexed skip logic below (settle declaration/wait, required guard).
|
|
531
|
+
// For a view-free tool this equals `def.meshJobDepIndex` (slot index ===
|
|
532
|
+
// edge index), so those loops are byte-identical to before.
|
|
533
|
+
const meshJobEdgeIndex = def.meshJobDepIndex === undefined
|
|
534
|
+
? undefined
|
|
535
|
+
: depSlots[def.meshJobDepIndex].edgeIndex;
|
|
452
536
|
// Settling-window grace (#1193): declare this tool's proxy deps with the
|
|
453
537
|
// process-wide settle state so the agent-level "all declared deps
|
|
454
538
|
// resolved" latch can flip eagerly. The MeshJob slot is excluded — its
|
|
455
539
|
// submitter is constructed locally, not resolved by an event.
|
|
456
540
|
const settleState = getSettleState();
|
|
457
541
|
normalizedDeps.forEach((_dep, depIndex) => {
|
|
458
|
-
if (depIndex !==
|
|
542
|
+
if (depIndex !== meshJobEdgeIndex) {
|
|
459
543
|
settleState.registerDeclared(`${toolName}:dep_${depIndex}`);
|
|
460
544
|
}
|
|
461
545
|
});
|
|
@@ -469,6 +553,10 @@ export class MeshAgent {
|
|
|
469
553
|
// registered in this.taskHandlers). Captured here so the closure
|
|
470
554
|
// sees a stable reference even if def is mutated post-registration.
|
|
471
555
|
const retryOn = def.retryOn;
|
|
556
|
+
// Issue #1277: per-tool durable-cursor resume opt-in. Captured here so
|
|
557
|
+
// the ClaimHandler registration below carries it into the dispatcher
|
|
558
|
+
// (which seeds the reclaimed controller from the claim's recv_cursor).
|
|
559
|
+
const resumeCursor = def.resumeCursor === true;
|
|
472
560
|
// Phase 1 MeshJob substrate: when a job-bound tool exists AND the
|
|
473
561
|
// user explicitly opted into worker isolation via env, log a single
|
|
474
562
|
// warning at registration time. The wrapper force-disables
|
|
@@ -487,6 +575,18 @@ export class MeshAgent {
|
|
|
487
575
|
`(controllers/AsyncLocalStorage don't cross worker boundaries). ` +
|
|
488
576
|
`Set 'task: true' explicitly if you intend a producer.`);
|
|
489
577
|
}
|
|
578
|
+
// RFC #1280: same force-disable + non-silent warning for view-bound tools.
|
|
579
|
+
// A serviceView facade wraps live closures over resolvedDeps and has no
|
|
580
|
+
// PROXY_DISPATCH_META, so it can't cross the worker_threads boundary — the
|
|
581
|
+
// wrapper runs view-bound tools inline. Warn once (like the job-bound case)
|
|
582
|
+
// so users who opted into MCP_MESH_TOOL_ISOLATION know it does not apply.
|
|
583
|
+
const isViewBoundForLog = depSlots.some((s) => s.kind === "view");
|
|
584
|
+
if (isViewBoundForLog && isolationEnvSet) {
|
|
585
|
+
console.warn(`[mesh-tool] '${toolName}' injects a mesh.serviceView facade; worker ` +
|
|
586
|
+
`isolation is disabled for view-bound tools (facades wrap live ` +
|
|
587
|
+
`closures over resolved dependencies that don't cross worker ` +
|
|
588
|
+
`boundaries).`);
|
|
589
|
+
}
|
|
490
590
|
// Create wrapper that injects dependencies positionally and handles tracing
|
|
491
591
|
const wrappedExecute = async (args) => {
|
|
492
592
|
// Settling-window grace (#1193): while the agent is still settling,
|
|
@@ -498,7 +598,7 @@ export class MeshAgent {
|
|
|
498
598
|
const pendingSettle = [];
|
|
499
599
|
normalizedDeps.forEach((dep, depIndex) => {
|
|
500
600
|
const depKey = `${toolName}:dep_${depIndex}`;
|
|
501
|
-
if (depIndex !==
|
|
601
|
+
if (depIndex !== meshJobEdgeIndex &&
|
|
502
602
|
!this.resolvedDeps.has(depKey)) {
|
|
503
603
|
pendingSettle.push({ depKey, capability: dep.capability });
|
|
504
604
|
}
|
|
@@ -507,14 +607,25 @@ export class MeshAgent {
|
|
|
507
607
|
await settleState.awaitPending(pendingSettle);
|
|
508
608
|
}
|
|
509
609
|
}
|
|
510
|
-
// Build positional deps array
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
|
|
610
|
+
// Build positional deps array (one entry per authored slot). A view slot
|
|
611
|
+
// yields a facade; the MeshJob slot yields a MeshJobSubmitter; every other
|
|
612
|
+
// slot yields resolvedDeps.get(...) ?? null.
|
|
613
|
+
const depsArray = this._buildDepSlots(toolName, depSlots, normalizedDeps, meshJobDepIndex, settleState);
|
|
614
|
+
// Trace metric: count resolved EDGES, not slots — a view slot always
|
|
615
|
+
// yields a non-null facade, so counting slots would inflate the numerator
|
|
616
|
+
// against the edge-based dependency total. Excludes the MeshJob submitter
|
|
617
|
+
// edge (built locally, never a resolved proxy) and adds it back as one
|
|
618
|
+
// injected slot so a view-free tool's count is byte-identical to before.
|
|
619
|
+
let injectedCount = 0;
|
|
620
|
+
for (let e = 0; e < normalizedDeps.length; e++) {
|
|
621
|
+
if (e === meshJobEdgeIndex)
|
|
622
|
+
continue;
|
|
623
|
+
if ((this.resolvedDeps.get(`${toolName}:dep_${e}`) ?? null) !== null) {
|
|
624
|
+
injectedCount++;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (meshJobDepIndex !== undefined)
|
|
628
|
+
injectedCount++;
|
|
518
629
|
// Issue #1273: detect a required dependency slot that is still
|
|
519
630
|
// unresolved AFTER the settle wait above (depsArray was built
|
|
520
631
|
// post-settle). The ACTION is deferred until the invocation flavor is
|
|
@@ -522,12 +633,16 @@ export class MeshAgent {
|
|
|
522
633
|
// direct tools/call refuses, an inbound JOB dispatch releases the
|
|
523
634
|
// lease. Optional deps keep null-passthrough; the MeshJob submitter
|
|
524
635
|
// slot is skipped (built locally, never a resolved proxy).
|
|
636
|
+
// Evaluate per-EDGE against resolvedDeps (not per-slot against depsArray):
|
|
637
|
+
// a view slot collapses N edges into one non-null facade, so a required
|
|
638
|
+
// view-method edge must be checked on the flat edge state. For a view-free
|
|
639
|
+
// tool this is equivalent to the old `depsArray[depIndex] == null` test.
|
|
525
640
|
let missingRequiredCap = null;
|
|
526
641
|
for (let depIndex = 0; depIndex < normalizedDeps.length; depIndex++) {
|
|
527
642
|
const dep = normalizedDeps[depIndex];
|
|
528
643
|
if (dep.required === true &&
|
|
529
|
-
depIndex !==
|
|
530
|
-
|
|
644
|
+
depIndex !== meshJobEdgeIndex &&
|
|
645
|
+
(this.resolvedDeps.get(`${toolName}:dep_${depIndex}`) ?? null) == null) {
|
|
531
646
|
missingRequiredCap = dep.capability;
|
|
532
647
|
break;
|
|
533
648
|
}
|
|
@@ -655,8 +770,14 @@ export class MeshAgent {
|
|
|
655
770
|
// cached client + connection pool intact across calls.
|
|
656
771
|
const isA2aBound = a2aClient !== null;
|
|
657
772
|
const isJobBound = isTaskTool || meshJobDepIndex !== undefined;
|
|
773
|
+
// RFC #1280: a service-view facade wraps live closures over resolvedDeps
|
|
774
|
+
// and cannot be serialised across the worker_threads boundary (it has no
|
|
775
|
+
// PROXY_DISPATCH_META). Force inline execution for view-bound tools, like
|
|
776
|
+
// job-bound and A2A-bound tools.
|
|
777
|
+
const isViewBound = depSlots.some((s) => s.kind === "view");
|
|
658
778
|
const isolationEnabled = !isJobBound &&
|
|
659
779
|
!isA2aBound &&
|
|
780
|
+
!isViewBound &&
|
|
660
781
|
(process.env.MCP_MESH_TOOL_ISOLATION ?? "true").toLowerCase() !== "false";
|
|
661
782
|
try {
|
|
662
783
|
let result;
|
|
@@ -832,7 +953,7 @@ export class MeshAgent {
|
|
|
832
953
|
const pendingSettle = [];
|
|
833
954
|
normalizedDeps.forEach((dep, depIndex) => {
|
|
834
955
|
const depKey = `${toolName}:dep_${depIndex}`;
|
|
835
|
-
if (depIndex !==
|
|
956
|
+
if (depIndex !== meshJobEdgeIndex &&
|
|
836
957
|
!this.resolvedDeps.has(depKey)) {
|
|
837
958
|
pendingSettle.push({ depKey, capability: dep.capability });
|
|
838
959
|
}
|
|
@@ -841,7 +962,7 @@ export class MeshAgent {
|
|
|
841
962
|
await settleState.awaitPending(pendingSettle);
|
|
842
963
|
}
|
|
843
964
|
}
|
|
844
|
-
const liveDeps = this._buildDepSlots(toolName, normalizedDeps, meshJobDepIndex, settleState);
|
|
965
|
+
const liveDeps = this._buildDepSlots(toolName, depSlots, normalizedDeps, meshJobDepIndex, settleState);
|
|
845
966
|
const callArgs = spliceJobController(payload, liveDeps, controller, meshJobParamIndex);
|
|
846
967
|
// Issue #917: A2A consumer tools dispatched via the claim
|
|
847
968
|
// path get the same trailing A2AClient argument as the
|
|
@@ -858,7 +979,7 @@ export class MeshAgent {
|
|
|
858
979
|
// covers both an absent key and an explicit null slot.
|
|
859
980
|
const requiredSlots = [];
|
|
860
981
|
normalizedDeps.forEach((dep, depIndex) => {
|
|
861
|
-
if (dep.required && depIndex !==
|
|
982
|
+
if (dep.required && depIndex !== meshJobEdgeIndex) {
|
|
862
983
|
requiredSlots.push([depIndex, dep.capability]);
|
|
863
984
|
}
|
|
864
985
|
});
|
|
@@ -873,7 +994,12 @@ export class MeshAgent {
|
|
|
873
994
|
return null;
|
|
874
995
|
}
|
|
875
996
|
: undefined;
|
|
876
|
-
this._taskHandlers.set(capability, {
|
|
997
|
+
this._taskHandlers.set(capability, {
|
|
998
|
+
handler,
|
|
999
|
+
retryOn,
|
|
1000
|
+
requiredProbe,
|
|
1001
|
+
resumeCursor,
|
|
1002
|
+
});
|
|
877
1003
|
}
|
|
878
1004
|
// Store mesh metadata with JSON Schema for LLM tool resolution
|
|
879
1005
|
const inputSchema = this.convertZodToJsonSchema(def.parameters);
|
|
@@ -884,6 +1010,28 @@ export class MeshAgent {
|
|
|
884
1010
|
if (def.outputSchema) {
|
|
885
1011
|
outputSchemaRaw = this.convertZodToJsonSchema(def.outputSchema);
|
|
886
1012
|
}
|
|
1013
|
+
// dependencyKwargs is authored per positional SLOT but consumed per flat
|
|
1014
|
+
// EDGE (handleDependencyAvailable reads meta.dependencyKwargs[depIndex] with
|
|
1015
|
+
// the event's edge index). For a view-free tool slot index === edge index,
|
|
1016
|
+
// so pass it through unchanged (byte-identical). When a view slot is present,
|
|
1017
|
+
// remap each slot's kwargs onto its edge(s) — a view's single kwargs entry
|
|
1018
|
+
// applies to each of its method edges.
|
|
1019
|
+
let edgeDependencyKwargs = def.dependencyKwargs;
|
|
1020
|
+
if (def.dependencyKwargs && depSlots.some((s) => s.kind === "view")) {
|
|
1021
|
+
const remapped = new Array(normalizedDeps.length);
|
|
1022
|
+
depSlots.forEach((slot, slotIndex) => {
|
|
1023
|
+
const kw = def.dependencyKwargs?.[slotIndex];
|
|
1024
|
+
if (slot.kind === "dep") {
|
|
1025
|
+
remapped[slot.edgeIndex] = kw;
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
for (const m of slot.methods) {
|
|
1029
|
+
remapped[m.edgeIndex] = kw;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
edgeDependencyKwargs = remapped;
|
|
1034
|
+
}
|
|
887
1035
|
this.tools.set(toolName, {
|
|
888
1036
|
capability: def.capability ?? toolName,
|
|
889
1037
|
version: def.version ?? "1.0.0",
|
|
@@ -894,7 +1042,7 @@ export class MeshAgent {
|
|
|
894
1042
|
// Issue #547 Phase 4: per-tool override (default true = current behavior).
|
|
895
1043
|
outputSchemaStrict: def.outputSchemaStrict !== false,
|
|
896
1044
|
dependencies: normalizedDeps,
|
|
897
|
-
dependencyKwargs:
|
|
1045
|
+
dependencyKwargs: edgeDependencyKwargs,
|
|
898
1046
|
// Phase 1 MeshJob substrate: stamp producer's long-running flag
|
|
899
1047
|
// so the heartbeat pipeline ships it to the registry. Consumers
|
|
900
1048
|
// read this to decide between job semantics and a regular
|
|
@@ -911,6 +1059,108 @@ export class MeshAgent {
|
|
|
911
1059
|
});
|
|
912
1060
|
return this;
|
|
913
1061
|
}
|
|
1062
|
+
/**
|
|
1063
|
+
* RFC #1280 producer sugar: publish each entry of `methods` as an ordinary
|
|
1064
|
+
* mesh tool with capability `prefix.<method>`, routed through the SAME
|
|
1065
|
+
* `addTool` machinery (schemas, heartbeat, DI, duplicate handling all fall
|
|
1066
|
+
* out). Entries are registered NAME-SORTED for deterministic registration.
|
|
1067
|
+
*
|
|
1068
|
+
* Each entry is either a bare execute function (shorthand) or an object
|
|
1069
|
+
* `{ execute, parameters?, tags?, version?, description?, dependencies?, ... }`
|
|
1070
|
+
* carrying any `addTool` passthrough (but NOT `name`/`capability` — those are
|
|
1071
|
+
* derived). A method with no `parameters` gets a permissive passthrough schema.
|
|
1072
|
+
*
|
|
1073
|
+
* The prefix AND every derived capability are validated against the segment-
|
|
1074
|
+
* wise dotted grammar (kept in lockstep with the Go registry validator —
|
|
1075
|
+
* src/core/registry/validation.go). addTool applies the same grammar to its
|
|
1076
|
+
* produced capability (#1293 item 6), so both surfaces validate a PRODUCED
|
|
1077
|
+
* capability name before it reaches the registry; dependency capability names
|
|
1078
|
+
* remain the registry's authority.
|
|
1079
|
+
*
|
|
1080
|
+
* @example
|
|
1081
|
+
* ```typescript
|
|
1082
|
+
* agent.addService("media", {
|
|
1083
|
+
* caption: async (args) => ({ caption: `...${args.text}` }),
|
|
1084
|
+
* thumbnail: { execute: async (args) => ({ url: "..." }), tags: ["fast"] },
|
|
1085
|
+
* });
|
|
1086
|
+
* // → tools/capabilities "media.caption" and "media.thumbnail"
|
|
1087
|
+
* ```
|
|
1088
|
+
*/
|
|
1089
|
+
addService(prefix, methods) {
|
|
1090
|
+
if (typeof prefix !== "string" || prefix.trim() === "") {
|
|
1091
|
+
throw new Error("addService: a non-empty capability prefix is required " +
|
|
1092
|
+
'(e.g. addService("media", { ... })).');
|
|
1093
|
+
}
|
|
1094
|
+
if (!CAPABILITY_NAME_PATTERN.test(prefix)) {
|
|
1095
|
+
throw new Error(`addService: prefix '${prefix}' is not a valid capability name — each ` +
|
|
1096
|
+
`dot-separated segment must start with a letter and contain only ` +
|
|
1097
|
+
`letters, digits, '_' or '-' (cross-ref src/core/registry/validation.go).`);
|
|
1098
|
+
}
|
|
1099
|
+
if (methods == null || typeof methods !== "object") {
|
|
1100
|
+
throw new Error(`addService("${prefix}", ...): methods must be an object mapping ` +
|
|
1101
|
+
`method names to producer functions/definitions.`);
|
|
1102
|
+
}
|
|
1103
|
+
const names = Object.keys(methods).sort();
|
|
1104
|
+
if (names.length === 0) {
|
|
1105
|
+
throw new Error(`addService("${prefix}", ...): at least one method is required.`);
|
|
1106
|
+
}
|
|
1107
|
+
// Pass 1 — validate EVERY derived name/entry before registering any tool so
|
|
1108
|
+
// an invalid method mid-map is atomic (registers nothing). Also rejects a
|
|
1109
|
+
// derived capability that collides with an already-registered tool (scoped
|
|
1110
|
+
// to addService: addTool's own last-wins behavior for hand-written tools is
|
|
1111
|
+
// unchanged).
|
|
1112
|
+
const prepared = [];
|
|
1113
|
+
for (const method of names) {
|
|
1114
|
+
const capability = `${prefix}.${method}`;
|
|
1115
|
+
// Validate the FULL derived capability, not just the prefix: a method
|
|
1116
|
+
// name may contain characters ('$', unicode, ...) the grammar rejects.
|
|
1117
|
+
if (!CAPABILITY_NAME_PATTERN.test(capability)) {
|
|
1118
|
+
throw new Error(`addService("${prefix}", ...): method '${method}' derives capability ` +
|
|
1119
|
+
`'${capability}', which is not a valid capability name (each ` +
|
|
1120
|
+
`dot-separated segment must start with a letter and contain only ` +
|
|
1121
|
+
`letters, digits, '_' or '-'). Rename the method.`);
|
|
1122
|
+
}
|
|
1123
|
+
if (this.tools.has(capability)) {
|
|
1124
|
+
throw new Error(`addService("${prefix}", ...): method '${method}' derives tool/` +
|
|
1125
|
+
`capability '${capability}', which is already registered. Rename ` +
|
|
1126
|
+
`the method or the conflicting tool.`);
|
|
1127
|
+
}
|
|
1128
|
+
const entry = methods[method];
|
|
1129
|
+
// Guard: a service-view value here is the consumer surface used in the
|
|
1130
|
+
// wrong role (producer). Fail loud.
|
|
1131
|
+
if (isServiceView(entry)) {
|
|
1132
|
+
throw new Error(`addService("${prefix}", ...): method '${method}' is a ` +
|
|
1133
|
+
`mesh.serviceView(...) — that is a CONSUMER view, not a producer ` +
|
|
1134
|
+
`method. Provide an execute function or { execute, ... } object.`);
|
|
1135
|
+
}
|
|
1136
|
+
let def;
|
|
1137
|
+
if (typeof entry === "function") {
|
|
1138
|
+
def = { execute: entry };
|
|
1139
|
+
}
|
|
1140
|
+
else if (entry != null &&
|
|
1141
|
+
typeof entry === "object" &&
|
|
1142
|
+
typeof entry.execute === "function") {
|
|
1143
|
+
def = entry;
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
throw new Error(`addService("${prefix}", ...): method '${method}' must be an execute ` +
|
|
1147
|
+
`function or an object with an execute function.`);
|
|
1148
|
+
}
|
|
1149
|
+
prepared.push({ capability, def });
|
|
1150
|
+
}
|
|
1151
|
+
// Pass 2 — register through the existing addTool machinery. Derived name ===
|
|
1152
|
+
// derived capability for deterministic uniqueness.
|
|
1153
|
+
for (const { capability, def } of prepared) {
|
|
1154
|
+
this.addTool({
|
|
1155
|
+
...def,
|
|
1156
|
+
name: capability,
|
|
1157
|
+
capability,
|
|
1158
|
+
parameters: def.parameters ?? defaultProducerParams(),
|
|
1159
|
+
execute: def.execute,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
return this;
|
|
1163
|
+
}
|
|
914
1164
|
/**
|
|
915
1165
|
* Add an LLM provider to the agent.
|
|
916
1166
|
*
|
|
@@ -1014,27 +1264,36 @@ export class MeshAgent {
|
|
|
1014
1264
|
* Build the positional dependency-slot array for one tool call, shared by
|
|
1015
1265
|
* BOTH the inbound HTTP wrapper (wrappedExecute) and the claim-dispatch
|
|
1016
1266
|
* path (the ClaimHandler for task:true tools). Centralising this keeps the
|
|
1017
|
-
* two slot-assembly sites identical:
|
|
1267
|
+
* two slot-assembly sites identical. One entry per AUTHORED slot:
|
|
1268
|
+
* - a service-view slot (RFC #1280) → a facade over its method edges
|
|
1018
1269
|
* - the MeshJob slot (meshJobDepIndex) → a freshly-bound MeshJobSubmitter
|
|
1019
|
-
* - every other slot → resolvedDeps.get(`${toolName}:dep_${
|
|
1270
|
+
* - every other slot → resolvedDeps.get(`${toolName}:dep_${edgeIndex}`) ?? null
|
|
1271
|
+
*
|
|
1272
|
+
* `depSlots` carries the slot→edge mapping (a `dep` slot owns one edge; a
|
|
1273
|
+
* `view` slot owns a contiguous edge range). `edges` is the flat edge array;
|
|
1274
|
+
* a slot's proxy/capability is read via its edge index.
|
|
1020
1275
|
*
|
|
1021
1276
|
* #1231: a typed dep slot can resolve at the registry yet never receive an
|
|
1022
1277
|
* injected proxy, leaving the parameter null. Once the settle latch has
|
|
1023
1278
|
* flipped (during settling the proxy may still land), warn ONCE per
|
|
1024
1279
|
* tool+slot — the array is rebuilt per call, so a per-call warning would
|
|
1025
|
-
* spam. The warn-once dedupe keys on `${toolName}:dep_${
|
|
1280
|
+
* spam. The warn-once dedupe keys on `${toolName}:dep_${edgeIndex}` via the
|
|
1026
1281
|
* module-level `_unwiredSlotWarned` Set, so a tool exercised via BOTH the
|
|
1027
1282
|
* inbound and claim paths warns at most once total.
|
|
1028
1283
|
*/
|
|
1029
|
-
_buildDepSlots(toolName,
|
|
1030
|
-
return
|
|
1031
|
-
if (
|
|
1284
|
+
_buildDepSlots(toolName, depSlots, edges, meshJobDepIndex, settleState) {
|
|
1285
|
+
return depSlots.map((slot, slotIndex) => {
|
|
1286
|
+
if (slot.kind === "view") {
|
|
1287
|
+
return this._buildServiceViewFacade(toolName, slot, edges, settleState);
|
|
1288
|
+
}
|
|
1289
|
+
const edgeIndex = slot.edgeIndex;
|
|
1290
|
+
if (slotIndex === meshJobDepIndex) {
|
|
1032
1291
|
// Build the submitter lazily per call so we always pick up the
|
|
1033
1292
|
// current registryUrl (test harnesses sometimes mutate it between
|
|
1034
1293
|
// calls).
|
|
1035
|
-
return new MeshJobSubmitter(
|
|
1294
|
+
return new MeshJobSubmitter(edges[edgeIndex].capability, this.agentId, this.config.registryUrl);
|
|
1036
1295
|
}
|
|
1037
|
-
const slotKey = `${toolName}:dep_${
|
|
1296
|
+
const slotKey = `${toolName}:dep_${edgeIndex}`;
|
|
1038
1297
|
const proxy = this.resolvedDeps.get(slotKey) ?? null;
|
|
1039
1298
|
// #1231: a declared typed dep slot that is still null AFTER settling
|
|
1040
1299
|
// never received an injected proxy. Warn ONCE per tool+slot. Note this
|
|
@@ -1043,15 +1302,111 @@ export class MeshAgent {
|
|
|
1043
1302
|
if (proxy === null && settleState.isSettled()) {
|
|
1044
1303
|
if (!_unwiredSlotWarned.has(slotKey)) {
|
|
1045
1304
|
_unwiredSlotWarned.add(slotKey);
|
|
1046
|
-
console.warn(`[mesh-tool] dependency '${
|
|
1047
|
-
`is still null after settling — no proxy was
|
|
1048
|
-
`positional slot ${
|
|
1049
|
-
`'${
|
|
1305
|
+
console.warn(`[mesh-tool] dependency '${edges[edgeIndex].capability}' on ` +
|
|
1306
|
+
`'${toolName}' is still null after settling — no proxy was ` +
|
|
1307
|
+
`injected into positional slot ${slotIndex}. Fix: ensure the ` +
|
|
1308
|
+
`provider for '${edges[edgeIndex].capability}' is registered ` +
|
|
1309
|
+
`and reachable.`);
|
|
1050
1310
|
}
|
|
1051
1311
|
}
|
|
1052
1312
|
return proxy;
|
|
1053
1313
|
});
|
|
1054
1314
|
}
|
|
1315
|
+
/**
|
|
1316
|
+
* RFC #1280: build the facade injected at a service-view slot. Each facade
|
|
1317
|
+
* method reads its edge's CURRENT proxy from `resolvedDeps` at call time —
|
|
1318
|
+
* rebinding-free: a `dependency_available`/`unavailable` event swapping the
|
|
1319
|
+
* per-edge proxy is observed on the next call without rebuilding the facade.
|
|
1320
|
+
*
|
|
1321
|
+
* The `minAvailable` floor is enforced BEFORE delegation on every call: below
|
|
1322
|
+
* the floor during the settling window (#1193) the method performs the same
|
|
1323
|
+
* bounded, capability-keyed wait the injected-proxy path uses, recounts, then
|
|
1324
|
+
* throws {@link MeshServiceUnavailableError}. An unresolved OPTIONAL method
|
|
1325
|
+
* edge throws a `TypeError` — the same failure shape a directly-injected
|
|
1326
|
+
* unresolved `McpMeshTool` (a null proxy) produces when called.
|
|
1327
|
+
*/
|
|
1328
|
+
_buildServiceViewFacade(toolName, slot, edges, settleState) {
|
|
1329
|
+
const total = slot.methods.length;
|
|
1330
|
+
const floor = slot.minAvailable;
|
|
1331
|
+
const viewName = slot.name;
|
|
1332
|
+
const countAvailable = () => {
|
|
1333
|
+
let n = 0;
|
|
1334
|
+
for (const m of slot.methods) {
|
|
1335
|
+
if ((this.resolvedDeps.get(`${toolName}:dep_${m.edgeIndex}`) ?? null) !== null) {
|
|
1336
|
+
n++;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return n;
|
|
1340
|
+
};
|
|
1341
|
+
const enforceFloor = async () => {
|
|
1342
|
+
if (floor <= 0)
|
|
1343
|
+
return;
|
|
1344
|
+
let available = countAvailable();
|
|
1345
|
+
// Below the floor during the settling window: perform a bounded wait that
|
|
1346
|
+
// RACES all of the view's still-pending edges (wake on ANY resolution),
|
|
1347
|
+
// recounts, and breaks the moment the floor is met — so a call gated on
|
|
1348
|
+
// the floor wakes when ANY qualifying edge lands, never sleeping the full
|
|
1349
|
+
// window on a still-pending sibling key. Once settled the waits are
|
|
1350
|
+
// no-ops and the floor fails fast.
|
|
1351
|
+
while (available < floor &&
|
|
1352
|
+
!settleState.isSettled() &&
|
|
1353
|
+
settleState.remainingMs() > 0) {
|
|
1354
|
+
// Only edges that can still fire a settle WAKE are worth waiting on: a
|
|
1355
|
+
// key already in the settle ratchet (resolved at least once) will never
|
|
1356
|
+
// re-fire its waiter, so if such an edge has since flapped to a null
|
|
1357
|
+
// proxy `waitForAny` would filter it out and return immediately — a hot
|
|
1358
|
+
// busy-loop until the window expires. Excluding ratchet-resolved keys
|
|
1359
|
+
// here means `pending` empties and we break to the final recount/throw
|
|
1360
|
+
// the instant no wakeable edge remains (no spin).
|
|
1361
|
+
const pending = [];
|
|
1362
|
+
for (const m of slot.methods) {
|
|
1363
|
+
const key = `${toolName}:dep_${m.edgeIndex}`;
|
|
1364
|
+
if ((this.resolvedDeps.get(key) ?? null) === null &&
|
|
1365
|
+
!settleState.isResolved(key)) {
|
|
1366
|
+
pending.push({ depKey: key, capability: edges[m.edgeIndex].capability });
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (pending.length === 0)
|
|
1370
|
+
break;
|
|
1371
|
+
await settleState.waitForAny(pending);
|
|
1372
|
+
available = countAvailable();
|
|
1373
|
+
}
|
|
1374
|
+
if (available < floor) {
|
|
1375
|
+
throw new MeshServiceUnavailableError(viewName, available, total, floor);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
const facade = {};
|
|
1379
|
+
for (const m of slot.methods) {
|
|
1380
|
+
const key = `${toolName}:dep_${m.edgeIndex}`;
|
|
1381
|
+
const capability = edges[m.edgeIndex].capability;
|
|
1382
|
+
// #1231 parity for view edges: a method edge that resolved at the registry
|
|
1383
|
+
// yet never received an injected proxy stays null after settling. Warn
|
|
1384
|
+
// ONCE per tool+edge (facade is rebuilt per call), naming the view method
|
|
1385
|
+
// and capability so the diagnostic matches the slot-dep warning.
|
|
1386
|
+
if ((this.resolvedDeps.get(key) ?? null) === null && settleState.isSettled()) {
|
|
1387
|
+
if (!_unwiredSlotWarned.has(key)) {
|
|
1388
|
+
_unwiredSlotWarned.add(key);
|
|
1389
|
+
console.warn(`[mesh-tool] service view '${viewName}' method '${m.method}' ` +
|
|
1390
|
+
`(capability '${capability}') on '${toolName}' is still null ` +
|
|
1391
|
+
`after settling — no proxy was injected. Fix: ensure the provider ` +
|
|
1392
|
+
`for '${capability}' is registered and reachable.`);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
facade[m.method] = async (args, options) => {
|
|
1396
|
+
await enforceFloor();
|
|
1397
|
+
const proxy = this.resolvedDeps.get(key) ?? null;
|
|
1398
|
+
if (proxy === null) {
|
|
1399
|
+
// Mirror the failure shape of calling an unresolved McpMeshTool
|
|
1400
|
+
// (a null proxy) directly — a TypeError.
|
|
1401
|
+
throw new TypeError(`service view '${viewName}' method '${m.method}' (capability ` +
|
|
1402
|
+
`'${capability}') is unavailable — the dependency did not ` +
|
|
1403
|
+
`resolve (unresolved optional edge).`);
|
|
1404
|
+
}
|
|
1405
|
+
return proxy(args, options);
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
return facade;
|
|
1409
|
+
}
|
|
1055
1410
|
_getOrBuildA2AClient(config) {
|
|
1056
1411
|
const key = [
|
|
1057
1412
|
config.url,
|
|
@@ -1259,7 +1614,7 @@ export class MeshAgent {
|
|
|
1259
1614
|
if (this._taskHandlers.size === 0)
|
|
1260
1615
|
return;
|
|
1261
1616
|
for (const [capability, entry] of this._taskHandlers.entries()) {
|
|
1262
|
-
const dispatcher = new ClaimDispatcher(capability, this.agentId, this.config.registryUrl, entry.handler, entry.retryOn, entry.requiredProbe);
|
|
1617
|
+
const dispatcher = new ClaimDispatcher(capability, this.agentId, this.config.registryUrl, entry.handler, entry.retryOn, entry.requiredProbe, entry.resumeCursor);
|
|
1263
1618
|
dispatcher.start();
|
|
1264
1619
|
this._claimDispatchers.push(dispatcher);
|
|
1265
1620
|
}
|
|
@@ -1657,8 +2012,16 @@ export class MeshAgent {
|
|
|
1657
2012
|
const meta = this.tools.get(requestingFunction);
|
|
1658
2013
|
const kwargs = meta?.dependencyKwargs?.[depIndex];
|
|
1659
2014
|
const depKey = `${requestingFunction}:dep_${depIndex}`;
|
|
2015
|
+
// #1314 idempotency guard: the Rust core re-emits dependency_available
|
|
2016
|
+
// for believed-delivered edges on a ~10s tick. If the incoming
|
|
2017
|
+
// resolution matches what is already wired, skip the rebuild entirely.
|
|
2018
|
+
const signature = depSignature(endpoint, functionName, kwargs, agentId);
|
|
2019
|
+
if (this.appliedDepSignatures.get(depKey) === signature) {
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
1660
2022
|
const proxy = createProxy(endpoint, capability, functionName, kwargs);
|
|
1661
2023
|
this.resolvedDeps.set(depKey, proxy);
|
|
2024
|
+
this.appliedDepSignatures.set(depKey, signature);
|
|
1662
2025
|
// Settling-window grace (#1193): wake any settling call waiting on
|
|
1663
2026
|
// this dependency AFTER the proxy is stored so the woken call
|
|
1664
2027
|
// re-reads a real proxy.
|
|
@@ -1676,8 +2039,14 @@ export class MeshAgent {
|
|
|
1676
2039
|
if (dep.capability === capability) {
|
|
1677
2040
|
const kwargs = meta.dependencyKwargs?.[idx];
|
|
1678
2041
|
const depKey = `${toolName}:dep_${idx}`;
|
|
2042
|
+
// #1314 idempotency guard (see position-info path above).
|
|
2043
|
+
const signature = depSignature(endpoint, functionName, kwargs, agentId);
|
|
2044
|
+
if (this.appliedDepSignatures.get(depKey) === signature) {
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
1679
2047
|
const proxy = createProxy(endpoint, capability, functionName, kwargs);
|
|
1680
2048
|
this.resolvedDeps.set(depKey, proxy);
|
|
2049
|
+
this.appliedDepSignatures.set(depKey, signature);
|
|
1681
2050
|
getSettleState().markResolved(depKey);
|
|
1682
2051
|
matchCount++;
|
|
1683
2052
|
}
|
|
@@ -1694,6 +2063,8 @@ export class MeshAgent {
|
|
|
1694
2063
|
if (requestingFunction !== undefined && depIndex !== undefined) {
|
|
1695
2064
|
const depKey = `${requestingFunction}:dep_${depIndex}`;
|
|
1696
2065
|
this.resolvedDeps.delete(depKey);
|
|
2066
|
+
// #1314: drop the applied signature so a later re-add rebuilds.
|
|
2067
|
+
this.appliedDepSignatures.delete(depKey);
|
|
1697
2068
|
console.log(`Dependency unavailable: ${capability} (tool: ${requestingFunction}, index: ${depIndex})`);
|
|
1698
2069
|
return;
|
|
1699
2070
|
}
|
|
@@ -1706,6 +2077,8 @@ export class MeshAgent {
|
|
|
1706
2077
|
if (dep.capability === capability) {
|
|
1707
2078
|
const depKey = `${toolName}:dep_${idx}`;
|
|
1708
2079
|
this.resolvedDeps.delete(depKey);
|
|
2080
|
+
// #1314: drop the applied signature so a later re-add rebuilds.
|
|
2081
|
+
this.appliedDepSignatures.delete(depKey);
|
|
1709
2082
|
removeCount++;
|
|
1710
2083
|
}
|
|
1711
2084
|
});
|