@adcp/sdk 15.4.0 → 15.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/lib/advanced.d.mts +2 -2
- package/dist/lib/advanced.d.ts +2 -2
- package/dist/lib/advanced.js +8 -2
- package/dist/lib/advanced.mjs +8 -2
- package/dist/lib/core/AgentClient.d.mts +4 -0
- package/dist/lib/core/AgentClient.d.ts +4 -0
- package/dist/lib/core/AgentClient.js +8 -0
- package/dist/lib/core/AgentClient.mjs +8 -0
- package/dist/lib/core/SingleAgentClient.d.mts +154 -0
- package/dist/lib/core/SingleAgentClient.d.ts +154 -0
- package/dist/lib/core/SingleAgentClient.js +661 -23
- package/dist/lib/core/SingleAgentClient.mjs +666 -23
- package/dist/lib/core/agent-discovery-cache.d.mts +103 -0
- package/dist/lib/core/agent-discovery-cache.d.ts +103 -0
- package/dist/lib/core/agent-discovery-cache.js +121 -0
- package/dist/lib/core/agent-discovery-cache.mjs +92 -0
- package/dist/lib/index.d.mts +5 -3
- package/dist/lib/index.d.ts +5 -3
- package/dist/lib/index.js +7 -0
- package/dist/lib/index.mjs +6 -0
- package/dist/lib/protocols/index.d.mts +2 -2
- package/dist/lib/protocols/index.d.ts +2 -2
- package/dist/lib/protocols/index.js +1 -1
- package/dist/lib/protocols/index.mjs +1 -1
- package/dist/lib/protocols/mcp-call-context.d.mts +109 -0
- package/dist/lib/protocols/mcp-call-context.d.ts +109 -0
- package/dist/lib/protocols/mcp-call-context.js +221 -0
- package/dist/lib/protocols/mcp-call-context.mjs +186 -0
- package/dist/lib/protocols/mcp-modern.d.mts +9 -1
- package/dist/lib/protocols/mcp-modern.d.ts +9 -1
- package/dist/lib/protocols/mcp-modern.js +180 -104
- package/dist/lib/protocols/mcp-modern.mjs +193 -104
- package/dist/lib/protocols/mcp-scope.d.mts +15 -0
- package/dist/lib/protocols/mcp-scope.d.ts +15 -0
- package/dist/lib/protocols/mcp.d.mts +4 -2
- package/dist/lib/protocols/mcp.d.ts +4 -2
- package/dist/lib/protocols/mcp.js +178 -154
- package/dist/lib/protocols/mcp.mjs +189 -153
- package/dist/lib/protocols/transportDiagnostics.d.mts +33 -1
- package/dist/lib/protocols/transportDiagnostics.d.ts +33 -1
- package/dist/lib/protocols/transportDiagnostics.js +128 -12
- package/dist/lib/protocols/transportDiagnostics.mjs +128 -12
- package/dist/lib/reporting/gcs/index.d.mts +2 -0
- package/dist/lib/reporting/gcs/index.d.ts +2 -0
- package/dist/lib/reporting/gcs/index.js +3 -0
- package/dist/lib/reporting/gcs/index.mjs +2 -0
- package/dist/lib/reporting/gcs/row-provider.d.mts +32 -0
- package/dist/lib/reporting/gcs/row-provider.d.ts +32 -0
- package/dist/lib/reporting/gcs/row-provider.js +205 -0
- package/dist/lib/reporting/gcs/row-provider.mjs +188 -0
- package/dist/lib/reporting/ledger/change-feed.d.mts +41 -0
- package/dist/lib/reporting/ledger/change-feed.d.ts +41 -0
- package/dist/lib/reporting/ledger/change-feed.js +101 -0
- package/dist/lib/reporting/ledger/change-feed.mjs +74 -0
- package/dist/lib/reporting/ledger/handler.js +22 -8
- package/dist/lib/reporting/ledger/handler.mjs +22 -8
- package/dist/lib/reporting/ledger/index.d.mts +4 -0
- package/dist/lib/reporting/ledger/index.d.ts +4 -0
- package/dist/lib/reporting/ledger/index.js +9 -1
- package/dist/lib/reporting/ledger/index.mjs +4 -0
- package/dist/lib/reporting/ledger/managed-postgres.d.mts +2 -0
- package/dist/lib/reporting/ledger/managed-postgres.d.ts +2 -0
- package/dist/lib/reporting/ledger/managed-postgres.js +21 -1
- package/dist/lib/reporting/ledger/managed-postgres.mjs +21 -1
- package/dist/lib/reporting/ledger/managed.d.mts +9 -0
- package/dist/lib/reporting/ledger/managed.d.ts +9 -0
- package/dist/lib/reporting/ledger/managed.js +18 -0
- package/dist/lib/reporting/ledger/managed.mjs +18 -0
- package/dist/lib/reporting/ledger/postgres.d.mts +175 -0
- package/dist/lib/reporting/ledger/postgres.d.ts +175 -0
- package/dist/lib/reporting/ledger/postgres.js +851 -26
- package/dist/lib/reporting/ledger/postgres.mjs +859 -26
- package/dist/lib/reporting/ledger/producer.js +10 -1
- package/dist/lib/reporting/ledger/producer.mjs +10 -1
- package/dist/lib/reporting/ledger/row-storage-errors.d.mts +7 -0
- package/dist/lib/reporting/ledger/row-storage-errors.d.ts +7 -0
- package/dist/lib/reporting/ledger/row-storage-errors.js +41 -0
- package/dist/lib/reporting/ledger/row-storage-errors.mjs +16 -0
- package/dist/lib/reporting/ledger/row-storage-migration.d.mts +41 -0
- package/dist/lib/reporting/ledger/row-storage-migration.d.ts +41 -0
- package/dist/lib/reporting/ledger/row-storage-migration.js +234 -0
- package/dist/lib/reporting/ledger/row-storage-migration.mjs +208 -0
- package/dist/lib/reporting/ledger/row-storage-object.d.mts +112 -0
- package/dist/lib/reporting/ledger/row-storage-object.d.ts +112 -0
- package/dist/lib/reporting/ledger/row-storage-object.js +281 -0
- package/dist/lib/reporting/ledger/row-storage-object.mjs +239 -0
- package/dist/lib/reporting/ledger/row-storage-provider-io.d.mts +51 -0
- package/dist/lib/reporting/ledger/row-storage-provider-io.d.ts +51 -0
- package/dist/lib/reporting/ledger/row-storage-provider-io.js +152 -0
- package/dist/lib/reporting/ledger/row-storage-provider-io.mjs +119 -0
- package/dist/lib/reporting/ledger/row-storage.d.mts +224 -0
- package/dist/lib/reporting/ledger/row-storage.d.ts +224 -0
- package/dist/lib/reporting/ledger/row-storage.js +692 -0
- package/dist/lib/reporting/ledger/row-storage.mjs +680 -0
- package/dist/lib/reporting/ledger/types.d.mts +8 -0
- package/dist/lib/reporting/ledger/types.d.ts +8 -0
- package/dist/lib/reporting/service/index.d.mts +41 -1
- package/dist/lib/reporting/service/index.d.ts +41 -1
- package/dist/lib/reporting/service/index.js +68 -1
- package/dist/lib/reporting/service/index.mjs +70 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/README.md +1 -1
- package/docs/TYPE-SUMMARY.md +35 -2
- package/docs/guides/DISCOVERY-CACHE-AND-SESSION-REUSE.md +142 -0
- package/docs/guides/EXISTING-PLATFORM.md +43 -12
- package/docs/guides/REPORTING-LEDGER.md +209 -0
- package/docs/llms.txt +4 -2
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/package.json +1 -1
|
@@ -43,6 +43,7 @@ import { withTaskDeadline } from "./task-deadline.mjs";
|
|
|
43
43
|
import { createMCPRequestHeaders } from "../auth/index.mjs";
|
|
44
44
|
import { isAbortOrTimeoutError } from "../protocols/abort.mjs";
|
|
45
45
|
import { getSignedRequestRejection } from "../protocols/signedRequestRejection.mjs";
|
|
46
|
+
import { withAmbientIdentityHeaders } from "../protocols/mcp-call-context.mjs";
|
|
46
47
|
import { ProtocolClient, normalizeTransportOptions } from "../protocols/index.mjs";
|
|
47
48
|
import {
|
|
48
49
|
AuthenticationCredentialsRejectedError,
|
|
@@ -113,6 +114,12 @@ function presentedStaticTransportCredential(authToken, headers, authProvider) {
|
|
|
113
114
|
return normalized === "authorization" || normalized === "x-adcp-auth";
|
|
114
115
|
});
|
|
115
116
|
}
|
|
117
|
+
import {
|
|
118
|
+
agentDiscoveryCacheKey,
|
|
119
|
+
MAX_AGENT_DISCOVERY_TTL_MS,
|
|
120
|
+
resolveAgentDiscoveryTtlMs,
|
|
121
|
+
validateAgentDiscoveryCacheConfig
|
|
122
|
+
} from "./agent-discovery-cache.mjs";
|
|
116
123
|
import {
|
|
117
124
|
buildSyntheticCapabilities,
|
|
118
125
|
buildSyntheticV3Capabilities,
|
|
@@ -726,6 +733,30 @@ const DURABLE_SETTLEMENT_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
|
726
733
|
"aborted"
|
|
727
734
|
]);
|
|
728
735
|
const MCP_WEBHOOK_REQUIRED_FIELDS = ["idempotency_key", "task_id", "task_type", "status", "timestamp"];
|
|
736
|
+
const sharedInvalidations = /* @__PURE__ */ new WeakMap();
|
|
737
|
+
const sharedInvalidationFloors = /* @__PURE__ */ new WeakMap();
|
|
738
|
+
let sharedInvalidationCounter = 0;
|
|
739
|
+
const MAX_SHARED_INVALIDATION_RECORDS = 1024;
|
|
740
|
+
const sharedMutationChains = /* @__PURE__ */ new WeakMap();
|
|
741
|
+
const MAX_PENDING_SHARED_MUTATIONS_PER_KEY = 16;
|
|
742
|
+
const MAX_SHARED_MUTATION_KEYS = 1024;
|
|
743
|
+
const sharedDiscoveryFlights = /* @__PURE__ */ new WeakMap();
|
|
744
|
+
const MAX_SHARED_DISCOVERY_WAITS = 3;
|
|
745
|
+
const SHARED_DISCOVERY_CACHE_TIMEOUT_MS = 2e3;
|
|
746
|
+
const SHARED_DISCOVERY_CLOCK_SKEW_MS = 5e3;
|
|
747
|
+
const CAPABILITY_STALE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
748
|
+
"VERSION_UNSUPPORTED",
|
|
749
|
+
"UNSUPPORTED_FEATURE",
|
|
750
|
+
"FEATURE_UNSUPPORTED",
|
|
751
|
+
"TOOL_NOT_FOUND",
|
|
752
|
+
"METHOD_NOT_FOUND",
|
|
753
|
+
"UNKNOWN_TOOL"
|
|
754
|
+
]);
|
|
755
|
+
function isToolSchemaRecord(value) {
|
|
756
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every(
|
|
757
|
+
(properties) => typeof properties === "object" && properties !== null && !Array.isArray(properties)
|
|
758
|
+
);
|
|
759
|
+
}
|
|
729
760
|
function isCapabilityEvidence(value) {
|
|
730
761
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
731
762
|
const candidate = value;
|
|
@@ -823,6 +854,7 @@ class SingleAgentClient {
|
|
|
823
854
|
...configuredWebhookVerification !== void 0 && { webhookVerification: configuredWebhookVerification }
|
|
824
855
|
};
|
|
825
856
|
config = this.config;
|
|
857
|
+
validateAgentDiscoveryCacheConfig(config.discoveryCache);
|
|
826
858
|
this.resolvedAdcpVersion = resolveAdcpVersion(config.adcpVersion);
|
|
827
859
|
this.webhookRegistrationStore = config.webhookRegistrationStore ?? new InMemoryWebhookRegistrationStore();
|
|
828
860
|
this.webhookReplayStore = config.webhookVerification?.replayStore ?? new InMemoryReplayStore();
|
|
@@ -1101,6 +1133,20 @@ class SingleAgentClient {
|
|
|
1101
1133
|
// Freshness bound for application-owned evidence only
|
|
1102
1134
|
capabilityEvidenceScopeKey = randomUUID();
|
|
1103
1135
|
capabilityEvidenceAuthMaterial;
|
|
1136
|
+
capabilitiesFromSharedCache = false;
|
|
1137
|
+
// Installed from config.discoveryCache rather than discovered live
|
|
1138
|
+
discoveryEpoch = 0;
|
|
1139
|
+
// bumped by invalidation so an older in-flight discovery cannot re-install what was dropped
|
|
1140
|
+
discoveredEndpointKey;
|
|
1141
|
+
// discoveryCache key the local endpoint was obtained under
|
|
1142
|
+
discoveredEndpointEpoch;
|
|
1143
|
+
// shared invalidation epoch of that key when the endpoint was obtained
|
|
1144
|
+
discoveredEndpointExpiresAt;
|
|
1145
|
+
// freshness bound for that endpoint (shared-cache mode only)
|
|
1146
|
+
sharedEvidenceEpoch;
|
|
1147
|
+
// shared invalidation epoch of that key when the evidence was obtained
|
|
1148
|
+
sharedEvidenceKey;
|
|
1149
|
+
// discoveryCache key the local capability cache was obtained under
|
|
1104
1150
|
_v2WarningFired = false;
|
|
1105
1151
|
// Gate: emit the v2-sunset warning once per client instance
|
|
1106
1152
|
_syntheticV3WarningFired = false;
|
|
@@ -1772,19 +1818,34 @@ class SingleAgentClient {
|
|
|
1772
1818
|
if (!needsDiscovery) {
|
|
1773
1819
|
return this.normalizedAgent;
|
|
1774
1820
|
}
|
|
1821
|
+
const effectiveEndpointKey = this.config.discoveryCache && !usesScopedFetch && this.discoveredEndpoint ? this.sharedDiscoveryKey(options) : void 0;
|
|
1822
|
+
if (this.config.discoveryCache && !usesScopedFetch && this.discoveredEndpoint && (this.discoveredEndpointKey !== effectiveEndpointKey || effectiveEndpointKey !== void 0 && this.discoveredEndpointEpoch !== this.sharedInvalidationEpoch(effectiveEndpointKey) || (this.discoveredEndpointExpiresAt ?? 0) <= Date.now())) {
|
|
1823
|
+
this.discoveredEndpoint = void 0;
|
|
1824
|
+
this.discoveredAgent = void 0;
|
|
1825
|
+
this.discoveredMcpEra = void 0;
|
|
1826
|
+
}
|
|
1775
1827
|
if (!usesScopedFetch && this.discoveredAgent) {
|
|
1776
1828
|
return this.discoveredAgent;
|
|
1777
1829
|
}
|
|
1830
|
+
if (!usesScopedFetch && !this.discoveredEndpoint && this.config.discoveryCache) {
|
|
1831
|
+
await this.adoptSharedEndpoint(options);
|
|
1832
|
+
if (this.discoveredEndpoint && (this.discoveredEndpointEpoch !== this.sharedInvalidationEpoch(this.sharedDiscoveryKey(options)) || (this.discoveredEndpointExpiresAt ?? 0) <= Date.now())) {
|
|
1833
|
+
this.discoveredEndpoint = void 0;
|
|
1834
|
+
this.discoveredAgent = void 0;
|
|
1835
|
+
this.discoveredMcpEra = void 0;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1778
1838
|
if (!usesScopedFetch && this.discoveredEndpoint) {
|
|
1779
|
-
|
|
1839
|
+
const adoptedAgent = {
|
|
1780
1840
|
...this.normalizedAgent,
|
|
1781
1841
|
agent_uri: this.discoveredEndpoint
|
|
1782
1842
|
};
|
|
1843
|
+
this.discoveredAgent = adoptedAgent;
|
|
1783
1844
|
if (this.normalizedAgent.oauth_tokens && !this.normalizedAgent.oauth_client_credentials) {
|
|
1784
1845
|
const { shareNonInteractiveOAuthProvider } = await import("../auth/oauth/provider-cache.mjs");
|
|
1785
|
-
shareNonInteractiveOAuthProvider(this.normalizedAgent,
|
|
1846
|
+
shareNonInteractiveOAuthProvider(this.normalizedAgent, adoptedAgent);
|
|
1786
1847
|
}
|
|
1787
|
-
return
|
|
1848
|
+
return adoptedAgent;
|
|
1788
1849
|
}
|
|
1789
1850
|
if (this.normalizedAgent.oauth_client_credentials) {
|
|
1790
1851
|
const { ensureClientCredentialsTokens, getAgentStorage } = await import("../auth/oauth/index.mjs");
|
|
@@ -1795,26 +1856,59 @@ class SingleAgentClient {
|
|
|
1795
1856
|
signal: options?.signal
|
|
1796
1857
|
});
|
|
1797
1858
|
}
|
|
1859
|
+
const endpointEpoch = this.discoveryEpochSnapshot(options);
|
|
1798
1860
|
const discoveredEndpoint = await this.discoverMCPEndpoint(this.normalizedAgent.agent_uri, options);
|
|
1799
1861
|
if (usesScopedFetch) {
|
|
1800
|
-
const
|
|
1862
|
+
const discoveredAgent2 = { ...this.normalizedAgent, agent_uri: discoveredEndpoint };
|
|
1801
1863
|
if (this.normalizedAgent.oauth_tokens && !this.normalizedAgent.oauth_client_credentials) {
|
|
1802
1864
|
const { shareNonInteractiveOAuthProvider } = await import("../auth/oauth/provider-cache.mjs");
|
|
1803
|
-
shareNonInteractiveOAuthProvider(this.normalizedAgent,
|
|
1865
|
+
shareNonInteractiveOAuthProvider(this.normalizedAgent, discoveredAgent2);
|
|
1804
1866
|
}
|
|
1805
|
-
return
|
|
1867
|
+
return discoveredAgent2;
|
|
1868
|
+
}
|
|
1869
|
+
if (endpointEpoch && !this.discoveryEpochIs(endpointEpoch)) {
|
|
1870
|
+
return { ...this.normalizedAgent, agent_uri: discoveredEndpoint };
|
|
1806
1871
|
}
|
|
1807
1872
|
this.discoveredEndpoint = discoveredEndpoint;
|
|
1808
|
-
|
|
1809
|
-
|
|
1873
|
+
if (this.config.discoveryCache) {
|
|
1874
|
+
this.discoveredEndpointKey = this.sharedDiscoveryKey(options);
|
|
1875
|
+
this.discoveredEndpointEpoch = endpointEpoch?.shared;
|
|
1876
|
+
this.discoveredEndpointExpiresAt = Date.now() + resolveAgentDiscoveryTtlMs(this.config.discoveryCache.ttlMs);
|
|
1877
|
+
const stored = await this.writeSharedDiscovery(
|
|
1878
|
+
options,
|
|
1879
|
+
{
|
|
1880
|
+
endpoint: {
|
|
1881
|
+
agentUri: discoveredEndpoint,
|
|
1882
|
+
...this.discoveredMcpEra && { mcpEra: this.discoveredMcpEra }
|
|
1883
|
+
}
|
|
1884
|
+
},
|
|
1885
|
+
false,
|
|
1886
|
+
endpointEpoch
|
|
1887
|
+
);
|
|
1888
|
+
if (endpointEpoch && !this.discoveryEpochIs(endpointEpoch)) {
|
|
1889
|
+
if (this.discoveredEndpoint === discoveredEndpoint && this.discoveredEndpointEpoch === endpointEpoch.shared) {
|
|
1890
|
+
this.discoveredEndpoint = void 0;
|
|
1891
|
+
this.discoveredEndpointKey = void 0;
|
|
1892
|
+
this.discoveredEndpointEpoch = void 0;
|
|
1893
|
+
this.discoveredEndpointExpiresAt = void 0;
|
|
1894
|
+
if (this.discoveredAgent?.agent_uri === discoveredEndpoint) this.discoveredAgent = void 0;
|
|
1895
|
+
this.discoveredMcpEra = void 0;
|
|
1896
|
+
}
|
|
1897
|
+
return { ...this.normalizedAgent, agent_uri: discoveredEndpoint };
|
|
1898
|
+
}
|
|
1899
|
+
if (stored && this.discoveredEndpoint === discoveredEndpoint) this.discoveredEndpointExpiresAt = stored.expiresAt;
|
|
1900
|
+
}
|
|
1901
|
+
this.canonicalBaseUrl = this.computeBaseUrl(discoveredEndpoint);
|
|
1902
|
+
const discoveredAgent = {
|
|
1810
1903
|
...this.normalizedAgent,
|
|
1811
|
-
agent_uri:
|
|
1904
|
+
agent_uri: discoveredEndpoint
|
|
1812
1905
|
};
|
|
1906
|
+
this.discoveredAgent = discoveredAgent;
|
|
1813
1907
|
if (this.normalizedAgent.oauth_tokens && !this.normalizedAgent.oauth_client_credentials) {
|
|
1814
1908
|
const { shareNonInteractiveOAuthProvider } = await import("../auth/oauth/provider-cache.mjs");
|
|
1815
|
-
shareNonInteractiveOAuthProvider(this.normalizedAgent,
|
|
1909
|
+
shareNonInteractiveOAuthProvider(this.normalizedAgent, discoveredAgent);
|
|
1816
1910
|
}
|
|
1817
|
-
return
|
|
1911
|
+
return discoveredAgent;
|
|
1818
1912
|
}
|
|
1819
1913
|
/**
|
|
1820
1914
|
* Ensure A2A canonical URL is resolved (lazy initialization)
|
|
@@ -3284,6 +3378,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
3284
3378
|
)
|
|
3285
3379
|
);
|
|
3286
3380
|
throwIfAborted(effectiveOptions?.signal);
|
|
3381
|
+
if (!result.success) await this.invalidateDiscoveryOnCapabilityError(result.adcpError?.code, effectiveOptions);
|
|
3287
3382
|
const postAdapterLogs = [...inputSchemaStripLogs, ...v25DriftLogs];
|
|
3288
3383
|
if (postAdapterLogs.length > 0) {
|
|
3289
3384
|
result.debug_logs = [...result.debug_logs ?? [], ...postAdapterLogs];
|
|
@@ -5792,6 +5887,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
5792
5887
|
beforeDispatch,
|
|
5793
5888
|
deferredClientContext
|
|
5794
5889
|
);
|
|
5890
|
+
if (!result.success) await this.invalidateDiscoveryOnCapabilityError(result.adcpError?.code, effectiveOptions);
|
|
5795
5891
|
const postAdapterLogs = [...inputSchemaStripLogs, ...v25DriftLogs];
|
|
5796
5892
|
if (postAdapterLogs.length > 0) {
|
|
5797
5893
|
result.debug_logs = [...result.debug_logs ?? [], ...postAdapterLogs];
|
|
@@ -6357,6 +6453,26 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6357
6453
|
tools
|
|
6358
6454
|
};
|
|
6359
6455
|
}
|
|
6456
|
+
const { withScopedLegacyConnection } = await import("../protocols/mcp.mjs");
|
|
6457
|
+
const scopedList = await withScopedLegacyConnection(
|
|
6458
|
+
connectOptions,
|
|
6459
|
+
"tools/list",
|
|
6460
|
+
(client) => withResponseSizeLimit(maxResponseBytes, () => client.listTools(mcpListParams, mcpRequestOptions))
|
|
6461
|
+
);
|
|
6462
|
+
if (scopedList) {
|
|
6463
|
+
return {
|
|
6464
|
+
name: this.normalizedAgent.name,
|
|
6465
|
+
description: void 0,
|
|
6466
|
+
protocol: this.normalizedAgent.protocol,
|
|
6467
|
+
url: agent.agent_uri,
|
|
6468
|
+
tools: scopedList.value.tools.map((tool) => ({
|
|
6469
|
+
name: tool.name,
|
|
6470
|
+
description: tool.description,
|
|
6471
|
+
inputSchema: tool.inputSchema,
|
|
6472
|
+
parameters: tool.inputSchema?.properties ? Object.keys(tool.inputSchema.properties) : []
|
|
6473
|
+
}))
|
|
6474
|
+
};
|
|
6475
|
+
}
|
|
6360
6476
|
const { client: mcpClient } = await connectMCP(connectOptions);
|
|
6361
6477
|
try {
|
|
6362
6478
|
const toolsList = await withResponseSizeLimit(
|
|
@@ -6470,12 +6586,98 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6470
6586
|
this.cachedCapabilities = void 0;
|
|
6471
6587
|
this.cachedToolSchemas = void 0;
|
|
6472
6588
|
this.primedCapabilitiesExpiresAt = void 0;
|
|
6589
|
+
this.capabilitiesFromSharedCache = false;
|
|
6590
|
+
}
|
|
6591
|
+
const effectiveSharedKey = this.config.discoveryCache && !usesScopedFetch && this.cachedCapabilities ? this.sharedDiscoveryKey(options) : void 0;
|
|
6592
|
+
if (this.config.discoveryCache && !usesScopedFetch && this.cachedCapabilities && (this.sharedEvidenceKey !== effectiveSharedKey || effectiveSharedKey !== void 0 && this.sharedEvidenceEpoch !== void 0 && this.sharedEvidenceEpoch !== this.sharedInvalidationEpoch(effectiveSharedKey))) {
|
|
6593
|
+
this.cachedCapabilities = void 0;
|
|
6594
|
+
this.cachedToolSchemas = void 0;
|
|
6595
|
+
this.primedCapabilitiesExpiresAt = void 0;
|
|
6596
|
+
this.capabilitiesFromSharedCache = false;
|
|
6473
6597
|
}
|
|
6474
6598
|
if (!usesScopedFetch && this.cachedCapabilities) {
|
|
6475
6599
|
if (discoveryContext) discoveryContext.toolSchemas = this.cachedToolSchemas;
|
|
6476
6600
|
this.maybeWarnV2Sunset(this.cachedCapabilities);
|
|
6477
6601
|
return this.cachedCapabilities;
|
|
6478
6602
|
}
|
|
6603
|
+
if (!usesScopedFetch && this.config.discoveryCache) {
|
|
6604
|
+
return this.discoverCapabilitiesThroughSharedCache(options, usesScopedFetch, discoveryContext);
|
|
6605
|
+
}
|
|
6606
|
+
return this.discoverCapabilitiesLive(options, usesScopedFetch, discoveryContext);
|
|
6607
|
+
}
|
|
6608
|
+
/**
|
|
6609
|
+
* Serve capabilities from the shared `discoveryCache`, or discover them live
|
|
6610
|
+
* as the single in-process "leader" for this cache key. Concurrent clients
|
|
6611
|
+
* for the same key wait for the leader under their *own* signal and deadline;
|
|
6612
|
+
* a leader that fails or is cancelled never fails its followers, which then
|
|
6613
|
+
* discover for themselves.
|
|
6614
|
+
*/
|
|
6615
|
+
async discoverCapabilitiesThroughSharedCache(options, usesScopedFetch, discoveryContext) {
|
|
6616
|
+
const config = this.config.discoveryCache;
|
|
6617
|
+
const key = this.sharedDiscoveryKey(options);
|
|
6618
|
+
if (!key) return this.discoverCapabilitiesLive(options, usesScopedFetch, discoveryContext);
|
|
6619
|
+
let flights = sharedDiscoveryFlights.get(config.cache);
|
|
6620
|
+
if (!flights) {
|
|
6621
|
+
flights = /* @__PURE__ */ new Map();
|
|
6622
|
+
sharedDiscoveryFlights.set(config.cache, flights);
|
|
6623
|
+
}
|
|
6624
|
+
for (let attempt = 0; ; attempt++) {
|
|
6625
|
+
const readEpoch = this.discoveryEpochSnapshot(options);
|
|
6626
|
+
const entry = await this.readSharedDiscovery(options);
|
|
6627
|
+
const installed = entry && (!readEpoch || this.discoveryEpochIs(readEpoch)) && this.installSharedCapabilities(entry, key, discoveryContext);
|
|
6628
|
+
if (installed) return installed;
|
|
6629
|
+
const flight = flights.get(key);
|
|
6630
|
+
if (flight && attempt < MAX_SHARED_DISCOVERY_WAITS) {
|
|
6631
|
+
const requestTimeoutMs = resolveRequestTimeoutMs(
|
|
6632
|
+
options?.transport?.requestTimeoutMs ?? this.config.transport?.requestTimeoutMs
|
|
6633
|
+
);
|
|
6634
|
+
await withAbortSignal([options?.signal], requestTimeoutMs, () => flight);
|
|
6635
|
+
continue;
|
|
6636
|
+
}
|
|
6637
|
+
let finish;
|
|
6638
|
+
const mine = new Promise((resolve) => finish = resolve);
|
|
6639
|
+
const ownsFlight = !flights.has(key);
|
|
6640
|
+
if (ownsFlight) flights.set(key, mine);
|
|
6641
|
+
try {
|
|
6642
|
+
return await this.discoverCapabilitiesLive(options, usesScopedFetch, discoveryContext);
|
|
6643
|
+
} finally {
|
|
6644
|
+
if (ownsFlight) {
|
|
6645
|
+
flights.delete(key);
|
|
6646
|
+
}
|
|
6647
|
+
finish();
|
|
6648
|
+
}
|
|
6649
|
+
}
|
|
6650
|
+
}
|
|
6651
|
+
/** Install capabilities from a shared entry; `undefined` when the entry is not usable evidence. */
|
|
6652
|
+
installSharedCapabilities(entry, key, discoveryContext) {
|
|
6653
|
+
const capabilities = entry.capabilities;
|
|
6654
|
+
if (!isCapabilityEvidence(capabilities) || capabilities._synthetic) return void 0;
|
|
6655
|
+
const toolSchemas = entry.toolSchemas;
|
|
6656
|
+
const hasToolEvidence = toolSchemas !== void 0 || Array.isArray(capabilities.discoveredTools);
|
|
6657
|
+
if (!hasToolEvidence) return void 0;
|
|
6658
|
+
if (toolSchemas !== void 0 && !isToolSchemaRecord(toolSchemas)) {
|
|
6659
|
+
return void 0;
|
|
6660
|
+
}
|
|
6661
|
+
let installed;
|
|
6662
|
+
let schemas;
|
|
6663
|
+
try {
|
|
6664
|
+
installed = structuredClone(capabilities);
|
|
6665
|
+
schemas = toolSchemas ? new Map(Object.entries(toolSchemas).map(([tool, properties]) => [tool, structuredClone(properties)])) : void 0;
|
|
6666
|
+
} catch {
|
|
6667
|
+
return void 0;
|
|
6668
|
+
}
|
|
6669
|
+
this.cachedCapabilities = installed;
|
|
6670
|
+
this.cachedToolSchemas = schemas;
|
|
6671
|
+
this.primedCapabilitiesExpiresAt = entry.expiresAt;
|
|
6672
|
+
this.capabilitiesFromSharedCache = true;
|
|
6673
|
+
this.recordSharedEvidence(key);
|
|
6674
|
+
if (discoveryContext) discoveryContext.toolSchemas = this.cachedToolSchemas;
|
|
6675
|
+
this.maybeWarnV2Sunset(installed);
|
|
6676
|
+
return installed;
|
|
6677
|
+
}
|
|
6678
|
+
async discoverCapabilitiesLive(options, usesScopedFetch, discoveryContext) {
|
|
6679
|
+
const startedAtEpoch = this.discoveryEpochSnapshot(options);
|
|
6680
|
+
const superseded = () => startedAtEpoch !== void 0 && !this.discoveryEpochIs(startedAtEpoch);
|
|
6479
6681
|
const agentInfo = await this.getAgentInfo(options);
|
|
6480
6682
|
const tools = agentInfo.tools.map((t) => ({
|
|
6481
6683
|
name: t.name,
|
|
@@ -6485,7 +6687,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6485
6687
|
agentInfo.tools.filter((t) => t.inputSchema?.properties).map((t) => [t.name, t.inputSchema.properties])
|
|
6486
6688
|
);
|
|
6487
6689
|
if (discoveryContext) discoveryContext.toolSchemas = discoveredToolSchemas;
|
|
6488
|
-
if (!usesScopedFetch) this.cachedToolSchemas = discoveredToolSchemas;
|
|
6690
|
+
if (!usesScopedFetch && !superseded()) this.cachedToolSchemas = discoveredToolSchemas;
|
|
6489
6691
|
const advertisesCapabilitiesTool = tools.some((t) => t.name === "get_adcp_capabilities");
|
|
6490
6692
|
const officialA2ALifecycle = this.normalizedAgent.protocol === "a2a" && tools.some((tool) => ["tasks/get", "get_task_status", "list_tasks"].includes(tool.name));
|
|
6491
6693
|
const hasCapabilitiesTool = advertisesCapabilitiesTool || officialA2ALifecycle;
|
|
@@ -6517,7 +6719,31 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6517
6719
|
}
|
|
6518
6720
|
if (result.success && result.data) {
|
|
6519
6721
|
const capabilities2 = augmentCapabilitiesFromTools(parseCapabilitiesResponse(result.data), tools);
|
|
6520
|
-
if (!usesScopedFetch
|
|
6722
|
+
if (!usesScopedFetch && !superseded()) {
|
|
6723
|
+
this.cachedCapabilities = capabilities2;
|
|
6724
|
+
this.capabilitiesFromSharedCache = false;
|
|
6725
|
+
if (!capabilities2._synthetic && this.config.discoveryCache) {
|
|
6726
|
+
this.recordSharedEvidence(this.sharedDiscoveryKey(options));
|
|
6727
|
+
const stored = await this.writeSharedDiscovery(
|
|
6728
|
+
options,
|
|
6729
|
+
{
|
|
6730
|
+
capabilities: structuredClone(capabilities2),
|
|
6731
|
+
toolSchemas: Object.fromEntries(discoveredToolSchemas)
|
|
6732
|
+
},
|
|
6733
|
+
false,
|
|
6734
|
+
startedAtEpoch
|
|
6735
|
+
);
|
|
6736
|
+
if (superseded()) {
|
|
6737
|
+
if (this.cachedCapabilities === capabilities2) {
|
|
6738
|
+
this.cachedCapabilities = void 0;
|
|
6739
|
+
this.cachedToolSchemas = void 0;
|
|
6740
|
+
this.primedCapabilitiesExpiresAt = void 0;
|
|
6741
|
+
}
|
|
6742
|
+
} else {
|
|
6743
|
+
this.primedCapabilitiesExpiresAt = stored?.expiresAt ?? Date.now() + resolveAgentDiscoveryTtlMs(this.config.discoveryCache.ttlMs);
|
|
6744
|
+
}
|
|
6745
|
+
}
|
|
6746
|
+
}
|
|
6521
6747
|
this.maybeWarnV2Sunset(capabilities2);
|
|
6522
6748
|
return capabilities2;
|
|
6523
6749
|
}
|
|
@@ -6536,7 +6762,11 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6536
6762
|
majorVersions: parsed.majorVersions.includes(3) ? parsed.majorVersions : [3]
|
|
6537
6763
|
};
|
|
6538
6764
|
const capabilities2 = augmentCapabilitiesFromTools(v3Capabilities, tools);
|
|
6539
|
-
if (!usesScopedFetch
|
|
6765
|
+
if (!usesScopedFetch && !superseded()) {
|
|
6766
|
+
this.cachedCapabilities = capabilities2;
|
|
6767
|
+
this.capabilitiesFromSharedCache = false;
|
|
6768
|
+
this.recordSharedEvidence(this.sharedDiscoveryKey(options));
|
|
6769
|
+
}
|
|
6540
6770
|
this.maybeWarnV2Sunset(capabilities2);
|
|
6541
6771
|
return capabilities2;
|
|
6542
6772
|
}
|
|
@@ -6560,7 +6790,11 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6560
6790
|
}
|
|
6561
6791
|
if (advertisesCapabilitiesTool) {
|
|
6562
6792
|
const capabilities2 = augmentCapabilitiesFromTools(buildSyntheticV3Capabilities(tools), tools);
|
|
6563
|
-
if (!usesScopedFetch
|
|
6793
|
+
if (!usesScopedFetch && !superseded()) {
|
|
6794
|
+
this.cachedCapabilities = capabilities2;
|
|
6795
|
+
this.capabilitiesFromSharedCache = false;
|
|
6796
|
+
this.recordSharedEvidence(this.sharedDiscoveryKey(options));
|
|
6797
|
+
}
|
|
6564
6798
|
this.maybeWarnV2Sunset(capabilities2);
|
|
6565
6799
|
return capabilities2;
|
|
6566
6800
|
}
|
|
@@ -6569,7 +6803,11 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6569
6803
|
`[AdCP] Agent "${this.agent.id}" detected as v2 (no get_adcp_capabilities tool). Tools: [${tools.map((t) => t.name).join(", ")}]`
|
|
6570
6804
|
);
|
|
6571
6805
|
const capabilities = buildSyntheticCapabilities(tools);
|
|
6572
|
-
if (!usesScopedFetch
|
|
6806
|
+
if (!usesScopedFetch && !superseded()) {
|
|
6807
|
+
this.cachedCapabilities = capabilities;
|
|
6808
|
+
this.capabilitiesFromSharedCache = false;
|
|
6809
|
+
this.recordSharedEvidence(this.sharedDiscoveryKey(options));
|
|
6810
|
+
}
|
|
6573
6811
|
return capabilities;
|
|
6574
6812
|
}
|
|
6575
6813
|
/**
|
|
@@ -6640,6 +6878,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6640
6878
|
this.cachedCapabilities = void 0;
|
|
6641
6879
|
this.cachedToolSchemas = void 0;
|
|
6642
6880
|
this.primedCapabilitiesExpiresAt = void 0;
|
|
6881
|
+
this.capabilitiesFromSharedCache = false;
|
|
6643
6882
|
return false;
|
|
6644
6883
|
}
|
|
6645
6884
|
try {
|
|
@@ -6648,6 +6887,8 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6648
6887
|
].map((name) => ({ name }));
|
|
6649
6888
|
this.cachedCapabilities = augmentCapabilitiesFromTools(structuredClone(capabilities), observedTools);
|
|
6650
6889
|
this.primedCapabilitiesExpiresAt = expiresAt;
|
|
6890
|
+
this.capabilitiesFromSharedCache = false;
|
|
6891
|
+
this.recordSharedEvidence(this.sharedDiscoveryKey(), true);
|
|
6651
6892
|
this.cachedToolSchemas = snapshot.toolSchemas ? new Map(
|
|
6652
6893
|
Object.entries(snapshot.toolSchemas).map(([tool, properties]) => [
|
|
6653
6894
|
tool,
|
|
@@ -6662,6 +6903,396 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6662
6903
|
return false;
|
|
6663
6904
|
}
|
|
6664
6905
|
}
|
|
6906
|
+
/** Which kind of credential this client presents (never its value), so an unauthenticated client cannot adopt an authenticated one's evidence. */
|
|
6907
|
+
discoveryCredentialKind() {
|
|
6908
|
+
const agent = this.normalizedAgent;
|
|
6909
|
+
if (agent.oauth_client_credentials) return "oauth-client-credentials";
|
|
6910
|
+
if (agent.oauth_tokens) return "oauth";
|
|
6911
|
+
if (agent.auth_token) return "bearer";
|
|
6912
|
+
const headerNames = Object.keys(agent.headers ?? {}).map((name) => name.toLowerCase());
|
|
6913
|
+
return headerNames.includes("authorization") || headerNames.includes("x-adcp-auth") ? "header" : "none";
|
|
6914
|
+
}
|
|
6915
|
+
/** Epoch of this client's evidence for the call's effective shared key; `undefined` without a shared cache. */
|
|
6916
|
+
discoveryEpochSnapshot(options) {
|
|
6917
|
+
const key = this.sharedDiscoveryKey(options);
|
|
6918
|
+
if (!key || !this.config.discoveryCache) return void 0;
|
|
6919
|
+
return { local: this.discoveryEpoch, shared: this.sharedInvalidationEpoch(key), key };
|
|
6920
|
+
}
|
|
6921
|
+
discoveryEpochIs(epoch) {
|
|
6922
|
+
return epoch.local === this.discoveryEpoch && epoch.shared === this.sharedInvalidationEpoch(epoch.key);
|
|
6923
|
+
}
|
|
6924
|
+
/** The latest invalidation that applies to `key`: its own, combined with the cache's floor of forgotten ones. */
|
|
6925
|
+
sharedInvalidationRecord(key) {
|
|
6926
|
+
const cache = this.config.discoveryCache.cache;
|
|
6927
|
+
const record = sharedInvalidations.get(cache)?.get(key);
|
|
6928
|
+
const floor = sharedInvalidationFloors.get(cache);
|
|
6929
|
+
if (!floor) return record;
|
|
6930
|
+
if (!record) return floor;
|
|
6931
|
+
return { epoch: Math.max(record.epoch, floor.epoch), at: Math.max(record.at, floor.at) };
|
|
6932
|
+
}
|
|
6933
|
+
sharedInvalidationEpoch(key) {
|
|
6934
|
+
return this.sharedInvalidationRecord(key)?.epoch ?? 0;
|
|
6935
|
+
}
|
|
6936
|
+
bumpSharedInvalidationEpoch(key) {
|
|
6937
|
+
const cache = this.config.discoveryCache.cache;
|
|
6938
|
+
let records = sharedInvalidations.get(cache);
|
|
6939
|
+
if (!records) {
|
|
6940
|
+
records = /* @__PURE__ */ new Map();
|
|
6941
|
+
sharedInvalidations.set(cache, records);
|
|
6942
|
+
}
|
|
6943
|
+
const now = Date.now();
|
|
6944
|
+
records.delete(key);
|
|
6945
|
+
records.set(key, { epoch: ++sharedInvalidationCounter, at: now });
|
|
6946
|
+
for (const [oldKey, record] of records) {
|
|
6947
|
+
if (records.size <= MAX_SHARED_INVALIDATION_RECORDS && record.at + MAX_AGENT_DISCOVERY_TTL_MS > now) break;
|
|
6948
|
+
const floor = sharedInvalidationFloors.get(cache);
|
|
6949
|
+
sharedInvalidationFloors.set(cache, {
|
|
6950
|
+
epoch: Math.max(record.epoch, floor?.epoch ?? 0),
|
|
6951
|
+
at: Math.max(record.at, floor?.at ?? 0)
|
|
6952
|
+
});
|
|
6953
|
+
records.delete(oldKey);
|
|
6954
|
+
}
|
|
6955
|
+
}
|
|
6956
|
+
/**
|
|
6957
|
+
* Remember which key the local capability evidence belongs to and, unless it
|
|
6958
|
+
* is application-owned (`primeCapabilities`), the shared invalidation epoch it
|
|
6959
|
+
* was obtained under. Application-owned evidence keeps its caller-declared
|
|
6960
|
+
* freshness and is not discarded by another instance's invalidation.
|
|
6961
|
+
*/
|
|
6962
|
+
recordSharedEvidence(key, applicationOwned = false) {
|
|
6963
|
+
this.sharedEvidenceKey = key;
|
|
6964
|
+
this.sharedEvidenceEpoch = key && this.config.discoveryCache && !applicationOwned ? this.sharedInvalidationEpoch(key) : void 0;
|
|
6965
|
+
}
|
|
6966
|
+
/** Non-secret fingerprint of the request-signing identity, so keys never mix signing identities. */
|
|
6967
|
+
discoverySigningFingerprint() {
|
|
6968
|
+
const signing = this.normalizedAgent.request_signing;
|
|
6969
|
+
if (!signing) return void 0;
|
|
6970
|
+
const provider = signing.kind === "provider" ? signing.provider : void 0;
|
|
6971
|
+
return {
|
|
6972
|
+
kind: signing.kind ?? "inline",
|
|
6973
|
+
agentUrl: signing.agent_url ?? null,
|
|
6974
|
+
kid: (provider ? provider.keyid : signing.kid) ?? null,
|
|
6975
|
+
alg: (provider ? provider.algorithm : signing.alg) ?? null,
|
|
6976
|
+
alwaysSign: signing.always_sign ?? null,
|
|
6977
|
+
signSupported: signing.sign_supported ?? null
|
|
6978
|
+
};
|
|
6979
|
+
}
|
|
6980
|
+
/**
|
|
6981
|
+
* Shared-discovery key for the effective transport policy of this call, or
|
|
6982
|
+
* `undefined` when sharing is off or the call uses a scoped `trustedFetchFn`
|
|
6983
|
+
* (whose network boundary is per-call, so its evidence is never shared).
|
|
6984
|
+
*/
|
|
6985
|
+
sharedDiscoveryKey(options) {
|
|
6986
|
+
const config = this.config.discoveryCache;
|
|
6987
|
+
if (!config) return void 0;
|
|
6988
|
+
const transport = normalizeTransportOptions(options?.transport ?? this.config.transport);
|
|
6989
|
+
if (transport?.trustedFetchFn !== void 0) return void 0;
|
|
6990
|
+
return agentDiscoveryCacheKey({
|
|
6991
|
+
protocol: this.normalizedAgent.protocol,
|
|
6992
|
+
agentUri: this.normalizedAgent.agent_uri,
|
|
6993
|
+
adcpVersion: this.resolvedAdcpVersion,
|
|
6994
|
+
wireAdcpVersion: this.config.wireAdcpVersion,
|
|
6995
|
+
authIdentity: config.authIdentity,
|
|
6996
|
+
credentialKind: this.discoveryCredentialKind(),
|
|
6997
|
+
headers: withAmbientIdentityHeaders(this.normalizedAgent.headers),
|
|
6998
|
+
signing: this.discoverySigningFingerprint(),
|
|
6999
|
+
allowPrivateIp: transport?.allowPrivateIp,
|
|
7000
|
+
maxResponseBytes: transport?.maxResponseBytes
|
|
7001
|
+
});
|
|
7002
|
+
}
|
|
7003
|
+
/**
|
|
7004
|
+
* Run a shared-cache operation under the caller's signal and a fixed backend
|
|
7005
|
+
* deadline, so an unresponsive store can neither make a call uncancellable nor
|
|
7006
|
+
* stall it beyond {@link SHARED_DISCOVERY_CACHE_TIMEOUT_MS}. Rejects on the
|
|
7007
|
+
* caller's abort or when the backend fails or times out; callers decide which
|
|
7008
|
+
* of those is a miss.
|
|
7009
|
+
*/
|
|
7010
|
+
boundedCacheOperation(operation, signal) {
|
|
7011
|
+
return withAbortSignal([signal], SHARED_DISCOVERY_CACHE_TIMEOUT_MS, () => Promise.resolve().then(operation));
|
|
7012
|
+
}
|
|
7013
|
+
/**
|
|
7014
|
+
* Run one backend mutation (or read-modify-write) for a shared key strictly
|
|
7015
|
+
* after every earlier mutation of that key in this process has actually
|
|
7016
|
+
* settled. The caller's wait is still bounded: an unresponsive backend makes
|
|
7017
|
+
* the caller see a miss, but later mutations stay queued behind the hung
|
|
7018
|
+
* operation, so an old callback can never land after a newer one.
|
|
7019
|
+
*
|
|
7020
|
+
* The queue is bounded. A `droppable` mutation (a write or cleanup) is
|
|
7021
|
+
* abandoned when its caller stopped waiting before it started, and is refused
|
|
7022
|
+
* when the key's queue is full; refusal is a failed write, never a call
|
|
7023
|
+
* failure. A `delete` is never refused for a known key: while one is queued
|
|
7024
|
+
* and not yet started, a newer invalidation joins it. Only for a new key beyond
|
|
7025
|
+
* the key cap is a delete refused — the invalidation tombstone still hides the
|
|
7026
|
+
* entry from this process's readers.
|
|
7027
|
+
*/
|
|
7028
|
+
serializedCacheMutation(key, operation, signal, kind = "droppable") {
|
|
7029
|
+
const cache = this.config.discoveryCache.cache;
|
|
7030
|
+
let chains = sharedMutationChains.get(cache);
|
|
7031
|
+
if (!chains) {
|
|
7032
|
+
chains = /* @__PURE__ */ new Map();
|
|
7033
|
+
sharedMutationChains.set(cache, chains);
|
|
7034
|
+
}
|
|
7035
|
+
let queue = chains.get(key);
|
|
7036
|
+
if (!queue && chains.size >= MAX_SHARED_MUTATION_KEYS) {
|
|
7037
|
+
return Promise.reject(new Error("Shared discovery cache mutation queue is full."));
|
|
7038
|
+
}
|
|
7039
|
+
if (kind === "delete" && queue?.waitingDelete && !queue.waitingDelete.started) {
|
|
7040
|
+
const joined = queue.waitingDelete.promise;
|
|
7041
|
+
return this.boundedCacheOperation(() => joined, signal);
|
|
7042
|
+
}
|
|
7043
|
+
if (kind === "droppable" && queue && queue.pending >= MAX_PENDING_SHARED_MUTATIONS_PER_KEY) {
|
|
7044
|
+
return Promise.reject(new Error("Shared discovery cache mutation queue is full."));
|
|
7045
|
+
}
|
|
7046
|
+
if (!queue) {
|
|
7047
|
+
queue = { tail: Promise.resolve(), pending: 0 };
|
|
7048
|
+
chains.set(key, queue);
|
|
7049
|
+
}
|
|
7050
|
+
const current = queue;
|
|
7051
|
+
const state = { abandoned: false, started: false };
|
|
7052
|
+
const run = current.tail.then(() => {
|
|
7053
|
+
if (kind === "droppable" && state.abandoned) throw new Error("Shared discovery cache mutation abandoned.");
|
|
7054
|
+
state.started = true;
|
|
7055
|
+
if (current.waitingDelete?.promise === run) current.waitingDelete.started = true;
|
|
7056
|
+
return operation();
|
|
7057
|
+
});
|
|
7058
|
+
current.pending++;
|
|
7059
|
+
if (kind === "delete") current.waitingDelete = { promise: run, started: false };
|
|
7060
|
+
const tail = run.then(
|
|
7061
|
+
() => void 0,
|
|
7062
|
+
() => void 0
|
|
7063
|
+
);
|
|
7064
|
+
current.tail = tail;
|
|
7065
|
+
void tail.then(() => {
|
|
7066
|
+
current.pending--;
|
|
7067
|
+
if (current.waitingDelete?.promise === run) current.waitingDelete = void 0;
|
|
7068
|
+
if (current.pending === 0) {
|
|
7069
|
+
if (chains.get(key) === current) chains.delete(key);
|
|
7070
|
+
if (chains.size === 0 && sharedMutationChains.get(cache) === chains) sharedMutationChains.delete(cache);
|
|
7071
|
+
}
|
|
7072
|
+
});
|
|
7073
|
+
return this.boundedCacheOperation(() => run, signal).catch((error) => {
|
|
7074
|
+
state.abandoned = true;
|
|
7075
|
+
throw error;
|
|
7076
|
+
});
|
|
7077
|
+
}
|
|
7078
|
+
/** Is this stored entry structurally sound and not expired (before any caller-TTL cap)? */
|
|
7079
|
+
isLiveSharedEntry(entry, now) {
|
|
7080
|
+
return Number.isFinite(entry.expiresAt) && Number.isFinite(entry.observedAt) && entry.expiresAt >= entry.observedAt && entry.expiresAt > now;
|
|
7081
|
+
}
|
|
7082
|
+
/**
|
|
7083
|
+
* Read a fresh, structurally valid shared entry. A backend failure or
|
|
7084
|
+
* timeout is a miss, never a call failure; the caller's own abort propagates.
|
|
7085
|
+
*/
|
|
7086
|
+
async readSharedDiscovery(options) {
|
|
7087
|
+
const key = this.sharedDiscoveryKey(options);
|
|
7088
|
+
if (!key) return void 0;
|
|
7089
|
+
const cache = this.config.discoveryCache.cache;
|
|
7090
|
+
const epoch = this.discoveryEpochSnapshot(options);
|
|
7091
|
+
try {
|
|
7092
|
+
const entry = await this.boundedCacheOperation(() => cache.get(key), options?.signal);
|
|
7093
|
+
if (epoch && !this.discoveryEpochIs(epoch)) return void 0;
|
|
7094
|
+
if (!entry) return void 0;
|
|
7095
|
+
const invalidation = this.sharedInvalidationRecord(key);
|
|
7096
|
+
if (invalidation && entry.observedAt <= invalidation.at) return void 0;
|
|
7097
|
+
const now = Date.now();
|
|
7098
|
+
if (!this.isLiveSharedEntry(entry, now)) {
|
|
7099
|
+
await this.serializedCacheMutation(
|
|
7100
|
+
key,
|
|
7101
|
+
async () => {
|
|
7102
|
+
const current = await cache.get(key);
|
|
7103
|
+
if (current && !this.isLiveSharedEntry(current, Date.now())) await cache.delete(key);
|
|
7104
|
+
},
|
|
7105
|
+
options?.signal
|
|
7106
|
+
);
|
|
7107
|
+
return void 0;
|
|
7108
|
+
}
|
|
7109
|
+
if (entry.observedAt > now + SHARED_DISCOVERY_CLOCK_SKEW_MS) return void 0;
|
|
7110
|
+
const callerExpiresAt = entry.observedAt + resolveAgentDiscoveryTtlMs(this.config.discoveryCache.ttlMs);
|
|
7111
|
+
if (!(callerExpiresAt > now)) return void 0;
|
|
7112
|
+
return callerExpiresAt < entry.expiresAt ? { ...entry, expiresAt: callerExpiresAt } : entry;
|
|
7113
|
+
} catch {
|
|
7114
|
+
throwIfAborted(options?.signal);
|
|
7115
|
+
return void 0;
|
|
7116
|
+
}
|
|
7117
|
+
}
|
|
7118
|
+
/**
|
|
7119
|
+
* Merge fresh evidence into the shared entry; the merged entry never outlives
|
|
7120
|
+
* its oldest part. Returns the stored entry, or `undefined` when it was not
|
|
7121
|
+
* stored: automatic writes
|
|
7122
|
+
* ignore a `false` (a failed write only costs a later rediscovery), while
|
|
7123
|
+
* {@link primeDiscoveryCache} reports it.
|
|
7124
|
+
*/
|
|
7125
|
+
async writeSharedDiscovery(options, patch, replace = false, epoch = this.discoveryEpochSnapshot(options)) {
|
|
7126
|
+
const key = this.sharedDiscoveryKey(options);
|
|
7127
|
+
if (!key) return void 0;
|
|
7128
|
+
if (patch.endpoint && !this.isDiscoverableEndpoint(patch.endpoint.agentUri)) {
|
|
7129
|
+
patch = { capabilities: patch.capabilities, toolSchemas: patch.toolSchemas };
|
|
7130
|
+
if (!patch.capabilities && !patch.toolSchemas) return void 0;
|
|
7131
|
+
}
|
|
7132
|
+
const config = this.config.discoveryCache;
|
|
7133
|
+
const stale = () => epoch !== void 0 && !this.discoveryEpochIs(epoch);
|
|
7134
|
+
try {
|
|
7135
|
+
return await this.serializedCacheMutation(
|
|
7136
|
+
key,
|
|
7137
|
+
async () => {
|
|
7138
|
+
if (stale()) return void 0;
|
|
7139
|
+
const invalidation = this.sharedInvalidationRecord(key);
|
|
7140
|
+
const now = Math.max(Date.now(), (invalidation?.at ?? 0) + 1);
|
|
7141
|
+
let existing;
|
|
7142
|
+
if (!replace) {
|
|
7143
|
+
const raw = await config.cache.get(key);
|
|
7144
|
+
if (raw && this.isLiveSharedEntry(raw, now) && raw.observedAt <= now + SHARED_DISCOVERY_CLOCK_SKEW_MS && !(invalidation && raw.observedAt <= invalidation.at)) {
|
|
7145
|
+
const endpointObservedAt = raw.endpoint?.observedAt ?? raw.observedAt;
|
|
7146
|
+
existing = { ...raw };
|
|
7147
|
+
if (existing.endpoint && (!this.isDiscoverableEndpoint(existing.endpoint.agentUri) || invalidation && endpointObservedAt <= invalidation.at))
|
|
7148
|
+
delete existing.endpoint;
|
|
7149
|
+
}
|
|
7150
|
+
if (stale()) return void 0;
|
|
7151
|
+
}
|
|
7152
|
+
const restartsClock = !existing || patch.capabilities !== void 0;
|
|
7153
|
+
const ttl = resolveAgentDiscoveryTtlMs(config.ttlMs);
|
|
7154
|
+
const entry = {
|
|
7155
|
+
...existing?.capabilities && { capabilities: existing.capabilities },
|
|
7156
|
+
...existing?.toolSchemas && { toolSchemas: existing.toolSchemas },
|
|
7157
|
+
...existing?.endpoint && { endpoint: existing.endpoint },
|
|
7158
|
+
...patch.endpoint && { endpoint: { ...patch.endpoint, observedAt: now } },
|
|
7159
|
+
...patch.capabilities && { capabilities: patch.capabilities },
|
|
7160
|
+
...patch.toolSchemas && { toolSchemas: patch.toolSchemas },
|
|
7161
|
+
observedAt: restartsClock || !existing ? now : Math.min(existing.observedAt, now),
|
|
7162
|
+
expiresAt: restartsClock || !existing ? now + ttl : Math.min(existing.expiresAt, now + ttl)
|
|
7163
|
+
};
|
|
7164
|
+
await config.cache.set(key, structuredClone(entry));
|
|
7165
|
+
return stale() ? void 0 : entry;
|
|
7166
|
+
},
|
|
7167
|
+
options?.signal
|
|
7168
|
+
);
|
|
7169
|
+
} catch {
|
|
7170
|
+
return void 0;
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
/** Is `agentUri` a credential-free URL on the configured agent's origin (all discovery ever tries)? */
|
|
7174
|
+
isDiscoverableEndpoint(agentUri) {
|
|
7175
|
+
if (typeof agentUri !== "string") return false;
|
|
7176
|
+
try {
|
|
7177
|
+
const candidate = new URL(agentUri);
|
|
7178
|
+
return candidate.username === "" && candidate.password === "" && candidate.search === "" && candidate.hash === "" && candidate.origin === new URL(this.normalizedAgent.agent_uri).origin;
|
|
7179
|
+
} catch {
|
|
7180
|
+
return false;
|
|
7181
|
+
}
|
|
7182
|
+
}
|
|
7183
|
+
/** Adopt a previously discovered MCP endpoint instead of re-probing the agent. */
|
|
7184
|
+
async adoptSharedEndpoint(options) {
|
|
7185
|
+
const readEpoch = this.discoveryEpochSnapshot(options);
|
|
7186
|
+
const entry = await this.readSharedDiscovery(options);
|
|
7187
|
+
if (readEpoch && !this.discoveryEpochIs(readEpoch)) return;
|
|
7188
|
+
const endpoint = entry?.endpoint;
|
|
7189
|
+
if (!entry || !endpoint || !this.isDiscoverableEndpoint(endpoint.agentUri)) return;
|
|
7190
|
+
const observedAt = Number.isFinite(endpoint.observedAt) ? endpoint.observedAt : entry.observedAt;
|
|
7191
|
+
const sharedKey = this.sharedDiscoveryKey(options);
|
|
7192
|
+
const tombstone = sharedKey ? this.sharedInvalidationRecord(sharedKey) : void 0;
|
|
7193
|
+
if (tombstone && observedAt <= tombstone.at) return;
|
|
7194
|
+
const expiresAt = Math.min(
|
|
7195
|
+
entry.expiresAt,
|
|
7196
|
+
observedAt + resolveAgentDiscoveryTtlMs(this.config.discoveryCache.ttlMs)
|
|
7197
|
+
);
|
|
7198
|
+
if (!(expiresAt > Date.now()) || observedAt > Date.now() + SHARED_DISCOVERY_CLOCK_SKEW_MS) return;
|
|
7199
|
+
this.discoveredEndpoint = endpoint.agentUri;
|
|
7200
|
+
this.discoveredEndpointKey = this.sharedDiscoveryKey(options);
|
|
7201
|
+
this.discoveredEndpointEpoch = this.discoveredEndpointKey ? this.sharedInvalidationEpoch(this.discoveredEndpointKey) : void 0;
|
|
7202
|
+
this.discoveredEndpointExpiresAt = expiresAt;
|
|
7203
|
+
this.canonicalBaseUrl = this.computeBaseUrl(endpoint.agentUri);
|
|
7204
|
+
if (endpoint.mcpEra === "legacy" || endpoint.mcpEra === "modern") {
|
|
7205
|
+
this.discoveredMcpEra = endpoint.mcpEra;
|
|
7206
|
+
this.discoveredMcpEraAt = observedAt;
|
|
7207
|
+
}
|
|
7208
|
+
}
|
|
7209
|
+
/**
|
|
7210
|
+
* Drop discovery evidence for this client: its in-memory capability and
|
|
7211
|
+
* tool-schema caches, the discovered endpoint, and the shared
|
|
7212
|
+
* `discoveryCache` entry (when configured). The next call rediscovers.
|
|
7213
|
+
*
|
|
7214
|
+
* Call this when you learn the seller changed (a deploy, a version bump, a
|
|
7215
|
+
* capability-dependent error you observed). The SDK also invalidates by
|
|
7216
|
+
* itself when a feature check fails against shared evidence, and when a
|
|
7217
|
+
* seller answers with a version or capability error.
|
|
7218
|
+
*/
|
|
7219
|
+
async invalidateDiscoveryCache(options) {
|
|
7220
|
+
this.cachedCapabilities = void 0;
|
|
7221
|
+
this.cachedToolSchemas = void 0;
|
|
7222
|
+
this.primedCapabilitiesExpiresAt = void 0;
|
|
7223
|
+
this.capabilitiesFromSharedCache = false;
|
|
7224
|
+
this.capabilityEvidenceScopeKey = randomUUID();
|
|
7225
|
+
this.discoveryEpoch++;
|
|
7226
|
+
if (this.normalizedAgent._needsDiscovery) {
|
|
7227
|
+
this.discoveredEndpoint = void 0;
|
|
7228
|
+
this.discoveredAgent = void 0;
|
|
7229
|
+
this.discoveredMcpEra = void 0;
|
|
7230
|
+
this.discoveredEndpointKey = void 0;
|
|
7231
|
+
this.discoveredEndpointEpoch = void 0;
|
|
7232
|
+
this.discoveredEndpointExpiresAt = void 0;
|
|
7233
|
+
}
|
|
7234
|
+
const key = this.sharedDiscoveryKey(options);
|
|
7235
|
+
if (!key) return;
|
|
7236
|
+
this.bumpSharedInvalidationEpoch(key);
|
|
7237
|
+
try {
|
|
7238
|
+
const cache = this.config.discoveryCache.cache;
|
|
7239
|
+
await this.serializedCacheMutation(key, () => cache.delete(key), void 0, "delete");
|
|
7240
|
+
} catch {
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
7243
|
+
/**
|
|
7244
|
+
* Pre-seed the shared `discoveryCache` with capability evidence the caller
|
|
7245
|
+
* already holds (for example from a registry sync), so no client sharing
|
|
7246
|
+
* this identity probes the seller within the TTL. Unlike
|
|
7247
|
+
* {@link primeCapabilities} the seeded evidence outlives this instance.
|
|
7248
|
+
*
|
|
7249
|
+
* Returns `true` only once the backing cache has accepted the entry. Returns
|
|
7250
|
+
* `false` — having seeded nothing — when sharing is not configured, the
|
|
7251
|
+
* client uses a scoped `trustedFetchFn`, or the evidence is synthetic,
|
|
7252
|
+
* malformed or not cloneable. When the cache rejects the write or does not
|
|
7253
|
+
* answer within the backend deadline, the result is also `false`, but the
|
|
7254
|
+
* outcome is indeterminate: a write already handed to the backend may still
|
|
7255
|
+
* land, whereas one still queued behind earlier operations is dropped. Treat
|
|
7256
|
+
* `false` as "not confirmed", and seed again (or invalidate) rather than
|
|
7257
|
+
* assuming the cache is empty. Seed `endpoint` as well
|
|
7258
|
+
* to skip the endpoint probe on the first call; without it that probe still
|
|
7259
|
+
* runs once per cold client.
|
|
7260
|
+
*/
|
|
7261
|
+
async primeDiscoveryCache(seed) {
|
|
7262
|
+
const key = this.sharedDiscoveryKey();
|
|
7263
|
+
const capabilities = seed?.capabilities;
|
|
7264
|
+
if (!key || !isCapabilityEvidence(capabilities) || capabilities._synthetic) return false;
|
|
7265
|
+
const toolSchemas = seed.toolSchemas;
|
|
7266
|
+
if (toolSchemas !== void 0 && !isToolSchemaRecord(toolSchemas)) {
|
|
7267
|
+
return false;
|
|
7268
|
+
}
|
|
7269
|
+
if (toolSchemas === void 0 && !Array.isArray(capabilities.discoveredTools)) return false;
|
|
7270
|
+
if (seed.endpoint !== void 0 && !this.isDiscoverableEndpoint(seed.endpoint.agentUri)) return false;
|
|
7271
|
+
try {
|
|
7272
|
+
const stored = await this.writeSharedDiscovery(
|
|
7273
|
+
void 0,
|
|
7274
|
+
{
|
|
7275
|
+
capabilities: structuredClone(capabilities),
|
|
7276
|
+
...toolSchemas && { toolSchemas: structuredClone(toolSchemas) },
|
|
7277
|
+
...seed.endpoint && {
|
|
7278
|
+
endpoint: {
|
|
7279
|
+
agentUri: seed.endpoint.agentUri,
|
|
7280
|
+
...seed.endpoint.mcpEra && { mcpEra: seed.endpoint.mcpEra }
|
|
7281
|
+
}
|
|
7282
|
+
}
|
|
7283
|
+
},
|
|
7284
|
+
true
|
|
7285
|
+
);
|
|
7286
|
+
return stored !== void 0;
|
|
7287
|
+
} catch {
|
|
7288
|
+
return false;
|
|
7289
|
+
}
|
|
7290
|
+
}
|
|
7291
|
+
/** Drop shared evidence when a seller answers with an error that says our cached view of it is stale. */
|
|
7292
|
+
async invalidateDiscoveryOnCapabilityError(code, options) {
|
|
7293
|
+
if (!this.config.discoveryCache || !code || !CAPABILITY_STALE_ERROR_CODES.has(code)) return;
|
|
7294
|
+
await this.invalidateDiscoveryCache(options);
|
|
7295
|
+
}
|
|
6665
7296
|
/**
|
|
6666
7297
|
* Emit a one-time warning when the agent reports v2 capabilities.
|
|
6667
7298
|
*
|
|
@@ -6810,8 +7441,21 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6810
7441
|
* with an actionable error message.
|
|
6811
7442
|
*/
|
|
6812
7443
|
async require(...features) {
|
|
6813
|
-
|
|
6814
|
-
|
|
7444
|
+
await this.assertFeaturesSupported(features);
|
|
7445
|
+
}
|
|
7446
|
+
/**
|
|
7447
|
+
* Throw {@link FeatureUnsupportedError} when a feature is missing, deciding on
|
|
7448
|
+
* live capabilities when the refusal would rest on shared evidence that may
|
|
7449
|
+
* predate a seller upgrade.
|
|
7450
|
+
*/
|
|
7451
|
+
async assertFeaturesSupported(requiredFeatures, options) {
|
|
7452
|
+
let capabilities = await this.getCapabilities(options);
|
|
7453
|
+
let missing = requiredFeatures.filter((f) => !resolveFeature(capabilities, f));
|
|
7454
|
+
if (missing.length > 0 && this.capabilitiesFromSharedCache) {
|
|
7455
|
+
await this.invalidateDiscoveryCache(options);
|
|
7456
|
+
capabilities = await this.getCapabilities(options);
|
|
7457
|
+
missing = requiredFeatures.filter((f) => !resolveFeature(capabilities, f));
|
|
7458
|
+
}
|
|
6815
7459
|
if (missing.length > 0) {
|
|
6816
7460
|
throw new FeatureUnsupportedError(missing, listDeclaredFeatures(capabilities), this.agent.agent_uri);
|
|
6817
7461
|
}
|
|
@@ -6824,7 +7468,10 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6824
7468
|
this.cachedCapabilities = void 0;
|
|
6825
7469
|
this.cachedToolSchemas = void 0;
|
|
6826
7470
|
this.primedCapabilitiesExpiresAt = void 0;
|
|
7471
|
+
this.capabilitiesFromSharedCache = false;
|
|
6827
7472
|
this.capabilityEvidenceScopeKey = randomUUID();
|
|
7473
|
+
this.discoveryEpoch++;
|
|
7474
|
+
if (this.config.discoveryCache) await this.invalidateDiscoveryCache();
|
|
6828
7475
|
return this.getCapabilities();
|
|
6829
7476
|
}
|
|
6830
7477
|
/**
|
|
@@ -6837,11 +7484,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
|
|
|
6837
7484
|
if (this.config.validateFeatures === false) return;
|
|
6838
7485
|
const requiredFeatures = TASK_FEATURE_MAP[taskName];
|
|
6839
7486
|
if (!requiredFeatures || requiredFeatures.length === 0) return;
|
|
6840
|
-
|
|
6841
|
-
const missing = requiredFeatures.filter((f) => !resolveFeature(capabilities, f));
|
|
6842
|
-
if (missing.length > 0) {
|
|
6843
|
-
throw new FeatureUnsupportedError(missing, listDeclaredFeatures(capabilities), this.agent.agent_uri);
|
|
6844
|
-
}
|
|
7487
|
+
await this.assertFeaturesSupported(requiredFeatures, options);
|
|
6845
7488
|
}
|
|
6846
7489
|
/**
|
|
6847
7490
|
* Fail version-incompatible request shapes before schema validation.
|