@adcp/client 3.3.3 → 3.5.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/dist/lib/auth/index.d.ts +1 -7
- package/dist/lib/auth/index.d.ts.map +1 -1
- package/dist/lib/auth/index.js +2 -33
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/core/ADCPMultiAgentClient.d.ts +5 -4
- package/dist/lib/core/ADCPMultiAgentClient.d.ts.map +1 -1
- package/dist/lib/core/ADCPMultiAgentClient.js +7 -6
- package/dist/lib/core/ADCPMultiAgentClient.js.map +1 -1
- package/dist/lib/core/AgentClient.d.ts +29 -1
- package/dist/lib/core/AgentClient.d.ts.map +1 -1
- package/dist/lib/core/AgentClient.js +51 -2
- package/dist/lib/core/AgentClient.js.map +1 -1
- package/dist/lib/core/AsyncHandler.d.ts +23 -27
- package/dist/lib/core/AsyncHandler.d.ts.map +1 -1
- package/dist/lib/core/AsyncHandler.js +9 -19
- package/dist/lib/core/AsyncHandler.js.map +1 -1
- package/dist/lib/core/ConfigurationManager.d.ts.map +1 -1
- package/dist/lib/core/ConfigurationManager.js +5 -14
- package/dist/lib/core/ConfigurationManager.js.map +1 -1
- package/dist/lib/core/CreativeAgentClient.js +1 -1
- package/dist/lib/core/CreativeAgentClient.js.map +1 -1
- package/dist/lib/core/SingleAgentClient.d.ts +126 -22
- package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.js +372 -84
- package/dist/lib/core/SingleAgentClient.js.map +1 -1
- package/dist/lib/core/TaskExecutor.d.ts.map +1 -1
- package/dist/lib/core/TaskExecutor.js +3 -0
- package/dist/lib/core/TaskExecutor.js.map +1 -1
- package/dist/lib/discovery/property-crawler.js +1 -1
- package/dist/lib/discovery/property-crawler.js.map +1 -1
- package/dist/lib/index.d.ts +2 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +36 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/testing/agent-tester.d.ts +33 -0
- package/dist/lib/testing/agent-tester.d.ts.map +1 -0
- package/dist/lib/testing/agent-tester.js +211 -0
- package/dist/lib/testing/agent-tester.js.map +1 -0
- package/dist/lib/testing/client.d.ts +54 -0
- package/dist/lib/testing/client.d.ts.map +1 -0
- package/dist/lib/testing/client.js +293 -0
- package/dist/lib/testing/client.js.map +1 -0
- package/dist/lib/testing/formatter.d.ts +17 -0
- package/dist/lib/testing/formatter.d.ts.map +1 -0
- package/dist/lib/testing/formatter.js +79 -0
- package/dist/lib/testing/formatter.js.map +1 -0
- package/dist/lib/testing/index.d.ts +1 -0
- package/dist/lib/testing/index.d.ts.map +1 -1
- package/dist/lib/testing/index.js +26 -1
- package/dist/lib/testing/index.js.map +1 -1
- package/dist/lib/testing/scenarios/creative.d.ts +24 -0
- package/dist/lib/testing/scenarios/creative.d.ts.map +1 -0
- package/dist/lib/testing/scenarios/creative.js +224 -0
- package/dist/lib/testing/scenarios/creative.js.map +1 -0
- package/dist/lib/testing/scenarios/discovery.d.ts +15 -0
- package/dist/lib/testing/scenarios/discovery.d.ts.map +1 -0
- package/dist/lib/testing/scenarios/discovery.js +66 -0
- package/dist/lib/testing/scenarios/discovery.js.map +1 -0
- package/dist/lib/testing/scenarios/edge-cases.d.ts +64 -0
- package/dist/lib/testing/scenarios/edge-cases.d.ts.map +1 -0
- package/dist/lib/testing/scenarios/edge-cases.js +610 -0
- package/dist/lib/testing/scenarios/edge-cases.js.map +1 -0
- package/dist/lib/testing/scenarios/health.d.ts +12 -0
- package/dist/lib/testing/scenarios/health.d.ts.map +1 -0
- package/dist/lib/testing/scenarios/health.js +21 -0
- package/dist/lib/testing/scenarios/health.js.map +1 -0
- package/dist/lib/testing/scenarios/index.d.ts +12 -0
- package/dist/lib/testing/scenarios/index.d.ts.map +1 -0
- package/dist/lib/testing/scenarios/index.js +38 -0
- package/dist/lib/testing/scenarios/index.js.map +1 -0
- package/dist/lib/testing/scenarios/media-buy.d.ts +59 -0
- package/dist/lib/testing/scenarios/media-buy.d.ts.map +1 -0
- package/dist/lib/testing/scenarios/media-buy.js +422 -0
- package/dist/lib/testing/scenarios/media-buy.js.map +1 -0
- package/dist/lib/testing/scenarios/signals.d.ts +26 -0
- package/dist/lib/testing/scenarios/signals.d.ts.map +1 -0
- package/dist/lib/testing/scenarios/signals.js +227 -0
- package/dist/lib/testing/scenarios/signals.js.map +1 -0
- package/dist/lib/testing/test-helpers.d.ts.map +1 -1
- package/dist/lib/testing/test-helpers.js +2 -6
- package/dist/lib/testing/test-helpers.js.map +1 -1
- package/dist/lib/testing/types.d.ts +92 -0
- package/dist/lib/testing/types.d.ts.map +1 -0
- package/dist/lib/testing/types.js +6 -0
- package/dist/lib/testing/types.js.map +1 -0
- package/dist/lib/types/adcp.d.ts +14 -29
- package/dist/lib/types/adcp.d.ts.map +1 -1
- package/dist/lib/types/core.generated.d.ts +489 -39
- package/dist/lib/types/core.generated.d.ts.map +1 -1
- package/dist/lib/types/core.generated.js +2 -2
- package/dist/lib/types/index.d.ts +1 -1
- 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 +834 -214
- package/dist/lib/types/schemas.generated.d.ts.map +1 -1
- package/dist/lib/types/schemas.generated.js +155 -137
- package/dist/lib/types/schemas.generated.js.map +1 -1
- package/dist/lib/types/tools.generated.d.ts +297 -216
- package/dist/lib/types/tools.generated.d.ts.map +1 -1
- package/dist/lib/types/tools.generated.js +1 -1
- package/dist/lib/types/tools.generated.js.map +1 -1
- package/dist/lib/utils/typeGuards.d.ts +147 -0
- package/dist/lib/utils/typeGuards.d.ts.map +1 -0
- package/dist/lib/utils/typeGuards.js +240 -0
- package/dist/lib/utils/typeGuards.js.map +1 -0
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/package.json +1 -1
|
@@ -62,7 +62,8 @@ class SingleAgentClient {
|
|
|
62
62
|
executor;
|
|
63
63
|
asyncHandler;
|
|
64
64
|
normalizedAgent;
|
|
65
|
-
discoveredEndpoint; // Cache discovered endpoint
|
|
65
|
+
discoveredEndpoint; // Cache discovered MCP endpoint
|
|
66
|
+
canonicalBaseUrl; // Cache canonical base URL (from agent card or stripped /mcp)
|
|
66
67
|
constructor(agent, config = {}) {
|
|
67
68
|
this.agent = agent;
|
|
68
69
|
this.config = config;
|
|
@@ -89,6 +90,7 @@ class SingleAgentClient {
|
|
|
89
90
|
*
|
|
90
91
|
* If the agent needs discovery, perform it now and cache the result.
|
|
91
92
|
* Returns the agent config with the discovered endpoint.
|
|
93
|
+
* Also computes the canonical base URL by stripping /mcp suffix.
|
|
92
94
|
*/
|
|
93
95
|
async ensureEndpointDiscovered() {
|
|
94
96
|
const needsDiscovery = this.normalizedAgent._needsDiscovery;
|
|
@@ -104,11 +106,101 @@ class SingleAgentClient {
|
|
|
104
106
|
}
|
|
105
107
|
// Perform discovery
|
|
106
108
|
this.discoveredEndpoint = await this.discoverMCPEndpoint(this.normalizedAgent.agent_uri);
|
|
109
|
+
// Compute canonical base URL by stripping /mcp suffix
|
|
110
|
+
this.canonicalBaseUrl = this.computeBaseUrl(this.discoveredEndpoint);
|
|
107
111
|
return {
|
|
108
112
|
...this.normalizedAgent,
|
|
109
113
|
agent_uri: this.discoveredEndpoint,
|
|
110
114
|
};
|
|
111
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Ensure A2A canonical URL is resolved (lazy initialization)
|
|
118
|
+
*
|
|
119
|
+
* Fetches the agent card and extracts the canonical URL.
|
|
120
|
+
* Returns the agent config with the canonical URL.
|
|
121
|
+
*/
|
|
122
|
+
async ensureCanonicalUrlResolved() {
|
|
123
|
+
const needsCanonicalUrl = this.normalizedAgent._needsCanonicalUrl;
|
|
124
|
+
if (!needsCanonicalUrl) {
|
|
125
|
+
return this.normalizedAgent;
|
|
126
|
+
}
|
|
127
|
+
// Already resolved? Use cached value
|
|
128
|
+
if (this.canonicalBaseUrl) {
|
|
129
|
+
return {
|
|
130
|
+
...this.normalizedAgent,
|
|
131
|
+
agent_uri: this.canonicalBaseUrl,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Fetch agent card to get canonical URL
|
|
135
|
+
const canonicalUrl = await this.fetchA2ACanonicalUrl(this.normalizedAgent.agent_uri);
|
|
136
|
+
this.canonicalBaseUrl = canonicalUrl;
|
|
137
|
+
return {
|
|
138
|
+
...this.normalizedAgent,
|
|
139
|
+
agent_uri: canonicalUrl,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Fetch the canonical URL from an A2A agent card
|
|
144
|
+
*/
|
|
145
|
+
async fetchA2ACanonicalUrl(agentUri) {
|
|
146
|
+
const clientModule = require('@a2a-js/sdk/client');
|
|
147
|
+
const A2AClient = clientModule.A2AClient;
|
|
148
|
+
const authToken = this.normalizedAgent.auth_token;
|
|
149
|
+
const fetchImpl = authToken
|
|
150
|
+
? async (url, options) => {
|
|
151
|
+
const headers = {
|
|
152
|
+
...options?.headers,
|
|
153
|
+
Authorization: `Bearer ${authToken}`,
|
|
154
|
+
'x-adcp-auth': authToken,
|
|
155
|
+
};
|
|
156
|
+
return fetch(url, { ...options, headers });
|
|
157
|
+
}
|
|
158
|
+
: undefined;
|
|
159
|
+
// Construct agent card URL
|
|
160
|
+
const cardUrl = agentUri.endsWith('/.well-known/agent-card.json')
|
|
161
|
+
? agentUri
|
|
162
|
+
: agentUri.replace(/\/$/, '') + '/.well-known/agent-card.json';
|
|
163
|
+
const client = await A2AClient.fromCardUrl(cardUrl, fetchImpl ? { fetchImpl } : {});
|
|
164
|
+
const agentCard = client.agentCardPromise ? await client.agentCardPromise : client.agentCard;
|
|
165
|
+
// Use the canonical URL from the agent card, falling back to computed base URL
|
|
166
|
+
if (agentCard?.url) {
|
|
167
|
+
return agentCard.url;
|
|
168
|
+
}
|
|
169
|
+
// Fallback: strip .well-known/agent-card.json if present
|
|
170
|
+
return this.computeBaseUrl(agentUri);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Compute base URL by stripping protocol-specific suffixes
|
|
174
|
+
*
|
|
175
|
+
* - Strips /mcp or /mcp/ suffix for MCP endpoints
|
|
176
|
+
* - Strips /.well-known/agent-card.json for A2A discovery URLs
|
|
177
|
+
* - Strips trailing slash for consistency
|
|
178
|
+
*/
|
|
179
|
+
computeBaseUrl(url) {
|
|
180
|
+
let baseUrl = url;
|
|
181
|
+
// Strip /.well-known/agent-card.json
|
|
182
|
+
if (baseUrl.match(/\/\.well-known\/agent-card\.json$/i)) {
|
|
183
|
+
baseUrl = baseUrl.replace(/\/\.well-known\/agent-card\.json$/i, '');
|
|
184
|
+
}
|
|
185
|
+
// Strip /mcp or /mcp/
|
|
186
|
+
if (baseUrl.match(/\/mcp\/?$/i)) {
|
|
187
|
+
baseUrl = baseUrl.replace(/\/mcp\/?$/i, '');
|
|
188
|
+
}
|
|
189
|
+
// Strip trailing slash for consistency
|
|
190
|
+
baseUrl = baseUrl.replace(/\/$/, '');
|
|
191
|
+
return baseUrl;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Check if URL is a .well-known/agent-card.json URL
|
|
195
|
+
*
|
|
196
|
+
* These URLs are A2A agent card discovery URLs and should use A2A protocol.
|
|
197
|
+
* Only matches when .well-known is at the root path (not in a subdirectory).
|
|
198
|
+
*/
|
|
199
|
+
isWellKnownAgentCardUrl(url) {
|
|
200
|
+
// Match: https://example.com/.well-known/agent-card.json
|
|
201
|
+
// Don't match: https://example.com/api/.well-known/agent-card.json
|
|
202
|
+
return /^https?:\/\/[^/]+\/\.well-known\/agent-card\.json$/i.test(url);
|
|
203
|
+
}
|
|
112
204
|
/**
|
|
113
205
|
* Discover MCP endpoint by testing the provided path, then trying variants
|
|
114
206
|
*
|
|
@@ -122,7 +214,7 @@ class SingleAgentClient {
|
|
|
122
214
|
async discoverMCPEndpoint(providedUri) {
|
|
123
215
|
const { Client: MCPClient } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/index.js')));
|
|
124
216
|
const { StreamableHTTPClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/streamableHttp.js')));
|
|
125
|
-
const authToken = this.agent.
|
|
217
|
+
const authToken = this.agent.auth_token;
|
|
126
218
|
const testEndpoint = async (url) => {
|
|
127
219
|
try {
|
|
128
220
|
const mcpClient = new MCPClient({
|
|
@@ -180,35 +272,56 @@ class SingleAgentClient {
|
|
|
180
272
|
`None responded to MCP protocol.`);
|
|
181
273
|
}
|
|
182
274
|
/**
|
|
183
|
-
* Normalize agent config
|
|
275
|
+
* Normalize agent config
|
|
184
276
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
277
|
+
* - If URL is a .well-known/agent-card.json URL, switch to A2A protocol
|
|
278
|
+
* (these are A2A discovery URLs, not MCP endpoints)
|
|
279
|
+
* - A2A agents are marked for canonical URL resolution (from agent card)
|
|
280
|
+
* - MCP agents are marked for endpoint discovery
|
|
187
281
|
*/
|
|
188
282
|
normalizeAgentConfig(agent) {
|
|
283
|
+
// If URL is a well-known agent card URL, use A2A protocol regardless of what was specified
|
|
284
|
+
// Mark for canonical URL resolution - we'll fetch the agent card and use its url field
|
|
285
|
+
if (this.isWellKnownAgentCardUrl(agent.agent_uri)) {
|
|
286
|
+
return {
|
|
287
|
+
...agent,
|
|
288
|
+
protocol: 'a2a',
|
|
289
|
+
_needsCanonicalUrl: true,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (agent.protocol === 'a2a') {
|
|
293
|
+
// A2A agents need canonical URL resolution from agent card
|
|
294
|
+
return {
|
|
295
|
+
...agent,
|
|
296
|
+
_needsCanonicalUrl: true,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
189
299
|
if (agent.protocol !== 'mcp') {
|
|
190
300
|
return agent;
|
|
191
301
|
}
|
|
192
|
-
//
|
|
302
|
+
// MCP agents need endpoint discovery - we'll test their path, then try adding /mcp
|
|
193
303
|
return {
|
|
194
304
|
...agent,
|
|
195
305
|
_needsDiscovery: true,
|
|
196
306
|
};
|
|
197
307
|
}
|
|
198
308
|
/**
|
|
199
|
-
* Handle webhook from agent (async task
|
|
309
|
+
* Handle webhook from agent (async task status updates and completions)
|
|
200
310
|
*
|
|
201
|
-
* Accepts
|
|
202
|
-
* 1.
|
|
203
|
-
* 2.
|
|
311
|
+
* Accepts webhook payloads from both MCP and A2A protocols:
|
|
312
|
+
* 1. MCP: MCPWebhookPayload envelope with AdCP data in .result field
|
|
313
|
+
* 2. A2A: Native Task/TaskStatusUpdateEvent with AdCP data in either:
|
|
314
|
+
* - status.message.parts[].data (for status updates)
|
|
315
|
+
* - artifacts (for task completion, per A2A spec)
|
|
204
316
|
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
317
|
+
* The method normalizes both formats so handlers receive the unwrapped
|
|
318
|
+
* AdCP response data (AdCPAsyncResponseData), not the raw protocol structure.
|
|
207
319
|
*
|
|
208
|
-
* @param payload -
|
|
320
|
+
* @param payload - Protocol-specific webhook payload (MCPWebhookPayload | Task | TaskStatusUpdateEvent)
|
|
321
|
+
* @param taskType - Task type (e.g create_media_buy) from url param or url part of the webhook delivery
|
|
322
|
+
* @param operationId - Operation id (e.g used for client app to track the operation) from the param or url part of the webhook delivery
|
|
209
323
|
* @param signature - X-ADCP-Signature header (format: "sha256=...")
|
|
210
324
|
* @param timestamp - X-ADCP-Timestamp header (Unix timestamp)
|
|
211
|
-
* @param taskType - Task type override (useful when not in payload, e.g., from URL path)
|
|
212
325
|
* @returns Whether webhook was handled successfully
|
|
213
326
|
*
|
|
214
327
|
* @example
|
|
@@ -226,7 +339,7 @@ class SingleAgentClient {
|
|
|
226
339
|
* });
|
|
227
340
|
* ```
|
|
228
341
|
*/
|
|
229
|
-
async handleWebhook(payload, signature, timestamp
|
|
342
|
+
async handleWebhook(payload, taskType, operationId, signature, timestamp) {
|
|
230
343
|
// Verify signature if secret is configured
|
|
231
344
|
if (this.config.webhookSecret) {
|
|
232
345
|
if (!signature || !timestamp) {
|
|
@@ -236,78 +349,124 @@ class SingleAgentClient {
|
|
|
236
349
|
if (!isValid) {
|
|
237
350
|
throw new Error('Invalid webhook signature or timestamp too old');
|
|
238
351
|
}
|
|
352
|
+
console.log('[ADCP Client]: Webhook signature is valid');
|
|
239
353
|
}
|
|
240
|
-
// Transform raw
|
|
241
|
-
const normalizedPayload = this.normalizeWebhookPayload(payload, taskType);
|
|
242
|
-
|
|
243
|
-
await this.config.onActivity?.({
|
|
244
|
-
type: 'webhook_received',
|
|
354
|
+
// Transform raw protocol payload to normalized format
|
|
355
|
+
const normalizedPayload = this.normalizeWebhookPayload(payload, taskType, operationId);
|
|
356
|
+
const metadata = {
|
|
245
357
|
operation_id: normalizedPayload.operation_id,
|
|
246
|
-
agent_id: this.agent.id,
|
|
247
358
|
context_id: normalizedPayload.context_id,
|
|
248
359
|
task_id: normalizedPayload.task_id,
|
|
360
|
+
agent_id: this.agent.id,
|
|
249
361
|
task_type: normalizedPayload.task_type,
|
|
250
362
|
status: normalizedPayload.status,
|
|
251
|
-
|
|
363
|
+
message: normalizedPayload.message,
|
|
252
364
|
timestamp: normalizedPayload.timestamp || new Date().toISOString(),
|
|
365
|
+
};
|
|
366
|
+
// Emit activity
|
|
367
|
+
await this.config.onActivity?.({
|
|
368
|
+
type: 'webhook_received',
|
|
369
|
+
operation_id: metadata.operation_id,
|
|
370
|
+
agent_id: metadata.agent_id,
|
|
371
|
+
context_id: metadata.context_id,
|
|
372
|
+
task_id: metadata.task_id,
|
|
373
|
+
task_type: metadata.task_type,
|
|
374
|
+
status: metadata.status,
|
|
375
|
+
payload: normalizedPayload.result,
|
|
376
|
+
timestamp: metadata.timestamp,
|
|
253
377
|
});
|
|
254
378
|
// Handle through async handler if configured
|
|
255
379
|
if (this.asyncHandler) {
|
|
256
|
-
await this.asyncHandler.handleWebhook(normalizedPayload,
|
|
380
|
+
await this.asyncHandler.handleWebhook({ result: normalizedPayload.result, metadata });
|
|
257
381
|
return true;
|
|
258
382
|
}
|
|
259
383
|
return false;
|
|
260
384
|
}
|
|
261
385
|
/**
|
|
262
|
-
* Normalize webhook payload -
|
|
386
|
+
* Normalize webhook payload - handles both MCP and A2A webhook formats
|
|
263
387
|
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
388
|
+
* MCP: Uses MCPWebhookPayload envelope with AdCP data in .result field
|
|
389
|
+
* A2A: Uses native Task/TaskStatusUpdateEvent messages with AdCP data in either:
|
|
390
|
+
* - status.message.parts[].data (for status updates)
|
|
391
|
+
* - artifacts (for task completion responses, per A2A spec)
|
|
266
392
|
*
|
|
267
|
-
* @param payload -
|
|
268
|
-
* @param taskType - Task type override
|
|
269
|
-
* @
|
|
393
|
+
* @param payload - Protocol-specific webhook payload (MCPWebhookPayload | Task | TaskStatusUpdateEvent)
|
|
394
|
+
* @param taskType - Task type override
|
|
395
|
+
* @param operationId - Operation id
|
|
396
|
+
* @returns Normalized webhook payload with extracted AdCP response
|
|
270
397
|
*/
|
|
271
|
-
normalizeWebhookPayload(payload, taskType) {
|
|
272
|
-
// Check
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
398
|
+
normalizeWebhookPayload(payload, taskType, operationId) {
|
|
399
|
+
// 1. Check for MCP Webhook Payload (has task_id, status, task_type fields)
|
|
400
|
+
if ('task_id' in payload && 'task_type' in payload && 'status' in payload) {
|
|
401
|
+
const mcpPayload = payload;
|
|
402
|
+
return {
|
|
403
|
+
operation_id: operationId || 'unknown',
|
|
404
|
+
context_id: mcpPayload.context_id,
|
|
405
|
+
task_id: mcpPayload.task_id,
|
|
406
|
+
task_type: taskType,
|
|
407
|
+
status: mcpPayload.status,
|
|
408
|
+
result: mcpPayload.result,
|
|
409
|
+
message: mcpPayload.message,
|
|
410
|
+
timestamp: mcpPayload.timestamp,
|
|
411
|
+
};
|
|
277
412
|
}
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
413
|
+
// 2. Check for A2A Task or TaskStatusUpdateEvent
|
|
414
|
+
if ('kind' in payload && (payload.kind === 'task' || payload.kind === 'status-update')) {
|
|
415
|
+
const a2aPayload = payload;
|
|
416
|
+
const a2aStatus = a2aPayload.status?.state || 'unknown';
|
|
417
|
+
let result = undefined;
|
|
418
|
+
// Try to extract data from status.message.parts first (for status updates)
|
|
419
|
+
const parts = a2aPayload.status?.message?.parts;
|
|
420
|
+
if (parts && Array.isArray(parts)) {
|
|
421
|
+
const dataPart = parts.find(p => 'data' in p && p.kind === 'data');
|
|
422
|
+
if (dataPart && 'data' in dataPart) {
|
|
423
|
+
result = dataPart.data;
|
|
424
|
+
}
|
|
287
425
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
426
|
+
// If not found in parts, check artifacts (standard A2A task output location)
|
|
427
|
+
if (!result && 'artifacts' in a2aPayload && a2aPayload.artifacts && a2aPayload.artifacts.length > 0) {
|
|
428
|
+
try {
|
|
429
|
+
// Try to unwrap artifacts for all statuses
|
|
430
|
+
result = (0, response_unwrapper_1.unwrapProtocolResponse)({ result: a2aPayload }, taskType, 'a2a');
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
console.warn('Failed to unwrap A2A webhook payload:', error);
|
|
434
|
+
// Fallback: pass raw artifacts so handler has something to work with
|
|
435
|
+
result = a2aPayload.artifacts;
|
|
436
|
+
}
|
|
293
437
|
}
|
|
438
|
+
// Extract message part from status.message.parts (A2A Message structure)
|
|
439
|
+
let message = undefined;
|
|
440
|
+
if (a2aPayload.status?.message?.parts) {
|
|
441
|
+
const textParts = a2aPayload.status.message.parts
|
|
442
|
+
.filter(p => p.kind === 'text' && 'text' in p)
|
|
443
|
+
.map(p => ('text' in p ? p.text : ''));
|
|
444
|
+
if (textParts.length > 0) {
|
|
445
|
+
message = textParts.join(' ');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Get task_id ensuring it's a string
|
|
449
|
+
let taskId = 'unknown';
|
|
450
|
+
if ('id' in a2aPayload && a2aPayload.id) {
|
|
451
|
+
taskId = String(a2aPayload.id);
|
|
452
|
+
}
|
|
453
|
+
else if ('taskId' in a2aPayload && a2aPayload.taskId) {
|
|
454
|
+
taskId = String(a2aPayload.taskId);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
operation_id: operationId,
|
|
458
|
+
context_id: 'contextId' in a2aPayload ? a2aPayload.contextId : undefined,
|
|
459
|
+
task_id: taskId,
|
|
460
|
+
task_type: taskType,
|
|
461
|
+
status: a2aStatus,
|
|
462
|
+
result,
|
|
463
|
+
message: message,
|
|
464
|
+
timestamp: a2aPayload.status?.timestamp || new Date().toISOString(),
|
|
465
|
+
};
|
|
294
466
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
// Build normalized WebhookPayload
|
|
300
|
-
return {
|
|
301
|
-
operation_id: payload.metadata?.operation_id || payload.id || 'unknown',
|
|
302
|
-
context_id: payload.contextId || payload.metadata?.adcp_context?.buyer_ref,
|
|
303
|
-
task_id: payload.id,
|
|
304
|
-
task_type: payload?.task_type || taskType || 'unknown',
|
|
305
|
-
status: a2aStatus,
|
|
306
|
-
result,
|
|
307
|
-
error: payload.status?.message?.message || payload.error,
|
|
308
|
-
message: payload.status?.message?.message,
|
|
309
|
-
timestamp: payload.status?.timestamp || new Date().toISOString(),
|
|
310
|
-
};
|
|
467
|
+
// 3. Unknown payload format
|
|
468
|
+
throw new Error('Unsupported webhook payload format. Expected MCPWebhookPayload, Task, or TaskStatusUpdateEvent. ' +
|
|
469
|
+
`Received: ${JSON.stringify(payload).substring(0, 200)}`);
|
|
311
470
|
}
|
|
312
471
|
/**
|
|
313
472
|
* Generate webhook URL using macro substitution
|
|
@@ -501,23 +660,34 @@ class SingleAgentClient {
|
|
|
501
660
|
* @param options - Task execution options
|
|
502
661
|
*/
|
|
503
662
|
async createMediaBuy(params, inputHandler, options) {
|
|
504
|
-
//
|
|
663
|
+
// Merge library defaults with consumer-provided reporting_webhook config
|
|
664
|
+
// Library provides url/auth/frequency defaults, consumer can override any field
|
|
505
665
|
// Generates a media_buy_delivery webhook URL using operation_id pattern: delivery_report_{agent_id}_{YYYY-MM}
|
|
506
|
-
if (
|
|
666
|
+
if (this.config.webhookUrlTemplate) {
|
|
507
667
|
const now = new Date();
|
|
508
668
|
const year = now.getUTCFullYear();
|
|
509
669
|
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
|
|
510
670
|
const operationId = `delivery_report_${this.agent.id}_${year}-${month}`;
|
|
511
671
|
const deliveryWebhookUrl = this.getWebhookUrl('media_buy_delivery', operationId);
|
|
672
|
+
// Library defaults
|
|
673
|
+
const libraryDefaults = {
|
|
674
|
+
url: deliveryWebhookUrl,
|
|
675
|
+
authentication: {
|
|
676
|
+
schemes: ['HMAC-SHA256'],
|
|
677
|
+
credentials: this.config.webhookSecret || 'placeholder_secret_min_32_characters_required',
|
|
678
|
+
},
|
|
679
|
+
reporting_frequency: (this.config.reportingWebhookFrequency || 'daily'),
|
|
680
|
+
};
|
|
681
|
+
// Deep merge: consumer overrides library defaults
|
|
512
682
|
params = {
|
|
513
683
|
...params,
|
|
514
684
|
reporting_webhook: {
|
|
515
|
-
|
|
685
|
+
...libraryDefaults,
|
|
686
|
+
...params.reporting_webhook,
|
|
516
687
|
authentication: {
|
|
517
|
-
|
|
518
|
-
|
|
688
|
+
...libraryDefaults.authentication,
|
|
689
|
+
...params.reporting_webhook?.authentication,
|
|
519
690
|
},
|
|
520
|
-
reporting_frequency: this.config.reportingWebhookFrequency || 'daily',
|
|
521
691
|
},
|
|
522
692
|
};
|
|
523
693
|
}
|
|
@@ -702,10 +872,39 @@ class SingleAgentClient {
|
|
|
702
872
|
}
|
|
703
873
|
// ====== AGENT INFORMATION ======
|
|
704
874
|
/**
|
|
705
|
-
* Get the agent configuration
|
|
875
|
+
* Get the agent configuration with normalized protocol
|
|
876
|
+
*
|
|
877
|
+
* Returns the agent config with:
|
|
878
|
+
* - Protocol normalized (e.g., .well-known URLs switch to A2A)
|
|
879
|
+
* - If canonical URL has been resolved, agent_uri will be the canonical URL
|
|
880
|
+
*
|
|
881
|
+
* For guaranteed canonical URL, use getResolvedAgent() instead.
|
|
706
882
|
*/
|
|
707
883
|
getAgent() {
|
|
708
|
-
return
|
|
884
|
+
// If we have resolved the canonical URL, return config with it
|
|
885
|
+
if (this.canonicalBaseUrl) {
|
|
886
|
+
const { _needsDiscovery, _needsCanonicalUrl, ...cleanAgent } = this.normalizedAgent;
|
|
887
|
+
return {
|
|
888
|
+
...cleanAgent,
|
|
889
|
+
agent_uri: this.canonicalBaseUrl,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
// Return normalized agent without internal flags
|
|
893
|
+
const { _needsDiscovery, _needsCanonicalUrl, ...cleanAgent } = this.normalizedAgent;
|
|
894
|
+
return { ...cleanAgent };
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Get the fully resolved agent configuration
|
|
898
|
+
*
|
|
899
|
+
* This async method ensures the agent config has the canonical URL resolved:
|
|
900
|
+
* - For A2A: Fetches the agent card and uses its 'url' field
|
|
901
|
+
* - For MCP: Performs endpoint discovery
|
|
902
|
+
*
|
|
903
|
+
* @returns Promise resolving to agent config with canonical URL
|
|
904
|
+
*/
|
|
905
|
+
async getResolvedAgent() {
|
|
906
|
+
await this.resolveCanonicalUrl();
|
|
907
|
+
return this.getAgent();
|
|
709
908
|
}
|
|
710
909
|
/**
|
|
711
910
|
* Get the agent ID
|
|
@@ -720,10 +919,99 @@ class SingleAgentClient {
|
|
|
720
919
|
return this.agent.name;
|
|
721
920
|
}
|
|
722
921
|
/**
|
|
723
|
-
* Get the agent protocol
|
|
922
|
+
* Get the agent protocol (may be normalized from original config)
|
|
724
923
|
*/
|
|
725
924
|
getProtocol() {
|
|
726
|
-
return this.
|
|
925
|
+
return this.normalizedAgent.protocol;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Get the canonical base URL for this agent
|
|
929
|
+
*
|
|
930
|
+
* Returns the canonical URL if already resolved, or computes it synchronously
|
|
931
|
+
* from the configured URL. For the most accurate canonical URL (especially for A2A
|
|
932
|
+
* where the agent card contains the authoritative URL), use resolveCanonicalUrl() first.
|
|
933
|
+
*
|
|
934
|
+
* The canonical URL is:
|
|
935
|
+
* - For A2A: The 'url' field from the agent card (if resolved), or base URL with
|
|
936
|
+
* /.well-known/agent-card.json stripped
|
|
937
|
+
* - For MCP: The discovered endpoint with /mcp stripped
|
|
938
|
+
*
|
|
939
|
+
* @returns The canonical base URL (synchronous, may not be fully resolved)
|
|
940
|
+
*/
|
|
941
|
+
getCanonicalUrl() {
|
|
942
|
+
// Return cached canonical URL if available
|
|
943
|
+
if (this.canonicalBaseUrl) {
|
|
944
|
+
return this.canonicalBaseUrl;
|
|
945
|
+
}
|
|
946
|
+
// Compute from configured URL (best effort without network call)
|
|
947
|
+
return this.computeBaseUrl(this.normalizedAgent.agent_uri);
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Resolve and return the canonical base URL for this agent
|
|
951
|
+
*
|
|
952
|
+
* This async method ensures the canonical URL is properly resolved:
|
|
953
|
+
* - For A2A: Fetches the agent card and uses its 'url' field
|
|
954
|
+
* - For MCP: Performs endpoint discovery and strips /mcp suffix
|
|
955
|
+
*
|
|
956
|
+
* The result is cached, so subsequent calls are fast.
|
|
957
|
+
*
|
|
958
|
+
* @returns Promise resolving to the canonical base URL
|
|
959
|
+
*/
|
|
960
|
+
async resolveCanonicalUrl() {
|
|
961
|
+
if (this.canonicalBaseUrl) {
|
|
962
|
+
return this.canonicalBaseUrl;
|
|
963
|
+
}
|
|
964
|
+
if (this.normalizedAgent.protocol === 'a2a') {
|
|
965
|
+
await this.ensureCanonicalUrlResolved();
|
|
966
|
+
}
|
|
967
|
+
else if (this.normalizedAgent.protocol === 'mcp') {
|
|
968
|
+
await this.ensureEndpointDiscovered();
|
|
969
|
+
}
|
|
970
|
+
return this.canonicalBaseUrl || this.computeBaseUrl(this.normalizedAgent.agent_uri);
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Check if this agent is the same as another agent
|
|
974
|
+
*
|
|
975
|
+
* Compares agents by their canonical base URLs. Two agents are considered
|
|
976
|
+
* the same if they have the same canonical URL, regardless of:
|
|
977
|
+
* - Protocol (MCP vs A2A)
|
|
978
|
+
* - URL format (with/without /mcp, with/without /.well-known/agent-card.json)
|
|
979
|
+
* - Trailing slashes
|
|
980
|
+
*
|
|
981
|
+
* @param other - Another agent configuration or SingleAgentClient to compare
|
|
982
|
+
* @returns true if agents have the same canonical URL
|
|
983
|
+
*/
|
|
984
|
+
isSameAgent(other) {
|
|
985
|
+
const thisUrl = this.getCanonicalUrl().toLowerCase();
|
|
986
|
+
let otherUrl;
|
|
987
|
+
if (other instanceof SingleAgentClient) {
|
|
988
|
+
otherUrl = other.getCanonicalUrl().toLowerCase();
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
otherUrl = this.computeBaseUrl(other.agent_uri).toLowerCase();
|
|
992
|
+
}
|
|
993
|
+
return thisUrl === otherUrl;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Async version of isSameAgent that resolves canonical URLs first
|
|
997
|
+
*
|
|
998
|
+
* This provides more accurate comparison for A2A agents since it fetches
|
|
999
|
+
* the agent card to get the authoritative canonical URL.
|
|
1000
|
+
*
|
|
1001
|
+
* @param other - Another agent configuration or SingleAgentClient to compare
|
|
1002
|
+
* @returns Promise resolving to true if agents have the same canonical URL
|
|
1003
|
+
*/
|
|
1004
|
+
async isSameAgentResolved(other) {
|
|
1005
|
+
const thisUrl = (await this.resolveCanonicalUrl()).toLowerCase();
|
|
1006
|
+
let otherUrl;
|
|
1007
|
+
if (other instanceof SingleAgentClient) {
|
|
1008
|
+
otherUrl = (await other.resolveCanonicalUrl()).toLowerCase();
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
// For raw AgentConfig, we can only compute from the URL
|
|
1012
|
+
otherUrl = this.computeBaseUrl(other.agent_uri).toLowerCase();
|
|
1013
|
+
}
|
|
1014
|
+
return thisUrl === otherUrl;
|
|
727
1015
|
}
|
|
728
1016
|
/**
|
|
729
1017
|
* Get active tasks for this agent
|
|
@@ -832,7 +1120,7 @@ class SingleAgentClient {
|
|
|
832
1120
|
* ```
|
|
833
1121
|
*/
|
|
834
1122
|
async getAgentInfo() {
|
|
835
|
-
if (this.
|
|
1123
|
+
if (this.normalizedAgent.protocol === 'mcp') {
|
|
836
1124
|
// Discover endpoint if needed
|
|
837
1125
|
const agent = await this.ensureEndpointDiscovered();
|
|
838
1126
|
// Use MCP SDK to list tools
|
|
@@ -842,7 +1130,7 @@ class SingleAgentClient {
|
|
|
842
1130
|
name: 'AdCP-Client',
|
|
843
1131
|
version: '1.0.0',
|
|
844
1132
|
});
|
|
845
|
-
const authToken = this.
|
|
1133
|
+
const authToken = this.normalizedAgent.auth_token;
|
|
846
1134
|
const customFetch = authToken
|
|
847
1135
|
? async (input, init) => {
|
|
848
1136
|
// IMPORTANT: Must preserve SDK's default headers (especially Accept header)
|
|
@@ -891,18 +1179,18 @@ class SingleAgentClient {
|
|
|
891
1179
|
parameters: tool.inputSchema?.properties ? Object.keys(tool.inputSchema.properties) : [],
|
|
892
1180
|
}));
|
|
893
1181
|
return {
|
|
894
|
-
name: this.
|
|
1182
|
+
name: this.normalizedAgent.name,
|
|
895
1183
|
description: undefined,
|
|
896
|
-
protocol: this.
|
|
1184
|
+
protocol: this.normalizedAgent.protocol,
|
|
897
1185
|
url: agent.agent_uri,
|
|
898
1186
|
tools,
|
|
899
1187
|
};
|
|
900
1188
|
}
|
|
901
|
-
else if (this.
|
|
1189
|
+
else if (this.normalizedAgent.protocol === 'a2a') {
|
|
902
1190
|
// Use A2A SDK to get agent card
|
|
903
1191
|
const clientModule = require('@a2a-js/sdk/client');
|
|
904
1192
|
const A2AClient = clientModule.A2AClient;
|
|
905
|
-
const authToken = this.
|
|
1193
|
+
const authToken = this.normalizedAgent.auth_token;
|
|
906
1194
|
const fetchImpl = authToken
|
|
907
1195
|
? async (url, options) => {
|
|
908
1196
|
const headers = {
|
|
@@ -927,14 +1215,14 @@ class SingleAgentClient {
|
|
|
927
1215
|
}))
|
|
928
1216
|
: [];
|
|
929
1217
|
return {
|
|
930
|
-
name: agentCard?.displayName || agentCard?.name || this.
|
|
1218
|
+
name: agentCard?.displayName || agentCard?.name || this.normalizedAgent.name,
|
|
931
1219
|
description: agentCard?.description,
|
|
932
|
-
protocol: this.
|
|
1220
|
+
protocol: this.normalizedAgent.protocol,
|
|
933
1221
|
url: this.normalizedAgent.agent_uri,
|
|
934
1222
|
tools,
|
|
935
1223
|
};
|
|
936
1224
|
}
|
|
937
|
-
throw new Error(`Unsupported protocol: ${this.
|
|
1225
|
+
throw new Error(`Unsupported protocol: ${this.normalizedAgent.protocol}`);
|
|
938
1226
|
}
|
|
939
1227
|
// ====== STATIC HELPER METHODS ======
|
|
940
1228
|
/**
|