@adcp/sdk 5.25.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -7
- package/dist/lib/compliance-fixtures/index.d.ts +1 -1
- package/dist/lib/compliance-fixtures/index.js +1 -1
- package/dist/lib/core/AgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.js +15 -0
- package/dist/lib/core/SingleAgentClient.js.map +1 -1
- package/dist/lib/core/TaskExecutor.d.ts +7 -0
- package/dist/lib/core/TaskExecutor.d.ts.map +1 -1
- package/dist/lib/core/TaskExecutor.js +9 -2
- package/dist/lib/core/TaskExecutor.js.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +7 -8
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/schemas/index.d.ts +1 -1
- package/dist/lib/schemas/index.js +1 -1
- package/dist/lib/server/create-adcp-server.d.ts +129 -11
- package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
- package/dist/lib/server/create-adcp-server.js +112 -2
- package/dist/lib/server/create-adcp-server.js.map +1 -1
- package/dist/lib/server/ctx-metadata/backends/memory.d.ts +27 -0
- package/dist/lib/server/ctx-metadata/backends/memory.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/backends/memory.js +72 -0
- package/dist/lib/server/ctx-metadata/backends/memory.js.map +1 -0
- package/dist/lib/server/ctx-metadata/backends/pg.d.ts +62 -0
- package/dist/lib/server/ctx-metadata/backends/pg.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/backends/pg.js +145 -0
- package/dist/lib/server/ctx-metadata/backends/pg.js.map +1 -0
- package/dist/lib/server/ctx-metadata/index.d.ts +15 -0
- package/dist/lib/server/ctx-metadata/index.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/index.js +28 -0
- package/dist/lib/server/ctx-metadata/index.js.map +1 -0
- package/dist/lib/server/ctx-metadata/store.d.ts +177 -0
- package/dist/lib/server/ctx-metadata/store.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/store.js +327 -0
- package/dist/lib/server/ctx-metadata/store.js.map +1 -0
- package/dist/lib/server/ctx-metadata/wire-shape.d.ts +55 -0
- package/dist/lib/server/ctx-metadata/wire-shape.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/wire-shape.js +121 -0
- package/dist/lib/server/ctx-metadata/wire-shape.js.map +1 -0
- package/dist/lib/server/decisioning/account.d.ts +309 -0
- package/dist/lib/server/decisioning/account.d.ts.map +1 -0
- package/dist/lib/server/decisioning/account.js +102 -0
- package/dist/lib/server/decisioning/account.js.map +1 -0
- package/dist/lib/server/decisioning/admin-router.d.ts +75 -0
- package/dist/lib/server/decisioning/admin-router.d.ts.map +1 -0
- package/dist/lib/server/decisioning/admin-router.js +120 -0
- package/dist/lib/server/decisioning/admin-router.js.map +1 -0
- package/dist/lib/server/decisioning/assembly-helpers.d.ts +204 -0
- package/dist/lib/server/decisioning/assembly-helpers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/assembly-helpers.js +173 -0
- package/dist/lib/server/decisioning/assembly-helpers.js.map +1 -0
- package/dist/lib/server/decisioning/async-outcome.d.ts +154 -0
- package/dist/lib/server/decisioning/async-outcome.d.ts.map +1 -0
- package/dist/lib/server/decisioning/async-outcome.js +239 -0
- package/dist/lib/server/decisioning/async-outcome.js.map +1 -0
- package/dist/lib/server/decisioning/capabilities.d.ts +251 -0
- package/dist/lib/server/decisioning/capabilities.d.ts.map +1 -0
- package/dist/lib/server/decisioning/capabilities.js +16 -0
- package/dist/lib/server/decisioning/capabilities.js.map +1 -0
- package/dist/lib/server/decisioning/context.d.ts +212 -0
- package/dist/lib/server/decisioning/context.d.ts.map +1 -0
- package/dist/lib/server/decisioning/context.js +26 -0
- package/dist/lib/server/decisioning/context.js.map +1 -0
- package/dist/lib/server/decisioning/errors-typed.d.ts +104 -0
- package/dist/lib/server/decisioning/errors-typed.d.ts.map +1 -0
- package/dist/lib/server/decisioning/errors-typed.js +304 -0
- package/dist/lib/server/decisioning/errors-typed.js.map +1 -0
- package/dist/lib/server/decisioning/helpers.d.ts +131 -0
- package/dist/lib/server/decisioning/helpers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/helpers.js +134 -0
- package/dist/lib/server/decisioning/helpers.js.map +1 -0
- package/dist/lib/server/decisioning/index.d.ts +46 -0
- package/dist/lib/server/decisioning/index.d.ts.map +1 -0
- package/dist/lib/server/decisioning/index.js +120 -0
- package/dist/lib/server/decisioning/index.js.map +1 -0
- package/dist/lib/server/decisioning/list-helpers.d.ts +53 -0
- package/dist/lib/server/decisioning/list-helpers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/list-helpers.js +96 -0
- package/dist/lib/server/decisioning/list-helpers.js.map +1 -0
- package/dist/lib/server/decisioning/manifest-helpers.d.ts +56 -0
- package/dist/lib/server/decisioning/manifest-helpers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/manifest-helpers.js +78 -0
- package/dist/lib/server/decisioning/manifest-helpers.js.map +1 -0
- package/dist/lib/server/decisioning/pagination.d.ts +21 -0
- package/dist/lib/server/decisioning/pagination.d.ts.map +1 -0
- package/dist/lib/server/decisioning/pagination.js +12 -0
- package/dist/lib/server/decisioning/pagination.js.map +1 -0
- package/dist/lib/server/decisioning/platform.d.ts +188 -0
- package/dist/lib/server/decisioning/platform.d.ts.map +1 -0
- package/dist/lib/server/decisioning/platform.js +19 -0
- package/dist/lib/server/decisioning/platform.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts +510 -0
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/from-platform.js +2196 -0
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts +114 -0
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.js +247 -0
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.d.ts +32 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.js +127 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/task-registry.d.ts +105 -0
- package/dist/lib/server/decisioning/runtime/task-registry.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/task-registry.js +96 -0
- package/dist/lib/server/decisioning/runtime/task-registry.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/to-context.d.ts +54 -0
- package/dist/lib/server/decisioning/runtime/to-context.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/to-context.js +166 -0
- package/dist/lib/server/decisioning/runtime/to-context.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/validate-platform.d.ts +20 -0
- package/dist/lib/server/decisioning/runtime/validate-platform.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/validate-platform.js +93 -0
- package/dist/lib/server/decisioning/runtime/validate-platform.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/audiences.d.ts +72 -0
- package/dist/lib/server/decisioning/specialisms/audiences.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/audiences.js +15 -0
- package/dist/lib/server/decisioning/specialisms/audiences.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/brand-rights.d.ts +92 -0
- package/dist/lib/server/decisioning/specialisms/brand-rights.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/brand-rights.js +28 -0
- package/dist/lib/server/decisioning/specialisms/brand-rights.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/campaign-governance.d.ts +67 -0
- package/dist/lib/server/decisioning/specialisms/campaign-governance.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/campaign-governance.js +31 -0
- package/dist/lib/server/decisioning/specialisms/campaign-governance.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/content-standards.d.ts +78 -0
- package/dist/lib/server/decisioning/specialisms/content-standards.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/content-standards.js +35 -0
- package/dist/lib/server/decisioning/specialisms/content-standards.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts +81 -0
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.js +28 -0
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/creative.d.ts +144 -0
- package/dist/lib/server/decisioning/specialisms/creative.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/creative.js +19 -0
- package/dist/lib/server/decisioning/specialisms/creative.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/lists.d.ts +61 -0
- package/dist/lib/server/decisioning/specialisms/lists.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/lists.js +30 -0
- package/dist/lib/server/decisioning/specialisms/lists.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/sales.d.ts +163 -0
- package/dist/lib/server/decisioning/specialisms/sales.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/sales.js +64 -0
- package/dist/lib/server/decisioning/specialisms/sales.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/signals.d.ts +64 -0
- package/dist/lib/server/decisioning/specialisms/signals.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/signals.js +28 -0
- package/dist/lib/server/decisioning/specialisms/signals.js.map +1 -0
- package/dist/lib/server/decisioning/start-time.d.ts +76 -0
- package/dist/lib/server/decisioning/start-time.d.ts.map +1 -0
- package/dist/lib/server/decisioning/start-time.js +81 -0
- package/dist/lib/server/decisioning/start-time.js.map +1 -0
- package/dist/lib/server/decisioning/status-changes.d.ts +165 -0
- package/dist/lib/server/decisioning/status-changes.d.ts.map +1 -0
- package/dist/lib/server/decisioning/status-changes.js +131 -0
- package/dist/lib/server/decisioning/status-changes.js.map +1 -0
- package/dist/lib/server/decisioning/status-mappers.d.ts +46 -0
- package/dist/lib/server/decisioning/status-mappers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/status-mappers.js +46 -0
- package/dist/lib/server/decisioning/status-mappers.js.map +1 -0
- package/dist/lib/server/decisioning/tenant-registry.d.ts +289 -0
- package/dist/lib/server/decisioning/tenant-registry.d.ts.map +1 -0
- package/dist/lib/server/decisioning/tenant-registry.js +503 -0
- package/dist/lib/server/decisioning/tenant-registry.js.map +1 -0
- package/dist/lib/server/express-adapter.d.ts +1 -1
- package/dist/lib/server/express-adapter.js +1 -1
- package/dist/lib/server/governance.d.ts +1 -1
- package/dist/lib/server/governance.js +1 -1
- package/dist/lib/server/idempotency/store.d.ts +1 -1
- package/dist/lib/server/idempotency/store.js +1 -1
- package/dist/lib/server/index.d.ts +9 -2
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +79 -4
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/server/legacy/v5/index.d.ts +38 -0
- package/dist/lib/server/legacy/v5/index.d.ts.map +1 -0
- package/dist/lib/server/legacy/v5/index.js +60 -0
- package/dist/lib/server/legacy/v5/index.js.map +1 -0
- package/dist/lib/server/normalize-errors.d.ts +88 -0
- package/dist/lib/server/normalize-errors.d.ts.map +1 -0
- package/dist/lib/server/normalize-errors.js +146 -0
- package/dist/lib/server/normalize-errors.js.map +1 -0
- package/dist/lib/server/pick-safe-details.d.ts +90 -0
- package/dist/lib/server/pick-safe-details.d.ts.map +1 -0
- package/dist/lib/server/pick-safe-details.js +148 -0
- package/dist/lib/server/pick-safe-details.js.map +1 -0
- package/dist/lib/server/postgres-state-store.d.ts +1 -1
- package/dist/lib/server/postgres-state-store.js +1 -1
- package/dist/lib/server/responses.d.ts +38 -0
- package/dist/lib/server/responses.d.ts.map +1 -1
- package/dist/lib/server/responses.js +38 -0
- package/dist/lib/server/responses.js.map +1 -1
- package/dist/lib/server/state-store.d.ts +1 -1
- package/dist/lib/server/state-store.js +1 -1
- package/dist/lib/server/test-controller.d.ts +10 -3
- package/dist/lib/server/test-controller.d.ts.map +1 -1
- package/dist/lib/server/test-controller.js +10 -3
- package/dist/lib/server/test-controller.js.map +1 -1
- package/dist/lib/testing/comply-controller.d.ts +47 -1
- package/dist/lib/testing/comply-controller.d.ts.map +1 -1
- package/dist/lib/testing/comply-controller.js +11 -4
- package/dist/lib/testing/comply-controller.js.map +1 -1
- package/dist/lib/testing/index.d.ts +1 -1
- package/dist/lib/testing/index.d.ts.map +1 -1
- package/dist/lib/testing/index.js.map +1 -1
- package/dist/lib/testing/personas/index.d.ts +143 -0
- package/dist/lib/testing/personas/index.d.ts.map +1 -0
- package/dist/lib/testing/personas/index.js +190 -0
- package/dist/lib/testing/personas/index.js.map +1 -0
- package/dist/lib/testing/storyboard/index.d.ts +1 -1
- package/dist/lib/testing/storyboard/index.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/index.js +3 -2
- package/dist/lib/testing/storyboard/index.js.map +1 -1
- package/dist/lib/testing/storyboard/runner.d.ts +13 -0
- package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/runner.js +179 -7
- package/dist/lib/testing/storyboard/runner.js.map +1 -1
- package/dist/lib/types/asset-instances.d.ts +1 -0
- package/dist/lib/types/asset-instances.d.ts.map +1 -1
- package/dist/lib/types/core.generated.d.ts +203 -98
- package/dist/lib/types/core.generated.d.ts.map +1 -1
- package/dist/lib/types/core.generated.js +1 -1
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/schemas.generated.d.ts +599 -159
- package/dist/lib/types/schemas.generated.d.ts.map +1 -1
- package/dist/lib/types/schemas.generated.js +175 -94
- package/dist/lib/types/schemas.generated.js.map +1 -1
- package/dist/lib/types/tools.generated.d.ts +315 -46
- package/dist/lib/types/tools.generated.d.ts.map +1 -1
- package/dist/lib/utils/capabilities.d.ts +1 -1
- package/dist/lib/utils/capabilities.d.ts.map +1 -1
- package/dist/lib/utils/capabilities.js +6 -0
- package/dist/lib/utils/capabilities.js.map +1 -1
- package/dist/lib/validation/schema-validator.d.ts +13 -0
- package/dist/lib/validation/schema-validator.d.ts.map +1 -1
- package/dist/lib/validation/schema-validator.js +240 -3
- package/dist/lib/validation/schema-validator.js.map +1 -1
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.d.ts.map +1 -1
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.js.map +1 -1
- package/docs/guides/BUILD-AN-AGENT.md +30 -5
- package/docs/llms.txt +28 -17
- package/examples/README.md +3 -1
- package/examples/decisioning-platform-broadcast-tv.ts +300 -0
- package/examples/decisioning-platform-identity-graph.ts +214 -0
- package/examples/decisioning-platform-mock-seller.ts +332 -0
- package/examples/decisioning-platform-multi-tenant.ts +128 -0
- package/examples/decisioning-platform-programmatic.ts +254 -0
- package/examples/signals-agent.ts +1 -1
- package/package.json +13 -2
- package/skills/build-brand-rights-agent/SKILL.md +10 -3
- package/skills/build-creative-agent/SKILL.md +94 -64
- package/skills/build-decisioning-creative-template/SKILL.md +554 -0
- package/skills/build-decisioning-platform/SKILL.md +304 -0
- package/skills/build-decisioning-platform/advanced/BRAND-RIGHTS.md +25 -0
- package/skills/build-decisioning-platform/advanced/COMPLIANCE.md +23 -0
- package/skills/build-decisioning-platform/advanced/GOVERNANCE.md +24 -0
- package/skills/build-decisioning-platform/advanced/HITL.md +34 -0
- package/skills/build-decisioning-platform/advanced/IDEMPOTENCY.md +52 -0
- package/skills/build-decisioning-platform/advanced/MULTI-TENANT.md +47 -0
- package/skills/build-decisioning-platform/advanced/OAUTH.md +22 -0
- package/skills/build-decisioning-platform/advanced/REFERENCE.md +991 -0
- package/skills/build-decisioning-platform/advanced/SANDBOX.md +24 -0
- package/skills/build-decisioning-platform/advanced/STATE-MACHINE.md +52 -0
- package/skills/build-decisioning-signal-marketplace/SKILL.md +269 -0
- package/skills/build-generative-seller-agent/SKILL.md +89 -53
- package/skills/build-governance-agent/SKILL.md +76 -45
- package/skills/build-retail-media-agent/SKILL.md +87 -62
- package/skills/build-seller-agent/SKILL.md +384 -255
- package/skills/build-seller-agent/deployment.md +5 -3
- package/skills/build-seller-agent/specialisms/audience-sync.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-broadcast-tv.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-guaranteed.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-non-guaranteed.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-proposal-mode.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-social.md +0 -2
- package/skills/build-seller-agent/specialisms/signed-requests.md +0 -2
- package/skills/build-si-agent/SKILL.md +40 -32
- package/skills/build-signals-agent/SKILL.md +139 -92
- package/skills/call-adcp-agent.previous/SKILL.md +5 -0
|
@@ -27,46 +27,61 @@ Every sales-_ specialism (including `sales-social`, `sales-broadcast-tv`, `sales
|
|
|
27
27
|
|
|
28
28
|
**Required tools** (tested by the `media_buy_seller` storyboard bundle at `compliance/cache/3.0.0/protocols/media-buy/`):
|
|
29
29
|
|
|
30
|
-
| Tool | Purpose | `
|
|
30
|
+
| Tool | Purpose | `SalesPlatform` method |
|
|
31
31
|
| ------------------------ | ---------------------------------------------------------------------------------- | ------------------------ |
|
|
32
32
|
| `get_adcp_capabilities` | Declare protocols + specialisms + features | auto (framework) |
|
|
33
|
-
| `sync_accounts` | Advertiser onboarding, per-tenant account creation | `accounts`
|
|
34
|
-
| `list_accounts` | Account lookup by brand/operator; buyers listing their accounts on your platform | `accounts`
|
|
35
|
-
| `get_products` | Product catalog discovery from a brief; returns `{ products: [...] }` | `
|
|
36
|
-
| `list_creative_formats` | Formats your agent accepts | `
|
|
37
|
-
| `create_media_buy` | Accept a campaign with packages, budget, flight dates | `
|
|
38
|
-
| `update_media_buy` | Bid, budget, status, package mutations over the campaign lifecycle | `
|
|
39
|
-
| `get_media_buys` | Read campaigns back with full state (status, budget, packages, targeting overlays) | `
|
|
40
|
-
| `sync_creatives` | Accept creative assets and return per-asset status | `
|
|
41
|
-
| `list_creatives` | Read the creative library back with pagination | `
|
|
42
|
-
| `get_media_buy_delivery` | Delivery + spend reporting with `reporting_period`, per-package billing rows | `
|
|
43
|
-
|
|
44
|
-
**Minimum
|
|
33
|
+
| `sync_accounts` | Advertiser onboarding, per-tenant account creation | `accounts.upsert` |
|
|
34
|
+
| `list_accounts` | Account lookup by brand/operator; buyers listing their accounts on your platform | `accounts.list` |
|
|
35
|
+
| `get_products` | Product catalog discovery from a brief; returns `{ products: [...] }` | `sales.getProducts` |
|
|
36
|
+
| `list_creative_formats` | Formats your agent accepts | `sales.listCreativeFormats` |
|
|
37
|
+
| `create_media_buy` | Accept a campaign with packages, budget, flight dates | `sales.createMediaBuy` |
|
|
38
|
+
| `update_media_buy` | Bid, budget, status, package mutations over the campaign lifecycle | `sales.updateMediaBuy` |
|
|
39
|
+
| `get_media_buys` | Read campaigns back with full state (status, budget, packages, targeting overlays) | `sales.getMediaBuys` |
|
|
40
|
+
| `sync_creatives` | Accept creative assets and return per-asset status | `sales.syncCreatives` |
|
|
41
|
+
| `list_creatives` | Read the creative library back with pagination | `sales.listCreatives` |
|
|
42
|
+
| `get_media_buy_delivery` | Delivery + spend reporting with `reporting_period`, per-package billing rows | `sales.getMediaBuyDelivery` |
|
|
43
|
+
|
|
44
|
+
**Minimum platform skeleton** — every sales-\* seller starts here, then adds specialism-specific behavior on top:
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
import { createAdcpServerFromPlatform, type DecisioningPlatform, type SalesPlatform, type AccountStore } from '@adcp/sdk/server';
|
|
48
|
+
|
|
49
|
+
class MySeller implements DecisioningPlatform<{ networkId: string }, MyMeta> {
|
|
50
|
+
capabilities = {
|
|
51
|
+
specialisms: ['sales-non-guaranteed'] as const,
|
|
52
|
+
pricingModels: ['cpm'] as const,
|
|
53
|
+
channels: ['display'] as const,
|
|
54
|
+
config: { networkId: 'NET_42' },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
accounts: AccountStore<MyMeta> = {
|
|
58
|
+
resolve: async (ref, ctx) => { /* … */ },
|
|
59
|
+
upsert: async (params, ctx) => { /* … */ },
|
|
60
|
+
list: async (params, ctx) => { /* … */ },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
sales: SalesPlatform<MyMeta> = {
|
|
58
64
|
getProducts: async (params, ctx) => { /* … */ },
|
|
59
65
|
listCreativeFormats: async () => ({ formats: [...] }),
|
|
60
66
|
createMediaBuy: async (params, ctx) => { /* … */ },
|
|
61
|
-
updateMediaBuy: async (
|
|
67
|
+
updateMediaBuy: async (id, patch, ctx) => { /* … */ },
|
|
62
68
|
getMediaBuys: async (params, ctx) => { /* … */ },
|
|
63
|
-
syncCreatives: async (
|
|
69
|
+
syncCreatives: async (creatives, ctx) => { /* … */ },
|
|
64
70
|
listCreatives: async (params, ctx) => { /* … */ },
|
|
65
|
-
getMediaBuyDelivery: async (
|
|
66
|
-
}
|
|
71
|
+
getMediaBuyDelivery: async (filter, ctx) => { /* … */ },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const server = createAdcpServerFromPlatform(new MySeller(), {
|
|
76
|
+
name: 'my-seller',
|
|
77
|
+
version: '1.0.0',
|
|
67
78
|
});
|
|
68
79
|
```
|
|
69
80
|
|
|
81
|
+
The `createAdcpServerFromPlatform` path wraps a typed `DecisioningPlatform` with compile-time specialism enforcement (claim `sales-non-guaranteed`, miss a required `sales.*` method, fail compile), ctx_metadata round-trip + auto-hydration, idempotency-principal synthesis, status mappers, and webhook auto-emit. **Reach for the lower-level `createAdcpServer` from `@adcp/sdk/server/legacy/v5` only when you need fine control over individual handlers, are mid-migration from a v5 codebase, or have custom tools the platform interface doesn't yet model.**
|
|
82
|
+
|
|
83
|
+
> **On a hydration miss, the framework leaves the hydrated field undefined and runs the handler anyway** — the cache is a hint, not source-of-truth. Your handler keeps its existence check (`patch.media_buy ?? (await db.findMediaBuy(id))`) and throws a typed `MediaBuyNotFoundError` / `PackageNotFoundError` / `ProductNotFoundError` from `@adcp/sdk/server` when both the hydrate and the DB-fallback come up empty. Full rationale in [Auto-hydration error contract](../../docs/migration-5.x-to-6.x.md#auto-hydration-error-contract).
|
|
84
|
+
|
|
70
85
|
If a specialism's storyboard doesn't exercise one of these tools, the tool is **not optional** — the storyboard is just focused elsewhere (e.g. `sales-social` covers audience sync + DPA + events; the media buy flow itself is covered by `sales-non-guaranteed` or `sales-guaranteed` which you also claim). See § [Tools and Required Response Shapes](#tools-and-required-response-shapes) below for the exact response shape each tool must return.
|
|
71
86
|
|
|
72
87
|
## Specialisms This Skill Covers
|
|
@@ -99,7 +114,7 @@ Three requirements apply to **every** production seller, regardless of which spe
|
|
|
99
114
|
|
|
100
115
|
### `idempotency_key` is required on every mutating request
|
|
101
116
|
|
|
102
|
-
`create_media_buy`, `update_media_buy`, `sync_accounts`, `sync_creatives`, `sync_audiences`, `sync_catalogs`, `sync_event_sources`, `provide_performance_feedback` — every mutating call carries a client-supplied `idempotency_key`. Wire `createIdempotencyStore` into `
|
|
117
|
+
`create_media_buy`, `update_media_buy`, `sync_accounts`, `sync_creatives`, `sync_audiences`, `sync_catalogs`, `sync_event_sources`, `provide_performance_feedback` — every mutating call carries a client-supplied `idempotency_key`. Wire `createIdempotencyStore` into `createAdcpServerFromPlatform(platform, { idempotency })` and the framework handles replay detection, payload-hash conflict (`IDEMPOTENCY_CONFLICT`), expiry (`IDEMPOTENCY_EXPIRED`), and in-flight parallelism. Don't implement this in handler code. See [§ Idempotency](#idempotency) below for the full wire-up.
|
|
103
118
|
|
|
104
119
|
### Authentication is mandatory
|
|
105
120
|
|
|
@@ -169,7 +184,7 @@ serve(createAgent, {
|
|
|
169
184
|
publicUrl: 'https://seller.example.com/mcp',
|
|
170
185
|
|
|
171
186
|
// 1. authenticate runs first. Bad/missing bearer → 401 Bearer challenge.
|
|
172
|
-
// serve() populates extra.authInfo, which
|
|
187
|
+
// serve() populates extra.authInfo, which the framework surfaces as ctx.authInfo.
|
|
173
188
|
authenticate: verifyBearer({
|
|
174
189
|
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
|
|
175
190
|
issuer: 'https://auth.example.com',
|
|
@@ -206,7 +221,7 @@ serve(createAgent, {
|
|
|
206
221
|
return false; // continue to MCP dispatch
|
|
207
222
|
},
|
|
208
223
|
|
|
209
|
-
// 3. MCP transport parses JSON and dispatches to
|
|
224
|
+
// 3. MCP transport parses JSON and dispatches to the framework server.
|
|
210
225
|
// 4. Framework applies the idempotency store per handler — you don't mount it.
|
|
211
226
|
});
|
|
212
227
|
```
|
|
@@ -214,7 +229,7 @@ serve(createAgent, {
|
|
|
214
229
|
**Principal threading.** `resolveSessionKey(ctx)` receives only `{toolName, params, account}` — no auth info. To compose the OAuth subject into the idempotency key you need `resolveIdempotencyPrincipal`, which receives the full `HandlerContext` including `ctx.authInfo` (populated by `verifyBearer` through MCP's `extra.authInfo`):
|
|
215
230
|
|
|
216
231
|
```typescript
|
|
217
|
-
|
|
232
|
+
createAdcpServerFromPlatform(myPlatform, {
|
|
218
233
|
// ...
|
|
219
234
|
// SessionKeyContext has no authInfo — use this for coarse per-account scoping:
|
|
220
235
|
resolveSessionKey: ctx => ctx.account?.id,
|
|
@@ -277,10 +292,16 @@ This means: the `task_id` you return on a `sales-guaranteed` `create_media_buy`
|
|
|
277
292
|
|
|
278
293
|
## Webhooks (async completion, signed outbound)
|
|
279
294
|
|
|
280
|
-
Most seller flows need outbound webhooks — `sales-guaranteed` fires on IO completion, `sales-broadcast-tv` fires `window_update` deliveries as C3/C7 data matures, `update_media_buy` fires on bid/budget application. **Don't hand-roll `fetch` with HMAC**.
|
|
295
|
+
Most seller flows need outbound webhooks — `sales-guaranteed` fires on IO completion, `sales-broadcast-tv` fires `window_update` deliveries as C3/C7 data matures, `update_media_buy` fires on bid/budget application. **Don't hand-roll `fetch` with HMAC**. Pass `webhooks: { signerKey }` to `createAdcpServerFromPlatform` and call `ctx.emitWebhook(...)` from any handler — the framework handles RFC 9421 signing, nonce minting, stable `idempotency_key` across retries, 5xx/429 backoff, byte-identical JSON serialization, and the "don't retry on signature failures" terminal behavior.
|
|
281
296
|
|
|
282
297
|
```typescript
|
|
283
|
-
import {
|
|
298
|
+
import {
|
|
299
|
+
createAdcpServerFromPlatform,
|
|
300
|
+
serve,
|
|
301
|
+
type DecisioningPlatform,
|
|
302
|
+
type SalesPlatform,
|
|
303
|
+
type AccountStore,
|
|
304
|
+
} from '@adcp/sdk/server';
|
|
284
305
|
|
|
285
306
|
// Dev: generate a signer JWK once at boot. Production: load from KMS/env with a stable `kid`,
|
|
286
307
|
// and publish the public half at your `jwks_uri` so buyers can verify without OOB exchange.
|
|
@@ -294,41 +315,67 @@ const signerJwk = {
|
|
|
294
315
|
key_ops: ['sign'],
|
|
295
316
|
};
|
|
296
317
|
|
|
318
|
+
class WebhookSeller implements DecisioningPlatform {
|
|
319
|
+
capabilities = {
|
|
320
|
+
specialisms: ['sales-guaranteed'] as const,
|
|
321
|
+
pricingModels: ['cpm'] as const,
|
|
322
|
+
channels: ['display'] as const,
|
|
323
|
+
config: {},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
accounts: AccountStore = {
|
|
327
|
+
resolve: async ref => ({
|
|
328
|
+
id: 'account_id' in ref ? ref.account_id : 'default',
|
|
329
|
+
operator: 'me',
|
|
330
|
+
ctx_metadata: {},
|
|
331
|
+
}),
|
|
332
|
+
upsert: async () => ({ ok: true, items: [] }),
|
|
333
|
+
list: async () => ({ items: [], nextCursor: null }),
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
sales: SalesPlatform = {
|
|
337
|
+
getProducts: async () => ({ products: [] }),
|
|
338
|
+
createMediaBuy: async (req, ctx) => {
|
|
339
|
+
// sales-guaranteed: IO signing completes async. Emit the final result on completion.
|
|
340
|
+
const taskId = `task_${randomUUID()}`;
|
|
341
|
+
|
|
342
|
+
// Capture ctx.emitWebhook into a local BEFORE scheduling — the handler returns
|
|
343
|
+
// immediately, but the closure outlives the request; ctx may be recycled.
|
|
344
|
+
const emit = ctx.emitWebhook!; // non-null: guaranteed populated when webhooks config is set
|
|
345
|
+
|
|
346
|
+
queueIoReview(req, async outcome => {
|
|
347
|
+
await emit({
|
|
348
|
+
url: (req as { push_notification_config?: { url: string } }).push_notification_config!.url,
|
|
349
|
+
payload: {
|
|
350
|
+
task: {
|
|
351
|
+
task_id: taskId,
|
|
352
|
+
status: outcome.approved ? 'completed' : 'rejected',
|
|
353
|
+
result: outcome.approved
|
|
354
|
+
? { media_buy_id: outcome.media_buy_id, packages: outcome.packages }
|
|
355
|
+
: undefined,
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
operation_id: `create_media_buy.${taskId}`, // stable across retries — framework reuses same idempotency_key
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
return { status: 'submitted', task_id: taskId }; // synchronous response is the task envelope
|
|
362
|
+
},
|
|
363
|
+
updateMediaBuy: async (id, patch) => ({ media_buy_id: id, status: 'active' }),
|
|
364
|
+
getMediaBuys: async () => ({ media_buys: [] }),
|
|
365
|
+
getMediaBuyDelivery: async () => ({ deliveries: [] }),
|
|
366
|
+
syncCreatives: async () => [],
|
|
367
|
+
listCreativeFormats: async () => ({ formats: [] }),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
297
371
|
serve(() =>
|
|
298
|
-
|
|
372
|
+
createAdcpServerFromPlatform(new WebhookSeller(), {
|
|
299
373
|
name: 'My Seller',
|
|
300
374
|
version: '1.0.0',
|
|
301
375
|
webhooks: {
|
|
302
376
|
signerKey: { keyid: signerJwk.kid, alg: 'ed25519', privateKey: signerJwk },
|
|
303
377
|
// Optional: retries, idempotencyKeyStore (swap memory → pg for multi-replica)
|
|
304
378
|
},
|
|
305
|
-
mediaBuy: {
|
|
306
|
-
createMediaBuy: async (params, ctx) => {
|
|
307
|
-
// sales-guaranteed: IO signing completes async. Emit the final result on completion.
|
|
308
|
-
const taskId = `task_${randomUUID()}`;
|
|
309
|
-
|
|
310
|
-
// Capture ctx.emitWebhook into a local BEFORE scheduling — the handler returns
|
|
311
|
-
// immediately, but the closure outlives the request; ctx may be recycled.
|
|
312
|
-
const emit = ctx.emitWebhook!; // non-null: guaranteed populated when webhooks config is set
|
|
313
|
-
|
|
314
|
-
queueIoReview(params, async outcome => {
|
|
315
|
-
await emit({
|
|
316
|
-
url: (params as { push_notification_config?: { url: string } }).push_notification_config!.url,
|
|
317
|
-
payload: {
|
|
318
|
-
task: {
|
|
319
|
-
task_id: taskId,
|
|
320
|
-
status: outcome.approved ? 'completed' : 'rejected',
|
|
321
|
-
result: outcome.approved
|
|
322
|
-
? { media_buy_id: outcome.media_buy_id, packages: outcome.packages }
|
|
323
|
-
: undefined,
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
operation_id: `create_media_buy.${taskId}`, // stable across retries — framework reuses same idempotency_key
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
return { status: 'submitted', task_id: taskId }; // synchronous response is the task envelope
|
|
330
|
-
},
|
|
331
|
-
},
|
|
332
379
|
})
|
|
333
380
|
);
|
|
334
381
|
```
|
|
@@ -406,7 +453,7 @@ Non-guaranteed buys are always instant confirmation.
|
|
|
406
453
|
>
|
|
407
454
|
> **Cross-cutting pitfalls matrix runs keep catching:**
|
|
408
455
|
>
|
|
409
|
-
> - **Declare `capabilities
|
|
456
|
+
> - **Declare `capabilities.specialisms: ['sales-guaranteed']` (or your actual specialism) on the platform you pass to `createAdcpServerFromPlatform`.** Value is `string[]` of enum ids (not `[{id, version}]`). Agents that don't declare their specialism fail the grader with "No applicable tracks found" even if every tool works — tracks are gated on the specialism claim.
|
|
410
457
|
> - `get_media_buy_delivery` response requires **top-level `currency: string`** (ISO 4217) — per-row `spend.currency` is NOT enough.
|
|
411
458
|
> - `get_media_buy_delivery /media_buy_deliveries[i]/by_package[j]` rows are strict: each requires `package_id`, `spend` (number), `pricing_model`, `rate` (number), and `currency`. A mock that returns `{package_id, impressions, clicks}` fails validation — include the billing quintet on every package row.
|
|
412
459
|
> - `get_media_buy_delivery /reporting_period/start` and `/end` are ISO 8601 **date-time** strings (`YYYY-MM-DDTHH:MM:SS.sssZ` via `new Date().toISOString()`), not date-only. A mock that returns `'2026-04-21'` fails the format check in GA.
|
|
@@ -622,7 +669,7 @@ Top-level `currency` is **required** per `get-media-buy-delivery-response.json`.
|
|
|
622
669
|
|
|
623
670
|
### Context and Ext Passthrough
|
|
624
671
|
|
|
625
|
-
|
|
672
|
+
The framework auto-echoes the request's `context` into every response — **do not set `context` yourself in your handler return values.** The framework injects it post-handler only when the field isn't already present.
|
|
626
673
|
|
|
627
674
|
**Crucial:** `context` is schema-typed as an object. If your handler hand-sets a string or narrative description (e.g., "E2E test run", a scenario label, `campaign_context` from the request body), validation fails with `/context: must be object` and the framework does not overwrite. Leave the field out entirely; the framework handles it.
|
|
628
675
|
|
|
@@ -697,13 +744,13 @@ productRepo.upsert(merged.product_id, merged);
|
|
|
697
744
|
**2. `bridgeFromTestControllerStore`** — wires your seeded `Map` into `get_products` responses automatically. Sandbox requests see seeded + handler products merged (with seeded winning collisions); production traffic (no sandbox marker, or resolved non-sandbox account) skips the bridge entirely.
|
|
698
745
|
|
|
699
746
|
```ts
|
|
700
|
-
import {
|
|
701
|
-
import { bridgeFromTestControllerStore } from '@adcp/sdk/server';
|
|
747
|
+
import { createAdcpServerFromPlatform, bridgeFromTestControllerStore, DEFAULT_REPORTING_CAPABILITIES } from '@adcp/sdk/server';
|
|
702
748
|
|
|
703
749
|
const seedStore = new Map<string, unknown>();
|
|
704
750
|
|
|
705
|
-
const server =
|
|
706
|
-
|
|
751
|
+
const server = createAdcpServerFromPlatform(myPlatform, {
|
|
752
|
+
name: 'My Seller',
|
|
753
|
+
version: '1.0.0',
|
|
707
754
|
testController: bridgeFromTestControllerStore(seedStore, {
|
|
708
755
|
delivery_type: 'guaranteed',
|
|
709
756
|
channels: ['display'],
|
|
@@ -785,8 +832,9 @@ Key SDK pieces you'll import from `@adcp/sdk`: `CONTROLLER_SCENARIOS`, `enforceM
|
|
|
785
832
|
|
|
786
833
|
| SDK piece | Usage |
|
|
787
834
|
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
788
|
-
| `
|
|
789
|
-
| `
|
|
835
|
+
| `createAdcpServerFromPlatform(platform, opts)` | Build a server from a typed `DecisioningPlatform` — compile-time specialism enforcement, ctx_metadata round-trip, idempotency-principal synthesis, status mappers, webhook auto-emit |
|
|
836
|
+
| `createAdcpServer(config)` *(legacy)* | v5 handler-bag entry. Mid-migration / escape-hatch only; reach via `@adcp/sdk/server/legacy/v5` |
|
|
837
|
+
| `serve(() => createAdcpServerFromPlatform(platform, opts))` | Start HTTP server on `:3001/mcp` |
|
|
790
838
|
| `ctx.store` | State store in every handler — `get`, `put`, `patch`, `delete`, `list` |
|
|
791
839
|
| `InMemoryStateStore` | Default state store (dev/testing) |
|
|
792
840
|
| `PostgresStateStore` | Production state store (shared across instances) |
|
|
@@ -802,7 +850,7 @@ Key SDK pieces you'll import from `@adcp/sdk`: `CONTROLLER_SCENARIOS`, `enforceM
|
|
|
802
850
|
| `enforceMapCap(map, key, label, cap?)` | Reject net-new keys once a session Map hits `SESSION_ENTRY_CAP` (1000) |
|
|
803
851
|
| `expectControllerError(result, code)` / `expectControllerSuccess(result)` | Unit-test assertions — narrow responses to error or success arms |
|
|
804
852
|
|
|
805
|
-
Response builders (`productsResponse`, `mediaBuyResponse`, `deliveryResponse`, etc.) are auto-applied by
|
|
853
|
+
Response builders (`productsResponse`, `mediaBuyResponse`, `deliveryResponse`, etc.) are auto-applied by the framework — you return the data, the framework wraps it. You only need to call them directly for tools without a dedicated builder.
|
|
806
854
|
|
|
807
855
|
Import everything from `@adcp/sdk`. Types from `@adcp/sdk` with `import type`.
|
|
808
856
|
|
|
@@ -833,48 +881,56 @@ Minimal `tsconfig.json`:
|
|
|
833
881
|
|
|
834
882
|
## Implementation
|
|
835
883
|
|
|
836
|
-
Use `
|
|
884
|
+
Use `createAdcpServerFromPlatform` — it auto-wires schemas, response builders, and `get_adcp_capabilities` from the typed `DecisioningPlatform` you provide. Handlers receive `(params, ctx)` where `ctx.store` persists state, `ctx.account` is the resolved account, and `ctx.ctxMetadata` is the resource-keyed cache (when wired).
|
|
837
885
|
|
|
838
886
|
**Imports**: most things live at `@adcp/sdk`. The idempotency store helpers (`createIdempotencyStore`, `memoryBackend`, `pgBackend`) live at the narrower `@adcp/sdk/server` subpath. Both are re-exported from the root — either works — but splitting them makes intent obvious.
|
|
839
887
|
|
|
840
888
|
```typescript
|
|
841
889
|
import { randomUUID } from 'node:crypto';
|
|
842
890
|
import {
|
|
843
|
-
|
|
891
|
+
createAdcpServerFromPlatform,
|
|
844
892
|
serve,
|
|
845
893
|
adcpError,
|
|
846
894
|
InMemoryStateStore,
|
|
847
895
|
checkGovernance,
|
|
848
896
|
governanceDeniedError,
|
|
849
|
-
|
|
850
|
-
|
|
897
|
+
createIdempotencyStore,
|
|
898
|
+
memoryBackend,
|
|
899
|
+
type DecisioningPlatform,
|
|
900
|
+
type SalesPlatform,
|
|
901
|
+
type AccountStore,
|
|
902
|
+
} from '@adcp/sdk/server';
|
|
851
903
|
import type { ServeContext } from '@adcp/sdk';
|
|
852
904
|
|
|
905
|
+
// Publisher-typed metadata blob round-tripped via Account.ctx_metadata.
|
|
906
|
+
// Whatever shape your adapter wants — the SDK doesn't inspect it.
|
|
907
|
+
interface MySellerMeta {
|
|
908
|
+
governanceUrl?: string;
|
|
909
|
+
brand?: string;
|
|
910
|
+
operator?: string;
|
|
911
|
+
[key: string]: unknown;
|
|
912
|
+
}
|
|
913
|
+
|
|
853
914
|
const stateStore = new InMemoryStateStore(); // shared across requests
|
|
854
915
|
|
|
855
|
-
// Idempotency — required for any
|
|
856
|
-
// requests. `createIdempotencyStore` throws if `ttlSeconds` is
|
|
857
|
-
// spec bounds (3600–604800).
|
|
916
|
+
// Idempotency — required for any AdCP-3-compliant seller that accepts
|
|
917
|
+
// mutating requests. `createIdempotencyStore` throws if `ttlSeconds` is
|
|
918
|
+
// outside the spec bounds (3600–604800).
|
|
858
919
|
const idempotency = createIdempotencyStore({
|
|
859
920
|
backend: memoryBackend(), // pgBackend(pool) for production
|
|
860
921
|
ttlSeconds: 86400, // 24 hours
|
|
861
922
|
});
|
|
862
923
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
924
|
+
class MySeller implements DecisioningPlatform<{}, MySellerMeta> {
|
|
925
|
+
capabilities = {
|
|
926
|
+
specialisms: ['sales-non-guaranteed'] as const,
|
|
927
|
+
pricingModels: ['cpm'] as const,
|
|
928
|
+
channels: ['display'] as const,
|
|
929
|
+
config: {},
|
|
930
|
+
};
|
|
870
931
|
|
|
871
|
-
|
|
872
|
-
//
|
|
873
|
-
// fine for a demo; for multi-tenant production use ctx.account typed
|
|
874
|
-
// via `createAdcpServer<MyAccount>({...})`.
|
|
875
|
-
resolveSessionKey: () => 'default-principal',
|
|
876
|
-
|
|
877
|
-
// resolveAccount runs BEFORE idempotency / handler dispatch. If it
|
|
932
|
+
accounts: AccountStore<MySellerMeta> = {
|
|
933
|
+
// accounts.resolve runs BEFORE idempotency / handler dispatch. If it
|
|
878
934
|
// returns null for a valid-shape reference, every mutating request
|
|
879
935
|
// short-circuits as ACCOUNT_NOT_FOUND — which masks idempotency
|
|
880
936
|
// conformance (missing-key / replay tests fail with the wrong code).
|
|
@@ -883,118 +939,157 @@ function createAgent({ taskStore }: ServeContext) {
|
|
|
883
939
|
// { brand: { domain }, operator } — the canonical spec shape.
|
|
884
940
|
// Conformance storyboards use this by default (e.g. brand.domain
|
|
885
941
|
// "acmeoutdoor.example", operator "pinnacle-agency.example").
|
|
886
|
-
|
|
887
|
-
if ('account_id' in ref)
|
|
942
|
+
resolve: async (ref, ctx) => {
|
|
943
|
+
if ('account_id' in ref) {
|
|
944
|
+
const acc = await stateStore.get('accounts', ref.account_id);
|
|
945
|
+
return acc ?? null;
|
|
946
|
+
}
|
|
888
947
|
if ('brand' in ref && ref.brand?.domain && ref.operator) {
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
|
|
894
|
-
|
|
948
|
+
// Dev/compliance mode: auto-materialize for any valid brand+operator
|
|
949
|
+
// so conformance tests reach the handler. Production replaces this
|
|
950
|
+
// with a real lookup against your tenant registry; returning null
|
|
951
|
+
// for unknown tenants surfaces ACCOUNT_NOT_FOUND correctly.
|
|
952
|
+
return {
|
|
953
|
+
id: `${ref.operator}:${ref.brand.domain}`,
|
|
954
|
+
operator: ref.operator,
|
|
955
|
+
ctx_metadata: { brand: ref.brand.domain, operator: ref.operator },
|
|
956
|
+
};
|
|
895
957
|
}
|
|
896
958
|
return null;
|
|
897
959
|
},
|
|
960
|
+
upsert: async (params, ctx) => {
|
|
961
|
+
/* sync_accounts impl */
|
|
962
|
+
return { ok: true, items: [] };
|
|
963
|
+
},
|
|
964
|
+
list: async (params, ctx) => ({ items: [], nextCursor: null }),
|
|
965
|
+
};
|
|
898
966
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
967
|
+
sales: SalesPlatform<MySellerMeta> = {
|
|
968
|
+
getProducts: async (req, ctx) => {
|
|
969
|
+
return { products: PRODUCTS, sandbox: true };
|
|
970
|
+
// productsResponse() auto-applied by framework
|
|
903
971
|
},
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
});
|
|
954
|
-
}
|
|
955
|
-
// Only merge the fields you want to persist — do NOT spread `params`
|
|
956
|
-
// wholesale. `params` carries envelope fields (idempotency_key,
|
|
957
|
-
// context) that have no business in your domain state. Spreading
|
|
958
|
-
// them pollutes `get_media_buys` responses and breaks dedup.
|
|
959
|
-
const updated = { ...existing, status: params.active === false ? 'paused' : 'active' };
|
|
960
|
-
await ctx.store.put('media_buys', params.media_buy_id, updated);
|
|
961
|
-
return {
|
|
962
|
-
media_buy_id: params.media_buy_id,
|
|
963
|
-
status: updated.status as 'paused' | 'active',
|
|
964
|
-
// `affected_packages` is `Package[]` (per `/schemas/latest/core/package.json`)
|
|
965
|
-
// — objects with at minimum `package_id`. Don't return bare strings;
|
|
966
|
-
// the update-media-buy-response oneOf discriminates against them and
|
|
967
|
-
// the error looks like `/affected_packages/0: must be object`.
|
|
968
|
-
affected_packages: (existing.packages ?? []).map((p: { package_id: string }) => ({
|
|
969
|
-
package_id: p.package_id,
|
|
970
|
-
})),
|
|
971
|
-
};
|
|
972
|
-
},
|
|
973
|
-
getMediaBuys: async (params, ctx) => {
|
|
974
|
-
const result = await ctx.store.list('media_buys');
|
|
975
|
-
return { media_buys: result.items };
|
|
976
|
-
},
|
|
977
|
-
getMediaBuyDelivery: async (params, ctx) => {
|
|
978
|
-
/* ... */
|
|
979
|
-
},
|
|
980
|
-
listCreativeFormats: async (params, ctx) => {
|
|
981
|
-
/* ... */
|
|
982
|
-
},
|
|
983
|
-
syncCreatives: async (params, ctx) => {
|
|
984
|
-
return {
|
|
985
|
-
// Response shape is `creatives: [{ creative_id, action }]` per the
|
|
986
|
-
// sync_creatives response schema — NOT `synced_creatives`.
|
|
987
|
-
creatives:
|
|
988
|
-
params.creatives?.map(c => ({
|
|
989
|
-
creative_id: c.creative_id ?? `cr_${randomUUID()}`,
|
|
990
|
-
action: 'created' as const,
|
|
991
|
-
})) ?? [],
|
|
992
|
-
};
|
|
993
|
-
},
|
|
972
|
+
|
|
973
|
+
createMediaBuy: async (req, ctx) => {
|
|
974
|
+
// Governance check for financial commitment. The publisher's
|
|
975
|
+
// governance URL rides on Account.ctx_metadata so any per-tenant
|
|
976
|
+
// override is read at request time.
|
|
977
|
+
const govUrl = ctx.account?.ctx_metadata?.governanceUrl;
|
|
978
|
+
if (typeof govUrl === 'string') {
|
|
979
|
+
const gov = await checkGovernance({
|
|
980
|
+
agentUrl: govUrl,
|
|
981
|
+
planId: (req as { plan_id?: string }).plan_id ?? 'default',
|
|
982
|
+
caller: 'https://my-agent.com/mcp',
|
|
983
|
+
tool: 'create_media_buy',
|
|
984
|
+
payload: req,
|
|
985
|
+
});
|
|
986
|
+
if (!gov.approved) return governanceDeniedError(gov);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Use randomUUID (not Date.now) so ids are unguessable — a guessable
|
|
990
|
+
// media_buy_id lets another buyer probe or cancel. Same applies to
|
|
991
|
+
// any seller-issued id (package_id, creative_id, etc.).
|
|
992
|
+
// `currency` + `total_budget` are REQUIRED on get_media_buys response
|
|
993
|
+
// rows. The request carries them under `total_budget: { amount, currency }`.
|
|
994
|
+
// Flatten to top-level fields at create time — storing only
|
|
995
|
+
// `packages[].budget` and reconstructing later fails schema validation
|
|
996
|
+
// in get_media_buys/update_media_buy.
|
|
997
|
+
const totalBudget = req.total_budget;
|
|
998
|
+
const currency = typeof totalBudget === 'object' && totalBudget ? (totalBudget.currency ?? 'USD') : 'USD';
|
|
999
|
+
const amount =
|
|
1000
|
+
typeof totalBudget === 'object' && totalBudget
|
|
1001
|
+
? (totalBudget.amount ?? 0)
|
|
1002
|
+
: typeof totalBudget === 'number'
|
|
1003
|
+
? totalBudget
|
|
1004
|
+
: 0;
|
|
1005
|
+
|
|
1006
|
+
const buy = {
|
|
1007
|
+
media_buy_id: `mb_${randomUUID()}`,
|
|
1008
|
+
status: 'pending_creatives' as const,
|
|
1009
|
+
currency,
|
|
1010
|
+
total_budget: amount,
|
|
1011
|
+
packages:
|
|
1012
|
+
req.packages?.map(pkg => ({
|
|
1013
|
+
package_id: `pkg_${randomUUID()}`,
|
|
1014
|
+
product_id: pkg.product_id,
|
|
1015
|
+
pricing_option_id: pkg.pricing_option_id,
|
|
1016
|
+
budget: pkg.budget,
|
|
1017
|
+
})) ?? [],
|
|
1018
|
+
};
|
|
1019
|
+
await ctx.store.put('media_buys', buy.media_buy_id, buy);
|
|
1020
|
+
return buy; // mediaBuyResponse() auto-applied (sets revision, confirmed_at, valid_actions)
|
|
994
1021
|
},
|
|
995
|
-
|
|
996
|
-
|
|
1022
|
+
|
|
1023
|
+
updateMediaBuy: async (mediaBuyId, patch, ctx) => {
|
|
1024
|
+
const existing = await ctx.store.get('media_buys', mediaBuyId);
|
|
1025
|
+
if (!existing) {
|
|
1026
|
+
return adcpError('MEDIA_BUY_NOT_FOUND', {
|
|
1027
|
+
message: `No media buy with id ${mediaBuyId}`,
|
|
1028
|
+
field: 'media_buy_id',
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
// Only merge the fields you want to persist — do NOT spread `patch`
|
|
1032
|
+
// wholesale. The patch carries envelope fields (idempotency_key,
|
|
1033
|
+
// context) that have no business in your domain state. Spreading
|
|
1034
|
+
// them pollutes `get_media_buys` responses and breaks dedup.
|
|
1035
|
+
const updated = { ...existing, status: patch.paused === true ? 'paused' : 'active' };
|
|
1036
|
+
await ctx.store.put('media_buys', mediaBuyId, updated);
|
|
1037
|
+
return {
|
|
1038
|
+
media_buy_id: mediaBuyId,
|
|
1039
|
+
status: updated.status as 'paused' | 'active',
|
|
1040
|
+
// `affected_packages` is `Package[]` (per `/schemas/latest/core/package.json`)
|
|
1041
|
+
// — objects with at minimum `package_id`. Don't return bare strings;
|
|
1042
|
+
// the update-media-buy-response oneOf discriminates against them and
|
|
1043
|
+
// the error looks like `/affected_packages/0: must be object`.
|
|
1044
|
+
affected_packages: (existing.packages ?? []).map((p: { package_id: string }) => ({
|
|
1045
|
+
package_id: p.package_id,
|
|
1046
|
+
})),
|
|
1047
|
+
};
|
|
997
1048
|
},
|
|
1049
|
+
|
|
1050
|
+
getMediaBuys: async (params, ctx) => {
|
|
1051
|
+
const result = await ctx.store.list('media_buys');
|
|
1052
|
+
return { media_buys: result.items };
|
|
1053
|
+
},
|
|
1054
|
+
|
|
1055
|
+
getMediaBuyDelivery: async (filter, ctx) => {
|
|
1056
|
+
/* ... */
|
|
1057
|
+
return {
|
|
1058
|
+
currency: 'USD',
|
|
1059
|
+
reporting_period: {
|
|
1060
|
+
start: filter.start_date ?? '2026-01-01',
|
|
1061
|
+
end: filter.end_date ?? '2026-01-31',
|
|
1062
|
+
},
|
|
1063
|
+
media_buy_deliveries: [],
|
|
1064
|
+
};
|
|
1065
|
+
},
|
|
1066
|
+
|
|
1067
|
+
listCreativeFormats: async (params, ctx) => ({ formats: [] }),
|
|
1068
|
+
|
|
1069
|
+
// Response is `creatives: [{ creative_id, action }]` per the spec response
|
|
1070
|
+
// schema — NOT `synced_creatives`. v6 takes the creatives array directly;
|
|
1071
|
+
// the framework unpacks the request envelope.
|
|
1072
|
+
syncCreatives: async (creatives, ctx) =>
|
|
1073
|
+
creatives.map(c => ({
|
|
1074
|
+
creative_id: (c as { creative_id?: string }).creative_id ?? `cr_${randomUUID()}`,
|
|
1075
|
+
action: 'created' as const,
|
|
1076
|
+
})),
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const platform = new MySeller();
|
|
1081
|
+
|
|
1082
|
+
function createAgent({ taskStore }: ServeContext) {
|
|
1083
|
+
return createAdcpServerFromPlatform(platform, {
|
|
1084
|
+
name: 'My Seller Agent',
|
|
1085
|
+
version: '1.0.0',
|
|
1086
|
+
taskStore,
|
|
1087
|
+
stateStore,
|
|
1088
|
+
idempotency,
|
|
1089
|
+
// Principal scoping for idempotency. MUST never return undefined — or
|
|
1090
|
+
// every mutating request rejects as SERVICE_UNAVAILABLE. A constant is
|
|
1091
|
+
// fine for a demo; for multi-tenant production use `ctx.account.id`.
|
|
1092
|
+
resolveSessionKey: () => 'default-principal',
|
|
998
1093
|
});
|
|
999
1094
|
}
|
|
1000
1095
|
|
|
@@ -1003,7 +1098,7 @@ serve(createAgent);
|
|
|
1003
1098
|
|
|
1004
1099
|
Key points:
|
|
1005
1100
|
|
|
1006
|
-
1. Single `.ts` file —
|
|
1101
|
+
1. Single `.ts` file — one `DecisioningPlatform` class passed to `createAdcpServerFromPlatform`
|
|
1007
1102
|
2. `get_adcp_capabilities` is auto-generated from your handlers — don't register it manually (idempotency capability is auto-declared too)
|
|
1008
1103
|
3. Response builders are auto-applied — just return the data
|
|
1009
1104
|
4. Use `ctx.store` for state — persists across stateless HTTP requests
|
|
@@ -1032,72 +1127,105 @@ The buyer signals this by setting `plan.human_review_required: true` on the gove
|
|
|
1032
1127
|
|
|
1033
1128
|
```typescript
|
|
1034
1129
|
import {
|
|
1035
|
-
|
|
1130
|
+
createAdcpServerFromPlatform,
|
|
1036
1131
|
serve,
|
|
1037
1132
|
adcpError,
|
|
1038
1133
|
buildHumanOverride,
|
|
1039
1134
|
checkGovernance,
|
|
1040
1135
|
governanceDeniedError,
|
|
1041
|
-
|
|
1042
|
-
|
|
1136
|
+
taskToolResponse,
|
|
1137
|
+
type DecisioningPlatform,
|
|
1138
|
+
type SalesPlatform,
|
|
1139
|
+
type AccountStore,
|
|
1140
|
+
type AdcpStateStore,
|
|
1141
|
+
} from '@adcp/sdk/server';
|
|
1043
1142
|
import { randomUUID } from 'node:crypto';
|
|
1044
1143
|
|
|
1144
|
+
interface RegulatedMeta {
|
|
1145
|
+
governanceUrl?: string;
|
|
1146
|
+
[key: string]: unknown;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
class RegulatedPublisher implements DecisioningPlatform<{}, RegulatedMeta> {
|
|
1150
|
+
capabilities = {
|
|
1151
|
+
specialisms: ['sales-guaranteed'] as const,
|
|
1152
|
+
pricingModels: ['cpm'] as const,
|
|
1153
|
+
channels: ['display'] as const,
|
|
1154
|
+
config: {},
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
accounts: AccountStore<RegulatedMeta> = {
|
|
1158
|
+
resolve: async ref => db.findAccount(ref),
|
|
1159
|
+
upsert: async () => ({ ok: true, items: [] }),
|
|
1160
|
+
list: async () => ({ items: [], nextCursor: null }),
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
sales: SalesPlatform<RegulatedMeta> = {
|
|
1164
|
+
getProducts: async () => ({ products: [] }),
|
|
1165
|
+
|
|
1166
|
+
createMediaBuy: async (req, ctx) => {
|
|
1167
|
+
if (!ctx.account) {
|
|
1168
|
+
return adcpError('ACCOUNT_NOT_FOUND', { field: 'account' });
|
|
1169
|
+
}
|
|
1170
|
+
const plan = await ctx.store.get('governance_plans', (req as { plan_id?: string }).plan_id ?? '');
|
|
1171
|
+
if (!plan) return adcpError('PLAN_NOT_FOUND', { field: 'plan_id' });
|
|
1172
|
+
|
|
1173
|
+
// Human-review gate — GDPR Art 22 / EU AI Act Annex III.
|
|
1174
|
+
if (plan.human_review_required === true) {
|
|
1175
|
+
const taskId = `task_${randomUUID()}`;
|
|
1176
|
+
await ctx.store.put('pending_reviews', taskId, {
|
|
1177
|
+
plan_id: (req as { plan_id?: string }).plan_id,
|
|
1178
|
+
params: req,
|
|
1179
|
+
enqueued_at: new Date().toISOString(),
|
|
1180
|
+
account_id: ctx.account.id,
|
|
1181
|
+
// Buyer's webhook target for async completion, if they supplied one.
|
|
1182
|
+
webhook_url: (req as { push_notification_config?: { url: string } }).push_notification_config?.url,
|
|
1183
|
+
});
|
|
1184
|
+
// Route this task_id to your human-review queue (Slack approval,
|
|
1185
|
+
// ops ticket, internal UI — whatever your reviewers use).
|
|
1186
|
+
await humanReviewQueue.enqueue(taskId);
|
|
1187
|
+
// Submitted envelope per CreateMediaBuySubmitted. Do NOT return a
|
|
1188
|
+
// populated MediaBuy here — media_buy_id and packages land on the
|
|
1189
|
+
// completion artifact once a human approves. taskToolResponse bypasses
|
|
1190
|
+
// the default mediaBuyResponse wrap, which would stamp revision /
|
|
1191
|
+
// confirmed_at / valid_actions — fields that don't belong on a task
|
|
1192
|
+
// envelope.
|
|
1193
|
+
return taskToolResponse({ status: 'submitted', task_id: taskId });
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Non-regulated path — normal governance check, commit synchronously.
|
|
1197
|
+
const govUrl = ctx.account.ctx_metadata?.governanceUrl;
|
|
1198
|
+
if (typeof govUrl === 'string') {
|
|
1199
|
+
const gov = await checkGovernance({
|
|
1200
|
+
agentUrl: govUrl,
|
|
1201
|
+
planId: (req as { plan_id?: string }).plan_id ?? 'default',
|
|
1202
|
+
caller: 'https://my-publisher.com/mcp',
|
|
1203
|
+
tool: 'create_media_buy',
|
|
1204
|
+
payload: req,
|
|
1205
|
+
});
|
|
1206
|
+
if (!gov.approved) return governanceDeniedError(gov);
|
|
1207
|
+
}
|
|
1208
|
+
return executeBuy(req, ctx.store);
|
|
1209
|
+
},
|
|
1210
|
+
|
|
1211
|
+
updateMediaBuy: async (id, patch) => ({ media_buy_id: id, status: 'active' }),
|
|
1212
|
+
getMediaBuys: async () => ({ media_buys: [] }),
|
|
1213
|
+
getMediaBuyDelivery: async () => ({ deliveries: [] }),
|
|
1214
|
+
syncCreatives: async () => [],
|
|
1215
|
+
listCreativeFormats: async () => ({ formats: [] }),
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1045
1219
|
serve(() =>
|
|
1046
|
-
|
|
1220
|
+
createAdcpServerFromPlatform(new RegulatedPublisher(), {
|
|
1047
1221
|
name: 'Regulated Publisher',
|
|
1048
1222
|
version: '1.0.0',
|
|
1049
|
-
resolveAccount: async ref => db.findAccount(ref),
|
|
1050
|
-
mediaBuy: {
|
|
1051
|
-
createMediaBuy: async (params, ctx) => {
|
|
1052
|
-
if (!ctx.account) {
|
|
1053
|
-
return adcpError('ACCOUNT_NOT_FOUND', { field: 'account' });
|
|
1054
|
-
}
|
|
1055
|
-
const plan = await ctx.store.get('governance_plans', params.plan_id ?? '');
|
|
1056
|
-
if (!plan) return adcpError('PLAN_NOT_FOUND', { field: 'plan_id' });
|
|
1057
|
-
|
|
1058
|
-
// Human-review gate — GDPR Art 22 / EU AI Act Annex III.
|
|
1059
|
-
if (plan.human_review_required === true) {
|
|
1060
|
-
const taskId = `task_${randomUUID()}`;
|
|
1061
|
-
await ctx.store.put('pending_reviews', taskId, {
|
|
1062
|
-
plan_id: params.plan_id,
|
|
1063
|
-
params,
|
|
1064
|
-
enqueued_at: new Date().toISOString(),
|
|
1065
|
-
account_id: ctx.account.id,
|
|
1066
|
-
// Buyer's webhook target for async completion, if they supplied one.
|
|
1067
|
-
webhook_url: params.push_notification_config?.url,
|
|
1068
|
-
});
|
|
1069
|
-
// Route this task_id to your human-review queue (Slack approval,
|
|
1070
|
-
// ops ticket, internal UI — whatever your reviewers use).
|
|
1071
|
-
await humanReviewQueue.enqueue(taskId);
|
|
1072
|
-
// Submitted envelope per CreateMediaBuySubmitted. Do NOT return a
|
|
1073
|
-
// populated MediaBuy here — media_buy_id and packages land on the
|
|
1074
|
-
// completion artifact once a human approves. taskToolResponse bypasses
|
|
1075
|
-
// the default mediaBuyResponse wrap, which would stamp revision /
|
|
1076
|
-
// confirmed_at / valid_actions — fields that don't belong on a task
|
|
1077
|
-
// envelope.
|
|
1078
|
-
return taskToolResponse({ status: 'submitted', task_id: taskId });
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// Non-regulated path — normal governance check, commit synchronously.
|
|
1082
|
-
if (ctx.account.governanceUrl) {
|
|
1083
|
-
const gov = await checkGovernance({
|
|
1084
|
-
agentUrl: ctx.account.governanceUrl,
|
|
1085
|
-
planId: params.plan_id ?? 'default',
|
|
1086
|
-
caller: 'https://my-publisher.com/mcp',
|
|
1087
|
-
tool: 'create_media_buy',
|
|
1088
|
-
payload: params,
|
|
1089
|
-
});
|
|
1090
|
-
if (!gov.approved) return governanceDeniedError(gov);
|
|
1091
|
-
}
|
|
1092
|
-
return executeBuy(params, ctx.store);
|
|
1093
|
-
},
|
|
1094
|
-
},
|
|
1095
1223
|
})
|
|
1096
1224
|
);
|
|
1097
1225
|
|
|
1098
1226
|
// Called by the human-review UI when a reviewer signs off. Lives outside any
|
|
1099
1227
|
// request handler, so it takes its own AdcpStateStore — the same instance you
|
|
1100
|
-
// passed to
|
|
1228
|
+
// passed to the framework via `stateStore` option. No ctx in scope here.
|
|
1101
1229
|
async function onHumanApproval(store: AdcpStateStore, taskId: string, approver: string, reason: string): Promise<void> {
|
|
1102
1230
|
const pending = await store.get('pending_reviews', taskId);
|
|
1103
1231
|
if (!pending) throw new Error(`No pending review with id ${taskId}`);
|
|
@@ -1120,9 +1248,9 @@ async function onHumanApproval(store: AdcpStateStore, taskId: string, approver:
|
|
|
1120
1248
|
await store.delete('pending_reviews', taskId);
|
|
1121
1249
|
|
|
1122
1250
|
// Notify the buyer. Two options, pick based on what your server wires up:
|
|
1123
|
-
// 1. If you configured `webhooks` on
|
|
1251
|
+
// 1. If you configured `webhooks` on the framework server and the buyer sent
|
|
1124
1252
|
// push_notification_config.url, POST the completion event from the
|
|
1125
|
-
// emitter built at boot (hoisted outside
|
|
1253
|
+
// emitter built at boot (hoisted outside the framework constructor so it's
|
|
1126
1254
|
// reachable here). See § Guaranteed delivery / IO signing for the
|
|
1127
1255
|
// emitter construction.
|
|
1128
1256
|
// 2. Otherwise the buyer polls — they already have the task_id and will
|
|
@@ -1154,7 +1282,7 @@ async function onHumanApproval(store: AdcpStateStore, taskId: string, approver:
|
|
|
1154
1282
|
|
|
1155
1283
|
AdCP v3 requires an `idempotency_key` on every mutating request. For sellers, that's `create_media_buy`, `update_media_buy`, `sync_creatives`, and any `sync_*` tools you implement. Idempotency is wired in the Implementation example above — this section explains what the framework does for you and the subtleties to know.
|
|
1156
1284
|
|
|
1157
|
-
**What the framework handles when you pass `idempotency` to `
|
|
1285
|
+
**What the framework handles when you pass `idempotency` to `createAdcpServerFromPlatform`:**
|
|
1158
1286
|
|
|
1159
1287
|
- Rejects missing or malformed `idempotency_key` with `INVALID_REQUEST`. The spec pattern is `^[A-Za-z0-9_.:-]{16,255}$` — a test key like `"key1"` will be rejected for length, not idempotency logic. **Ordering gotcha**: idempotency runs AFTER `resolveAccount`. If your `resolveAccount` returns null for a valid-shape reference, the buyer gets `ACCOUNT_NOT_FOUND` — NOT the missing-key error they expected — and conformance tests fail with the wrong code. Either handle both AccountReference branches (see Implementation above) or accept dev-mode brand+operator wildcards so compliance graders reach the idempotency layer.
|
|
1160
1288
|
- Hashes the request payload with RFC 8785 JCS. The emitted error codes and their semantics are in the table at [§ Composing OAuth, signing, and idempotency](#composing-oauth-signing-and-idempotency).
|
|
@@ -1197,7 +1325,7 @@ The quick-start uses `memoryBackend()` + `InMemoryStateStore` — both reset on
|
|
|
1197
1325
|
|
|
1198
1326
|
```ts
|
|
1199
1327
|
const store = createIdempotencyStore({ backend: pgBackend(pool), ttlSeconds: 86400 });
|
|
1200
|
-
pool.on('error',
|
|
1328
|
+
pool.on('error', err => console.error('pg pool error', err)); // prevent crash on idle-client errors
|
|
1201
1329
|
serve(createAgent, {
|
|
1202
1330
|
readinessCheck: () => store.probe(), // throws with a descriptive error if pool/table is broken
|
|
1203
1331
|
});
|
|
@@ -1351,9 +1479,10 @@ Common failure decoder:
|
|
|
1351
1479
|
|
|
1352
1480
|
| Mistake | Fix |
|
|
1353
1481
|
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1354
|
-
| Using `createTaskCapableServer` + `server.tool()` | Use `
|
|
1482
|
+
| Using `createTaskCapableServer` + `server.tool()` | Use `createAdcpServerFromPlatform(platform, opts)` — handles schemas, response builders, capabilities, ctx_metadata round-trip, idempotency-principal synthesis |
|
|
1483
|
+
| Calling `createAdcpServer` directly in new code | Reach for `createAdcpServerFromPlatform` first; `createAdcpServer` lives at `@adcp/sdk/server/legacy/v5` for mid-migration / escape-hatch use only |
|
|
1355
1484
|
| Using module-level Maps for state | Use `ctx.store` — persists across HTTP requests, swappable for postgres |
|
|
1356
|
-
| Return raw JSON without response builders |
|
|
1485
|
+
| Return raw JSON without response builders | The framework auto-applies response builders — just return the data |
|
|
1357
1486
|
| Missing `brand`/`operator` in sync_accounts response | Echo them back from the request — they're required |
|
|
1358
1487
|
| sync_governance returns wrong shape | Must include `status: 'synced'` and `governance_agents` array |
|
|
1359
1488
|
| `sandbox: false` on mock data | Buyers may treat mock data as real |
|
|
@@ -1399,7 +1528,7 @@ Claim exactly the specialisms your agent actually implements in `capabilities.sp
|
|
|
1399
1528
|
|
|
1400
1529
|
## Reference
|
|
1401
1530
|
|
|
1402
|
-
- `docs/guides/BUILD-AN-AGENT.md` —
|
|
1531
|
+
- `docs/guides/BUILD-AN-AGENT.md` — `createAdcpServerFromPlatform` patterns, async tools, state persistence
|
|
1403
1532
|
- `docs/llms.txt` — full protocol reference
|
|
1404
1533
|
- `docs/TYPE-SUMMARY.md` — curated type signatures
|
|
1405
1534
|
- `storyboards/media_buy_seller.yaml` — full buyer interaction sequence
|