Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/add-dated-delivery-simulations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"adcontextprotocol": minor
---

Add optional `delivery_date` support to `simulate_delivery` so compliance
storyboards can seed deterministic delivery rows and verify half-open
`get_media_buy_delivery` date filters. The training agent now aggregates dated
simulations within `[start_date, end_date)` while preserving cumulative behavior
for legacy undated simulations. Date-bounded training-agent responses now follow
the task's documented half-open range semantics, including an exclusive midnight
`reporting_period.end` and rejection of empty ranges where the dates are equal.
7 changes: 7 additions & 0 deletions .changeset/verify-deterministic-read-filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"adcontextprotocol": patch
---

Add behavioral conformance scenarios for media-buy status and ID filters,
half-open delivery date ranges, and sales-agent creative status and assignment
filters. Update the training agent to honor `list_creatives` media-buy filters.
13 changes: 12 additions & 1 deletion docs/building/by-layer/L3/comply-test-controller.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Sellers that implement compliance test controller MUST:
},
"params": {
"type": "object",
"description": "Scenario-specific parameters. Omit for list_scenarios. force_creative_status: {creative_id, status, rejection_reason?}. force_creative_purge: {creative_id, purge_kind?, reason_code?, reason_detail?}. force_account_status: {account_id, status}. force_media_buy_status: {media_buy_id, status, rejection_reason?}. force_create_media_buy_arm: {arm, task_id?, message?} - task_id required when arm = submitted. force_get_products_arm: {arm, task_id?, message?, reason?, suggestions?} - task_id required when arm = submitted; reason required when arm = rejected. force_get_signals_arm: {arm, task_id?, message?} - task_id required when arm = submitted. force_task_completion: {task_id, result}. force_session_status: {session_id, status, termination_reason?}. simulate_delivery: {media_buy_id, impressions?, clicks?, reported_spend?, conversions?, reach?, frequency?, reach_window?, viewability?}. simulate_budget_spend: {account_id|media_buy_id, spend_percentage}. seed_account: {account_id, fixture?}. seed_rights_grant: {rights_id, fixture?}. seed_product: {product_id, fixture?}. seed_pricing_option: {product_id, pricing_option_id, fixture?}. seed_creative: {creative_id, fixture?}. seed_plan: {plan_id, fixture?}. seed_media_buy: {media_buy_id, fixture?}. seed_creative_format: {format_id, fixture?} (deprecated named-format compatibility tests only). seed_measurement_catalog: {vendor, metrics[]}. query_upstream_traffic: {since_timestamp?, endpoint_pattern?, limit?, attestation_mode?, identifier_value_digests?}. query_provenance_audit_observations: {creative_id}. force_upstream_unavailable: {tool, upstream_name?}."
"description": "Scenario-specific parameters. Omit for list_scenarios. force_creative_status: {creative_id, status, rejection_reason?}. force_creative_purge: {creative_id, purge_kind?, reason_code?, reason_detail?}. force_account_status: {account_id, status}. force_media_buy_status: {media_buy_id, status, rejection_reason?}. force_create_media_buy_arm: {arm, task_id?, message?} - task_id required when arm = submitted. force_get_products_arm: {arm, task_id?, message?, reason?, suggestions?} - task_id required when arm = submitted; reason required when arm = rejected. force_get_signals_arm: {arm, task_id?, message?} - task_id required when arm = submitted. force_task_completion: {task_id, result}. force_session_status: {session_id, status, termination_reason?}. simulate_delivery: {media_buy_id, delivery_date?, impressions?, clicks?, reported_spend?, conversions?, reach?, frequency?, reach_window?, viewability?}. simulate_budget_spend: {account_id|media_buy_id, spend_percentage}. seed_account: {account_id, fixture?}. seed_rights_grant: {rights_id, fixture?}. seed_product: {product_id, fixture?}. seed_pricing_option: {product_id, pricing_option_id, fixture?}. seed_creative: {creative_id, fixture?}. seed_plan: {plan_id, fixture?}. seed_media_buy: {media_buy_id, fixture?}. seed_creative_format: {format_id, fixture?} (deprecated named-format compatibility tests only). seed_measurement_catalog: {vendor, metrics[]}. query_upstream_traffic: {since_timestamp?, endpoint_pattern?, limit?, attestation_mode?, identifier_value_digests?}. query_provenance_audit_observations: {creative_id}. force_upstream_unavailable: {tool, upstream_name?}."
}
},
"required": ["scenario"]
Expand Down Expand Up @@ -428,6 +428,7 @@ Injects synthetic delivery data for a media buy. Subsequent calls to [`get_media
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `media_buy_id` | string | Yes | Media buy to add delivery to |
| `delivery_date` | string (`YYYY-MM-DD`) | No | UTC calendar date assigned to this simulated batch for reporting-period tests |
| `impressions` | integer | No | Impressions to simulate |
| `clicks` | integer | No | Clicks to simulate |
| `reported_spend` | object | No | `{ amount: number, currency: string }` — spend as reported in delivery data, does not affect budget |
Expand All @@ -437,13 +438,22 @@ Injects synthetic delivery data for a media buy. Subsequent calls to [`get_media
| `reach_window` | object | No | Measurement window for simulated reach/frequency. Shape: `{ kind: "cumulative" }`, `{ kind: "period", period: Duration }`, or `{ kind: "rolling", period: Duration }` |
| `viewability` | object | No | Viewability block to surface at `totals.viewability`, including `measurable_impressions`, `viewable_impressions`, `viewable_rate`, `viewed_seconds`, and `standard`. `standard` SHOULD be supplied whenever measured viewability values are present |

When `delivery_date` is present, the controller records the batch as a dated
snapshot. A later `get_media_buy_delivery` request with date bounds includes
snapshots whose dates fall in the exact half-open interval
`[start_date, end_date)`: `start_date` is inclusive and `end_date` is exclusive.
Omitting date bounds preserves cumulative reporting. Implementations that only
store legacy undated simulations continue to expose their cumulative totals
when a caller supplies date bounds.

**Example:**

```json
{
"scenario": "simulate_delivery",
"params": {
"media_buy_id": "mb-789",
"delivery_date": "2026-04-15",
"impressions": 10000,
"clicks": 150,
"reach": 4000,
Expand Down Expand Up @@ -738,6 +748,7 @@ literal when one of those strategies binds a seller-issued ID. See
{
"success": true,
"simulated": {
"delivery_date": "2026-04-15",
"impressions": 10000,
"clicks": 150,
"reach": 4000,
Expand Down
105 changes: 101 additions & 4 deletions server/src/training-agent/comply-test-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import {
TestControllerError,
SESSION_ENTRY_CAP,
createSeedFixtureCache,
enforceMapCap,
handleTestControllerRequest,
Expand Down Expand Up @@ -208,6 +209,40 @@ export function getDeliverySimulation(session: SessionState, mediaBuyId: string)
return session.complyExtensions.deliverySimulations.get(mediaBuyId);
}

/**
* Get simulated delivery attributable to a half-open reporting period.
* Undated legacy simulations retain their cumulative behavior until a caller
* starts supplying delivery_date snapshots for the media buy.
*/
export function getDeliverySimulationForPeriod(
session: SessionState,
mediaBuyId: string,
start: Date,
end: Date,
): ComplyDeliveryAccumulator | undefined {
const cumulative = getDeliverySimulation(session, mediaBuyId);
if (!cumulative?.datedSimulations?.length) return cumulative;

const filtered: ComplyDeliveryAccumulator = {
impressions: 0,
clicks: 0,
reportedSpend: { amount: 0, currency: cumulative.reportedSpend.currency },
conversions: 0,
};
for (const simulation of cumulative.datedSimulations) {
const timestamp = new Date(`${simulation.deliveryDate}T00:00:00.000Z`).getTime();
if (timestamp < start.getTime() || timestamp >= end.getTime()) continue;
const { impressions, clicks, conversions, reportedSpend, ...extensions } = simulation.metrics;
filtered.impressions += impressions;
filtered.clicks += clicks;
filtered.conversions += conversions;
filtered.reportedSpend.amount += reportedSpend.amount;
filtered.reportedSpend.currency = reportedSpend.currency;
Object.assign(filtered, extensions);
}
return filtered;
}

/** Get budget simulation data for an entity (used by get_account_financials). */
export function getBudgetSimulation(session: SessionState, entityId: string): ComplyBudgetSimulation | undefined {
return session.complyExtensions.budgetSimulations.get(entityId);
Expand All @@ -229,6 +264,33 @@ function getOrCreateDeliveryAccumulator(session: SessionState, mediaBuyId: strin
return cumulative;
}

function isCanonicalDeliveryDate(value: unknown): value is string {
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
const parsed = new Date(`${value}T00:00:00.000Z`);
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
}

function deliverySimulationSnapshot(
params: Record<string, unknown>,
currency: string,
): Omit<ComplyDeliveryAccumulator, 'datedSimulations'> {
const reportedSpend = isRecord(params.reported_spend) ? params.reported_spend : undefined;
const snapshot: Omit<ComplyDeliveryAccumulator, 'datedSimulations'> = {
impressions: typeof params.impressions === 'number' ? params.impressions : 0,
clicks: typeof params.clicks === 'number' ? params.clicks : 0,
conversions: typeof params.conversions === 'number' ? params.conversions : 0,
reportedSpend: {
amount: typeof reportedSpend?.amount === 'number' ? reportedSpend.amount : 0,
currency: typeof reportedSpend?.currency === 'string' ? reportedSpend.currency : currency,
},
};
applyExtendedDeliveryParams(snapshot, params);
if (Array.isArray(params.vendor_metric_values)) {
snapshot.vendorMetricValues = params.vendor_metric_values;
}
return snapshot;
}

type VendorMetricIdentity = {
vendor: { domain: string; brand_id?: string };
metric_id: string;
Expand Down Expand Up @@ -806,6 +868,19 @@ function createStore(session: SessionState, sessionKey: string, principal?: stri
const reportedSpend = params.reported_spend;
const typedParams = params as Record<string, unknown>;

const deliveryDate = typedParams.delivery_date;
if (deliveryDate !== undefined && !isCanonicalDeliveryDate(deliveryDate)) {
throw new TestControllerError('INVALID_PARAMS', 'delivery_date must be a real calendar date in YYYY-MM-DD format');
}

const existing = getDeliverySimulation(session, mediaBuyId);
if (deliveryDate !== undefined && (existing?.datedSimulations?.length ?? 0) >= SESSION_ENTRY_CAP) {
throw new TestControllerError(
'INVALID_STATE',
`Cannot add more than ${SESSION_ENTRY_CAP} dated delivery simulations to one media buy`,
);
}

const cumulative = getOrCreateDeliveryAccumulator(session, mediaBuyId, reportedSpend?.currency || mb.currency);

cumulative.impressions += impressions;
Expand All @@ -816,6 +891,13 @@ function createStore(session: SessionState, sessionKey: string, principal?: stri
cumulative.reportedSpend.currency = reportedSpend.currency;
}
applyExtendedDeliveryParams(cumulative, typedParams);
if (deliveryDate !== undefined) {
cumulative.datedSimulations ??= [];
cumulative.datedSimulations.push({
deliveryDate,
metrics: deliverySimulationSnapshot(typedParams, reportedSpend?.currency || mb.currency),
});
}

const simulated: Record<string, unknown> = {};
if (impressions) simulated.impressions = impressions;
Expand All @@ -831,6 +913,7 @@ function createStore(session: SessionState, sessionKey: string, principal?: stri
if (typedParams.is_final !== undefined) simulated.is_final = typedParams.is_final;
if (typedParams.finalized_at !== undefined) simulated.finalized_at = typedParams.finalized_at;
if (typedParams.measurement_window !== undefined) simulated.measurement_window = typedParams.measurement_window;
if (deliveryDate !== undefined) simulated.delivery_date = deliveryDate;

return {
success: true,
Expand Down Expand Up @@ -1223,6 +1306,8 @@ export async function handleComplyTestController(args: ToolArgs, ctx: TrainingCo
const targetsControllerFixtureState = scenario === 'seed_product'
|| scenario === 'seed_pricing_option'
|| scenario === 'seed_measurement_catalog';
const targetsPublicTaskState = scenario === 'seed_media_buy'
|| scenario === 'seed_creative';
// The frozen 3.0 runner injects a synthetic natural account into controller
// and fixture calls, sometimes without copying its brand to the top level.
// Platform methods on that compatibility surface historically key by brand.
Expand All @@ -1237,7 +1322,8 @@ export async function handleComplyTestController(args: ToolArgs, ctx: TrainingCo
? args.account.account_id
: undefined;
let staticFixtureAccount: ToolArgs['account'] | undefined;
if (targetsControllerFixtureState && ctx.principal?.startsWith('static:') && args.account) {
let staticTaskAccount: ToolArgs['account'] | undefined;
if ((targetsControllerFixtureState || targetsPublicTaskState) && ctx.principal?.startsWith('static:') && args.account) {
try {
const canonical = canonicalizeAccountRef(args.account);
if (canonical.kind === 'natural' && canonical.sandbox) {
Expand All @@ -1246,11 +1332,20 @@ export async function handleComplyTestController(args: ToolArgs, ctx: TrainingCo
// task examples may name the buyer operator. Canonicalize only this
// fixture projection to the brand-owned sandbox partition; real
// principals keep the complete natural account identity.
staticFixtureAccount = {
const brandOwnedAccount = {
brand: canonical.brand,
operator: canonical.brand.domain,
sandbox: true,
};
if (targetsControllerFixtureState) {
staticFixtureAccount = { ...brandOwnedAccount, sandbox: true };
} else {
// The SDK strips the controller-only sandbox assertion before
// ordinary media-buy reads. Store public demo entity fixtures in
// that exact brand-owned task partition so seed_media_buy and
// seed_creative remain observable without projecting entity state
// across the isolated controller-fixture boundary.
staticTaskAccount = { ...brandOwnedAccount, sandbox: false };
}
}
} catch {
staticFixtureAccount = undefined;
Expand All @@ -1268,7 +1363,9 @@ export async function handleComplyTestController(args: ToolArgs, ctx: TrainingCo
? { ...args, account: { account_id: opaqueAccountId }, brand: undefined }
: staticFixtureAccount
? { ...args, account: staticFixtureAccount }
: args;
: staticTaskAccount
? { ...args, account: staticTaskAccount }
: args;
let sessionKey = targetsGetProductsState
? getProductsSessionKeyFromArgs(sessionArgs, ctx.mode, ctx.userId, ctx.moduleId)
: sessionKeyFromArgs(
Expand Down
35 changes: 27 additions & 8 deletions server/src/training-agent/task-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1778,6 +1778,7 @@ import {
COMPLY_TEST_CONTROLLER_TOOL,
handleComplyTestController,
getDeliverySimulation,
getDeliverySimulationForPeriod,
getAccountStatus,
getSeededCreativeFormats,
} from './comply-test-controller.js';
Expand Down Expand Up @@ -8782,10 +8783,10 @@ export async function handleGetMediaBuyDelivery(args: ToolArgs, ctx: TrainingCon
const start = new Date(mb.startTime);
const end = new Date(mb.endTime);
const reportingStart = req.start_date ? new Date(`${req.start_date}T00:00:00.000Z`) : start;
const reportingEnd = req.end_date ? new Date(`${req.end_date}T23:59:59.999Z`) : now;
if (req.start_date && req.end_date && reportingStart.getTime() > reportingEnd.getTime()) {
const reportingEnd = req.end_date ? new Date(`${req.end_date}T00:00:00.000Z`) : now;
Comment thread
bokelley marked this conversation as resolved.
if (req.start_date && req.end_date && reportingStart.getTime() >= reportingEnd.getTime()) {
return {
errors: [{ code: 'INVALID_REQUEST', message: 'start_date must be on or before end_date', field: 'start_date' }],
errors: [{ code: 'VALIDATION_ERROR', message: 'start_date must be before end_date', field: 'start_date' }],
Comment thread
bokelley marked this conversation as resolved.
};
Comment thread
bokelley marked this conversation as resolved.
}
const durationMs = end.getTime() - start.getTime();
Expand All @@ -8795,7 +8796,9 @@ export async function handleGetMediaBuyDelivery(args: ToolArgs, ctx: TrainingCon

// Read simulated delivery upfront so vendor_metric_values can be spread into
// per-package entries inside the map below.
const simDeliveryEarly = getDeliverySimulation(session, mb.mediaBuyId);
const simDeliveryEarly = req.start_date || req.end_date
? getDeliverySimulationForPeriod(session, mb.mediaBuyId, reportingStart, reportingEnd)
: getDeliverySimulation(session, mb.mediaBuyId);

// Build per-package metrics
let totalImpressions = 0;
Expand Down Expand Up @@ -8830,8 +8833,10 @@ export async function handleGetMediaBuyDelivery(args: ToolArgs, ctx: TrainingCon

const { model: pricingModel, rate } = derivePricing(pkg, productMap);
const isRevenueShare = pricingModel === 'revenue_share';
const useScopedSimulation = simDelivery !== undefined
&& Boolean(req.start_date || req.end_date);
const budget = pkg.budget;
const spend = isRevenueShare
const spend = isRevenueShare || useScopedSimulation
? (simDelivery?.reportedSpend.amount ?? 0)
: Math.round(budget * elapsed * 100) / 100;

Expand All @@ -8847,12 +8852,14 @@ export async function handleGetMediaBuyDelivery(args: ToolArgs, ctx: TrainingCon
else if (channels?.some(c => ['print'].includes(c))) ctr = 0;
else ctr = 0.001;

const impressions = isRevenueShare
const impressions = isRevenueShare || useScopedSimulation
? (simDelivery?.impressions ?? 0)
: rate > 0 ? Math.round((spend / rate) * 1000) : 0;
const clicks = isRevenueShare ? (simDelivery?.clicks ?? 0) : Math.round(impressions * ctr);
const clicks = isRevenueShare || useScopedSimulation
? (simDelivery?.clicks ?? 0)
: Math.round(impressions * ctr);

if (!isRevenueShare) {
if (!isRevenueShare && !useScopedSimulation) {
totalImpressions += impressions;
totalSpend += spend;
totalClicks += clicks;
Expand Down Expand Up @@ -9376,6 +9383,7 @@ function accountRefsOverlap(stored: AccountRef | undefined, requested: AccountRe
type CreativeListFilters = {
creative_ids?: string[];
statuses?: string[];
media_buy_ids?: string[];
format_ids?: FormatID[];
asset_types?: string[];
};
Expand Down Expand Up @@ -9499,6 +9507,17 @@ export async function handleListCreatives(args: ToolArgs, ctx: TrainingContext)
const statuses = new Set(filters.statuses);
creatives = creatives.filter(c => statuses.has(c.status));
}
if (filters.media_buy_ids?.length) {
const requestedMediaBuyIds = new Set(filters.media_buy_ids);
const assignedCreativeIds = new Set<string>();
for (const mediaBuy of session.mediaBuys.values()) {
if (!requestedMediaBuyIds.has(mediaBuy.mediaBuyId)) continue;
for (const pkg of mediaBuy.packages) {
for (const creativeId of pkg.creativeAssignments) assignedCreativeIds.add(creativeId);
}
}
creatives = creatives.filter(c => assignedCreativeIds.has(c.creativeId));
}
const formatKinds = (req.filters as unknown as { format_kinds?: string[] } | undefined)?.format_kinds;
const filterProjectionAdapters = formatKinds?.length || filters.format_ids?.length
? creativeProjectionAdapters()
Expand Down
7 changes: 7 additions & 0 deletions server/src/training-agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,13 @@ export interface ComplyDeliveryAccumulator {
vendor: { domain: string; brand_id?: string };
metric_id: string;
}>>;
/** Per-call snapshots with a UTC delivery date for deterministic range tests. */
datedSimulations?: ComplyDatedDeliverySimulation[];
}

export interface ComplyDatedDeliverySimulation {
deliveryDate: string;
metrics: Omit<ComplyDeliveryAccumulator, 'datedSimulations'>;
}

export interface ComplyBudgetSimulation {
Expand Down
Loading
Loading