@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.
- package/bin/adcp.js +15 -3
- package/dist/lib/adapters/derived-account-store.d.ts +152 -0
- package/dist/lib/adapters/derived-account-store.d.ts.map +1 -0
- package/dist/lib/adapters/derived-account-store.js +135 -0
- package/dist/lib/adapters/derived-account-store.js.map +1 -0
- package/dist/lib/adapters/implicit-account-store.d.ts +3 -1
- package/dist/lib/adapters/implicit-account-store.d.ts.map +1 -1
- package/dist/lib/adapters/implicit-account-store.js +3 -1
- package/dist/lib/adapters/implicit-account-store.js.map +1 -1
- package/dist/lib/adapters/index.d.ts +1 -0
- package/dist/lib/adapters/index.d.ts.map +1 -1
- package/dist/lib/adapters/index.js +7 -1
- package/dist/lib/adapters/index.js.map +1 -1
- package/dist/lib/adapters/oauth-passthrough-resolver.d.ts +3 -1
- package/dist/lib/adapters/oauth-passthrough-resolver.d.ts.map +1 -1
- package/dist/lib/adapters/oauth-passthrough-resolver.js +3 -1
- package/dist/lib/adapters/oauth-passthrough-resolver.js.map +1 -1
- package/dist/lib/adapters/roster-account-store.d.ts +85 -24
- package/dist/lib/adapters/roster-account-store.d.ts.map +1 -1
- package/dist/lib/adapters/roster-account-store.js +52 -30
- package/dist/lib/adapters/roster-account-store.js.map +1 -1
- package/dist/lib/index.d.ts +3 -3
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +13 -8
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/mock-server/creative-ad-server/seed-data.d.ts +81 -0
- package/dist/lib/mock-server/creative-ad-server/seed-data.d.ts.map +1 -0
- package/dist/lib/mock-server/creative-ad-server/seed-data.js +200 -0
- package/dist/lib/mock-server/creative-ad-server/seed-data.js.map +1 -0
- package/dist/lib/mock-server/creative-ad-server/server.d.ts +39 -0
- package/dist/lib/mock-server/creative-ad-server/server.d.ts.map +1 -0
- package/dist/lib/mock-server/creative-ad-server/server.js +618 -0
- package/dist/lib/mock-server/creative-ad-server/server.js.map +1 -0
- package/dist/lib/mock-server/index.d.ts.map +1 -1
- package/dist/lib/mock-server/index.js +180 -24
- package/dist/lib/mock-server/index.js.map +1 -1
- package/dist/lib/mock-server/sales-non-guaranteed/seed-data.d.ts +66 -0
- package/dist/lib/mock-server/sales-non-guaranteed/seed-data.d.ts.map +1 -0
- package/dist/lib/mock-server/sales-non-guaranteed/seed-data.js +193 -0
- package/dist/lib/mock-server/sales-non-guaranteed/seed-data.js.map +1 -0
- package/dist/lib/mock-server/sales-non-guaranteed/server.d.ts +33 -0
- package/dist/lib/mock-server/sales-non-guaranteed/server.d.ts.map +1 -0
- package/dist/lib/mock-server/sales-non-guaranteed/server.js +782 -0
- package/dist/lib/mock-server/sales-non-guaranteed/server.js.map +1 -0
- package/dist/lib/mock-server/sponsored-intelligence/seed-data.d.ts +50 -0
- package/dist/lib/mock-server/sponsored-intelligence/seed-data.d.ts.map +1 -0
- package/dist/lib/mock-server/sponsored-intelligence/seed-data.js +133 -0
- package/dist/lib/mock-server/sponsored-intelligence/seed-data.js.map +1 -0
- package/dist/lib/mock-server/sponsored-intelligence/server.d.ts +13 -0
- package/dist/lib/mock-server/sponsored-intelligence/server.d.ts.map +1 -0
- package/dist/lib/mock-server/sponsored-intelligence/server.js +609 -0
- package/dist/lib/mock-server/sponsored-intelligence/server.js.map +1 -0
- package/dist/lib/protocols/mcp.d.ts.map +1 -1
- package/dist/lib/protocols/mcp.js +1 -41
- package/dist/lib/protocols/mcp.js.map +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/account-mode.d.ts +113 -0
- package/dist/lib/server/account-mode.d.ts.map +1 -0
- package/dist/lib/server/account-mode.js +125 -0
- package/dist/lib/server/account-mode.js.map +1 -0
- package/dist/lib/server/adcp-server.js +41 -0
- package/dist/lib/server/adcp-server.js.map +1 -1
- package/dist/lib/server/auth.d.ts +35 -0
- package/dist/lib/server/auth.d.ts.map +1 -1
- package/dist/lib/server/auth.js.map +1 -1
- package/dist/lib/server/create-adcp-server.d.ts +26 -9
- package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
- package/dist/lib/server/create-adcp-server.js +46 -20
- package/dist/lib/server/create-adcp-server.js.map +1 -1
- package/dist/lib/server/ctx-metadata/store.d.ts +1 -1
- package/dist/lib/server/ctx-metadata/store.d.ts.map +1 -1
- package/dist/lib/server/ctx-metadata/store.js +1 -0
- package/dist/lib/server/ctx-metadata/store.js.map +1 -1
- package/dist/lib/server/decisioning/account.d.ts +5 -0
- package/dist/lib/server/decisioning/account.d.ts.map +1 -1
- package/dist/lib/server/decisioning/account.js.map +1 -1
- package/dist/lib/server/decisioning/buyer-agent.d.ts +37 -4
- package/dist/lib/server/decisioning/buyer-agent.d.ts.map +1 -1
- package/dist/lib/server/decisioning/buyer-agent.js +12 -2
- package/dist/lib/server/decisioning/buyer-agent.js.map +1 -1
- package/dist/lib/server/decisioning/compose.d.ts +33 -2
- package/dist/lib/server/decisioning/compose.d.ts.map +1 -1
- package/dist/lib/server/decisioning/compose.js +13 -46
- package/dist/lib/server/decisioning/compose.js.map +1 -1
- package/dist/lib/server/decisioning/index.d.ts +2 -1
- package/dist/lib/server/decisioning/index.d.ts.map +1 -1
- package/dist/lib/server/decisioning/index.js +2 -1
- package/dist/lib/server/decisioning/index.js.map +1 -1
- package/dist/lib/server/decisioning/platform-helpers.d.ts +18 -0
- package/dist/lib/server/decisioning/platform-helpers.d.ts.map +1 -1
- package/dist/lib/server/decisioning/platform-helpers.js +20 -0
- package/dist/lib/server/decisioning/platform-helpers.js.map +1 -1
- package/dist/lib/server/decisioning/platform.d.ts +19 -21
- package/dist/lib/server/decisioning/platform.d.ts.map +1 -1
- package/dist/lib/server/decisioning/platform.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.js +334 -44
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/observed-modes.d.ts +40 -0
- package/dist/lib/server/decisioning/runtime/observed-modes.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/observed-modes.js +82 -0
- package/dist/lib/server/decisioning/runtime/observed-modes.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.js +2 -2
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/validate-platform.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/validate-platform.js +9 -1
- package/dist/lib/server/decisioning/runtime/validate-platform.js.map +1 -1
- package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.d.ts +125 -0
- package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.js +52 -0
- package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.js.map +1 -0
- package/dist/lib/server/decisioning/tenant-registry.d.ts +16 -0
- package/dist/lib/server/decisioning/tenant-registry.d.ts.map +1 -1
- package/dist/lib/server/decisioning/tenant-registry.js.map +1 -1
- package/dist/lib/server/index.d.ts +4 -1
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +9 -3
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/server/serve.js +20 -2
- package/dist/lib/server/serve.js.map +1 -1
- package/dist/lib/server/test-controller.d.ts +3 -0
- package/dist/lib/server/test-controller.d.ts.map +1 -1
- package/dist/lib/server/test-controller.js +23 -20
- package/dist/lib/server/test-controller.js.map +1 -1
- package/dist/lib/testing/comply-controller.d.ts +23 -2
- package/dist/lib/testing/comply-controller.d.ts.map +1 -1
- package/dist/lib/testing/comply-controller.js +19 -2
- 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/storyboard/validations.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/validations.js +36 -54
- package/dist/lib/testing/storyboard/validations.js.map +1 -1
- package/dist/lib/testing/test-controller.d.ts +10 -4
- package/dist/lib/testing/test-controller.d.ts.map +1 -1
- package/dist/lib/testing/test-controller.js +9 -3
- package/dist/lib/testing/test-controller.js.map +1 -1
- package/dist/lib/types/index.d.ts +3 -2
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js +3 -0
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/utils/glob.d.ts +4 -2
- package/dist/lib/utils/glob.d.ts.map +1 -1
- package/dist/lib/utils/glob.js +4 -2
- package/dist/lib/utils/glob.js.map +1 -1
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/docs/llms.txt +2 -2
- package/examples/README.md +29 -13
- package/examples/hello-cluster.ts +62 -23
- package/examples/hello_creative_adapter_ad_server.ts +790 -0
- package/examples/hello_seller_adapter_guaranteed.ts +80 -22
- package/examples/hello_seller_adapter_non_guaranteed.ts +1020 -0
- package/examples/hello_si_adapter_brand.ts +572 -0
- package/package.json +3 -2
- package/skills/build-creative-agent/SKILL.md +103 -183
- package/skills/build-generative-seller-agent/SKILL.md +15 -9
- package/skills/build-governance-agent/SKILL.md +20 -11
- package/skills/build-retail-media-agent/SKILL.md +10 -8
- package/skills/build-seller-agent/SKILL.md +15 -13
- package/skills/build-seller-agent/specialisms/sales-non-guaranteed.md +9 -31
- package/skills/build-si-agent/SKILL.md +251 -196
- package/skills/build-signals-agent/SKILL.md +2 -0
- package/skills/call-adcp-agent/SKILL.md +7 -1
- 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.
|
|
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",
|