@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
@@ -41,6 +41,7 @@ var import_schema_loader = require('../validation/schema-loader.js');
41
41
  var import_canonicalize = require('../signing/agent-resolver/canonicalize.js');
42
42
  var import_products = require('../supply-path/products.js');
43
43
  var import_node_crypto = require("node:crypto");
44
+ var import_account_reduction = require('./account-reduction.js');
44
45
  var import_adcp = require('../types/adcp.js');
45
46
  var import_version = require('../version.js');
46
47
  var import_adcp_version_config = require('../utils/adcp-version-config.js');
@@ -275,8 +276,9 @@ function creativeMatchesProductOption(creative, declaration) {
275
276
  function isDeferredClientFinalizationContext(value) {
276
277
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
277
278
  const context = value;
278
- 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");
279
+ 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");
279
280
  }
281
+ const MAX_REMEMBERED_NEGOTIATED_RELEASES = 1024;
280
282
  const CANONICAL_PACKAGE_ROUTE_TASKS = /* @__PURE__ */ new Set(["create_media_buy", "update_media_buy", "get_media_buys"]);
281
283
  const canonicalCreativeRoutingSnapshots = /* @__PURE__ */ new WeakSet();
282
284
  function canonicalAccountRoutingSnapshot(value) {
@@ -822,6 +824,12 @@ class SingleAgentClient {
822
824
  this.resolvedAdcpVersion = (0, import_adcp_version_config.resolveAdcpVersion)(
823
825
  config.adcpVersion ?? (config.experimentalIdentity33 ? "3.3" : void 0)
824
826
  );
827
+ if (config.accountIdentityDowngrade === "reduce" && typeof config.accountStorage?.compareAndSet !== "function") {
828
+ throw new import_errors.ConfigurationError(
829
+ "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'.",
830
+ "accountIdentityDowngrade"
831
+ );
832
+ }
825
833
  this.webhookRegistrationStore = config.webhookRegistrationStore ?? new import_webhook_registration.InMemoryWebhookRegistrationStore();
826
834
  this.webhookReplayStore = config.webhookVerification?.replayStore ?? new import_server.InMemoryReplayStore();
827
835
  this.webhookRevocationStore = config.webhookVerification?.revocationStore ?? new import_server.InMemoryRevocationStore();
@@ -1142,6 +1150,8 @@ class SingleAgentClient {
1142
1150
  runAgentInfo;
1143
1151
  cachedToolSchemas;
1144
1152
  // inputSchema.properties per tool name
1153
+ /** Release negotiated per operation, for converting webhook completions up. */
1154
+ negotiatedReleaseByOperation = /* @__PURE__ */ new Map();
1145
1155
  primedCapabilitiesExpiresAt;
1146
1156
  // Freshness bound for application-owned evidence only
1147
1157
  capabilityEvidenceScopeKey = (0, import_node_crypto.randomUUID)();
@@ -2951,7 +2961,11 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
2951
2961
  let externalStatus;
2952
2962
  let forgetPolicyState = false;
2953
2963
  try {
2954
- const canonicalResult = this.canonicalizeWebhookCreativeResult(metadata, parsed.result, canonicalCreativeTask);
2964
+ const canonicalResult = this.canonicalizeWebhookCreativeResult(
2965
+ metadata,
2966
+ this.convertWebhookResultToClientRelease(parsed),
2967
+ canonicalCreativeTask
2968
+ );
2955
2969
  const dedupResult = canonicalResult;
2956
2970
  const dedupMetadata = { ...metadata };
2957
2971
  let webhookResult = dedupResult;
@@ -3532,32 +3546,47 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
3532
3546
  };
3533
3547
  const serverVersion = await this.detectServerVersion(detectionOptions);
3534
3548
  (0, import_abort2.throwIfAborted)(options?.signal);
3549
+ const negotiatedRelease = this.negotiateCallRelease(capabilityDiscoveryContext.capabilities, options);
3550
+ this.assertToolAvailableAtRelease(taskType, negotiatedRelease, capabilityDiscoveryContext.toolSchemas);
3535
3551
  this.assertRequestSupportedByTargetVersion(
3536
3552
  taskType,
3537
3553
  normalizedParams,
3538
3554
  capabilityDiscoveryContext.capabilities,
3539
3555
  canonicalCreativeInvocation,
3540
- serverVersion
3556
+ serverVersion,
3557
+ negotiatedRelease
3541
3558
  );
3542
- const { options: effectiveOptions, driftLog: webhookDriftLog } = this.suppressPre31DiscoveryWebhook(
3559
+ const { options: webhookOptions, driftLog: webhookDriftLog } = this.suppressPre31DiscoveryWebhook(
3543
3560
  taskType,
3544
3561
  options,
3545
3562
  capabilityDiscoveryContext.capabilities
3546
3563
  );
3564
+ const effectiveOptions = this.withNegotiatedRelease(webhookOptions, negotiatedRelease);
3547
3565
  const inputSchemaStripLogs = [];
3548
3566
  if (options?.preserveGovernedPayload && serverVersion !== "v3") {
3549
3567
  throw new import_errors.ConfigurationError(
3550
3568
  "Governed storyboard requests require a v3 seller to preserve the approved payload."
3551
3569
  );
3552
3570
  }
3553
- const { params: adaptedParams, driftLogs: adaptDriftLogs } = options?.preserveGovernedPayload ? { params: normalizedParams, driftLogs: [] } : this.adaptRequest(
3571
+ const { params: convertedParams, driftLogs: adaptDriftLogs } = options?.preserveGovernedPayload ? { params: normalizedParams, driftLogs: [] } : this.adaptRequest(
3554
3572
  taskType,
3555
3573
  normalizedParams,
3556
3574
  serverVersion,
3557
3575
  inputSchemaStripLogs,
3558
3576
  capabilityDiscoveryContext.toolSchemas,
3559
- capabilityDiscoveryContext.capabilities
3577
+ capabilityDiscoveryContext.capabilities,
3578
+ negotiatedRelease
3560
3579
  );
3580
+ const adaptedParams = options?.preserveGovernedPayload ? convertedParams : await this.reconcileAccountKeyReductions(
3581
+ taskType,
3582
+ normalizedParams,
3583
+ convertedParams,
3584
+ negotiatedRelease,
3585
+ adaptDriftLogs
3586
+ );
3587
+ if (negotiatedRelease?.downgraded && !options?.skipIdempotencyAutoInject && !options?.skipAccountValidation && !options?.skipRequestValidation) {
3588
+ this.executor.validateRequest(taskType, adaptedParams, inputSchemaStripLogs, negotiatedRelease.version);
3589
+ }
3561
3590
  const adaptationLogs = [...adaptDriftLogs];
3562
3591
  if (webhookDriftLog) adaptationLogs.push(webhookDriftLog);
3563
3592
  const canonicalInputHandler = this.canonicalCreativeInputHandler(
@@ -3575,6 +3604,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
3575
3604
  ...capabilityDiscoveryContext.capabilities?._synthetic !== void 0 && {
3576
3605
  serverVersionSynthetic: capabilityDiscoveryContext.capabilities._synthetic
3577
3606
  },
3607
+ ...negotiatedRelease?.downgraded && { negotiatedAdcpVersion: negotiatedRelease.version },
3578
3608
  productPolicyRequest: productPolicyRequestSnapshot(normalizedParams),
3579
3609
  accountRegistry: this.accountRegistryContext(taskType, normalizedParams, effectiveOptions),
3580
3610
  ...projectionCatalogs !== void 0 && { projectionCatalogs },
@@ -3642,6 +3672,21 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
3642
3672
  }
3643
3673
  };
3644
3674
  }
3675
+ if (context.negotiatedAdcpVersion !== void 0) {
3676
+ result.metadata = { ...result.metadata, negotiatedAdcpVersion: context.negotiatedAdcpVersion };
3677
+ if (result.metadata.taskId) this.rememberNegotiatedRelease(result.metadata.taskId, context.negotiatedAdcpVersion);
3678
+ if (result.success && result.data) {
3679
+ const converted = this.convertResponseToClientRelease(
3680
+ context.taskType,
3681
+ result.data,
3682
+ context.negotiatedAdcpVersion
3683
+ );
3684
+ result.data = converted.data;
3685
+ if (converted.driftLogs.length > 0) {
3686
+ result.debug_logs = [...result.debug_logs ?? [], ...converted.driftLogs];
3687
+ }
3688
+ }
3689
+ }
3645
3690
  const completionHandlerAlreadyPublished = (0, import_TaskExecutor.hasCompletionHandlerAlreadyPublished)(result);
3646
3691
  const settlementAcknowledger = result[import_TaskExecutor.DEFERRED_SETTLEMENT_ACK];
3647
3692
  const taskType = context.taskType;
@@ -4626,6 +4671,228 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
4626
4671
  suppressHandler: !policyResult.success
4627
4672
  };
4628
4673
  }
4674
+ get accountIdentityDowngrade() {
4675
+ return this.config.accountIdentityDowngrade ?? "refuse";
4676
+ }
4677
+ /**
4678
+ * Negotiate the AdCP release to speak with this seller for one call: the
4679
+ * highest release both sides support. Returns `undefined` when the call
4680
+ * keeps its configured wire version: an explicit `wireAdcpVersion`
4681
+ * override, a governed payload that must reach the seller byte-for-byte,
4682
+ * the identity 3.3 experiment, or no authoritative seller release
4683
+ * (synthetic capabilities, no release in common, or no release metadata at
4684
+ * all: AdCP 3.1 does not require `supported_versions`, so its absence does
4685
+ * not prove a 3.0 seller, and such sellers keep the 3.0 request adapters).
4686
+ */
4687
+ negotiateCallRelease(capabilities, options) {
4688
+ if (this.config.wireAdcpVersion !== void 0 || options?.wireAdcpVersion !== void 0) return void 0;
4689
+ if (options?.preserveGovernedPayload || this.config.experimentalIdentity33) return void 0;
4690
+ const negotiated = (0, import_version2.negotiateAdcpRelease)(this.resolvedAdcpVersion, capabilities);
4691
+ if (!negotiated || (0, import_version2.isNegotiationFailure)(negotiated) || negotiated.source === "legacy_v3") return void 0;
4692
+ return negotiated;
4693
+ }
4694
+ /**
4695
+ * Per-call options carrying the negotiated release to the executor, which
4696
+ * stamps it on the version envelope and validates the response against it.
4697
+ */
4698
+ withNegotiatedRelease(options, negotiated) {
4699
+ if (!negotiated?.downgraded) return options;
4700
+ return { ...options ?? {}, wireAdcpVersion: negotiated.version, negotiatedAdcpVersion: negotiated.version };
4701
+ }
4702
+ /**
4703
+ * Refuse a tool the negotiated release does not define (a 3.2-only task
4704
+ * sent to a 3.1 seller) with a typed error instead of a seller-side schema
4705
+ * or unknown-tool failure. Sellers that list the tool anyway are called.
4706
+ */
4707
+ assertToolAvailableAtRelease(taskType, negotiated, toolSchemas) {
4708
+ if (!negotiated?.downgraded) return;
4709
+ if ((0, import_schema_loader2.isExternalSchemaRootActive)(negotiated.version)) return;
4710
+ if ((0, import_version2.releaseDefinesTool)(taskType, negotiated.version)) return;
4711
+ if (!(0, import_version2.releaseDefinesTool)(taskType, this.resolvedAdcpVersion)) return;
4712
+ if ((toolSchemas ?? this.cachedToolSchemas)?.has(taskType)) return;
4713
+ const suggestion = `Call a task AdCP ${negotiated.version} defines, or reach this seller once it supports AdCP ${(0, import_schema_loader2.toReleasePrecisionWire)(this.resolvedAdcpVersion)}.`;
4714
+ throw new import_errors.ProtocolFeatureUnsupportedError([taskType], [], this.agent.agent_uri, {
4715
+ message: `${taskType} is not defined in AdCP ${negotiated.version}, the newest release this seller supports. No request was sent. ${suggestion}`,
4716
+ suggestion,
4717
+ details: {
4718
+ feature: taskType,
4719
+ required_version: (0, import_schema_loader2.toReleasePrecisionWire)(this.resolvedAdcpVersion),
4720
+ capability_path: "adcp.supported_versions",
4721
+ current_version: negotiated.version,
4722
+ tool: taskType
4723
+ }
4724
+ });
4725
+ }
4726
+ /**
4727
+ * Account-key bookkeeping for `accountIdentityDowngrade: 'reduce'`.
4728
+ *
4729
+ * Converting down to an older seller release: claim each reduced key for
4730
+ * the buyer key it came from, and refuse the request when the reduced key
4731
+ * (or an unreduced key the request sends as is) already belongs to a
4732
+ * different buyer key. Without this, two buyer accounts would silently
4733
+ * share one seller account, with merged settings, spend and reads.
4734
+ *
4735
+ * Seller on the client's release: a buyer key that earlier reached this
4736
+ * seller reduced is still addressed by its reduced key, which is the key
4737
+ * the seller's account has, until the caller moves the account to the full
4738
+ * key and calls `client.accounts.clearReduction(key)`.
4739
+ *
4740
+ * Runs only once the account registry is in use or under `'reduce'`.
4741
+ */
4742
+ async reconcileAccountKeyReductions(taskType, original, adapted, negotiated, driftLogs) {
4743
+ if (this.accountIdentityDowngrade !== "reduce" && !this._accounts) return adapted;
4744
+ const sites = (0, import_account_reduction.accountKeySites)(original);
4745
+ if (sites.length === 0) return adapted;
4746
+ const registry = this.getAccountRegistry();
4747
+ if (negotiated?.downgraded) {
4748
+ if (this.accountIdentityDowngrade !== "reduce") return adapted;
4749
+ const claims = sites.flatMap((site) => {
4750
+ const reduced = (0, import_account_reduction.naturalAccountKey)((0, import_account_reduction.valueAt)(adapted, site.path));
4751
+ return reduced ? [{ site, account: site.key, reduced, adcpVersion: negotiated.version }] : [];
4752
+ });
4753
+ const conflict = await registry.claimReductions(claims);
4754
+ if (conflict) {
4755
+ this.throwAccountKeyCollision(taskType, claims[conflict.index].site.path, conflict.owner, negotiated.version);
4756
+ }
4757
+ return adapted;
4758
+ }
4759
+ let rewritten = adapted;
4760
+ const pending = [];
4761
+ for (const site of sites) {
4762
+ const reduction = await registry.reductionFor(site.key);
4763
+ if (!reduction) continue;
4764
+ rewritten = (0, import_account_reduction.withReducedKeyAt)(rewritten, site.path, reduction.account);
4765
+ pending.push({
4766
+ path: site.path.join("."),
4767
+ fields: (0, import_account_reduction.reducedFields)(site.key, reduction.account),
4768
+ adcpVersion: reduction.adcpVersion
4769
+ });
4770
+ }
4771
+ if (pending.length > 0) {
4772
+ const inPlace = pending.every((entry) => entry.fields.every((field) => field === "operator_unit"));
4773
+ driftLogs.push({
4774
+ type: "account_identity_migration_pending",
4775
+ 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).",
4776
+ accounts: pending,
4777
+ taskName: taskType,
4778
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4779
+ });
4780
+ }
4781
+ return rewritten;
4782
+ }
4783
+ throwAccountKeyCollision(taskType, path, owner, negotiatedVersion) {
4784
+ const field = path.join(".");
4785
+ 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.`;
4786
+ throw new import_errors.ProtocolFeatureUnsupportedError([`${taskType}.${field}`], [], this.agent.agent_uri, {
4787
+ 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}`,
4788
+ field,
4789
+ suggestion,
4790
+ details: {
4791
+ feature: `${taskType}.${field}`,
4792
+ reason: "account_identity_collision",
4793
+ current_version: negotiatedVersion,
4794
+ tool: taskType,
4795
+ field,
4796
+ conflicting_account: owner
4797
+ }
4798
+ });
4799
+ }
4800
+ throwFieldsUnsupportedAtRelease(taskType, fields, negotiatedVersion) {
4801
+ const [first] = fields;
4802
+ const paths = fields.map((field) => field.path);
4803
+ const accountMove = paths.some((path) => /(?:^|\.)(?:operator_identity|revision)$/.test(path));
4804
+ 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.`;
4805
+ throw new import_errors.ProtocolFeatureUnsupportedError(
4806
+ paths.map((path) => `${taskType}.${path}`),
4807
+ [],
4808
+ this.agent.agent_uri,
4809
+ {
4810
+ message: `${taskType} ${paths.join(", ")} ${paths.length === 1 ? "requires" : "require"} AdCP ${first.introducedIn}; this seller supports AdCP ${negotiatedVersion}. No request was sent. ${suggestion}`,
4811
+ field: first.path,
4812
+ suggestion,
4813
+ details: {
4814
+ feature: `${taskType}.${first.path}`,
4815
+ required_version: first.introducedIn,
4816
+ capability_path: "adcp.supported_versions",
4817
+ current_version: negotiatedVersion,
4818
+ tool: taskType,
4819
+ field: first.path,
4820
+ fields: paths
4821
+ }
4822
+ }
4823
+ );
4824
+ }
4825
+ throwAccountIdentityUnsupported(taskType, fields, negotiatedVersion) {
4826
+ const [first] = fields;
4827
+ const paths = fields.map((field) => field.path);
4828
+ 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.`;
4829
+ throw new import_errors.ProtocolFeatureUnsupportedError(
4830
+ paths.map((path) => `${taskType}.${path}`),
4831
+ [],
4832
+ this.agent.agent_uri,
4833
+ {
4834
+ 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}`,
4835
+ field: first.path,
4836
+ suggestion,
4837
+ details: {
4838
+ feature: `${taskType}.${first.path}`,
4839
+ required_version: first.introducedIn,
4840
+ capability_path: "adcp.supported_versions",
4841
+ current_version: negotiatedVersion,
4842
+ tool: taskType,
4843
+ field: first.path,
4844
+ fields: paths
4845
+ }
4846
+ }
4847
+ );
4848
+ }
4849
+ rememberNegotiatedRelease(operationId, version) {
4850
+ this.negotiatedReleaseByOperation.delete(operationId);
4851
+ this.negotiatedReleaseByOperation.set(operationId, version);
4852
+ if (this.negotiatedReleaseByOperation.size > MAX_REMEMBERED_NEGOTIATED_RELEASES) {
4853
+ const oldest = this.negotiatedReleaseByOperation.keys().next().value;
4854
+ if (oldest !== void 0) this.negotiatedReleaseByOperation.delete(oldest);
4855
+ }
4856
+ }
4857
+ /**
4858
+ * Webhook completions bypass task finalization, so convert their result up
4859
+ * here. The seller's own `adcp_version` names the release it served; an
4860
+ * AdCP 3.0 seller sends none, so fall back to the release this client
4861
+ * negotiated for the operation.
4862
+ */
4863
+ convertWebhookResultToClientRelease(parsed) {
4864
+ const served = [parsed.result, parsed.envelope].map(
4865
+ (value) => value && typeof value === "object" && typeof value.adcp_version === "string" ? value.adcp_version : void 0
4866
+ ).find((version) => version !== void 0);
4867
+ const release = served ?? this.negotiatedReleaseByOperation.get(parsed.metadata.operationId);
4868
+ if (release === void 0 || parsed.result === void 0 || parsed.result === null) return parsed.result;
4869
+ return (0, import_version2.convertResponse)(parsed.metadata.taskType, parsed.result, {
4870
+ fromVersion: release,
4871
+ toVersion: this.resolvedAdcpVersion
4872
+ }).data;
4873
+ }
4874
+ /**
4875
+ * Convert a response served at the negotiated release up to the client's
4876
+ * release. Returns drift entries for each conversion applied.
4877
+ */
4878
+ convertResponseToClientRelease(taskType, data, negotiatedVersion) {
4879
+ if (negotiatedVersion === void 0 || data === void 0 || data === null) return { data, driftLogs: [] };
4880
+ const translation = (0, import_version2.convertResponse)(taskType, data, {
4881
+ fromVersion: negotiatedVersion,
4882
+ toVersion: this.resolvedAdcpVersion
4883
+ });
4884
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4885
+ return {
4886
+ data: translation.data,
4887
+ driftLogs: translation.drifts.map((drift) => ({
4888
+ ...drift,
4889
+ taskName: taskType,
4890
+ clientVersion: this.resolvedAdcpVersion,
4891
+ targetVersion: negotiatedVersion,
4892
+ timestamp
4893
+ }))
4894
+ };
4895
+ }
4629
4896
  /**
4630
4897
  * Adapt a request for the detected server wire version and the seller's
4631
4898
  * AdCP protocol version. Applies protocol-version adapters (e.g. stripping
@@ -4635,7 +4902,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
4635
4902
  * Runs after `detectServerVersion` so capabilities are available and the
4636
4903
  * current call's tool schemas can be supplied without cross-tenant caching.
4637
4904
  */
4638
- adaptRequest(taskType, params, serverVersion, debugLogs, perCallToolSchemas, perCallCapabilities) {
4905
+ adaptRequest(taskType, params, serverVersion, debugLogs, perCallToolSchemas, perCallCapabilities, negotiated) {
4639
4906
  if (serverVersion !== "v3") {
4640
4907
  throw new import_errors.VersionUnsupportedError(taskType, "version", "v2", this.agent.agent_uri);
4641
4908
  }
@@ -4672,10 +4939,34 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
4672
4939
  }
4673
4940
  adapted = filtered;
4674
4941
  }
4942
+ if (negotiated?.downgraded) {
4943
+ const translation = (0, import_version2.convertRequest)(taskType, adapted, {
4944
+ fromVersion: this.resolvedAdcpVersion,
4945
+ toVersion: negotiated.version,
4946
+ accountIdentity: this.accountIdentityDowngrade
4947
+ });
4948
+ if (translation.identityFields.length > 0 && this.accountIdentityDowngrade === "refuse") {
4949
+ this.throwAccountIdentityUnsupported(taskType, translation.identityFields, negotiated.version);
4950
+ }
4951
+ if (translation.refusedFields.length > 0) {
4952
+ this.throwFieldsUnsupportedAtRelease(taskType, translation.refusedFields, negotiated.version);
4953
+ }
4954
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4955
+ for (const drift of translation.drifts) {
4956
+ driftLogs.push({
4957
+ ...drift,
4958
+ taskName: taskType,
4959
+ clientVersion: this.resolvedAdcpVersion,
4960
+ targetVersion: negotiated.version,
4961
+ timestamp
4962
+ });
4963
+ }
4964
+ return { params: translation.params, driftLogs };
4965
+ }
4675
4966
  const adapterKey = (0, import_version2.resolveAdapterKey)(this.resolvedAdcpVersion, perCallCapabilities ?? this.cachedCapabilities);
4676
4967
  if (adapterKey) {
4677
4968
  const versionAdapter = (0, import_version2.getVersionAdapter)(adapterKey, taskType);
4678
- if (versionAdapter) {
4969
+ if (versionAdapter?.adaptRequest) {
4679
4970
  const result = versionAdapter.adaptRequest(adapted);
4680
4971
  adapted = result.params;
4681
4972
  if (result.drift) {
@@ -6294,32 +6585,47 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
6294
6585
  const serverVersion = await this.detectServerVersion(detectionOptions);
6295
6586
  detectedServerVersion = serverVersion;
6296
6587
  detectedServerVersionSynthetic = capabilityDiscoveryContext.capabilities?._synthetic;
6588
+ const negotiatedRelease = this.negotiateCallRelease(capabilityDiscoveryContext.capabilities, options);
6589
+ this.assertToolAvailableAtRelease(taskName, negotiatedRelease, capabilityDiscoveryContext.toolSchemas);
6297
6590
  this.assertRequestSupportedByTargetVersion(
6298
6591
  taskName,
6299
6592
  normalizedParams,
6300
6593
  capabilityDiscoveryContext.capabilities,
6301
6594
  false,
6302
- serverVersion
6595
+ serverVersion,
6596
+ negotiatedRelease
6303
6597
  );
6304
- const { options: effectiveOptions, driftLog: webhookDriftLog } = this.suppressPre31DiscoveryWebhook(
6598
+ const { options: webhookOptions, driftLog: webhookDriftLog } = this.suppressPre31DiscoveryWebhook(
6305
6599
  taskName,
6306
6600
  options,
6307
6601
  capabilityDiscoveryContext.capabilities
6308
6602
  );
6603
+ const effectiveOptions = this.withNegotiatedRelease(webhookOptions, negotiatedRelease);
6309
6604
  const inputSchemaStripLogs = [];
6310
6605
  if (options?.preserveGovernedPayload && serverVersion !== "v3") {
6311
6606
  throw new import_errors.ConfigurationError(
6312
6607
  "Governed storyboard requests require a v3 seller to preserve the approved payload."
6313
6608
  );
6314
6609
  }
6315
- const { params: adaptedParams, driftLogs: adaptDriftLogs } = options?.preserveGovernedPayload ? { params: normalizedParams, driftLogs: [] } : this.adaptRequest(
6610
+ const { params: convertedParams, driftLogs: adaptDriftLogs } = options?.preserveGovernedPayload ? { params: normalizedParams, driftLogs: [] } : this.adaptRequest(
6316
6611
  taskName,
6317
6612
  normalizedParams,
6318
6613
  serverVersion,
6319
6614
  inputSchemaStripLogs,
6320
6615
  capabilityDiscoveryContext.toolSchemas,
6321
- capabilityDiscoveryContext.capabilities
6616
+ capabilityDiscoveryContext.capabilities,
6617
+ negotiatedRelease
6322
6618
  );
6619
+ const adaptedParams = options?.preserveGovernedPayload ? convertedParams : await this.reconcileAccountKeyReductions(
6620
+ taskName,
6621
+ normalizedParams,
6622
+ convertedParams,
6623
+ negotiatedRelease,
6624
+ adaptDriftLogs
6625
+ );
6626
+ if (negotiatedRelease?.downgraded && !options?.skipIdempotencyAutoInject && !options?.skipAccountValidation && !options?.skipRequestValidation) {
6627
+ this.executor.validateRequest(taskName, adaptedParams, inputSchemaStripLogs, negotiatedRelease.version);
6628
+ }
6323
6629
  const adaptationLogs = [...adaptDriftLogs];
6324
6630
  if (webhookDriftLog) adaptationLogs.push(webhookDriftLog);
6325
6631
  const deferredClientContext = {
@@ -6331,6 +6637,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
6331
6637
  ...capabilityDiscoveryContext.capabilities?._synthetic !== void 0 && {
6332
6638
  serverVersionSynthetic: capabilityDiscoveryContext.capabilities._synthetic
6333
6639
  },
6640
+ ...negotiatedRelease?.downgraded && { negotiatedAdcpVersion: negotiatedRelease.version },
6334
6641
  productPolicyRequest: productPolicyRequestSnapshot(normalizedParams),
6335
6642
  accountRegistry: this.accountRegistryContext(taskName, normalizedParams, effectiveOptions),
6336
6643
  ...effectiveOptions?.taskId !== void 0 && { optionTaskId: effectiveOptions.taskId },
@@ -8025,8 +8332,9 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
8025
8332
  * first (cold-cache) call; the configured-version gate above still catches
8026
8333
  * an explicitly pre-3.1 client before validation or network I/O.
8027
8334
  */
8028
- assertRequestSupportedByTargetVersion(taskName, params, capabilities, canonicalCreativeInvocation = false, serverVersion) {
8029
- if (taskName === "sync_accounts") {
8335
+ assertRequestSupportedByTargetVersion(taskName, params, capabilities, canonicalCreativeInvocation = false, serverVersion, negotiated) {
8336
+ const reducesAccountIdentity = this.accountIdentityDowngrade === "reduce" && negotiated?.downgraded === true;
8337
+ if (taskName === "sync_accounts" && !reducesAccountIdentity) {
8030
8338
  const advertisedVersions2 = capabilities?.supportedVersions ?? [];
8031
8339
  const servedVersion = capabilities?.servedVersion ?? capabilities?._raw?.adcp_version;
8032
8340
  const supports32 = serverVersion !== "v2" && (typeof servedVersion === "string" ? (0, import_adcp_version_config.isAdcpVersionAtLeast)(servedVersion, "3.2.0-0") : advertisedVersions2.some((version) => (0, import_adcp_version_config.isAdcpVersionAtLeast)(version, "3.2.0-0")));
@@ -8196,7 +8504,7 @@ Hint: this usually means agent_uri does not include the MCP endpoint path. The S
8196
8504
  }
8197
8505
  const supportedVersions = capabilities.supportedVersions;
8198
8506
  if (supportedVersions !== void 0 && supportedVersions.length > 0) {
8199
- if (!(0, import_adcp_version_config.isAdcpVersionSupported)(this.resolvedAdcpVersion, supportedVersions)) {
8507
+ if (!(0, import_adcp_version_config.isAdcpVersionSupported)(this.resolvedAdcpVersion, supportedVersions) && this.negotiateCallRelease(capabilities, options) === void 0) {
8200
8508
  throw new import_errors.VersionUnsupportedError(taskType, "version", capabilities.version, this.agent.agent_uri);
8201
8509
  }
8202
8510
  } else {