@relaycore/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PUBLISHING.md +37 -0
- package/README.md +434 -0
- package/agent-sdk.ts +392 -0
- package/consumer-sdk.ts +434 -0
- package/dist/index.js +14116 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +14057 -0
- package/dist/index.mjs.map +1 -0
- package/hooks.ts +250 -0
- package/index.ts +153 -0
- package/lib/erc8004.ts +51 -0
- package/lib/facilitator.ts +65 -0
- package/lib/ipfs.ts +71 -0
- package/lib/supabase.ts +5 -0
- package/lib/x402.ts +220 -0
- package/package.json +38 -0
- package/provider-sdk.ts +311 -0
- package/relay-agent.ts +1414 -0
- package/relay-rwa.ts +128 -0
- package/relay-service.ts +886 -0
- package/tsconfig.json +23 -0
- package/tsup.config.ts +19 -0
- package/types/chat.types.ts +146 -0
- package/types/x402.types.ts +114 -0
package/relay-agent.ts
ADDED
|
@@ -0,0 +1,1414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay Core - Agent SDK
|
|
3
|
+
*
|
|
4
|
+
* For AI agents to discover services, make decisions, and execute workflows.
|
|
5
|
+
*
|
|
6
|
+
* Design Principles:
|
|
7
|
+
* - Decision abstraction, not CRUD
|
|
8
|
+
* - Workflow primitives first
|
|
9
|
+
* - Failure is first-class
|
|
10
|
+
* - 10-minute quickstart
|
|
11
|
+
*
|
|
12
|
+
* @example Quickstart
|
|
13
|
+
* ```ts
|
|
14
|
+
* const agent = new RelayAgent({ wallet, network: "cronos-testnet" });
|
|
15
|
+
*
|
|
16
|
+
* // Select best service by policy
|
|
17
|
+
* const service = await agent.selectService({
|
|
18
|
+
* category: "data.prices",
|
|
19
|
+
* constraints: { minReputation: 90 }
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Execute with automatic payment
|
|
23
|
+
* const result = await agent.execute(service, { pair: "BTC/USD" });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { ethers } from 'ethers';
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// TYPES - Clear, descriptive names
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/** Network configuration */
|
|
34
|
+
export type Network = 'cronos-mainnet' | 'cronos-testnet' | 'cronos-zkevm';
|
|
35
|
+
|
|
36
|
+
/** Agent configuration - minimal required, progressive optional */
|
|
37
|
+
export interface AgentConfig {
|
|
38
|
+
/** Connected wallet (ethers.Signer or address for read-only) */
|
|
39
|
+
wallet: ethers.Signer | string;
|
|
40
|
+
/** Relay Core API Key (starts with rc_...) */
|
|
41
|
+
apiKey: string;
|
|
42
|
+
/** Target network */
|
|
43
|
+
network?: Network;
|
|
44
|
+
/** API endpoint (defaults to production) */
|
|
45
|
+
apiUrl?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Trust policy for service selection */
|
|
49
|
+
export interface TrustPolicy {
|
|
50
|
+
/** Minimum reputation score (0-100) */
|
|
51
|
+
minReputation?: number;
|
|
52
|
+
/** Maximum acceptable latency in ms */
|
|
53
|
+
maxLatency?: number;
|
|
54
|
+
/** Maximum price per call in USDC */
|
|
55
|
+
maxPrice?: number;
|
|
56
|
+
/** Require verified/reliable services only */
|
|
57
|
+
verifiedOnly?: boolean;
|
|
58
|
+
/** Preferred service providers (addresses) */
|
|
59
|
+
preferredProviders?: string[];
|
|
60
|
+
/** Blacklisted providers (addresses) */
|
|
61
|
+
blacklistedProviders?: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Service selection criteria */
|
|
65
|
+
export interface ServiceCriteria {
|
|
66
|
+
/** Service category (e.g., "data.prices", "trading.execution") */
|
|
67
|
+
category?: string;
|
|
68
|
+
/** Required input type */
|
|
69
|
+
inputType?: string;
|
|
70
|
+
/** Required output type */
|
|
71
|
+
outputType?: string;
|
|
72
|
+
/** Required tags */
|
|
73
|
+
tags?: string[];
|
|
74
|
+
/** Required capabilities */
|
|
75
|
+
capabilities?: string[];
|
|
76
|
+
/** Trust constraints */
|
|
77
|
+
constraints?: TrustPolicy;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Selected service with selection explanation */
|
|
81
|
+
export interface SelectedService {
|
|
82
|
+
id: string;
|
|
83
|
+
name: string;
|
|
84
|
+
endpoint: string;
|
|
85
|
+
price: string;
|
|
86
|
+
provider: string;
|
|
87
|
+
reputation: number;
|
|
88
|
+
latency: number;
|
|
89
|
+
/** Why this service was selected */
|
|
90
|
+
selectionReason: string;
|
|
91
|
+
/** Score breakdown */
|
|
92
|
+
scoreBreakdown: {
|
|
93
|
+
reputation: number;
|
|
94
|
+
latency: number;
|
|
95
|
+
price: number;
|
|
96
|
+
total: number;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Execution result with full context */
|
|
101
|
+
export interface ExecutionResult<T = unknown> {
|
|
102
|
+
success: boolean;
|
|
103
|
+
data?: T;
|
|
104
|
+
error?: ExecutionError;
|
|
105
|
+
/** Payment details if payment was made */
|
|
106
|
+
payment?: {
|
|
107
|
+
id: string;
|
|
108
|
+
txHash: string;
|
|
109
|
+
amount: string;
|
|
110
|
+
};
|
|
111
|
+
/** Performance metrics */
|
|
112
|
+
metrics: {
|
|
113
|
+
totalMs: number;
|
|
114
|
+
paymentMs?: number;
|
|
115
|
+
serviceMs?: number;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Structured error with actionable information */
|
|
120
|
+
export interface ExecutionError {
|
|
121
|
+
code: ErrorCode;
|
|
122
|
+
message: string;
|
|
123
|
+
/** Can this operation be retried? */
|
|
124
|
+
retryable: boolean;
|
|
125
|
+
/** If retryable, suggested wait time in ms */
|
|
126
|
+
retryAfterMs?: number;
|
|
127
|
+
/** Original error details */
|
|
128
|
+
details?: unknown;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Error codes for programmatic handling */
|
|
132
|
+
export type ErrorCode =
|
|
133
|
+
| 'SERVICE_NOT_FOUND'
|
|
134
|
+
| 'SERVICE_UNAVAILABLE'
|
|
135
|
+
| 'PAYMENT_FAILED'
|
|
136
|
+
| 'PAYMENT_TIMEOUT'
|
|
137
|
+
| 'EXECUTION_FAILED'
|
|
138
|
+
| 'EXECUTION_TIMEOUT'
|
|
139
|
+
| 'INSUFFICIENT_BALANCE'
|
|
140
|
+
| 'UNAUTHORIZED'
|
|
141
|
+
| 'RATE_LIMITED'
|
|
142
|
+
| 'INVALID_INPUT'
|
|
143
|
+
| 'PARTIAL_SUCCESS'
|
|
144
|
+
| 'UNKNOWN';
|
|
145
|
+
|
|
146
|
+
/** Workflow step definition */
|
|
147
|
+
export interface WorkflowStep<TInput = unknown, TOutput = unknown> {
|
|
148
|
+
name: string;
|
|
149
|
+
service?: SelectedService;
|
|
150
|
+
serviceId?: string;
|
|
151
|
+
criteria?: ServiceCriteria;
|
|
152
|
+
transform?: (input: TInput) => TOutput | Promise<TOutput>;
|
|
153
|
+
timeout?: number;
|
|
154
|
+
retries?: number;
|
|
155
|
+
fallback?: WorkflowStep<TInput, TOutput>;
|
|
156
|
+
onSuccess?: (result: TOutput) => void | Promise<void>;
|
|
157
|
+
onFailure?: (error: ExecutionError) => void | Promise<void>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Workflow execution result */
|
|
161
|
+
export interface WorkflowResult<T = unknown> {
|
|
162
|
+
success: boolean;
|
|
163
|
+
data?: T;
|
|
164
|
+
/** Results from each step */
|
|
165
|
+
stepResults: Array<{
|
|
166
|
+
stepName: string;
|
|
167
|
+
success: boolean;
|
|
168
|
+
data?: unknown;
|
|
169
|
+
error?: ExecutionError;
|
|
170
|
+
durationMs: number;
|
|
171
|
+
}>;
|
|
172
|
+
/** Total workflow duration */
|
|
173
|
+
totalMs: number;
|
|
174
|
+
/** Number of steps completed */
|
|
175
|
+
completedSteps: number;
|
|
176
|
+
/** Number of steps that failed */
|
|
177
|
+
failedSteps: number;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Outcome record for memory */
|
|
181
|
+
export interface OutcomeRecord {
|
|
182
|
+
timestamp: Date;
|
|
183
|
+
serviceId: string;
|
|
184
|
+
success: boolean;
|
|
185
|
+
latencyMs: number;
|
|
186
|
+
paymentAmount?: string;
|
|
187
|
+
error?: ExecutionError;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Memory hooks for agent learning */
|
|
191
|
+
export interface AgentMemory {
|
|
192
|
+
record(outcome: OutcomeRecord): void;
|
|
193
|
+
getHistory(serviceId?: string): OutcomeRecord[];
|
|
194
|
+
getStats(): { totalCalls: number; successRate: number; avgLatency: number };
|
|
195
|
+
clear(): void;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A2A Agent Card Resource */
|
|
199
|
+
export interface AgentCardResource {
|
|
200
|
+
id: string;
|
|
201
|
+
title: string;
|
|
202
|
+
url: string;
|
|
203
|
+
price?: string;
|
|
204
|
+
paywall: {
|
|
205
|
+
protocol: 'x402';
|
|
206
|
+
settlement: string;
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A2A Agent Card for discovery */
|
|
211
|
+
export interface AgentCard {
|
|
212
|
+
name: string;
|
|
213
|
+
description: string;
|
|
214
|
+
url: string;
|
|
215
|
+
version?: string;
|
|
216
|
+
network: string;
|
|
217
|
+
capabilities?: string[];
|
|
218
|
+
resources: AgentCardResource[];
|
|
219
|
+
contracts?: {
|
|
220
|
+
escrowSession?: string;
|
|
221
|
+
identityRegistry?: string;
|
|
222
|
+
reputationRegistry?: string;
|
|
223
|
+
usdcToken?: string;
|
|
224
|
+
};
|
|
225
|
+
x402?: {
|
|
226
|
+
facilitator?: string;
|
|
227
|
+
token?: string;
|
|
228
|
+
chainId?: number;
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Task state enum */
|
|
233
|
+
export type TaskState = 'idle' | 'pending' | 'settled' | 'failed';
|
|
234
|
+
|
|
235
|
+
/** Task Artifact for tracking agent actions */
|
|
236
|
+
export interface TaskArtifact {
|
|
237
|
+
task_id: string;
|
|
238
|
+
agent_id: string;
|
|
239
|
+
service_id?: string;
|
|
240
|
+
session_id?: string;
|
|
241
|
+
state: TaskState;
|
|
242
|
+
payment_id?: string;
|
|
243
|
+
facilitator_tx?: string;
|
|
244
|
+
retries: number;
|
|
245
|
+
timestamps: {
|
|
246
|
+
created: string;
|
|
247
|
+
updated: string;
|
|
248
|
+
completed?: string;
|
|
249
|
+
};
|
|
250
|
+
inputs: Record<string, unknown>;
|
|
251
|
+
outputs: Record<string, unknown>;
|
|
252
|
+
error?: {
|
|
253
|
+
code: string;
|
|
254
|
+
message: string;
|
|
255
|
+
retryable: boolean;
|
|
256
|
+
};
|
|
257
|
+
metrics?: {
|
|
258
|
+
total_ms: number;
|
|
259
|
+
payment_ms?: number;
|
|
260
|
+
service_ms?: number;
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Task statistics summary */
|
|
265
|
+
export interface TaskStats {
|
|
266
|
+
total: number;
|
|
267
|
+
pending: number;
|
|
268
|
+
settled: number;
|
|
269
|
+
failed: number;
|
|
270
|
+
success_rate: number;
|
|
271
|
+
avg_duration_ms: number;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// IMPLEMENTATION
|
|
276
|
+
// ============================================================================
|
|
277
|
+
|
|
278
|
+
const NETWORK_CONFIG: Record<Network, { apiUrl: string; chainId: number }> = {
|
|
279
|
+
'cronos-mainnet': { apiUrl: 'https://api.relaycore.xyz', chainId: 25 },
|
|
280
|
+
'cronos-testnet': { apiUrl: 'https://testnet-api.relaycore.xyz', chainId: 338 },
|
|
281
|
+
'cronos-zkevm': { apiUrl: 'https://zkevm-api.relaycore.xyz', chainId: 388 },
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const FACILITATOR_URL = 'https://facilitator.cronoslabs.org/v2/x402';
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Relay Agent SDK
|
|
288
|
+
*
|
|
289
|
+
* The main entry point for AI agents to interact with Relay Core services.
|
|
290
|
+
*/
|
|
291
|
+
/** Internal discovered service type with all fields */
|
|
292
|
+
interface DiscoveredServiceInternal {
|
|
293
|
+
id: string;
|
|
294
|
+
name: string;
|
|
295
|
+
endpoint: string;
|
|
296
|
+
price: string;
|
|
297
|
+
provider: string;
|
|
298
|
+
reputation: number;
|
|
299
|
+
latency: number;
|
|
300
|
+
category: string;
|
|
301
|
+
inputType?: string;
|
|
302
|
+
outputType?: string;
|
|
303
|
+
tags: string[];
|
|
304
|
+
capabilities: string[];
|
|
305
|
+
verified: boolean;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export class RelayAgent {
|
|
309
|
+
private signer: ethers.Signer | null = null;
|
|
310
|
+
private _address: string = '';
|
|
311
|
+
private network: Network;
|
|
312
|
+
private apiUrl: string;
|
|
313
|
+
private apiKey: string;
|
|
314
|
+
private trustPolicy: TrustPolicy = {};
|
|
315
|
+
private memoryStore: OutcomeRecord[] = [];
|
|
316
|
+
|
|
317
|
+
constructor(config: AgentConfig) {
|
|
318
|
+
this.network = config.network || 'cronos-mainnet';
|
|
319
|
+
this.apiUrl = config.apiUrl || NETWORK_CONFIG[this.network].apiUrl;
|
|
320
|
+
this.apiKey = config.apiKey;
|
|
321
|
+
|
|
322
|
+
if (typeof config.wallet === 'string') {
|
|
323
|
+
this._address = config.wallet.toLowerCase();
|
|
324
|
+
} else {
|
|
325
|
+
this.signer = config.wallet;
|
|
326
|
+
// Get address async
|
|
327
|
+
config.wallet.getAddress().then(addr => {
|
|
328
|
+
this._address = addr.toLowerCase();
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get authenticated headers for API requests
|
|
335
|
+
*/
|
|
336
|
+
private getHeaders(): HeadersInit {
|
|
337
|
+
return {
|
|
338
|
+
'Content-Type': 'application/json',
|
|
339
|
+
'x-api-key': this.apiKey
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ==========================================================================
|
|
344
|
+
// CONFIGURATION - Progressive, not overwhelming
|
|
345
|
+
// ==========================================================================
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get agent's wallet address
|
|
349
|
+
*/
|
|
350
|
+
async getAddress(): Promise<string> {
|
|
351
|
+
if (this.signer && !this._address) {
|
|
352
|
+
this._address = (await this.signer.getAddress()).toLowerCase();
|
|
353
|
+
}
|
|
354
|
+
return this._address;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Set trust policy for service selection
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* agent.setTrustPolicy({
|
|
362
|
+
* minReputation: 90,
|
|
363
|
+
* maxLatency: 500,
|
|
364
|
+
* verifiedOnly: true
|
|
365
|
+
* });
|
|
366
|
+
*/
|
|
367
|
+
setTrustPolicy(policy: TrustPolicy): void {
|
|
368
|
+
this.trustPolicy = { ...this.trustPolicy, ...policy };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get current trust policy
|
|
373
|
+
*/
|
|
374
|
+
getTrustPolicy(): TrustPolicy {
|
|
375
|
+
return { ...this.trustPolicy };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ==========================================================================
|
|
379
|
+
// SERVICE SELECTION - Decision abstraction, not CRUD
|
|
380
|
+
// ==========================================================================
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Select the best service matching criteria
|
|
384
|
+
*
|
|
385
|
+
* This is the main decision interface. The SDK handles:
|
|
386
|
+
* - Filtering by criteria
|
|
387
|
+
* - Scoring by trust policy
|
|
388
|
+
* - Explaining why a service was selected
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* const service = await agent.selectService({
|
|
392
|
+
* category: "data.prices",
|
|
393
|
+
* constraints: { minReputation: 90, maxLatency: 200 }
|
|
394
|
+
* });
|
|
395
|
+
*
|
|
396
|
+
* console.log(service.selectionReason);
|
|
397
|
+
* // "Highest score (92.5) based on: reputation=95, latency=150ms, price=$0.01"
|
|
398
|
+
*/
|
|
399
|
+
async selectService(criteria: ServiceCriteria): Promise<SelectedService | null> {
|
|
400
|
+
const services = await this.discoverServices(criteria);
|
|
401
|
+
|
|
402
|
+
if (services.length === 0) return null;
|
|
403
|
+
|
|
404
|
+
// Apply trust policy + criteria constraints
|
|
405
|
+
const constraints = { ...this.trustPolicy, ...criteria.constraints };
|
|
406
|
+
|
|
407
|
+
const scored = services
|
|
408
|
+
.filter(s => this.passesConstraints(s, constraints))
|
|
409
|
+
.map(s => this.scoreService(s, constraints))
|
|
410
|
+
.sort((a, b) => b.score - a.score);
|
|
411
|
+
|
|
412
|
+
if (scored.length === 0) return null;
|
|
413
|
+
|
|
414
|
+
const best = scored[0];
|
|
415
|
+
return {
|
|
416
|
+
id: best.service.id,
|
|
417
|
+
name: best.service.name,
|
|
418
|
+
endpoint: best.service.endpoint,
|
|
419
|
+
price: best.service.price,
|
|
420
|
+
provider: best.service.provider,
|
|
421
|
+
reputation: best.service.reputation,
|
|
422
|
+
latency: best.service.latency,
|
|
423
|
+
selectionReason: this.explainSelection(best),
|
|
424
|
+
scoreBreakdown: {
|
|
425
|
+
reputation: best.repScore,
|
|
426
|
+
latency: best.latScore,
|
|
427
|
+
price: best.priceScore,
|
|
428
|
+
total: best.score,
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Discover all services matching criteria (raw, unfiltered)
|
|
435
|
+
*/
|
|
436
|
+
async discoverServices(criteria: ServiceCriteria): Promise<Array<{
|
|
437
|
+
id: string;
|
|
438
|
+
name: string;
|
|
439
|
+
endpoint: string;
|
|
440
|
+
price: string;
|
|
441
|
+
provider: string;
|
|
442
|
+
reputation: number;
|
|
443
|
+
latency: number;
|
|
444
|
+
category: string;
|
|
445
|
+
inputType?: string;
|
|
446
|
+
outputType?: string;
|
|
447
|
+
tags: string[];
|
|
448
|
+
capabilities: string[];
|
|
449
|
+
verified: boolean;
|
|
450
|
+
}>> {
|
|
451
|
+
const params = new URLSearchParams();
|
|
452
|
+
if (criteria.category) params.set('category', criteria.category);
|
|
453
|
+
if (criteria.inputType) params.set('inputType', criteria.inputType);
|
|
454
|
+
if (criteria.outputType) params.set('outputType', criteria.outputType);
|
|
455
|
+
if (criteria.tags) params.set('tags', criteria.tags.join(','));
|
|
456
|
+
if (criteria.capabilities) params.set('capabilities', criteria.capabilities.join(','));
|
|
457
|
+
params.set('limit', '100');
|
|
458
|
+
|
|
459
|
+
const response = await fetch(`${this.apiUrl}/api/services?${params}`, {
|
|
460
|
+
headers: this.getHeaders()
|
|
461
|
+
});
|
|
462
|
+
if (!response.ok) {
|
|
463
|
+
throw this.createError('SERVICE_UNAVAILABLE', 'Failed to discover services', true);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const data = await response.json();
|
|
467
|
+
return (data.services || []).map((s: Record<string, unknown>) => ({
|
|
468
|
+
id: s.id as string,
|
|
469
|
+
name: s.name as string,
|
|
470
|
+
endpoint: s.endpointUrl as string || s.endpoint_url as string || '',
|
|
471
|
+
price: s.pricePerCall as string || s.price_per_call as string || '0',
|
|
472
|
+
provider: s.ownerAddress as string || s.owner_address as string || '',
|
|
473
|
+
reputation: (s.reputationScore as number) || (s.reputation_score as number) || 0,
|
|
474
|
+
latency: (s.avgLatencyMs as number) || (s.avg_latency_ms as number) || 0,
|
|
475
|
+
category: s.category as string || '',
|
|
476
|
+
inputType: (s.schema as Record<string, unknown>)?.inputType as string,
|
|
477
|
+
outputType: (s.schema as Record<string, unknown>)?.outputType as string,
|
|
478
|
+
tags: ((s.schema as Record<string, unknown>)?.tags as string[]) || [],
|
|
479
|
+
capabilities: ((s.schema as Record<string, unknown>)?.capabilities as string[]) || [],
|
|
480
|
+
verified: (s.health as Record<string, unknown>)?.reliable as boolean || false,
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ==========================================================================
|
|
485
|
+
// EXECUTION - Payment-first, failure-aware
|
|
486
|
+
// ==========================================================================
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Execute a service with automatic payment handling
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* const result = await agent.execute(service, { pair: "BTC/USD" });
|
|
493
|
+
*
|
|
494
|
+
* if (result.success) {
|
|
495
|
+
* console.log("Price:", result.data.price);
|
|
496
|
+
* } else if (result.error?.retryable) {
|
|
497
|
+
* // Wait and retry
|
|
498
|
+
* await sleep(result.error.retryAfterMs);
|
|
499
|
+
* const retry = await agent.execute(service, input);
|
|
500
|
+
* }
|
|
501
|
+
*/
|
|
502
|
+
async execute<TInput = unknown, TOutput = unknown>(
|
|
503
|
+
service: SelectedService | string,
|
|
504
|
+
input?: TInput,
|
|
505
|
+
options: { timeout?: number } = {}
|
|
506
|
+
): Promise<ExecutionResult<TOutput>> {
|
|
507
|
+
const startTime = performance.now();
|
|
508
|
+
const timeout = options.timeout || 30000;
|
|
509
|
+
|
|
510
|
+
// Resolve service
|
|
511
|
+
const resolvedService = typeof service === 'string'
|
|
512
|
+
? await this.getServiceById(service)
|
|
513
|
+
: service;
|
|
514
|
+
|
|
515
|
+
if (!resolvedService) {
|
|
516
|
+
return {
|
|
517
|
+
success: false,
|
|
518
|
+
error: this.createError('SERVICE_NOT_FOUND', 'Service not found', false),
|
|
519
|
+
metrics: { totalMs: Math.round(performance.now() - startTime) },
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Make initial request
|
|
524
|
+
const controller = new AbortController();
|
|
525
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
let response = await fetch(resolvedService.endpoint, {
|
|
529
|
+
method: input ? 'POST' : 'GET',
|
|
530
|
+
headers: this.getHeaders(),
|
|
531
|
+
body: input ? JSON.stringify(input) : undefined,
|
|
532
|
+
signal: controller.signal,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
let paymentInfo: ExecutionResult<TOutput>['payment'];
|
|
536
|
+
let paymentMs: number | undefined;
|
|
537
|
+
|
|
538
|
+
// Handle 402 Payment Required
|
|
539
|
+
if (response.status === 402) {
|
|
540
|
+
if (!this.signer) {
|
|
541
|
+
clearTimeout(timeoutId);
|
|
542
|
+
return {
|
|
543
|
+
success: false,
|
|
544
|
+
error: this.createError('UNAUTHORIZED', 'Signer required for paid services', false),
|
|
545
|
+
metrics: { totalMs: Math.round(performance.now() - startTime) },
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const paymentRequired = await response.json();
|
|
550
|
+
const requirements = paymentRequired.paymentRequirements;
|
|
551
|
+
|
|
552
|
+
const paymentStart = performance.now();
|
|
553
|
+
const payment = await this.makePayment(requirements);
|
|
554
|
+
paymentMs = Math.round(performance.now() - paymentStart);
|
|
555
|
+
|
|
556
|
+
paymentInfo = {
|
|
557
|
+
id: payment.paymentId,
|
|
558
|
+
txHash: payment.txHash,
|
|
559
|
+
amount: requirements.maxAmountRequired,
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// Retry with payment
|
|
563
|
+
response = await fetch(resolvedService.endpoint, {
|
|
564
|
+
method: input ? 'POST' : 'GET',
|
|
565
|
+
headers: {
|
|
566
|
+
...this.getHeaders(),
|
|
567
|
+
'X-Payment': payment.txHash,
|
|
568
|
+
'X-Payment-Id': payment.paymentId,
|
|
569
|
+
},
|
|
570
|
+
body: input ? JSON.stringify(input) : undefined,
|
|
571
|
+
signal: controller.signal,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
clearTimeout(timeoutId);
|
|
576
|
+
const totalMs = Math.round(performance.now() - startTime);
|
|
577
|
+
const serviceMs = paymentMs ? totalMs - paymentMs : totalMs;
|
|
578
|
+
|
|
579
|
+
if (!response.ok) {
|
|
580
|
+
const error = await response.json().catch(() => ({}));
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
error: this.createError(
|
|
584
|
+
response.status === 429 ? 'RATE_LIMITED' : 'EXECUTION_FAILED',
|
|
585
|
+
error.message || `Service returned ${response.status}`,
|
|
586
|
+
response.status === 429 || response.status >= 500
|
|
587
|
+
),
|
|
588
|
+
payment: paymentInfo,
|
|
589
|
+
metrics: { totalMs, paymentMs, serviceMs },
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const data = await response.json();
|
|
594
|
+
|
|
595
|
+
// Record outcome for memory
|
|
596
|
+
this.recordOutcome({
|
|
597
|
+
timestamp: new Date(),
|
|
598
|
+
serviceId: resolvedService.id,
|
|
599
|
+
success: true,
|
|
600
|
+
latencyMs: serviceMs,
|
|
601
|
+
paymentAmount: paymentInfo?.amount,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
success: true,
|
|
606
|
+
data,
|
|
607
|
+
payment: paymentInfo,
|
|
608
|
+
metrics: { totalMs, paymentMs, serviceMs },
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
} catch (err) {
|
|
612
|
+
clearTimeout(timeoutId);
|
|
613
|
+
const totalMs = Math.round(performance.now() - startTime);
|
|
614
|
+
|
|
615
|
+
const isTimeout = err instanceof Error && err.name === 'AbortError';
|
|
616
|
+
return {
|
|
617
|
+
success: false,
|
|
618
|
+
error: this.createError(
|
|
619
|
+
isTimeout ? 'EXECUTION_TIMEOUT' : 'EXECUTION_FAILED',
|
|
620
|
+
isTimeout ? 'Request timed out' : (err instanceof Error ? err.message : 'Unknown error'),
|
|
621
|
+
true,
|
|
622
|
+
isTimeout ? 5000 : 1000
|
|
623
|
+
),
|
|
624
|
+
metrics: { totalMs },
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ==========================================================================
|
|
630
|
+
// WORKFLOWS - Multi-step execution with retries and fallbacks
|
|
631
|
+
// ==========================================================================
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Execute a multi-step workflow
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* const result = await agent.executeWorkflow([
|
|
638
|
+
* { name: 'getPrice', criteria: { category: 'data.prices' } },
|
|
639
|
+
* { name: 'validate', transform: (price) => price.value > 0 ? price : null },
|
|
640
|
+
* { name: 'trade', criteria: { category: 'trading.execution' } },
|
|
641
|
+
* ], { pair: 'BTC/USD' });
|
|
642
|
+
*/
|
|
643
|
+
async executeWorkflow<TInput = unknown, TOutput = unknown>(
|
|
644
|
+
steps: WorkflowStep[],
|
|
645
|
+
initialInput: TInput
|
|
646
|
+
): Promise<WorkflowResult<TOutput>> {
|
|
647
|
+
const startTime = performance.now();
|
|
648
|
+
const stepResults: WorkflowResult['stepResults'] = [];
|
|
649
|
+
let currentInput: unknown = initialInput;
|
|
650
|
+
let failedSteps = 0;
|
|
651
|
+
|
|
652
|
+
for (const step of steps) {
|
|
653
|
+
const stepStart = performance.now();
|
|
654
|
+
const maxRetries = step.retries || 0;
|
|
655
|
+
let lastError: ExecutionError | undefined;
|
|
656
|
+
|
|
657
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
658
|
+
try {
|
|
659
|
+
let result: unknown;
|
|
660
|
+
|
|
661
|
+
if (step.transform) {
|
|
662
|
+
// Pure transformation step
|
|
663
|
+
result = await step.transform(currentInput as never);
|
|
664
|
+
} else {
|
|
665
|
+
// Service execution step
|
|
666
|
+
let service = step.service;
|
|
667
|
+
if (!service && step.criteria) {
|
|
668
|
+
service = await this.selectService(step.criteria) || undefined;
|
|
669
|
+
}
|
|
670
|
+
if (!service && step.serviceId) {
|
|
671
|
+
service = await this.getServiceById(step.serviceId) || undefined;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (!service) {
|
|
675
|
+
throw this.createError('SERVICE_NOT_FOUND', `No service found for step: ${step.name}`, false);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const execResult = await this.execute(service, currentInput, { timeout: step.timeout });
|
|
679
|
+
if (!execResult.success) {
|
|
680
|
+
throw execResult.error;
|
|
681
|
+
}
|
|
682
|
+
result = execResult.data;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Success
|
|
686
|
+
if (step.onSuccess) await step.onSuccess(result as never);
|
|
687
|
+
|
|
688
|
+
stepResults.push({
|
|
689
|
+
stepName: step.name,
|
|
690
|
+
success: true,
|
|
691
|
+
data: result,
|
|
692
|
+
durationMs: Math.round(performance.now() - stepStart),
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
currentInput = result;
|
|
696
|
+
lastError = undefined;
|
|
697
|
+
break;
|
|
698
|
+
|
|
699
|
+
} catch (err) {
|
|
700
|
+
// Check if it's already an ExecutionError
|
|
701
|
+
if (err && typeof err === 'object' && 'code' in err && 'retryable' in err) {
|
|
702
|
+
lastError = err as ExecutionError;
|
|
703
|
+
} else {
|
|
704
|
+
lastError = this.createError('EXECUTION_FAILED', err instanceof Error ? err.message : 'Unknown', false);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Try fallback if available and no retries left
|
|
708
|
+
if (attempt === maxRetries && step.fallback) {
|
|
709
|
+
try {
|
|
710
|
+
const fallbackResult = await this.executeWorkflow([step.fallback], currentInput);
|
|
711
|
+
if (fallbackResult.success) {
|
|
712
|
+
currentInput = fallbackResult.data;
|
|
713
|
+
lastError = undefined;
|
|
714
|
+
stepResults.push({
|
|
715
|
+
stepName: `${step.name} (fallback)`,
|
|
716
|
+
success: true,
|
|
717
|
+
data: fallbackResult.data,
|
|
718
|
+
durationMs: Math.round(performance.now() - stepStart),
|
|
719
|
+
});
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
} catch {
|
|
723
|
+
// Fallback failed, continue with error
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (lastError) {
|
|
730
|
+
if (step.onFailure) await step.onFailure(lastError);
|
|
731
|
+
|
|
732
|
+
stepResults.push({
|
|
733
|
+
stepName: step.name,
|
|
734
|
+
success: false,
|
|
735
|
+
error: lastError,
|
|
736
|
+
durationMs: Math.round(performance.now() - stepStart),
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
failedSteps++;
|
|
740
|
+
|
|
741
|
+
// Stop workflow on non-retryable error
|
|
742
|
+
if (!lastError.retryable) break;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const completedSteps = stepResults.filter(r => r.success).length;
|
|
747
|
+
const totalMs = Math.round(performance.now() - startTime);
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
success: failedSteps === 0,
|
|
751
|
+
data: currentInput as TOutput,
|
|
752
|
+
stepResults,
|
|
753
|
+
totalMs,
|
|
754
|
+
completedSteps,
|
|
755
|
+
failedSteps,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ==========================================================================
|
|
760
|
+
// MEMORY - Built-in hooks for agent learning
|
|
761
|
+
// ==========================================================================
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Get agent memory interface
|
|
765
|
+
*/
|
|
766
|
+
get memory(): AgentMemory {
|
|
767
|
+
return {
|
|
768
|
+
record: (outcome) => this.recordOutcome(outcome),
|
|
769
|
+
getHistory: (serviceId) => serviceId
|
|
770
|
+
? this.memoryStore.filter(o => o.serviceId === serviceId)
|
|
771
|
+
: [...this.memoryStore],
|
|
772
|
+
getStats: () => {
|
|
773
|
+
const total = this.memoryStore.length;
|
|
774
|
+
const successful = this.memoryStore.filter(o => o.success).length;
|
|
775
|
+
const avgLatency = total > 0
|
|
776
|
+
? this.memoryStore.reduce((sum, o) => sum + o.latencyMs, 0) / total
|
|
777
|
+
: 0;
|
|
778
|
+
return {
|
|
779
|
+
totalCalls: total,
|
|
780
|
+
successRate: total > 0 ? successful / total : 0,
|
|
781
|
+
avgLatency: Math.round(avgLatency),
|
|
782
|
+
};
|
|
783
|
+
},
|
|
784
|
+
clear: () => { this.memoryStore = []; },
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Register callback for outcomes (for external memory systems)
|
|
790
|
+
*/
|
|
791
|
+
onOutcome(callback: (outcome: OutcomeRecord) => void): () => void {
|
|
792
|
+
this.outcomeCallbacks.push(callback);
|
|
793
|
+
return () => {
|
|
794
|
+
const idx = this.outcomeCallbacks.indexOf(callback);
|
|
795
|
+
if (idx > -1) this.outcomeCallbacks.splice(idx, 1);
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private outcomeCallbacks: Array<(outcome: OutcomeRecord) => void> = [];
|
|
800
|
+
|
|
801
|
+
private recordOutcome(outcome: OutcomeRecord): void {
|
|
802
|
+
this.memoryStore.push(outcome);
|
|
803
|
+
// Keep last 1000 outcomes
|
|
804
|
+
if (this.memoryStore.length > 1000) {
|
|
805
|
+
this.memoryStore = this.memoryStore.slice(-1000);
|
|
806
|
+
}
|
|
807
|
+
this.outcomeCallbacks.forEach(cb => cb(outcome));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ==========================================================================
|
|
811
|
+
// HELPERS
|
|
812
|
+
// ==========================================================================
|
|
813
|
+
|
|
814
|
+
private async getServiceById(serviceId: string): Promise<SelectedService | null> {
|
|
815
|
+
const response = await fetch(`${this.apiUrl}/api/services/${serviceId}`, {
|
|
816
|
+
headers: this.getHeaders()
|
|
817
|
+
});
|
|
818
|
+
if (!response.ok) return null;
|
|
819
|
+
|
|
820
|
+
const s = await response.json();
|
|
821
|
+
return {
|
|
822
|
+
id: s.id,
|
|
823
|
+
name: s.name,
|
|
824
|
+
endpoint: s.endpointUrl || s.endpoint_url,
|
|
825
|
+
price: s.pricePerCall || '0',
|
|
826
|
+
provider: s.ownerAddress || '',
|
|
827
|
+
reputation: s.reputationScore || 0,
|
|
828
|
+
latency: s.avgLatencyMs || 0,
|
|
829
|
+
selectionReason: 'Direct lookup',
|
|
830
|
+
scoreBreakdown: { reputation: 0, latency: 0, price: 0, total: 0 },
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
private passesConstraints(
|
|
835
|
+
service: { reputation: number; latency: number; price: string; provider: string; verified: boolean },
|
|
836
|
+
constraints: TrustPolicy
|
|
837
|
+
): boolean {
|
|
838
|
+
if (constraints.minReputation && service.reputation < constraints.minReputation) return false;
|
|
839
|
+
if (constraints.maxLatency && service.latency > constraints.maxLatency) return false;
|
|
840
|
+
if (constraints.maxPrice && parseFloat(service.price) > constraints.maxPrice) return false;
|
|
841
|
+
if (constraints.verifiedOnly && !service.verified) return false;
|
|
842
|
+
if (constraints.blacklistedProviders?.includes(service.provider)) return false;
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private scoreService(
|
|
847
|
+
service: DiscoveredServiceInternal,
|
|
848
|
+
constraints: TrustPolicy
|
|
849
|
+
): {
|
|
850
|
+
service: DiscoveredServiceInternal;
|
|
851
|
+
score: number;
|
|
852
|
+
repScore: number;
|
|
853
|
+
latScore: number;
|
|
854
|
+
priceScore: number;
|
|
855
|
+
} {
|
|
856
|
+
// Normalize scores to 0-100
|
|
857
|
+
const repScore = service.reputation; // Already 0-100
|
|
858
|
+
const latScore = Math.max(0, 100 - (service.latency / 10)); // Lower is better
|
|
859
|
+
const priceScore = Math.max(0, 100 - (parseFloat(service.price) * 100)); // Lower is better
|
|
860
|
+
|
|
861
|
+
// Boost for preferred providers
|
|
862
|
+
const preferredBoost = constraints.preferredProviders?.includes(service.provider) ? 10 : 0;
|
|
863
|
+
|
|
864
|
+
// Weighted score
|
|
865
|
+
const score = (repScore * 0.5 + latScore * 0.3 + priceScore * 0.2) + preferredBoost;
|
|
866
|
+
|
|
867
|
+
return { service, score, repScore, latScore, priceScore };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private explainSelection(scored: { service: { reputation: number; latency: number; price: string }; score: number }): string {
|
|
871
|
+
return `Highest score (${scored.score.toFixed(1)}) based on: reputation=${scored.service.reputation}, ` +
|
|
872
|
+
`latency=${scored.service.latency}ms, price=$${scored.service.price}`;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private async makePayment(requirements: {
|
|
876
|
+
payTo: string;
|
|
877
|
+
maxAmountRequired: string;
|
|
878
|
+
asset?: string;
|
|
879
|
+
}): Promise<{ paymentId: string; txHash: string }> {
|
|
880
|
+
if (!this.signer) throw this.createError('UNAUTHORIZED', 'Signer required', false);
|
|
881
|
+
|
|
882
|
+
const signerAddress = await this.signer.getAddress();
|
|
883
|
+
const chainId = NETWORK_CONFIG[this.network].chainId;
|
|
884
|
+
|
|
885
|
+
// EIP-3009 authorization
|
|
886
|
+
const domain = {
|
|
887
|
+
name: 'USD Coin',
|
|
888
|
+
version: '2',
|
|
889
|
+
chainId,
|
|
890
|
+
verifyingContract: requirements.asset || '0xf951eC28187D9E5Ca673Da8FE6757E6f0Be5F77C',
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
const types = {
|
|
894
|
+
TransferWithAuthorization: [
|
|
895
|
+
{ name: 'from', type: 'address' },
|
|
896
|
+
{ name: 'to', type: 'address' },
|
|
897
|
+
{ name: 'value', type: 'uint256' },
|
|
898
|
+
{ name: 'validAfter', type: 'uint256' },
|
|
899
|
+
{ name: 'validBefore', type: 'uint256' },
|
|
900
|
+
{ name: 'nonce', type: 'bytes32' },
|
|
901
|
+
],
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
const nonce = ethers.hexlify(ethers.randomBytes(32));
|
|
905
|
+
const validAfter = 0;
|
|
906
|
+
const validBefore = Math.floor(Date.now() / 1000) + 3600;
|
|
907
|
+
|
|
908
|
+
const value = {
|
|
909
|
+
from: signerAddress,
|
|
910
|
+
to: requirements.payTo,
|
|
911
|
+
value: ethers.parseUnits(requirements.maxAmountRequired, 6),
|
|
912
|
+
validAfter,
|
|
913
|
+
validBefore,
|
|
914
|
+
nonce,
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
const signature = await (this.signer as ethers.Signer & {
|
|
918
|
+
signTypedData: (d: typeof domain, t: typeof types, v: typeof value) => Promise<string>;
|
|
919
|
+
}).signTypedData(domain, types, value);
|
|
920
|
+
|
|
921
|
+
const response = await fetch(`${FACILITATOR_URL}/settle`, {
|
|
922
|
+
method: 'POST',
|
|
923
|
+
headers: { 'Content-Type': 'application/json' },
|
|
924
|
+
body: JSON.stringify({
|
|
925
|
+
from: signerAddress,
|
|
926
|
+
to: requirements.payTo,
|
|
927
|
+
value: requirements.maxAmountRequired,
|
|
928
|
+
validAfter,
|
|
929
|
+
validBefore,
|
|
930
|
+
nonce,
|
|
931
|
+
signature,
|
|
932
|
+
network: this.network === 'cronos-mainnet' ? 'cronos-mainnet' : 'cronos-testnet',
|
|
933
|
+
}),
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
if (!response.ok) {
|
|
937
|
+
throw this.createError('PAYMENT_FAILED', 'Payment settlement failed', true, 5000);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const result = await response.json();
|
|
941
|
+
return {
|
|
942
|
+
paymentId: `pay_${result.txHash?.slice(2, 18) || Date.now()}`,
|
|
943
|
+
txHash: result.txHash || '',
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ==========================================================================
|
|
948
|
+
// A2A DISCOVERY - Agent-to-Agent Protocol Support
|
|
949
|
+
// ==========================================================================
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Discover an agent's capabilities via their agent card
|
|
953
|
+
*
|
|
954
|
+
* @example
|
|
955
|
+
* const card = await agent.discoverAgentCard("https://api.service.com");
|
|
956
|
+
* console.log(card.resources); // Available x402-gated resources
|
|
957
|
+
*/
|
|
958
|
+
async discoverAgentCard(baseUrl: string): Promise<AgentCard | null> {
|
|
959
|
+
const paths = ['/.well-known/agent-card.json', '/.well-known/agent.json'];
|
|
960
|
+
|
|
961
|
+
for (const path of paths) {
|
|
962
|
+
try {
|
|
963
|
+
const response = await fetch(`${baseUrl.replace(/\/$/, '')}${path}`, {
|
|
964
|
+
headers: { 'Accept': 'application/json' },
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
if (response.ok) {
|
|
968
|
+
return await response.json();
|
|
969
|
+
}
|
|
970
|
+
} catch {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Discover multiple remote agents in parallel
|
|
980
|
+
*
|
|
981
|
+
* @example
|
|
982
|
+
* const agents = await agent.discoverRemoteAgents([
|
|
983
|
+
* "https://perpai.relaycore.xyz",
|
|
984
|
+
* "https://rwa.relaycore.xyz"
|
|
985
|
+
* ]);
|
|
986
|
+
*/
|
|
987
|
+
async discoverRemoteAgents(urls: string[]): Promise<Array<{
|
|
988
|
+
url: string;
|
|
989
|
+
card: AgentCard | null;
|
|
990
|
+
online: boolean;
|
|
991
|
+
}>> {
|
|
992
|
+
return Promise.all(
|
|
993
|
+
urls.map(async (url) => {
|
|
994
|
+
const card = await this.discoverAgentCard(url);
|
|
995
|
+
return {
|
|
996
|
+
url,
|
|
997
|
+
card,
|
|
998
|
+
online: card !== null,
|
|
999
|
+
};
|
|
1000
|
+
})
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Get local Relay Core agent card
|
|
1006
|
+
*/
|
|
1007
|
+
async getLocalAgentCard(): Promise<AgentCard> {
|
|
1008
|
+
const response = await fetch(`${this.apiUrl}/.well-known/agent-card.json`);
|
|
1009
|
+
if (!response.ok) {
|
|
1010
|
+
throw this.createError('SERVICE_UNAVAILABLE', 'Failed to fetch agent card', true);
|
|
1011
|
+
}
|
|
1012
|
+
return response.json();
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ==========================================================================
|
|
1016
|
+
// TASK ARTIFACTS - Track and audit all agent actions
|
|
1017
|
+
// ==========================================================================
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Create a new task artifact
|
|
1021
|
+
*
|
|
1022
|
+
* @example
|
|
1023
|
+
* const task = await agent.createTask({
|
|
1024
|
+
* service_id: "perpai-quote",
|
|
1025
|
+
* inputs: { pair: "BTC/USD" }
|
|
1026
|
+
* });
|
|
1027
|
+
*/
|
|
1028
|
+
async createTask(params: {
|
|
1029
|
+
service_id?: string;
|
|
1030
|
+
session_id?: string;
|
|
1031
|
+
inputs: Record<string, unknown>;
|
|
1032
|
+
}): Promise<TaskArtifact> {
|
|
1033
|
+
const agentId = await this.getAddress();
|
|
1034
|
+
|
|
1035
|
+
const response = await fetch(`${this.apiUrl}/api/tasks`, {
|
|
1036
|
+
method: 'POST',
|
|
1037
|
+
headers: this.getHeaders(),
|
|
1038
|
+
body: JSON.stringify({
|
|
1039
|
+
agent_id: agentId,
|
|
1040
|
+
service_id: params.service_id,
|
|
1041
|
+
session_id: params.session_id,
|
|
1042
|
+
inputs: params.inputs,
|
|
1043
|
+
}),
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
if (!response.ok) {
|
|
1047
|
+
throw this.createError('EXECUTION_FAILED', 'Failed to create task', true);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return response.json();
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Get a task artifact by ID
|
|
1055
|
+
*/
|
|
1056
|
+
async getTask(taskId: string): Promise<TaskArtifact | null> {
|
|
1057
|
+
const response = await fetch(`${this.apiUrl}/api/tasks/${taskId}`, {
|
|
1058
|
+
headers: this.getHeaders()
|
|
1059
|
+
});
|
|
1060
|
+
if (response.status === 404) return null;
|
|
1061
|
+
if (!response.ok) {
|
|
1062
|
+
throw this.createError('EXECUTION_FAILED', 'Failed to get task', true);
|
|
1063
|
+
}
|
|
1064
|
+
return response.json();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Query task artifacts
|
|
1069
|
+
*
|
|
1070
|
+
* @example
|
|
1071
|
+
* const tasks = await agent.getTasks({ state: 'settled', limit: 10 });
|
|
1072
|
+
*/
|
|
1073
|
+
async getTasks(params?: {
|
|
1074
|
+
service_id?: string;
|
|
1075
|
+
session_id?: string;
|
|
1076
|
+
state?: TaskState;
|
|
1077
|
+
from?: Date;
|
|
1078
|
+
to?: Date;
|
|
1079
|
+
limit?: number;
|
|
1080
|
+
}): Promise<TaskArtifact[]> {
|
|
1081
|
+
const agentId = await this.getAddress();
|
|
1082
|
+
const queryParams = new URLSearchParams();
|
|
1083
|
+
queryParams.set('agent_id', agentId);
|
|
1084
|
+
|
|
1085
|
+
if (params?.service_id) queryParams.set('service_id', params.service_id);
|
|
1086
|
+
if (params?.session_id) queryParams.set('session_id', params.session_id.toString());
|
|
1087
|
+
if (params?.state) queryParams.set('state', params.state);
|
|
1088
|
+
if (params?.from) queryParams.set('from', params.from.toISOString());
|
|
1089
|
+
if (params?.to) queryParams.set('to', params.to.toISOString());
|
|
1090
|
+
if (params?.limit) queryParams.set('limit', params.limit.toString());
|
|
1091
|
+
|
|
1092
|
+
const response = await fetch(`${this.apiUrl}/api/tasks?${queryParams}`, {
|
|
1093
|
+
headers: this.getHeaders()
|
|
1094
|
+
});
|
|
1095
|
+
if (!response.ok) {
|
|
1096
|
+
throw this.createError('EXECUTION_FAILED', 'Failed to query tasks', true);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const data = await response.json();
|
|
1100
|
+
return data.tasks || [];
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Get task statistics
|
|
1105
|
+
*/
|
|
1106
|
+
async getTaskStats(): Promise<TaskStats> {
|
|
1107
|
+
const agentId = await this.getAddress();
|
|
1108
|
+
const response = await fetch(`${this.apiUrl}/api/tasks/stats?agent_id=${agentId}`, {
|
|
1109
|
+
headers: this.getHeaders()
|
|
1110
|
+
});
|
|
1111
|
+
if (!response.ok) {
|
|
1112
|
+
throw this.createError('EXECUTION_FAILED', 'Failed to get task stats', true);
|
|
1113
|
+
}
|
|
1114
|
+
return response.json();
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Mark a task as settled (success)
|
|
1119
|
+
*/
|
|
1120
|
+
async settleTask(taskId: string, outputs: Record<string, unknown>, metrics?: TaskArtifact['metrics']): Promise<TaskArtifact> {
|
|
1121
|
+
const response = await fetch(`${this.apiUrl}/api/tasks/${taskId}/settle`, {
|
|
1122
|
+
method: 'POST',
|
|
1123
|
+
headers: this.getHeaders(),
|
|
1124
|
+
body: JSON.stringify({ outputs, metrics }),
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
if (!response.ok) {
|
|
1128
|
+
throw this.createError('EXECUTION_FAILED', 'Failed to settle task', true);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return response.json();
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Mark a task as failed
|
|
1136
|
+
*/
|
|
1137
|
+
async failTask(taskId: string, error: { code: string; message: string; retryable: boolean }, metrics?: TaskArtifact['metrics']): Promise<TaskArtifact> {
|
|
1138
|
+
const response = await fetch(`${this.apiUrl}/api/tasks/${taskId}/fail`, {
|
|
1139
|
+
method: 'POST',
|
|
1140
|
+
headers: this.getHeaders(),
|
|
1141
|
+
body: JSON.stringify({ error, metrics }),
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
if (!response.ok) {
|
|
1145
|
+
throw this.createError('EXECUTION_FAILED', 'Failed to fail task', true);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
return response.json();
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// ========================================================================
|
|
1152
|
+
// META-AGENT METHODS (Agent Discovery & Hiring)
|
|
1153
|
+
// ========================================================================
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Discover agents based on criteria
|
|
1157
|
+
*/
|
|
1158
|
+
async discoverAgents(query: {
|
|
1159
|
+
capability?: string;
|
|
1160
|
+
category?: string;
|
|
1161
|
+
minReputation?: number;
|
|
1162
|
+
maxPricePerCall?: string;
|
|
1163
|
+
limit?: number;
|
|
1164
|
+
}): Promise<Array<{
|
|
1165
|
+
agentId: string;
|
|
1166
|
+
agentName: string;
|
|
1167
|
+
agentUrl: string;
|
|
1168
|
+
reputationScore: number;
|
|
1169
|
+
pricePerCall: string;
|
|
1170
|
+
successRate: number;
|
|
1171
|
+
compositeScore: number;
|
|
1172
|
+
}>> {
|
|
1173
|
+
const response = await fetch(`${this.apiUrl}/api/meta-agent/discover`, {
|
|
1174
|
+
method: 'POST',
|
|
1175
|
+
headers: this.getHeaders(),
|
|
1176
|
+
body: JSON.stringify(query)
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
if (!response.ok) {
|
|
1180
|
+
throw this.createError('EXECUTION_FAILED', 'Agent discovery failed', true);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const data = await response.json();
|
|
1184
|
+
return data.agents || [];
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Hire an agent to perform a task
|
|
1189
|
+
*/
|
|
1190
|
+
async hireAgent(request: {
|
|
1191
|
+
agentId: string;
|
|
1192
|
+
resourceId: string;
|
|
1193
|
+
budget: string;
|
|
1194
|
+
task: Record<string, unknown>;
|
|
1195
|
+
}): Promise<{
|
|
1196
|
+
success: boolean;
|
|
1197
|
+
taskId: string;
|
|
1198
|
+
agentId: string;
|
|
1199
|
+
cost: string;
|
|
1200
|
+
}> {
|
|
1201
|
+
const agentId = await this.getAddress();
|
|
1202
|
+
|
|
1203
|
+
const response = await fetch(`${this.apiUrl}/api/meta-agent/hire`, {
|
|
1204
|
+
method: 'POST',
|
|
1205
|
+
headers: {
|
|
1206
|
+
...this.getHeaders(),
|
|
1207
|
+
'X-Agent-Id': agentId
|
|
1208
|
+
},
|
|
1209
|
+
body: JSON.stringify(request)
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
if (!response.ok) {
|
|
1213
|
+
const error = await response.json();
|
|
1214
|
+
throw this.createError('EXECUTION_FAILED', error.error || 'Agent hiring failed', true);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return response.json();
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Execute a delegated task
|
|
1222
|
+
*/
|
|
1223
|
+
async executeDelegation(taskId: string): Promise<{
|
|
1224
|
+
success: boolean;
|
|
1225
|
+
outcome: {
|
|
1226
|
+
taskId: string;
|
|
1227
|
+
agentId: string;
|
|
1228
|
+
state: string;
|
|
1229
|
+
outputs?: Record<string, unknown>;
|
|
1230
|
+
error?: { code: string; message: string };
|
|
1231
|
+
};
|
|
1232
|
+
}> {
|
|
1233
|
+
const response = await fetch(`${this.apiUrl}/api/meta-agent/execute/${taskId}`, {
|
|
1234
|
+
method: 'POST',
|
|
1235
|
+
headers: this.getHeaders()
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
if (!response.ok) {
|
|
1239
|
+
throw this.createError('EXECUTION_FAILED', 'Delegation execution failed', true);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
return response.json();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Get delegation status
|
|
1247
|
+
*/
|
|
1248
|
+
async getDelegationStatus(taskId: string): Promise<{
|
|
1249
|
+
taskId: string;
|
|
1250
|
+
agentId: string;
|
|
1251
|
+
state: string;
|
|
1252
|
+
cost: string;
|
|
1253
|
+
outputs?: Record<string, unknown>;
|
|
1254
|
+
}> {
|
|
1255
|
+
const response = await fetch(`${this.apiUrl}/api/meta-agent/status/${taskId}`, {
|
|
1256
|
+
headers: this.getHeaders()
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
if (!response.ok) {
|
|
1260
|
+
throw this.createError('EXECUTION_FAILED', 'Failed to get delegation status', true);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const data = await response.json();
|
|
1264
|
+
return data.outcome;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Fetch agent card for a specific agent
|
|
1269
|
+
*/
|
|
1270
|
+
async getAgentCard(agentId: string): Promise<{
|
|
1271
|
+
name: string;
|
|
1272
|
+
description: string;
|
|
1273
|
+
url: string;
|
|
1274
|
+
network: string;
|
|
1275
|
+
resources: Array<{
|
|
1276
|
+
id: string;
|
|
1277
|
+
title: string;
|
|
1278
|
+
url: string;
|
|
1279
|
+
}>;
|
|
1280
|
+
} | null> {
|
|
1281
|
+
const response = await fetch(`${this.apiUrl}/api/meta-agent/agent-card/${agentId}`, {
|
|
1282
|
+
headers: this.getHeaders()
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
if (!response.ok) {
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const data = await response.json();
|
|
1290
|
+
return data.card || null;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// ========================================================================
|
|
1294
|
+
// SESSION MANAGEMENT (x402)
|
|
1295
|
+
// ========================================================================
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Create a new x402 session
|
|
1299
|
+
*
|
|
1300
|
+
* @returns Session ID and payment request to activate it
|
|
1301
|
+
*/
|
|
1302
|
+
async createSession(params: {
|
|
1303
|
+
maxSpend: number;
|
|
1304
|
+
durationHours: number;
|
|
1305
|
+
authorizedAgents?: string[];
|
|
1306
|
+
}): Promise<{
|
|
1307
|
+
sessionId: string;
|
|
1308
|
+
paymentRequest: {
|
|
1309
|
+
amount: string;
|
|
1310
|
+
payTo: string;
|
|
1311
|
+
asset: string;
|
|
1312
|
+
};
|
|
1313
|
+
}> {
|
|
1314
|
+
const ownerAddress = await this.getAddress();
|
|
1315
|
+
|
|
1316
|
+
const response = await fetch(`${this.apiUrl}/api/sessions/create`, {
|
|
1317
|
+
method: 'POST',
|
|
1318
|
+
headers: this.getHeaders(),
|
|
1319
|
+
body: JSON.stringify({
|
|
1320
|
+
ownerAddress,
|
|
1321
|
+
maxSpend: params.maxSpend,
|
|
1322
|
+
durationHours: params.durationHours,
|
|
1323
|
+
authorizedAgents: params.authorizedAgents || []
|
|
1324
|
+
})
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
if (!response.ok) {
|
|
1328
|
+
const error = await response.json();
|
|
1329
|
+
throw this.createError('EXECUTION_FAILED', error.message || 'Failed to create session', false);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const result = await response.json();
|
|
1333
|
+
return {
|
|
1334
|
+
sessionId: result.session.session_id,
|
|
1335
|
+
paymentRequest: result.paymentRequest
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Activate a session after paying USDC
|
|
1341
|
+
*/
|
|
1342
|
+
async activateSession(sessionId: string, txHash: string, amount: string): Promise<{ success: boolean }> {
|
|
1343
|
+
const response = await fetch(`${this.apiUrl}/api/sessions/${sessionId}/activate`, {
|
|
1344
|
+
method: 'POST',
|
|
1345
|
+
headers: this.getHeaders(),
|
|
1346
|
+
body: JSON.stringify({ txHash, amount })
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
if (!response.ok) {
|
|
1350
|
+
const error = await response.json();
|
|
1351
|
+
throw this.createError('EXECUTION_FAILED', error.message || 'Failed to activate session', true);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
return { success: true };
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Get session details
|
|
1359
|
+
*/
|
|
1360
|
+
async getSession(sessionId: string): Promise<{
|
|
1361
|
+
id: string;
|
|
1362
|
+
owner: string;
|
|
1363
|
+
maxSpend: string;
|
|
1364
|
+
spent: string;
|
|
1365
|
+
isActive: boolean;
|
|
1366
|
+
expiresAt: string;
|
|
1367
|
+
} | null> {
|
|
1368
|
+
const response = await fetch(`${this.apiUrl}/api/sessions/${sessionId}`, {
|
|
1369
|
+
headers: this.getHeaders()
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
if (response.status === 404) return null;
|
|
1373
|
+
if (!response.ok) {
|
|
1374
|
+
throw this.createError('EXECUTION_FAILED', 'Failed to get session', true);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const data = await response.json();
|
|
1378
|
+
const session = data.session;
|
|
1379
|
+
return {
|
|
1380
|
+
id: session.session_id,
|
|
1381
|
+
owner: session.owner_address,
|
|
1382
|
+
maxSpend: session.max_spend,
|
|
1383
|
+
spent: session.spent,
|
|
1384
|
+
isActive: session.is_active,
|
|
1385
|
+
expiresAt: session.expires_at
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
private createError(
|
|
1391
|
+
code: ErrorCode,
|
|
1392
|
+
message: string,
|
|
1393
|
+
retryable: boolean,
|
|
1394
|
+
retryAfterMs?: number
|
|
1395
|
+
): ExecutionError {
|
|
1396
|
+
return { code, message, retryable, retryAfterMs };
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// ============================================================================
|
|
1401
|
+
// FACTORY & EXPORTS
|
|
1402
|
+
// ============================================================================
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Create a Relay Agent instance
|
|
1406
|
+
*
|
|
1407
|
+
* @example
|
|
1408
|
+
* const agent = createAgent({ wallet, network: "cronos-testnet" });
|
|
1409
|
+
*/
|
|
1410
|
+
export function createAgent(config: AgentConfig): RelayAgent {
|
|
1411
|
+
return new RelayAgent(config);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
export default RelayAgent;
|