@adcp/sdk 16.3.0 → 16.4.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.
Files changed (80) hide show
  1. package/dist/lib/adapters/version/3.1/assignments.d.mts +12 -0
  2. package/dist/lib/adapters/version/3.1/assignments.d.ts +12 -0
  3. package/dist/lib/adapters/version/3.1/assignments.js +67 -0
  4. package/dist/lib/adapters/version/3.1/assignments.mjs +43 -0
  5. package/dist/lib/adapters/version/3.1/bidding.d.mts +3 -0
  6. package/dist/lib/adapters/version/3.1/bidding.d.ts +3 -0
  7. package/dist/lib/adapters/version/3.1/bidding.js +115 -0
  8. package/dist/lib/adapters/version/3.1/bidding.mjs +90 -0
  9. package/dist/lib/adapters/version/3.1/delivery.d.mts +7 -0
  10. package/dist/lib/adapters/version/3.1/delivery.d.ts +7 -0
  11. package/dist/lib/adapters/version/3.1/delivery.js +78 -0
  12. package/dist/lib/adapters/version/3.1/delivery.mjs +54 -0
  13. package/dist/lib/adapters/version/3.1/index.d.mts +3 -0
  14. package/dist/lib/adapters/version/3.1/index.d.ts +3 -0
  15. package/dist/lib/adapters/version/3.1/index.js +36 -0
  16. package/dist/lib/adapters/version/3.1/index.mjs +12 -0
  17. package/dist/lib/adapters/version/index.d.mts +59 -7
  18. package/dist/lib/adapters/version/index.d.ts +59 -7
  19. package/dist/lib/adapters/version/index.js +51 -2
  20. package/dist/lib/adapters/version/index.mjs +49 -1
  21. package/dist/lib/adapters/version/negotiate.d.mts +71 -0
  22. package/dist/lib/adapters/version/negotiate.d.ts +71 -0
  23. package/dist/lib/adapters/version/negotiate.js +101 -0
  24. package/dist/lib/adapters/version/negotiate.mjs +73 -0
  25. package/dist/lib/adapters/version/schema-projection.d.mts +33 -0
  26. package/dist/lib/adapters/version/schema-projection.d.ts +33 -0
  27. package/dist/lib/adapters/version/schema-projection.js +294 -0
  28. package/dist/lib/adapters/version/schema-projection.mjs +269 -0
  29. package/dist/lib/adapters/version/translate.d.mts +122 -0
  30. package/dist/lib/adapters/version/translate.d.ts +122 -0
  31. package/dist/lib/adapters/version/translate.js +169 -0
  32. package/dist/lib/adapters/version/translate.mjs +141 -0
  33. package/dist/lib/adapters/version/types.d.mts +51 -10
  34. package/dist/lib/adapters/version/types.d.ts +51 -10
  35. package/dist/lib/core/AgentClient.d.mts +1 -1
  36. package/dist/lib/core/AgentClient.d.ts +1 -1
  37. package/dist/lib/core/ConversationTypes.d.mts +7 -0
  38. package/dist/lib/core/ConversationTypes.d.ts +7 -0
  39. package/dist/lib/core/SingleAgentClient.d.mts +82 -0
  40. package/dist/lib/core/SingleAgentClient.d.ts +82 -0
  41. package/dist/lib/core/SingleAgentClient.js +323 -15
  42. package/dist/lib/core/SingleAgentClient.mjs +337 -17
  43. package/dist/lib/core/TaskExecutor.d.mts +1 -1
  44. package/dist/lib/core/TaskExecutor.d.ts +1 -1
  45. package/dist/lib/core/TaskExecutor.js +97 -21
  46. package/dist/lib/core/TaskExecutor.mjs +97 -21
  47. package/dist/lib/core/account-reduction.d.mts +36 -0
  48. package/dist/lib/core/account-reduction.d.ts +36 -0
  49. package/dist/lib/core/account-reduction.js +127 -0
  50. package/dist/lib/core/account-reduction.mjs +98 -0
  51. package/dist/lib/core/buyer-account-registry.d.mts +52 -0
  52. package/dist/lib/core/buyer-account-registry.d.ts +52 -0
  53. package/dist/lib/core/buyer-account-registry.js +142 -6
  54. package/dist/lib/core/buyer-account-registry.mjs +142 -6
  55. package/dist/lib/media-buy/action-contracts.js +4 -4
  56. package/dist/lib/media-buy/action-contracts.mjs +4 -4
  57. package/dist/lib/media-buy/action-types.d.mts +3 -2
  58. package/dist/lib/media-buy/action-types.d.ts +3 -2
  59. package/dist/lib/media-buy/update-fields.generated.d.mts +3 -2
  60. package/dist/lib/media-buy/update-fields.generated.d.ts +3 -2
  61. package/dist/lib/registry/types.generated.d.mts +1 -1
  62. package/dist/lib/registry/types.generated.d.ts +1 -1
  63. package/dist/lib/server/create-adcp-server.d.mts +21 -0
  64. package/dist/lib/server/create-adcp-server.d.ts +21 -0
  65. package/dist/lib/server/create-adcp-server.js +97 -15
  66. package/dist/lib/server/create-adcp-server.mjs +83 -1
  67. package/dist/lib/server/index.d.mts +1 -1
  68. package/dist/lib/server/index.d.ts +1 -1
  69. package/dist/lib/storage/interfaces.d.mts +2 -0
  70. package/dist/lib/storage/interfaces.d.ts +2 -0
  71. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  72. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  73. package/dist/lib/version.d.mts +3 -3
  74. package/dist/lib/version.d.ts +3 -3
  75. package/dist/lib/version.js +3 -3
  76. package/dist/lib/version.mjs +3 -3
  77. package/docs/TYPE-SUMMARY.md +2 -2
  78. package/docs/llms.txt +3 -3
  79. package/docs/migration-14.x-rc-worksheet.md +4 -4
  80. package/package.json +1 -1
@@ -21,6 +21,7 @@ import {
21
21
  import { randomUUID, scryptSync } from "node:crypto";
22
22
  const SKIP_PRODUCT_CACHE = /* @__PURE__ */ Symbol("adcp.skipProductCache");
23
23
  const SKIP_ACCOUNT_REGISTRY_OBSERVATION = /* @__PURE__ */ Symbol("adcp.skipAccountRegistryObservation");
24
+ import { accountKeySites, naturalAccountKey, reducedFields, valueAt, withReducedKeyAt } from "./account-reduction.mjs";
24
25
  import { ADCP_ENVELOPE_FIELDS } from "../types/adcp.mjs";
25
26
  import { parseAdcpMajorVersion, toReleasePrecisionVersion } from "../version.mjs";
26
27
  import {
@@ -31,8 +32,20 @@ import {
31
32
  resolveAdcpVersion
32
33
  } from "../utils/adcp-version-config.mjs";
33
34
  import { beta6ReportingRequestIssue } from "../media-buy/reporting-version.mjs";
34
- import { getVersionAdapter, resolveAdapterKey } from "../adapters/version/index.mjs";
35
- import { isExternalSchemaRootActive, schemaAllowsTopLevelField } from "../validation/schema-loader.mjs";
35
+ import {
36
+ convertRequest,
37
+ convertResponse,
38
+ getVersionAdapter,
39
+ isNegotiationFailure,
40
+ negotiateAdcpRelease,
41
+ releaseDefinesTool,
42
+ resolveAdapterKey
43
+ } from "../adapters/version/index.mjs";
44
+ import {
45
+ isExternalSchemaRootActive,
46
+ schemaAllowsTopLevelField,
47
+ toReleasePrecisionWire
48
+ } from "../validation/schema-loader.mjs";
36
49
  import { generateIdempotencyKey, requestUsesIdempotency } from "../utils/idempotency.mjs";
37
50
  import { createA2AClientFromCardUrl } from "../protocols/a2a.mjs";
38
51
  import {
@@ -372,8 +385,9 @@ function creativeMatchesProductOption(creative, declaration) {
372
385
  function isDeferredClientFinalizationContext(value) {
373
386
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
374
387
  const context = value;
375
- return context.kind === "single-agent" && typeof context.taskType === "string" && typeof context.canonical === "boolean" && !!context.productPolicyRequest && typeof context.productPolicyRequest === "object" && !Array.isArray(context.productPolicyRequest) && (context.serverVersion === void 0 || context.serverVersion === "v2" || context.serverVersion === "v3") && (context.serverVersionSynthetic === void 0 || typeof context.serverVersionSynthetic === "boolean") && (context.projectionCatalogs === void 0 || Array.isArray(context.projectionCatalogs)) && (context.handlerName === void 0 || typeof context.handlerName === "string") && (context.skipStatusHandlers === void 0 || typeof context.skipStatusHandlers === "boolean");
388
+ return context.kind === "single-agent" && typeof context.taskType === "string" && typeof context.canonical === "boolean" && !!context.productPolicyRequest && typeof context.productPolicyRequest === "object" && !Array.isArray(context.productPolicyRequest) && (context.serverVersion === void 0 || context.serverVersion === "v2" || context.serverVersion === "v3") && (context.serverVersionSynthetic === void 0 || typeof context.serverVersionSynthetic === "boolean") && (context.negotiatedAdcpVersion === void 0 || typeof context.negotiatedAdcpVersion === "string") && (context.projectionCatalogs === void 0 || Array.isArray(context.projectionCatalogs)) && (context.handlerName === void 0 || typeof context.handlerName === "string") && (context.skipStatusHandlers === void 0 || typeof context.skipStatusHandlers === "boolean");
376
389
  }
390
+ const MAX_REMEMBERED_NEGOTIATED_RELEASES = 1024;
377
391
  const CANONICAL_PACKAGE_ROUTE_TASKS = /* @__PURE__ */ new Set(["create_media_buy", "update_media_buy", "get_media_buys"]);
378
392
  const canonicalCreativeRoutingSnapshots = /* @__PURE__ */ new WeakSet();
379
393
  function canonicalAccountRoutingSnapshot(value) {
@@ -919,6 +933,12 @@ class SingleAgentClient {
919
933
  this.resolvedAdcpVersion = resolveAdcpVersion(
920
934
  config.adcpVersion ?? (config.experimentalIdentity33 ? "3.3" : void 0)
921
935
  );
936
+ if (config.accountIdentityDowngrade === "reduce" && typeof config.accountStorage?.compareAndSet !== "function") {
937
+ throw new ConfigurationError(
938
+ "accountIdentityDowngrade: 'reduce' requires accountStorage with compareAndSet. Reduced account keys are claimed in that storage so that no two buyer accounts reach one seller account, across instances and restarts. Configure durable accountStorage that implements compareAndSet, or use 'refuse'.",
939
+ "accountIdentityDowngrade"
940
+ );
941
+ }
922
942
  this.webhookRegistrationStore = config.webhookRegistrationStore ?? new InMemoryWebhookRegistrationStore();
923
943
  this.webhookReplayStore = config.webhookVerification?.replayStore ?? new InMemoryReplayStore();
924
944
  this.webhookRevocationStore = config.webhookVerification?.revocationStore ?? new InMemoryRevocationStore();
@@ -1239,6 +1259,8 @@ class SingleAgentClient {
1239
1259
  runAgentInfo;
1240
1260
  cachedToolSchemas;
1241
1261
  // inputSchema.properties per tool name
1262
+ /** Release negotiated per operation, for converting webhook completions up. */
1263
+ negotiatedReleaseByOperation = /* @__PURE__ */ new Map();
1242
1264
  primedCapabilitiesExpiresAt;
1243
1265
  // Freshness bound for application-owned evidence only
1244
1266
  capabilityEvidenceScopeKey = randomUUID();
@@ -3048,7 +3070,11 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
3048
3070
  let externalStatus;
3049
3071
  let forgetPolicyState = false;
3050
3072
  try {
3051
- const canonicalResult = this.canonicalizeWebhookCreativeResult(metadata, parsed.result, canonicalCreativeTask);
3073
+ const canonicalResult = this.canonicalizeWebhookCreativeResult(
3074
+ metadata,
3075
+ this.convertWebhookResultToClientRelease(parsed),
3076
+ canonicalCreativeTask
3077
+ );
3052
3078
  const dedupResult = canonicalResult;
3053
3079
  const dedupMetadata = { ...metadata };
3054
3080
  let webhookResult = dedupResult;
@@ -3629,32 +3655,47 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
3629
3655
  };
3630
3656
  const serverVersion = await this.detectServerVersion(detectionOptions);
3631
3657
  throwIfAborted(options?.signal);
3658
+ const negotiatedRelease = this.negotiateCallRelease(capabilityDiscoveryContext.capabilities, options);
3659
+ this.assertToolAvailableAtRelease(taskType, negotiatedRelease, capabilityDiscoveryContext.toolSchemas);
3632
3660
  this.assertRequestSupportedByTargetVersion(
3633
3661
  taskType,
3634
3662
  normalizedParams,
3635
3663
  capabilityDiscoveryContext.capabilities,
3636
3664
  canonicalCreativeInvocation,
3637
- serverVersion
3665
+ serverVersion,
3666
+ negotiatedRelease
3638
3667
  );
3639
- const { options: effectiveOptions, driftLog: webhookDriftLog } = this.suppressPre31DiscoveryWebhook(
3668
+ const { options: webhookOptions, driftLog: webhookDriftLog } = this.suppressPre31DiscoveryWebhook(
3640
3669
  taskType,
3641
3670
  options,
3642
3671
  capabilityDiscoveryContext.capabilities
3643
3672
  );
3673
+ const effectiveOptions = this.withNegotiatedRelease(webhookOptions, negotiatedRelease);
3644
3674
  const inputSchemaStripLogs = [];
3645
3675
  if (options?.preserveGovernedPayload && serverVersion !== "v3") {
3646
3676
  throw new ConfigurationError(
3647
3677
  "Governed storyboard requests require a v3 seller to preserve the approved payload."
3648
3678
  );
3649
3679
  }
3650
- const { params: adaptedParams, driftLogs: adaptDriftLogs } = options?.preserveGovernedPayload ? { params: normalizedParams, driftLogs: [] } : this.adaptRequest(
3680
+ const { params: convertedParams, driftLogs: adaptDriftLogs } = options?.preserveGovernedPayload ? { params: normalizedParams, driftLogs: [] } : this.adaptRequest(
3651
3681
  taskType,
3652
3682
  normalizedParams,
3653
3683
  serverVersion,
3654
3684
  inputSchemaStripLogs,
3655
3685
  capabilityDiscoveryContext.toolSchemas,
3656
- capabilityDiscoveryContext.capabilities
3686
+ capabilityDiscoveryContext.capabilities,
3687
+ negotiatedRelease
3688
+ );
3689
+ const adaptedParams = options?.preserveGovernedPayload ? convertedParams : await this.reconcileAccountKeyReductions(
3690
+ taskType,
3691
+ normalizedParams,
3692
+ convertedParams,
3693
+ negotiatedRelease,
3694
+ adaptDriftLogs
3657
3695
  );
3696
+ if (negotiatedRelease?.downgraded && !options?.skipIdempotencyAutoInject && !options?.skipAccountValidation && !options?.skipRequestValidation) {
3697
+ this.executor.validateRequest(taskType, adaptedParams, inputSchemaStripLogs, negotiatedRelease.version);
3698
+ }
3658
3699
  const adaptationLogs = [...adaptDriftLogs];
3659
3700
  if (webhookDriftLog) adaptationLogs.push(webhookDriftLog);
3660
3701
  const canonicalInputHandler = this.canonicalCreativeInputHandler(
@@ -3672,6 +3713,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
3672
3713
  ...capabilityDiscoveryContext.capabilities?._synthetic !== void 0 && {
3673
3714
  serverVersionSynthetic: capabilityDiscoveryContext.capabilities._synthetic
3674
3715
  },
3716
+ ...negotiatedRelease?.downgraded && { negotiatedAdcpVersion: negotiatedRelease.version },
3675
3717
  productPolicyRequest: productPolicyRequestSnapshot(normalizedParams),
3676
3718
  accountRegistry: this.accountRegistryContext(taskType, normalizedParams, effectiveOptions),
3677
3719
  ...projectionCatalogs !== void 0 && { projectionCatalogs },
@@ -3739,6 +3781,21 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
3739
3781
  }
3740
3782
  };
3741
3783
  }
3784
+ if (context.negotiatedAdcpVersion !== void 0) {
3785
+ result.metadata = { ...result.metadata, negotiatedAdcpVersion: context.negotiatedAdcpVersion };
3786
+ if (result.metadata.taskId) this.rememberNegotiatedRelease(result.metadata.taskId, context.negotiatedAdcpVersion);
3787
+ if (result.success && result.data) {
3788
+ const converted = this.convertResponseToClientRelease(
3789
+ context.taskType,
3790
+ result.data,
3791
+ context.negotiatedAdcpVersion
3792
+ );
3793
+ result.data = converted.data;
3794
+ if (converted.driftLogs.length > 0) {
3795
+ result.debug_logs = [...result.debug_logs ?? [], ...converted.driftLogs];
3796
+ }
3797
+ }
3798
+ }
3742
3799
  const completionHandlerAlreadyPublished = hasCompletionHandlerAlreadyPublished(result);
3743
3800
  const settlementAcknowledger = result[DEFERRED_SETTLEMENT_ACK];
3744
3801
  const taskType = context.taskType;
@@ -4723,6 +4780,228 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
4723
4780
  suppressHandler: !policyResult.success
4724
4781
  };
4725
4782
  }
4783
+ get accountIdentityDowngrade() {
4784
+ return this.config.accountIdentityDowngrade ?? "refuse";
4785
+ }
4786
+ /**
4787
+ * Negotiate the AdCP release to speak with this seller for one call: the
4788
+ * highest release both sides support. Returns `undefined` when the call
4789
+ * keeps its configured wire version: an explicit `wireAdcpVersion`
4790
+ * override, a governed payload that must reach the seller byte-for-byte,
4791
+ * the identity 3.3 experiment, or no authoritative seller release
4792
+ * (synthetic capabilities, no release in common, or no release metadata at
4793
+ * all: AdCP 3.1 does not require `supported_versions`, so its absence does
4794
+ * not prove a 3.0 seller, and such sellers keep the 3.0 request adapters).
4795
+ */
4796
+ negotiateCallRelease(capabilities, options) {
4797
+ if (this.config.wireAdcpVersion !== void 0 || options?.wireAdcpVersion !== void 0) return void 0;
4798
+ if (options?.preserveGovernedPayload || this.config.experimentalIdentity33) return void 0;
4799
+ const negotiated = negotiateAdcpRelease(this.resolvedAdcpVersion, capabilities);
4800
+ if (!negotiated || isNegotiationFailure(negotiated) || negotiated.source === "legacy_v3") return void 0;
4801
+ return negotiated;
4802
+ }
4803
+ /**
4804
+ * Per-call options carrying the negotiated release to the executor, which
4805
+ * stamps it on the version envelope and validates the response against it.
4806
+ */
4807
+ withNegotiatedRelease(options, negotiated) {
4808
+ if (!negotiated?.downgraded) return options;
4809
+ return { ...options ?? {}, wireAdcpVersion: negotiated.version, negotiatedAdcpVersion: negotiated.version };
4810
+ }
4811
+ /**
4812
+ * Refuse a tool the negotiated release does not define (a 3.2-only task
4813
+ * sent to a 3.1 seller) with a typed error instead of a seller-side schema
4814
+ * or unknown-tool failure. Sellers that list the tool anyway are called.
4815
+ */
4816
+ assertToolAvailableAtRelease(taskType, negotiated, toolSchemas) {
4817
+ if (!negotiated?.downgraded) return;
4818
+ if (isExternalSchemaRootActive(negotiated.version)) return;
4819
+ if (releaseDefinesTool(taskType, negotiated.version)) return;
4820
+ if (!releaseDefinesTool(taskType, this.resolvedAdcpVersion)) return;
4821
+ if ((toolSchemas ?? this.cachedToolSchemas)?.has(taskType)) return;
4822
+ const suggestion = `Call a task AdCP ${negotiated.version} defines, or reach this seller once it supports AdCP ${toReleasePrecisionWire(this.resolvedAdcpVersion)}.`;
4823
+ throw new ProtocolFeatureUnsupportedError([taskType], [], this.agent.agent_uri, {
4824
+ message: `${taskType} is not defined in AdCP ${negotiated.version}, the newest release this seller supports. No request was sent. ${suggestion}`,
4825
+ suggestion,
4826
+ details: {
4827
+ feature: taskType,
4828
+ required_version: toReleasePrecisionWire(this.resolvedAdcpVersion),
4829
+ capability_path: "adcp.supported_versions",
4830
+ current_version: negotiated.version,
4831
+ tool: taskType
4832
+ }
4833
+ });
4834
+ }
4835
+ /**
4836
+ * Account-key bookkeeping for `accountIdentityDowngrade: 'reduce'`.
4837
+ *
4838
+ * Converting down to an older seller release: claim each reduced key for
4839
+ * the buyer key it came from, and refuse the request when the reduced key
4840
+ * (or an unreduced key the request sends as is) already belongs to a
4841
+ * different buyer key. Without this, two buyer accounts would silently
4842
+ * share one seller account, with merged settings, spend and reads.
4843
+ *
4844
+ * Seller on the client's release: a buyer key that earlier reached this
4845
+ * seller reduced is still addressed by its reduced key, which is the key
4846
+ * the seller's account has, until the caller moves the account to the full
4847
+ * key and calls `client.accounts.clearReduction(key)`.
4848
+ *
4849
+ * Runs only once the account registry is in use or under `'reduce'`.
4850
+ */
4851
+ async reconcileAccountKeyReductions(taskType, original, adapted, negotiated, driftLogs) {
4852
+ if (this.accountIdentityDowngrade !== "reduce" && !this._accounts) return adapted;
4853
+ const sites = accountKeySites(original);
4854
+ if (sites.length === 0) return adapted;
4855
+ const registry = this.getAccountRegistry();
4856
+ if (negotiated?.downgraded) {
4857
+ if (this.accountIdentityDowngrade !== "reduce") return adapted;
4858
+ const claims = sites.flatMap((site) => {
4859
+ const reduced = naturalAccountKey(valueAt(adapted, site.path));
4860
+ return reduced ? [{ site, account: site.key, reduced, adcpVersion: negotiated.version }] : [];
4861
+ });
4862
+ const conflict = await registry.claimReductions(claims);
4863
+ if (conflict) {
4864
+ this.throwAccountKeyCollision(taskType, claims[conflict.index].site.path, conflict.owner, negotiated.version);
4865
+ }
4866
+ return adapted;
4867
+ }
4868
+ let rewritten = adapted;
4869
+ const pending = [];
4870
+ for (const site of sites) {
4871
+ const reduction = await registry.reductionFor(site.key);
4872
+ if (!reduction) continue;
4873
+ rewritten = withReducedKeyAt(rewritten, site.path, reduction.account);
4874
+ pending.push({
4875
+ path: site.path.join("."),
4876
+ fields: reducedFields(site.key, reduction.account),
4877
+ adcpVersion: reduction.adcpVersion
4878
+ });
4879
+ }
4880
+ if (pending.length > 0) {
4881
+ const inPlace = pending.every((entry) => entry.fields.every((field) => field === "operator_unit"));
4882
+ driftLogs.push({
4883
+ type: "account_identity_migration_pending",
4884
+ message: `${taskType}: addressed ${pending.map((entry) => entry.path).join(", ")} by the reduced account key this seller has, without ${[...new Set(pending.flatMap((entry) => entry.fields))].join(", ")}. ` + (inPlace ? "Rekey the account in place with a sync_accounts settings update (account, operator_identity, revision), " : "currency, timezone and brand.countries cannot change on an existing account, so open a new account under the full key (and decide which key keeps the existing history), ") + "then call client.accounts.clearReduction(key).",
4885
+ accounts: pending,
4886
+ taskName: taskType,
4887
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4888
+ });
4889
+ }
4890
+ return rewritten;
4891
+ }
4892
+ throwAccountKeyCollision(taskType, path, owner, negotiatedVersion) {
4893
+ const field = path.join(".");
4894
+ const suggestion = `Reach this seller with one of the two account keys, address the account by account_id, or wait until the seller supports the release that tells them apart.`;
4895
+ throw new ProtocolFeatureUnsupportedError([`${taskType}.${field}`], [], this.agent.agent_uri, {
4896
+ message: `${taskType} ${field} would reach the same seller account as another of this buyer's account keys: AdCP ${negotiatedVersion} cannot tell them apart. No request was sent. ${suggestion}`,
4897
+ field,
4898
+ suggestion,
4899
+ details: {
4900
+ feature: `${taskType}.${field}`,
4901
+ reason: "account_identity_collision",
4902
+ current_version: negotiatedVersion,
4903
+ tool: taskType,
4904
+ field,
4905
+ conflicting_account: owner
4906
+ }
4907
+ });
4908
+ }
4909
+ throwFieldsUnsupportedAtRelease(taskType, fields, negotiatedVersion) {
4910
+ const [first] = fields;
4911
+ const paths = fields.map((field) => field.path);
4912
+ const accountMove = paths.some((path) => /(?:^|\.)(?:operator_identity|revision)$/.test(path));
4913
+ const suggestion = accountMove ? `AdCP ${negotiatedVersion} cannot rekey an account or hand it to another operator (operator_identity, revision). Make the move once the seller supports AdCP ${first.introducedIn}; until then the account keeps its current key and operator.` : `Remove ${paths.join(", ")} for this seller, or reach it once it supports AdCP ${first.introducedIn}. Sending the request without them would change what the seller does.`;
4914
+ throw new ProtocolFeatureUnsupportedError(
4915
+ paths.map((path) => `${taskType}.${path}`),
4916
+ [],
4917
+ this.agent.agent_uri,
4918
+ {
4919
+ message: `${taskType} ${paths.join(", ")} ${paths.length === 1 ? "requires" : "require"} AdCP ${first.introducedIn}; this seller supports AdCP ${negotiatedVersion}. No request was sent. ${suggestion}`,
4920
+ field: first.path,
4921
+ suggestion,
4922
+ details: {
4923
+ feature: `${taskType}.${first.path}`,
4924
+ required_version: first.introducedIn,
4925
+ capability_path: "adcp.supported_versions",
4926
+ current_version: negotiatedVersion,
4927
+ tool: taskType,
4928
+ field: first.path,
4929
+ fields: paths
4930
+ }
4931
+ }
4932
+ );
4933
+ }
4934
+ throwAccountIdentityUnsupported(taskType, fields, negotiatedVersion) {
4935
+ const [first] = fields;
4936
+ const paths = fields.map((field) => field.path);
4937
+ const suggestion = `Set accountIdentityDowngrade: 'reduce' to send the older account key (allowed while no other buyer key reaches the same seller account without ${paths.join(", ")}), or remove those fields for this seller.`;
4938
+ throw new ProtocolFeatureUnsupportedError(
4939
+ paths.map((path) => `${taskType}.${path}`),
4940
+ [],
4941
+ this.agent.agent_uri,
4942
+ {
4943
+ message: `${taskType} ${paths.join(", ")} ${paths.length === 1 ? "is" : "are"} part of the AdCP ${first.introducedIn} account key, which AdCP ${negotiatedVersion} cannot express. No request was sent. ${suggestion}`,
4944
+ field: first.path,
4945
+ suggestion,
4946
+ details: {
4947
+ feature: `${taskType}.${first.path}`,
4948
+ required_version: first.introducedIn,
4949
+ capability_path: "adcp.supported_versions",
4950
+ current_version: negotiatedVersion,
4951
+ tool: taskType,
4952
+ field: first.path,
4953
+ fields: paths
4954
+ }
4955
+ }
4956
+ );
4957
+ }
4958
+ rememberNegotiatedRelease(operationId, version) {
4959
+ this.negotiatedReleaseByOperation.delete(operationId);
4960
+ this.negotiatedReleaseByOperation.set(operationId, version);
4961
+ if (this.negotiatedReleaseByOperation.size > MAX_REMEMBERED_NEGOTIATED_RELEASES) {
4962
+ const oldest = this.negotiatedReleaseByOperation.keys().next().value;
4963
+ if (oldest !== void 0) this.negotiatedReleaseByOperation.delete(oldest);
4964
+ }
4965
+ }
4966
+ /**
4967
+ * Webhook completions bypass task finalization, so convert their result up
4968
+ * here. The seller's own `adcp_version` names the release it served; an
4969
+ * AdCP 3.0 seller sends none, so fall back to the release this client
4970
+ * negotiated for the operation.
4971
+ */
4972
+ convertWebhookResultToClientRelease(parsed) {
4973
+ const served = [parsed.result, parsed.envelope].map(
4974
+ (value) => value && typeof value === "object" && typeof value.adcp_version === "string" ? value.adcp_version : void 0
4975
+ ).find((version) => version !== void 0);
4976
+ const release = served ?? this.negotiatedReleaseByOperation.get(parsed.metadata.operationId);
4977
+ if (release === void 0 || parsed.result === void 0 || parsed.result === null) return parsed.result;
4978
+ return convertResponse(parsed.metadata.taskType, parsed.result, {
4979
+ fromVersion: release,
4980
+ toVersion: this.resolvedAdcpVersion
4981
+ }).data;
4982
+ }
4983
+ /**
4984
+ * Convert a response served at the negotiated release up to the client's
4985
+ * release. Returns drift entries for each conversion applied.
4986
+ */
4987
+ convertResponseToClientRelease(taskType, data, negotiatedVersion) {
4988
+ if (negotiatedVersion === void 0 || data === void 0 || data === null) return { data, driftLogs: [] };
4989
+ const translation = convertResponse(taskType, data, {
4990
+ fromVersion: negotiatedVersion,
4991
+ toVersion: this.resolvedAdcpVersion
4992
+ });
4993
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4994
+ return {
4995
+ data: translation.data,
4996
+ driftLogs: translation.drifts.map((drift) => ({
4997
+ ...drift,
4998
+ taskName: taskType,
4999
+ clientVersion: this.resolvedAdcpVersion,
5000
+ targetVersion: negotiatedVersion,
5001
+ timestamp
5002
+ }))
5003
+ };
5004
+ }
4726
5005
  /**
4727
5006
  * Adapt a request for the detected server wire version and the seller's
4728
5007
  * AdCP protocol version. Applies protocol-version adapters (e.g. stripping
@@ -4732,7 +5011,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
4732
5011
  * Runs after `detectServerVersion` so capabilities are available and the
4733
5012
  * current call's tool schemas can be supplied without cross-tenant caching.
4734
5013
  */
4735
- adaptRequest(taskType, params, serverVersion, debugLogs, perCallToolSchemas, perCallCapabilities) {
5014
+ adaptRequest(taskType, params, serverVersion, debugLogs, perCallToolSchemas, perCallCapabilities, negotiated) {
4736
5015
  if (serverVersion !== "v3") {
4737
5016
  throw new VersionUnsupportedError(taskType, "version", "v2", this.agent.agent_uri);
4738
5017
  }
@@ -4769,10 +5048,34 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
4769
5048
  }
4770
5049
  adapted = filtered;
4771
5050
  }
5051
+ if (negotiated?.downgraded) {
5052
+ const translation = convertRequest(taskType, adapted, {
5053
+ fromVersion: this.resolvedAdcpVersion,
5054
+ toVersion: negotiated.version,
5055
+ accountIdentity: this.accountIdentityDowngrade
5056
+ });
5057
+ if (translation.identityFields.length > 0 && this.accountIdentityDowngrade === "refuse") {
5058
+ this.throwAccountIdentityUnsupported(taskType, translation.identityFields, negotiated.version);
5059
+ }
5060
+ if (translation.refusedFields.length > 0) {
5061
+ this.throwFieldsUnsupportedAtRelease(taskType, translation.refusedFields, negotiated.version);
5062
+ }
5063
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
5064
+ for (const drift of translation.drifts) {
5065
+ driftLogs.push({
5066
+ ...drift,
5067
+ taskName: taskType,
5068
+ clientVersion: this.resolvedAdcpVersion,
5069
+ targetVersion: negotiated.version,
5070
+ timestamp
5071
+ });
5072
+ }
5073
+ return { params: translation.params, driftLogs };
5074
+ }
4772
5075
  const adapterKey = resolveAdapterKey(this.resolvedAdcpVersion, perCallCapabilities ?? this.cachedCapabilities);
4773
5076
  if (adapterKey) {
4774
5077
  const versionAdapter = getVersionAdapter(adapterKey, taskType);
4775
- if (versionAdapter) {
5078
+ if (versionAdapter?.adaptRequest) {
4776
5079
  const result = versionAdapter.adaptRequest(adapted);
4777
5080
  adapted = result.params;
4778
5081
  if (result.drift) {
@@ -6391,32 +6694,47 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
6391
6694
  const serverVersion = await this.detectServerVersion(detectionOptions);
6392
6695
  detectedServerVersion = serverVersion;
6393
6696
  detectedServerVersionSynthetic = capabilityDiscoveryContext.capabilities?._synthetic;
6697
+ const negotiatedRelease = this.negotiateCallRelease(capabilityDiscoveryContext.capabilities, options);
6698
+ this.assertToolAvailableAtRelease(taskName, negotiatedRelease, capabilityDiscoveryContext.toolSchemas);
6394
6699
  this.assertRequestSupportedByTargetVersion(
6395
6700
  taskName,
6396
6701
  normalizedParams,
6397
6702
  capabilityDiscoveryContext.capabilities,
6398
6703
  false,
6399
- serverVersion
6704
+ serverVersion,
6705
+ negotiatedRelease
6400
6706
  );
6401
- const { options: effectiveOptions, driftLog: webhookDriftLog } = this.suppressPre31DiscoveryWebhook(
6707
+ const { options: webhookOptions, driftLog: webhookDriftLog } = this.suppressPre31DiscoveryWebhook(
6402
6708
  taskName,
6403
6709
  options,
6404
6710
  capabilityDiscoveryContext.capabilities
6405
6711
  );
6712
+ const effectiveOptions = this.withNegotiatedRelease(webhookOptions, negotiatedRelease);
6406
6713
  const inputSchemaStripLogs = [];
6407
6714
  if (options?.preserveGovernedPayload && serverVersion !== "v3") {
6408
6715
  throw new ConfigurationError(
6409
6716
  "Governed storyboard requests require a v3 seller to preserve the approved payload."
6410
6717
  );
6411
6718
  }
6412
- const { params: adaptedParams, driftLogs: adaptDriftLogs } = options?.preserveGovernedPayload ? { params: normalizedParams, driftLogs: [] } : this.adaptRequest(
6719
+ const { params: convertedParams, driftLogs: adaptDriftLogs } = options?.preserveGovernedPayload ? { params: normalizedParams, driftLogs: [] } : this.adaptRequest(
6413
6720
  taskName,
6414
6721
  normalizedParams,
6415
6722
  serverVersion,
6416
6723
  inputSchemaStripLogs,
6417
6724
  capabilityDiscoveryContext.toolSchemas,
6418
- capabilityDiscoveryContext.capabilities
6725
+ capabilityDiscoveryContext.capabilities,
6726
+ negotiatedRelease
6419
6727
  );
6728
+ const adaptedParams = options?.preserveGovernedPayload ? convertedParams : await this.reconcileAccountKeyReductions(
6729
+ taskName,
6730
+ normalizedParams,
6731
+ convertedParams,
6732
+ negotiatedRelease,
6733
+ adaptDriftLogs
6734
+ );
6735
+ if (negotiatedRelease?.downgraded && !options?.skipIdempotencyAutoInject && !options?.skipAccountValidation && !options?.skipRequestValidation) {
6736
+ this.executor.validateRequest(taskName, adaptedParams, inputSchemaStripLogs, negotiatedRelease.version);
6737
+ }
6420
6738
  const adaptationLogs = [...adaptDriftLogs];
6421
6739
  if (webhookDriftLog) adaptationLogs.push(webhookDriftLog);
6422
6740
  const deferredClientContext = {
@@ -6428,6 +6746,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
6428
6746
  ...capabilityDiscoveryContext.capabilities?._synthetic !== void 0 && {
6429
6747
  serverVersionSynthetic: capabilityDiscoveryContext.capabilities._synthetic
6430
6748
  },
6749
+ ...negotiatedRelease?.downgraded && { negotiatedAdcpVersion: negotiatedRelease.version },
6431
6750
  productPolicyRequest: productPolicyRequestSnapshot(normalizedParams),
6432
6751
  accountRegistry: this.accountRegistryContext(taskName, normalizedParams, effectiveOptions),
6433
6752
  ...effectiveOptions?.taskId !== void 0 && { optionTaskId: effectiveOptions.taskId },
@@ -8122,8 +8441,9 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
8122
8441
  * first (cold-cache) call; the configured-version gate above still catches
8123
8442
  * an explicitly pre-3.1 client before validation or network I/O.
8124
8443
  */
8125
- assertRequestSupportedByTargetVersion(taskName, params, capabilities, canonicalCreativeInvocation = false, serverVersion) {
8126
- if (taskName === "sync_accounts") {
8444
+ assertRequestSupportedByTargetVersion(taskName, params, capabilities, canonicalCreativeInvocation = false, serverVersion, negotiated) {
8445
+ const reducesAccountIdentity = this.accountIdentityDowngrade === "reduce" && negotiated?.downgraded === true;
8446
+ if (taskName === "sync_accounts" && !reducesAccountIdentity) {
8127
8447
  const advertisedVersions2 = capabilities?.supportedVersions ?? [];
8128
8448
  const servedVersion = capabilities?.servedVersion ?? capabilities?._raw?.adcp_version;
8129
8449
  const supports32 = serverVersion !== "v2" && (typeof servedVersion === "string" ? isAdcpVersionAtLeast(servedVersion, "3.2.0-0") : advertisedVersions2.some((version) => isAdcpVersionAtLeast(version, "3.2.0-0")));
@@ -8293,7 +8613,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
8293
8613
  }
8294
8614
  const supportedVersions = capabilities.supportedVersions;
8295
8615
  if (supportedVersions !== void 0 && supportedVersions.length > 0) {
8296
- if (!isAdcpVersionSupported(this.resolvedAdcpVersion, supportedVersions)) {
8616
+ if (!isAdcpVersionSupported(this.resolvedAdcpVersion, supportedVersions) && this.negotiateCallRelease(capabilities, options) === void 0) {
8297
8617
  throw new VersionUnsupportedError(taskType, "version", capabilities.version, this.agent.agent_uri);
8298
8618
  }
8299
8619
  } else {
@@ -226,7 +226,7 @@ export declare class TaskExecutor {
226
226
  * adaptation runs, so the validator sees the same object the caller wrote.
227
227
  * Throws in strict, logs to `debugLogs` in warn, no-ops in off.
228
228
  */
229
- validateRequest(taskName: string, params: unknown, debugLogs?: any[]): void;
229
+ validateRequest(taskName: string, params: unknown, debugLogs?: any[], adcpVersion?: string): void;
230
230
  /**
231
231
  * Generate webhook URL for protocol-level webhook support
232
232
  */
@@ -226,7 +226,7 @@ export declare class TaskExecutor {
226
226
  * adaptation runs, so the validator sees the same object the caller wrote.
227
227
  * Throws in strict, logs to `debugLogs` in warn, no-ops in off.
228
228
  */
229
- validateRequest(taskName: string, params: unknown, debugLogs?: any[]): void;
229
+ validateRequest(taskName: string, params: unknown, debugLogs?: any[], adcpVersion?: string): void;
230
230
  /**
231
231
  * Generate webhook URL for protocol-level webhook support
232
232
  */