/**
* UCP Capability Negotiation
* Implements the intersection algorithm between agent and server capabilities.
*/
import type {
AgentProfile,
UCPCapability,
UCPPaymentHandler,
NegotiationResult,
DiscountOffer,
ShippingOption,
} from '../types.js';
/** Well-known parent capability prefixes for extension pruning. */
const PARENT_CAPABILITIES = [
'dev.ucp.shopping.checkout',
'dev.ucp.shopping.catalog',
'dev.ucp.shopping.order',
'dev.ucp.shopping.fulfillment',
] as const;
/**
* Returns true if `capName` is a child extension of one of the given parent names.
* e.g., "dev.ucp.shopping.checkout.loyalty" is a child of "dev.ucp.shopping.checkout".
*/
function isExtensionOf(capName: string, parentNames: Set<string>): boolean {
for (const parent of parentNames) {
if (capName.startsWith(parent + '.')) {
return true;
}
}
return false;
}
/**
* Checks whether a capability name is a base (non-extension) capability.
*/
function isBaseCapability(capName: string): boolean {
return PARENT_CAPABILITIES.some((p) => p === capName);
}
/**
* Default shipping options offered during negotiation.
*/
function getDefaultShippingOptions(): ShippingOption[] {
return [
{
id: 'standard',
name: 'Standard Shipping',
price: 599,
currency: 'USD',
estimated_days_min: 5,
estimated_days_max: 7,
},
{
id: 'express',
name: 'Express Shipping',
price: 1499,
currency: 'USD',
estimated_days_min: 2,
estimated_days_max: 3,
},
{
id: 'overnight',
name: 'Overnight Shipping',
price: 2999,
currency: 'USD',
estimated_days_min: 1,
estimated_days_max: 1,
},
];
}
/**
* Default discount offers surfaced during negotiation.
*/
function getDefaultDiscounts(): DiscountOffer[] {
return [
{
code: 'AGENT10',
type: 'percentage',
value: 10,
description: '10% off for agent-assisted purchases',
min_purchase: 5000, // $50.00 in minor units
},
];
}
/**
* Negotiate capabilities between an agent profile and the server's capabilities.
*
* Algorithm:
* 1. Find the intersection: server capabilities whose name matches an agent capability.
* 2. Prune extensions whose parent capability is NOT in the intersection.
* 3. Return active capabilities plus default discounts, shipping, and payment handlers.
*/
export function negotiateCapabilities(
agentProfile: AgentProfile,
serverCapabilities: UCPCapability[],
serverPaymentHandlers: UCPPaymentHandler[] = [],
): NegotiationResult {
const agentCapNames = new Set(agentProfile.capabilities);
// Step 1: Intersection — server caps where agent declares the same name
const intersection = serverCapabilities.filter((cap) => agentCapNames.has(cap.name));
// Step 2: Prune extensions whose parent isn't in the intersection
const intersectedNames = new Set(intersection.map((c) => c.name));
const baseNamesInIntersection = new Set(
[...intersectedNames].filter((name) => isBaseCapability(name)),
);
const active = intersection.filter((cap) => {
// Keep all base capabilities
if (isBaseCapability(cap.name)) return true;
// Keep extensions only if their parent is in the intersection
return isExtensionOf(cap.name, baseNamesInIntersection);
});
return {
active_capabilities: active,
available_discounts: getDefaultDiscounts(),
shipping_options: getDefaultShippingOptions(),
payment_handlers: serverPaymentHandlers,
};
}