@adcp/sdk 6.7.0 → 6.8.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 (166) hide show
  1. package/bin/adcp.js +15 -3
  2. package/dist/lib/adapters/derived-account-store.d.ts +152 -0
  3. package/dist/lib/adapters/derived-account-store.d.ts.map +1 -0
  4. package/dist/lib/adapters/derived-account-store.js +135 -0
  5. package/dist/lib/adapters/derived-account-store.js.map +1 -0
  6. package/dist/lib/adapters/implicit-account-store.d.ts +3 -1
  7. package/dist/lib/adapters/implicit-account-store.d.ts.map +1 -1
  8. package/dist/lib/adapters/implicit-account-store.js +3 -1
  9. package/dist/lib/adapters/implicit-account-store.js.map +1 -1
  10. package/dist/lib/adapters/index.d.ts +1 -0
  11. package/dist/lib/adapters/index.d.ts.map +1 -1
  12. package/dist/lib/adapters/index.js +7 -1
  13. package/dist/lib/adapters/index.js.map +1 -1
  14. package/dist/lib/adapters/oauth-passthrough-resolver.d.ts +3 -1
  15. package/dist/lib/adapters/oauth-passthrough-resolver.d.ts.map +1 -1
  16. package/dist/lib/adapters/oauth-passthrough-resolver.js +3 -1
  17. package/dist/lib/adapters/oauth-passthrough-resolver.js.map +1 -1
  18. package/dist/lib/adapters/roster-account-store.d.ts +85 -24
  19. package/dist/lib/adapters/roster-account-store.d.ts.map +1 -1
  20. package/dist/lib/adapters/roster-account-store.js +52 -30
  21. package/dist/lib/adapters/roster-account-store.js.map +1 -1
  22. package/dist/lib/index.d.ts +3 -3
  23. package/dist/lib/index.d.ts.map +1 -1
  24. package/dist/lib/index.js +13 -8
  25. package/dist/lib/index.js.map +1 -1
  26. package/dist/lib/mock-server/creative-ad-server/seed-data.d.ts +81 -0
  27. package/dist/lib/mock-server/creative-ad-server/seed-data.d.ts.map +1 -0
  28. package/dist/lib/mock-server/creative-ad-server/seed-data.js +200 -0
  29. package/dist/lib/mock-server/creative-ad-server/seed-data.js.map +1 -0
  30. package/dist/lib/mock-server/creative-ad-server/server.d.ts +39 -0
  31. package/dist/lib/mock-server/creative-ad-server/server.d.ts.map +1 -0
  32. package/dist/lib/mock-server/creative-ad-server/server.js +618 -0
  33. package/dist/lib/mock-server/creative-ad-server/server.js.map +1 -0
  34. package/dist/lib/mock-server/index.d.ts.map +1 -1
  35. package/dist/lib/mock-server/index.js +180 -24
  36. package/dist/lib/mock-server/index.js.map +1 -1
  37. package/dist/lib/mock-server/sales-non-guaranteed/seed-data.d.ts +66 -0
  38. package/dist/lib/mock-server/sales-non-guaranteed/seed-data.d.ts.map +1 -0
  39. package/dist/lib/mock-server/sales-non-guaranteed/seed-data.js +193 -0
  40. package/dist/lib/mock-server/sales-non-guaranteed/seed-data.js.map +1 -0
  41. package/dist/lib/mock-server/sales-non-guaranteed/server.d.ts +33 -0
  42. package/dist/lib/mock-server/sales-non-guaranteed/server.d.ts.map +1 -0
  43. package/dist/lib/mock-server/sales-non-guaranteed/server.js +782 -0
  44. package/dist/lib/mock-server/sales-non-guaranteed/server.js.map +1 -0
  45. package/dist/lib/mock-server/sponsored-intelligence/seed-data.d.ts +50 -0
  46. package/dist/lib/mock-server/sponsored-intelligence/seed-data.d.ts.map +1 -0
  47. package/dist/lib/mock-server/sponsored-intelligence/seed-data.js +133 -0
  48. package/dist/lib/mock-server/sponsored-intelligence/seed-data.js.map +1 -0
  49. package/dist/lib/mock-server/sponsored-intelligence/server.d.ts +13 -0
  50. package/dist/lib/mock-server/sponsored-intelligence/server.d.ts.map +1 -0
  51. package/dist/lib/mock-server/sponsored-intelligence/server.js +609 -0
  52. package/dist/lib/mock-server/sponsored-intelligence/server.js.map +1 -0
  53. package/dist/lib/protocols/mcp.d.ts.map +1 -1
  54. package/dist/lib/protocols/mcp.js +1 -41
  55. package/dist/lib/protocols/mcp.js.map +1 -1
  56. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  57. package/dist/lib/server/account-mode.d.ts +113 -0
  58. package/dist/lib/server/account-mode.d.ts.map +1 -0
  59. package/dist/lib/server/account-mode.js +125 -0
  60. package/dist/lib/server/account-mode.js.map +1 -0
  61. package/dist/lib/server/adcp-server.js +41 -0
  62. package/dist/lib/server/adcp-server.js.map +1 -1
  63. package/dist/lib/server/auth.d.ts +35 -0
  64. package/dist/lib/server/auth.d.ts.map +1 -1
  65. package/dist/lib/server/auth.js.map +1 -1
  66. package/dist/lib/server/create-adcp-server.d.ts +26 -9
  67. package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
  68. package/dist/lib/server/create-adcp-server.js +46 -20
  69. package/dist/lib/server/create-adcp-server.js.map +1 -1
  70. package/dist/lib/server/ctx-metadata/store.d.ts +1 -1
  71. package/dist/lib/server/ctx-metadata/store.d.ts.map +1 -1
  72. package/dist/lib/server/ctx-metadata/store.js +1 -0
  73. package/dist/lib/server/ctx-metadata/store.js.map +1 -1
  74. package/dist/lib/server/decisioning/account.d.ts +5 -0
  75. package/dist/lib/server/decisioning/account.d.ts.map +1 -1
  76. package/dist/lib/server/decisioning/account.js.map +1 -1
  77. package/dist/lib/server/decisioning/buyer-agent.d.ts +37 -4
  78. package/dist/lib/server/decisioning/buyer-agent.d.ts.map +1 -1
  79. package/dist/lib/server/decisioning/buyer-agent.js +12 -2
  80. package/dist/lib/server/decisioning/buyer-agent.js.map +1 -1
  81. package/dist/lib/server/decisioning/compose.d.ts +33 -2
  82. package/dist/lib/server/decisioning/compose.d.ts.map +1 -1
  83. package/dist/lib/server/decisioning/compose.js +13 -46
  84. package/dist/lib/server/decisioning/compose.js.map +1 -1
  85. package/dist/lib/server/decisioning/index.d.ts +2 -1
  86. package/dist/lib/server/decisioning/index.d.ts.map +1 -1
  87. package/dist/lib/server/decisioning/index.js +2 -1
  88. package/dist/lib/server/decisioning/index.js.map +1 -1
  89. package/dist/lib/server/decisioning/platform-helpers.d.ts +18 -0
  90. package/dist/lib/server/decisioning/platform-helpers.d.ts.map +1 -1
  91. package/dist/lib/server/decisioning/platform-helpers.js +20 -0
  92. package/dist/lib/server/decisioning/platform-helpers.js.map +1 -1
  93. package/dist/lib/server/decisioning/platform.d.ts +19 -21
  94. package/dist/lib/server/decisioning/platform.d.ts.map +1 -1
  95. package/dist/lib/server/decisioning/platform.js.map +1 -1
  96. package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
  97. package/dist/lib/server/decisioning/runtime/from-platform.js +334 -44
  98. package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
  99. package/dist/lib/server/decisioning/runtime/observed-modes.d.ts +40 -0
  100. package/dist/lib/server/decisioning/runtime/observed-modes.d.ts.map +1 -0
  101. package/dist/lib/server/decisioning/runtime/observed-modes.js +82 -0
  102. package/dist/lib/server/decisioning/runtime/observed-modes.js.map +1 -0
  103. package/dist/lib/server/decisioning/runtime/protocol-for-tool.js +2 -2
  104. package/dist/lib/server/decisioning/runtime/protocol-for-tool.js.map +1 -1
  105. package/dist/lib/server/decisioning/runtime/validate-platform.d.ts.map +1 -1
  106. package/dist/lib/server/decisioning/runtime/validate-platform.js +9 -1
  107. package/dist/lib/server/decisioning/runtime/validate-platform.js.map +1 -1
  108. package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.d.ts +125 -0
  109. package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.d.ts.map +1 -0
  110. package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.js +52 -0
  111. package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.js.map +1 -0
  112. package/dist/lib/server/decisioning/tenant-registry.d.ts +16 -0
  113. package/dist/lib/server/decisioning/tenant-registry.d.ts.map +1 -1
  114. package/dist/lib/server/decisioning/tenant-registry.js.map +1 -1
  115. package/dist/lib/server/index.d.ts +4 -1
  116. package/dist/lib/server/index.d.ts.map +1 -1
  117. package/dist/lib/server/index.js +9 -3
  118. package/dist/lib/server/index.js.map +1 -1
  119. package/dist/lib/server/serve.js +20 -2
  120. package/dist/lib/server/serve.js.map +1 -1
  121. package/dist/lib/server/test-controller.d.ts +3 -0
  122. package/dist/lib/server/test-controller.d.ts.map +1 -1
  123. package/dist/lib/server/test-controller.js +23 -20
  124. package/dist/lib/server/test-controller.js.map +1 -1
  125. package/dist/lib/testing/comply-controller.d.ts +23 -2
  126. package/dist/lib/testing/comply-controller.d.ts.map +1 -1
  127. package/dist/lib/testing/comply-controller.js +19 -2
  128. package/dist/lib/testing/comply-controller.js.map +1 -1
  129. package/dist/lib/testing/index.d.ts +1 -1
  130. package/dist/lib/testing/index.d.ts.map +1 -1
  131. package/dist/lib/testing/index.js.map +1 -1
  132. package/dist/lib/testing/storyboard/validations.d.ts.map +1 -1
  133. package/dist/lib/testing/storyboard/validations.js +36 -54
  134. package/dist/lib/testing/storyboard/validations.js.map +1 -1
  135. package/dist/lib/testing/test-controller.d.ts +10 -4
  136. package/dist/lib/testing/test-controller.d.ts.map +1 -1
  137. package/dist/lib/testing/test-controller.js +9 -3
  138. package/dist/lib/testing/test-controller.js.map +1 -1
  139. package/dist/lib/types/index.d.ts +3 -2
  140. package/dist/lib/types/index.d.ts.map +1 -1
  141. package/dist/lib/types/index.js +3 -0
  142. package/dist/lib/types/index.js.map +1 -1
  143. package/dist/lib/utils/glob.d.ts +4 -2
  144. package/dist/lib/utils/glob.d.ts.map +1 -1
  145. package/dist/lib/utils/glob.js +4 -2
  146. package/dist/lib/utils/glob.js.map +1 -1
  147. package/dist/lib/version.d.ts +3 -3
  148. package/dist/lib/version.js +3 -3
  149. package/docs/llms.txt +2 -2
  150. package/examples/README.md +29 -13
  151. package/examples/hello-cluster.ts +62 -23
  152. package/examples/hello_creative_adapter_ad_server.ts +790 -0
  153. package/examples/hello_seller_adapter_guaranteed.ts +80 -22
  154. package/examples/hello_seller_adapter_non_guaranteed.ts +1020 -0
  155. package/examples/hello_si_adapter_brand.ts +572 -0
  156. package/package.json +3 -2
  157. package/skills/build-creative-agent/SKILL.md +103 -183
  158. package/skills/build-generative-seller-agent/SKILL.md +15 -9
  159. package/skills/build-governance-agent/SKILL.md +20 -11
  160. package/skills/build-retail-media-agent/SKILL.md +10 -8
  161. package/skills/build-seller-agent/SKILL.md +15 -13
  162. package/skills/build-seller-agent/specialisms/sales-non-guaranteed.md +9 -31
  163. package/skills/build-si-agent/SKILL.md +251 -196
  164. package/skills/build-signals-agent/SKILL.md +2 -0
  165. package/skills/call-adcp-agent/SKILL.md +7 -1
  166. package/skills/call-adcp-agent.previous/SKILL.md +0 -261
@@ -0,0 +1,572 @@
1
+ /**
2
+ * hello_si_adapter_brand — worked starting point for an AdCP Sponsored
3
+ * Intelligence agent (protocol `sponsored_intelligence`) that wraps an
4
+ * upstream brand-agent platform via HTTP.
5
+ *
6
+ * Fork this. Replace `upstream` with calls to your real backend
7
+ * (Salesforce Agentforce, OpenAI Assistants brand mode, custom brand
8
+ * chat). The AdCP-facing platform methods stay the same.
9
+ *
10
+ * **Status**: SI is a *protocol* in AdCP 3.0, not a specialism. Spec change
11
+ * to add it to `AdCPSpecialism` is tracked at adcontextprotocol/adcp#3961
12
+ * for 3.1. Until then the SDK dispatches off the
13
+ * `platform.sponsoredIntelligence` field's presence — which auto-derives
14
+ * `'sponsored_intelligence'` into the wire-side `supported_protocols`
15
+ * via `detectProtocols`.
16
+ *
17
+ * FORK CHECKLIST
18
+ * 1. Replace every `// SWAP:` marker with calls to your backend.
19
+ * 2. Replace `DEFAULT_LISTING_BRAND` with `ctx.authInfo`-derived per-tenant
20
+ * binding (the env-driven default is a multi-brand footgun in production).
21
+ * 3. Production brand engines almost always own full transcript state in
22
+ * their own backend (Postgres, Redis, vector DB). The auto-hydrated
23
+ * `req.session` covers fixture/mock cases and the
24
+ * "what-was-the-original-scope" lookup; do NOT model full transcripts
25
+ * into ctx_metadata — you'll hit the 16KB blob cap.
26
+ * 4. Validate: `node --test test/examples/hello-si-adapter-brand.test.js`
27
+ *
28
+ * Demo:
29
+ * npx @adcp/sdk@latest mock-server sponsored-intelligence --port 4504
30
+ * UPSTREAM_URL=http://127.0.0.1:4504 \
31
+ * npx tsx examples/hello_si_adapter_brand.ts
32
+ * curl http://127.0.0.1:4504/_debug/traffic
33
+ *
34
+ * Production:
35
+ * UPSTREAM_URL=https://my-brand-platform.example/api UPSTREAM_API_KEY=… \
36
+ * PUBLIC_AGENT_URL=https://my-agent.example.com \
37
+ * npx tsx examples/hello_si_adapter_brand.ts
38
+ */
39
+
40
+ import {
41
+ createAdcpServerFromPlatform,
42
+ definePlatform,
43
+ defineSponsoredIntelligencePlatform,
44
+ serve,
45
+ verifyApiKey,
46
+ createIdempotencyStore,
47
+ createUpstreamHttpClient,
48
+ memoryBackend,
49
+ AdcpError,
50
+ type AccountStore,
51
+ type Account,
52
+ } from '@adcp/sdk/server';
53
+ import type {
54
+ SIGetOfferingRequest,
55
+ SIGetOfferingResponse,
56
+ SIInitiateSessionRequest,
57
+ SIInitiateSessionResponse,
58
+ SISendMessageRequest,
59
+ SISendMessageResponse,
60
+ SITerminateSessionRequest,
61
+ SITerminateSessionResponse,
62
+ SIUIElement,
63
+ SISessionStatus,
64
+ } from '@adcp/sdk';
65
+
66
+ const UPSTREAM_URL = process.env['UPSTREAM_URL'] ?? 'http://127.0.0.1:4504';
67
+ const UPSTREAM_API_KEY = process.env['UPSTREAM_API_KEY'] ?? 'mock_si_brand_key_do_not_use_in_prod';
68
+ const PORT = Number(process.env['PORT'] ?? 3004);
69
+ const ADCP_AUTH_TOKEN = process.env['ADCP_AUTH_TOKEN'] ?? 'sk_harness_do_not_use_in_prod';
70
+ // Default brand used when a tool call lacks `account` resolution context.
71
+ // SI tool requests don't carry `account` on the wire (the schema omits
72
+ // it — session continuity flows through `session_id` instead), so this
73
+ // default is what `accounts.resolve(undefined)` falls back to.
74
+ //
75
+ // Defaults to `brand_nova_motors` because that's the canonical compliance
76
+ // fixture — the `si_baseline` storyboard at
77
+ // `compliance/cache/latest/protocols/sponsored-intelligence/index.yaml`
78
+ // requests `novamotors_conversational_v1`, which lives under that brand.
79
+ //
80
+ // SWAP: production agents are typically deployed per-brand (one agent per
81
+ // tenant), so a hardcoded default is fine. Multi-brand deployments derive
82
+ // from `ctx.authInfo` per-API-key binding instead.
83
+ const DEFAULT_LISTING_BRAND = process.env['DEFAULT_LISTING_BRAND'] ?? 'brand_nova_motors';
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Upstream client — SWAP for production.
87
+ // ---------------------------------------------------------------------------
88
+
89
+ interface UpstreamProduct {
90
+ sku: string;
91
+ name: string;
92
+ display_price: string;
93
+ list_price?: string;
94
+ thumbnail_url: string;
95
+ pdp_url: string;
96
+ inventory_status: string;
97
+ }
98
+
99
+ interface UpstreamOffering {
100
+ offering_id: string;
101
+ brand_id: string;
102
+ name: string;
103
+ summary: string;
104
+ tagline?: string;
105
+ hero_image_url: string;
106
+ landing_page_url: string;
107
+ price_hint: string;
108
+ expires_at: string;
109
+ available: boolean;
110
+ products: UpstreamProduct[];
111
+ total_matching: number;
112
+ offering_query_id?: string;
113
+ offering_query_expires_at?: string;
114
+ offering_query_ttl_seconds?: number;
115
+ }
116
+
117
+ /** Upstream component vocabulary — `kind` field discriminates. AdCP uses
118
+ * `type` (rename) on `SIUIElement`. */
119
+ interface UpstreamComponent {
120
+ kind: string;
121
+ [k: string]: unknown;
122
+ }
123
+
124
+ interface UpstreamTurn {
125
+ turn_id: string;
126
+ conversation_id: string;
127
+ user_message: string | null;
128
+ assistant_message: string;
129
+ components: UpstreamComponent[];
130
+ close_recommended: { type: 'txn_ready' | 'done'; payload?: Record<string, unknown> } | null;
131
+ created_at: string;
132
+ conversation_status?: 'active' | 'closed';
133
+ session_ttl_seconds?: number;
134
+ }
135
+
136
+ interface UpstreamConversation {
137
+ conversation_id: string;
138
+ brand_id: string;
139
+ status: 'active' | 'closed';
140
+ offering_id: string | null;
141
+ offering_query_id: string | null;
142
+ shown_product_skus: string[];
143
+ intent: string;
144
+ turns: UpstreamTurn[];
145
+ close: {
146
+ reason: 'txn_ready' | 'done' | 'user_left' | 'idle_timeout' | 'host_closed';
147
+ closed_at: string;
148
+ transaction_handoff: {
149
+ checkout_url?: string;
150
+ checkout_token?: string;
151
+ expires_at?: string;
152
+ payload?: Record<string, unknown>;
153
+ } | null;
154
+ } | null;
155
+ session_ttl_seconds: number;
156
+ created_at: string;
157
+ updated_at: string;
158
+ }
159
+
160
+ const http = createUpstreamHttpClient({
161
+ baseUrl: UPSTREAM_URL,
162
+ auth: { kind: 'static_bearer', token: UPSTREAM_API_KEY },
163
+ });
164
+
165
+ const upstream = {
166
+ // SWAP: AdCP-side brand identifier → upstream brand_id. Real platforms
167
+ // typically expose this through a directory service or per-API-key
168
+ // tenant binding; the mock has a public discovery endpoint.
169
+ async lookupBrand(brandIdentifier: string): Promise<string | null> {
170
+ const { body } = await http.get<{ brand_id?: string }>('/_lookup/brand', { adcp_brand: brandIdentifier });
171
+ return body?.brand_id ?? null;
172
+ },
173
+
174
+ // SWAP: GET offering details. `include_products=true` causes the upstream
175
+ // to mint an offering_query_id; pass that to startConversation so the
176
+ // brand can resolve "the second one" against the products actually shown.
177
+ async getOffering(
178
+ brandId: string,
179
+ offeringId: string,
180
+ opts: { includeProducts?: boolean; productLimit?: number } = {}
181
+ ): Promise<UpstreamOffering | null> {
182
+ const params: Record<string, string> = {};
183
+ if (opts.includeProducts) params['include_products'] = 'true';
184
+ if (opts.productLimit !== undefined) params['product_limit'] = String(opts.productLimit);
185
+ const { body } = await http.get<UpstreamOffering>(
186
+ `/v1/brands/${encodeURIComponent(brandId)}/offerings/${encodeURIComponent(offeringId)}`,
187
+ params
188
+ );
189
+ return body;
190
+ },
191
+
192
+ // SWAP: start a conversation. `client_request_id` carries the AdCP
193
+ // idempotency_key — replay protection lives in the upstream.
194
+ async startConversation(
195
+ brandId: string,
196
+ body: {
197
+ intent: string;
198
+ offering_id?: string;
199
+ offering_query_id?: string;
200
+ identity?: unknown;
201
+ client_request_id?: string;
202
+ }
203
+ ): Promise<UpstreamConversation> {
204
+ const r = await http.post<UpstreamConversation>(`/v1/brands/${encodeURIComponent(brandId)}/conversations`, body);
205
+ if (r.body === null) {
206
+ throw new AdcpError('INVALID_REQUEST', { message: 'conversation creation rejected by upstream' });
207
+ }
208
+ return r.body;
209
+ },
210
+
211
+ // SWAP: send a turn. Mismatched body on reused client_request_id → 409.
212
+ async sendTurn(
213
+ brandId: string,
214
+ conversationId: string,
215
+ body: { message?: string; action_response?: unknown; client_request_id?: string }
216
+ ): Promise<UpstreamTurn> {
217
+ const r = await http.post<UpstreamTurn>(
218
+ `/v1/brands/${encodeURIComponent(brandId)}/conversations/${encodeURIComponent(conversationId)}/turns`,
219
+ body
220
+ );
221
+ if (r.body === null) {
222
+ throw new AdcpError('INVALID_REQUEST', { message: 'turn rejected by upstream' });
223
+ }
224
+ return r.body;
225
+ },
226
+
227
+ // SWAP: close a conversation. Naturally idempotent on conversation_id
228
+ // (mirrors AdCP's omission of `idempotency_key` on terminate).
229
+ async closeConversation(
230
+ brandId: string,
231
+ conversationId: string,
232
+ body: { reason: 'txn_ready' | 'done' | 'user_left' | 'idle_timeout' | 'host_closed'; summary?: string }
233
+ ): Promise<UpstreamConversation> {
234
+ const r = await http.post<UpstreamConversation>(
235
+ `/v1/brands/${encodeURIComponent(brandId)}/conversations/${encodeURIComponent(conversationId)}/close`,
236
+ body
237
+ );
238
+ if (r.body === null) {
239
+ throw new AdcpError('INVALID_REQUEST', { message: 'close rejected by upstream' });
240
+ }
241
+ return r.body;
242
+ },
243
+ };
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Translation tables — upstream ↔ AdCP renames are deliberate per the SI mock
247
+ // design. Keeping them as small named functions makes the seams explicit.
248
+ // ---------------------------------------------------------------------------
249
+
250
+ /** AdCP `SITerminateSessionRequest.reason` → upstream close reason. The SI
251
+ * mock rejects AdCP values directly (loud rename gap by design). */
252
+ function adcpReasonToUpstream(
253
+ reason: SITerminateSessionRequest['reason']
254
+ ): 'txn_ready' | 'done' | 'user_left' | 'idle_timeout' | 'host_closed' {
255
+ switch (reason) {
256
+ case 'handoff_transaction':
257
+ return 'txn_ready';
258
+ case 'handoff_complete':
259
+ return 'done';
260
+ case 'user_exit':
261
+ return 'user_left';
262
+ case 'session_timeout':
263
+ return 'idle_timeout';
264
+ case 'host_terminated':
265
+ return 'host_closed';
266
+ }
267
+ }
268
+
269
+ /** Project an upstream component (`{ kind, ... }`) onto an AdCP `SIUIElement`
270
+ * (`{ type, data }`). Each type has its own per-data shape required by
271
+ * the spec — `product_card` needs `title` + `price`, `action_button`
272
+ * needs `label` + `action`, etc. We project per-type rather than a flat
273
+ * spread so wire-validation passes. */
274
+ function projectComponent(c: UpstreamComponent): SIUIElement {
275
+ switch (c.kind) {
276
+ case 'product_card': {
277
+ // Upstream → AdCP renames inside data: name → title, display_price →
278
+ // price, list_price → subtitle (or badge if you prefer), thumbnail_url →
279
+ // image_url. AdCP requires title + price.
280
+ const data: Record<string, unknown> = {
281
+ title: typeof c['name'] === 'string' ? c['name'] : '(unnamed product)',
282
+ price: typeof c['display_price'] === 'string' ? c['display_price'] : '',
283
+ };
284
+ if (typeof c['thumbnail_url'] === 'string') data['image_url'] = c['thumbnail_url'];
285
+ if (typeof c['list_price'] === 'string') data['subtitle'] = c['list_price'];
286
+ if (typeof c['inventory_status'] === 'string') data['badge'] = c['inventory_status'];
287
+ return { type: 'product_card', data };
288
+ }
289
+ case 'action_button': {
290
+ const data: Record<string, unknown> = {
291
+ label: typeof c['label'] === 'string' ? c['label'] : 'OK',
292
+ action: typeof c['action'] === 'string' ? c['action'] : 'noop',
293
+ };
294
+ if (c['payload'] !== undefined) data['payload'] = c['payload'];
295
+ return { type: 'action_button', data };
296
+ }
297
+ case 'text':
298
+ case 'link':
299
+ case 'image':
300
+ case 'carousel':
301
+ case 'app_handoff':
302
+ case 'integration_actions': {
303
+ // Pass-through: the upstream produces the spec-correct data shape
304
+ // for these kinds, or the data shape is permissive enough that
305
+ // additional properties don't fail wire validation.
306
+ const { kind: _k, ...data } = c;
307
+ void _k;
308
+ return { type: c.kind, data };
309
+ }
310
+ default: {
311
+ // Unknown upstream kind → safe fallback to `text` so wire validation
312
+ // doesn't reject a legitimate response. Production adapters should
313
+ // log + map every upstream-specific kind explicitly.
314
+ return { type: 'text', data: { text: `[unsupported upstream kind: ${c.kind}]` } };
315
+ }
316
+ }
317
+ }
318
+
319
+ /** Project an upstream `close_recommended` hint onto AdCP
320
+ * `session_status: 'pending_handoff'` + `handoff: { type, ... }` on a
321
+ * `si_send_message` response. The brand emits the hint mid-conversation;
322
+ * this adapter chooses the eager projection (surface as pending_handoff
323
+ * immediately) over lazy (wait for terminate). Either is spec-valid. */
324
+ function projectCloseHint(hint: NonNullable<UpstreamTurn['close_recommended']>): {
325
+ status: SISessionStatus;
326
+ handoff: NonNullable<SISendMessageResponse['handoff']>;
327
+ } {
328
+ if (hint.type === 'txn_ready') {
329
+ const product = (hint.payload?.['product'] as Record<string, unknown> | undefined) ?? {};
330
+ return {
331
+ status: 'pending_handoff',
332
+ handoff: {
333
+ type: 'transaction',
334
+ intent: {
335
+ action: 'purchase',
336
+ product,
337
+ ...(typeof product['display_price'] === 'string'
338
+ ? { price: { amount: parsePriceAmount(product['display_price']), currency: 'USD' } }
339
+ : {}),
340
+ },
341
+ },
342
+ };
343
+ }
344
+ return { status: 'pending_handoff', handoff: { type: 'complete' } };
345
+ }
346
+
347
+ function parsePriceAmount(displayPrice: string): number {
348
+ // Upstream display_price is "$129" / "$89.99" — strip non-numeric and parse.
349
+ const numeric = displayPrice.replace(/[^0-9.]/g, '');
350
+ const value = Number(numeric);
351
+ return Number.isFinite(value) ? value : 0;
352
+ }
353
+
354
+ // ---------------------------------------------------------------------------
355
+ // AdCP-side adapter — typed against SponsoredIntelligencePlatform.
356
+ // ---------------------------------------------------------------------------
357
+
358
+ interface SiBrandMeta {
359
+ /** Resolved upstream brand_id, cached on the Account by accounts.resolve. */
360
+ brand_id: string;
361
+ /** AdCP-side brand identifier — preserved for logging / debugging. */
362
+ brand_identifier: string;
363
+ [key: string]: unknown;
364
+ }
365
+
366
+ // SI isn't yet a specialism (adcp#3961). The platform field's presence
367
+ // is the declaration; framework auto-derives 'sponsored_intelligence'
368
+ // into supported_protocols from the four SI tools getting registered.
369
+ // Build with `definePlatform` so the empty-`specialisms[]` flows through
370
+ // `RequiredPlatformsFor`'s `[S] extends [never]` short-circuit cleanly.
371
+
372
+ const accounts: AccountStore<SiBrandMeta> = {
373
+ resolve: async ref => {
374
+ if (!ref) {
375
+ // No-account tools (the SI surface tools all carry account context
376
+ // via session_id correlation, but `resolve(undefined)` may still
377
+ // fire on capability discovery). Default-listing-brand fallback so
378
+ // ctx.account is non-null at runtime.
379
+ return {
380
+ id: DEFAULT_LISTING_BRAND,
381
+ name: DEFAULT_LISTING_BRAND,
382
+ status: 'active',
383
+ ctx_metadata: { brand_id: DEFAULT_LISTING_BRAND, brand_identifier: '' },
384
+ };
385
+ }
386
+ if ('account_id' in ref) {
387
+ // SWAP: production lookup keyed by your seller-assigned account_id.
388
+ return null;
389
+ }
390
+ const brandIdentifier = ref.brand.domain;
391
+ const brandId = await upstream.lookupBrand(brandIdentifier);
392
+ if (!brandId) return null;
393
+ return {
394
+ id: brandId,
395
+ name: brandIdentifier,
396
+ status: 'active',
397
+ ctx_metadata: { brand_id: brandId, brand_identifier: brandIdentifier },
398
+ };
399
+ },
400
+ };
401
+
402
+ const sponsoredIntelligence = defineSponsoredIntelligencePlatform<SiBrandMeta>({
403
+ getOffering: async (req: SIGetOfferingRequest, ctx): Promise<SIGetOfferingResponse> => {
404
+ const brandId = ctx.account?.ctx_metadata.brand_id ?? DEFAULT_LISTING_BRAND;
405
+ const offering = await upstream.getOffering(brandId, req.offering_id, {
406
+ includeProducts: req.include_products === true,
407
+ ...(req.product_limit !== undefined ? { productLimit: req.product_limit } : {}),
408
+ });
409
+ if (!offering) {
410
+ throw new AdcpError('NOT_FOUND', {
411
+ message: `Offering ${req.offering_id} not found in brand ${brandId}.`,
412
+ field: 'offering_id',
413
+ });
414
+ }
415
+ // Project upstream → AdCP. The rename pattern: hero_image_url → image_url,
416
+ // landing_page_url → landing_url, sku → product_id, thumbnail_url →
417
+ // image_url, pdp_url → url, inventory_status → availability_summary,
418
+ // offering_query_id → offering_token.
419
+ const matching = req.include_products
420
+ ? offering.products.map(p => ({
421
+ product_id: p.sku,
422
+ name: p.name,
423
+ price: p.display_price,
424
+ ...(p.list_price ? { original_price: p.list_price } : {}),
425
+ image_url: p.thumbnail_url,
426
+ availability_summary: p.inventory_status,
427
+ url: p.pdp_url,
428
+ }))
429
+ : undefined;
430
+ const response: SIGetOfferingResponse = {
431
+ available: offering.available,
432
+ ...(offering.offering_query_id !== undefined ? { offering_token: offering.offering_query_id } : {}),
433
+ ...(offering.offering_query_ttl_seconds !== undefined
434
+ ? { ttl_seconds: offering.offering_query_ttl_seconds }
435
+ : {}),
436
+ checked_at: new Date().toISOString(),
437
+ offering: {
438
+ offering_id: offering.offering_id,
439
+ title: offering.name,
440
+ summary: offering.summary,
441
+ ...(offering.tagline !== undefined ? { tagline: offering.tagline } : {}),
442
+ expires_at: offering.expires_at,
443
+ price_hint: offering.price_hint,
444
+ image_url: offering.hero_image_url,
445
+ landing_url: offering.landing_page_url,
446
+ },
447
+ ...(matching ? { matching_products: matching } : {}),
448
+ total_matching: offering.total_matching,
449
+ };
450
+ // Top-level `offering_id` mirror. The canonical AdCP wire location is
451
+ // `offering.offering_id` (above), but the `si_baseline` compliance
452
+ // storyboard captures with `path: 'offering_id'` (top-level). The
453
+ // schema allows `additionalProperties: true` at the response root so
454
+ // the mirror is permitted at the wire layer; the generated TS type
455
+ // doesn't model extra properties, so widen via cast. Drop once the
456
+ // storyboard path is corrected to `offering.offering_id` upstream.
457
+ return Object.assign({}, response, { offering_id: offering.offering_id }) as SIGetOfferingResponse;
458
+ },
459
+
460
+ initiateSession: async (req: SIInitiateSessionRequest, ctx): Promise<SIInitiateSessionResponse> => {
461
+ const brandId = ctx.account?.ctx_metadata.brand_id ?? DEFAULT_LISTING_BRAND;
462
+ const conversation = await upstream.startConversation(brandId, {
463
+ intent: req.intent,
464
+ ...(req.offering_id !== undefined ? { offering_id: req.offering_id } : {}),
465
+ ...(req.offering_token !== undefined ? { offering_query_id: req.offering_token } : {}),
466
+ ...(req.identity !== undefined ? { identity: req.identity } : {}),
467
+ client_request_id: req.idempotency_key,
468
+ });
469
+ const initial = conversation.turns[0];
470
+ return {
471
+ // conversation_id → session_id (rename only — the brand-side and
472
+ // AdCP-side identifiers are the same opaque token).
473
+ session_id: conversation.conversation_id,
474
+ ...(initial
475
+ ? {
476
+ response: {
477
+ message: initial.assistant_message,
478
+ ui_elements: initial.components.map(projectComponent),
479
+ },
480
+ }
481
+ : {}),
482
+ session_status: conversation.status === 'active' ? 'active' : 'terminated',
483
+ session_ttl_seconds: conversation.session_ttl_seconds,
484
+ };
485
+ },
486
+
487
+ sendMessage: async (req: SISendMessageRequest, ctx): Promise<SISendMessageResponse> => {
488
+ const brandId = ctx.account?.ctx_metadata.brand_id ?? DEFAULT_LISTING_BRAND;
489
+ // `req.session` is auto-hydrated by the framework when a prior
490
+ // initiateSession landed via this same SDK instance — useful for
491
+ // recalling original intent / offering scope without a separate
492
+ // store call. Production brand engines own full transcripts in
493
+ // their own backend; the upstream call below replays through the
494
+ // brand's session-keyed API which is the source of truth for
495
+ // transcript state.
496
+ const turn = await upstream.sendTurn(brandId, req.session_id, {
497
+ ...(typeof req.message === 'string' ? { message: req.message } : {}),
498
+ ...(req.action_response !== undefined ? { action_response: req.action_response } : {}),
499
+ client_request_id: req.idempotency_key,
500
+ });
501
+ // Eager close-hint projection: surface `pending_handoff` mid-conversation
502
+ // when the brand signals txn_ready / done. The host then calls
503
+ // `si_terminate_session` to formally close.
504
+ const closeProjection = turn.close_recommended ? projectCloseHint(turn.close_recommended) : null;
505
+ return {
506
+ session_id: turn.conversation_id,
507
+ response: {
508
+ message: turn.assistant_message,
509
+ ui_elements: turn.components.map(projectComponent),
510
+ },
511
+ session_status: closeProjection?.status ?? (turn.conversation_status === 'closed' ? 'terminated' : 'active'),
512
+ ...(closeProjection ? { handoff: closeProjection.handoff } : {}),
513
+ };
514
+ },
515
+
516
+ terminateSession: async (req: SITerminateSessionRequest, ctx): Promise<SITerminateSessionResponse> => {
517
+ const brandId = ctx.account?.ctx_metadata.brand_id ?? DEFAULT_LISTING_BRAND;
518
+ const conversation = await upstream.closeConversation(brandId, req.session_id, {
519
+ reason: adcpReasonToUpstream(req.reason),
520
+ ...(req.termination_context?.summary ? { summary: req.termination_context.summary } : {}),
521
+ });
522
+ const handoff = conversation.close?.transaction_handoff ?? null;
523
+ return {
524
+ session_id: conversation.conversation_id,
525
+ terminated: conversation.status === 'closed',
526
+ session_status: 'terminated',
527
+ ...(handoff
528
+ ? {
529
+ acp_handoff: {
530
+ ...(handoff.checkout_url ? { checkout_url: handoff.checkout_url } : {}),
531
+ ...(handoff.checkout_token ? { checkout_token: handoff.checkout_token } : {}),
532
+ ...(handoff.expires_at ? { expires_at: handoff.expires_at } : {}),
533
+ ...(handoff.payload ? { payload: handoff.payload } : {}),
534
+ },
535
+ }
536
+ : {}),
537
+ };
538
+ },
539
+ });
540
+
541
+ // ---------------------------------------------------------------------------
542
+ // Boot
543
+ // ---------------------------------------------------------------------------
544
+
545
+ const platform = definePlatform<Record<string, never>, SiBrandMeta>({
546
+ capabilities: { specialisms: [] as const, config: {} },
547
+ accounts,
548
+ sponsoredIntelligence,
549
+ });
550
+ const idempotencyStore = createIdempotencyStore({ backend: memoryBackend(), ttlSeconds: 86_400 });
551
+
552
+ serve(
553
+ ({ taskStore }) =>
554
+ createAdcpServerFromPlatform(platform, {
555
+ name: 'hello-si-adapter-brand',
556
+ version: '1.0.0',
557
+ taskStore,
558
+ idempotency: idempotencyStore,
559
+ resolveSessionKey: ctx => {
560
+ const acct = ctx.account as Account<SiBrandMeta> | undefined;
561
+ return acct?.id ?? 'anonymous';
562
+ },
563
+ }),
564
+ {
565
+ port: PORT,
566
+ authenticate: verifyApiKey({
567
+ keys: { [ADCP_AUTH_TOKEN]: { principal: 'compliance-runner' } },
568
+ }),
569
+ }
570
+ );
571
+
572
+ console.log(`sponsored-intelligence adapter on http://127.0.0.1:${PORT}/mcp · upstream: ${UPSTREAM_URL}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcp/sdk",
3
- "version": "6.7.0",
3
+ "version": "6.8.0",
4
4
  "description": "AdCP SDK — client, server, and compliance harnesses for the AdContext Protocol (MCP + A2A)",
5
5
  "workspaces": [
6
6
  ".",
@@ -258,8 +258,9 @@
258
258
  "ci:validate": "node scripts/ci-validate.js",
259
259
  "ci:quick": "npm run format:check && npm run typecheck && npm run build:lib && npm test",
260
260
  "ci:schema-check": "npm run sync-schemas && npm run generate-types && npm run generate-registry-types && git diff --exit-code -I '// Generated at:' src/lib/types/ src/lib/agents/ src/lib/registry/types.generated.ts schemas/registry/registry.yaml || (echo '⚠️ Generated files are out of sync. Run: npm run sync-schemas && npm run generate-types && npm run generate-registry-types' && exit 1)",
261
+ "ci:codegen-strict": "tsx scripts/check-no-loose-oneof.ts",
261
262
  "ci:docs-check": "npm run generate-agent-docs && git diff --exit-code -I '> Generated at:' -I '> (Library: )?@adcp/sdk v' docs/llms.txt docs/TYPE-SUMMARY.md || (echo '⚠️ Agent docs are out of sync. Run: npm run generate-agent-docs' && exit 1)",
262
- "ci:pre-push": "npm run ci:schema-check && npm run ci:quick",
263
+ "ci:pre-push": "npm run ci:schema-check && npm run ci:codegen-strict && npm run ci:quick",
263
264
  "hooks:install": "node scripts/install-hooks.js",
264
265
  "hooks:uninstall": "rm -f .git/hooks/pre-push",
265
266
  "hello-cluster": "tsx examples/hello-cluster.ts",