@ophirai/sdk 0.1.0 → 0.2.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/buyer.d.ts +7 -1
- package/dist/buyer.js +15 -2
- package/dist/index.d.ts +14 -0
- package/dist/index.js +6 -0
- package/dist/metrics.d.ts +55 -0
- package/dist/metrics.js +153 -0
- package/dist/registry.d.ts +69 -0
- package/dist/registry.js +166 -0
- package/package.json +1 -1
package/dist/buyer.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ export interface BuyerAgentConfig {
|
|
|
14
14
|
escrowConfig?: EscrowConfig;
|
|
15
15
|
/** Optional Lockstep verification endpoint for SLA compliance monitoring. */
|
|
16
16
|
lockstepEndpoint?: string;
|
|
17
|
+
/** Optional registry endpoints for agent discovery. */
|
|
18
|
+
registryEndpoints?: string[];
|
|
19
|
+
/** Optional fallback endpoints for A2A discovery when registry is unavailable. */
|
|
20
|
+
fallbackEndpoints?: string[];
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* Buy-side negotiation agent. Sends RFQs, collects quotes, ranks them,
|
|
@@ -30,6 +34,8 @@ export declare class BuyerAgent {
|
|
|
30
34
|
private quoteListeners;
|
|
31
35
|
/** Tracks processed message IDs within the replay window to reject duplicate/replayed messages. */
|
|
32
36
|
private seenMessageIds;
|
|
37
|
+
private registryEndpoints?;
|
|
38
|
+
private fallbackEndpoints?;
|
|
33
39
|
constructor(config: BuyerAgentConfig);
|
|
34
40
|
/** Check if a message ID has already been processed (replay protection).
|
|
35
41
|
* Records the ID if new; throws DUPLICATE_MESSAGE if already seen.
|
|
@@ -49,7 +55,7 @@ export declare class BuyerAgent {
|
|
|
49
55
|
* const sellers = await buyer.discover({ category: 'llm-inference' });
|
|
50
56
|
* ```
|
|
51
57
|
*/
|
|
52
|
-
discover(
|
|
58
|
+
discover(query: {
|
|
53
59
|
category: string;
|
|
54
60
|
requirements?: Record<string, unknown>;
|
|
55
61
|
}): Promise<SellerInfo[]>;
|
package/dist/buyer.js
CHANGED
|
@@ -5,6 +5,7 @@ import { NegotiationServer } from './server.js';
|
|
|
5
5
|
import { NegotiationSession } from './negotiation.js';
|
|
6
6
|
import { JsonRpcClient } from './transport.js';
|
|
7
7
|
import { buildRFQ, buildCounter, buildAccept, buildReject, buildDispute } from './messages.js';
|
|
8
|
+
import { autoDiscover } from './registry.js';
|
|
8
9
|
/**
|
|
9
10
|
* Buy-side negotiation agent. Sends RFQs, collects quotes, ranks them,
|
|
10
11
|
* and accepts/counters/rejects offers. Verifies seller signatures on all
|
|
@@ -20,10 +21,14 @@ export class BuyerAgent {
|
|
|
20
21
|
quoteListeners = new Map();
|
|
21
22
|
/** Tracks processed message IDs within the replay window to reject duplicate/replayed messages. */
|
|
22
23
|
seenMessageIds = new Map();
|
|
24
|
+
registryEndpoints;
|
|
25
|
+
fallbackEndpoints;
|
|
23
26
|
constructor(config) {
|
|
24
27
|
this.keypair = config.keypair ?? generateKeyPair();
|
|
25
28
|
this.agentId = publicKeyToDid(this.keypair.publicKey);
|
|
26
29
|
this.endpoint = config.endpoint;
|
|
30
|
+
this.registryEndpoints = config.registryEndpoints;
|
|
31
|
+
this.fallbackEndpoints = config.fallbackEndpoints;
|
|
27
32
|
this.transport = new JsonRpcClient();
|
|
28
33
|
this.server = new NegotiationServer();
|
|
29
34
|
this.registerHandlers();
|
|
@@ -146,8 +151,16 @@ export class BuyerAgent {
|
|
|
146
151
|
* const sellers = await buyer.discover({ category: 'llm-inference' });
|
|
147
152
|
* ```
|
|
148
153
|
*/
|
|
149
|
-
async discover(
|
|
150
|
-
|
|
154
|
+
async discover(query) {
|
|
155
|
+
const agents = await autoDiscover(query.category, {
|
|
156
|
+
registries: this.registryEndpoints,
|
|
157
|
+
fallbackEndpoints: this.fallbackEndpoints,
|
|
158
|
+
});
|
|
159
|
+
return agents.map((a) => ({
|
|
160
|
+
agentId: a.agentId,
|
|
161
|
+
endpoint: a.endpoint,
|
|
162
|
+
services: a.services,
|
|
163
|
+
}));
|
|
151
164
|
}
|
|
152
165
|
/** Send an RFQ to one or more sellers and return the negotiation session.
|
|
153
166
|
* @param params - RFQ parameters including seller targets, service requirements, budget, and SLA
|
package/dist/index.d.ts
CHANGED
|
@@ -76,6 +76,20 @@ LockstepBehavioralRequirement,
|
|
|
76
76
|
LockstepMonitorConfig,
|
|
77
77
|
/** Result of an SLA compliance check (compliant flag and violation details). */
|
|
78
78
|
ComplianceResult, } from './lockstep.js';
|
|
79
|
+
/** Collect metric observations and evaluate SLA compliance locally. */
|
|
80
|
+
export { MetricCollector } from './metrics.js';
|
|
81
|
+
export type {
|
|
82
|
+
/** A single metric observation recorded by the buyer. */
|
|
83
|
+
MetricSample,
|
|
84
|
+
/** A detected SLA violation with evidence. */
|
|
85
|
+
Violation, } from './metrics.js';
|
|
86
|
+
/** Client for the Ophir Agent Registry and autonomous agent discovery. */
|
|
87
|
+
export { OphirRegistry, autoDiscover, BOOTSTRAP_REGISTRIES } from './registry.js';
|
|
88
|
+
export type {
|
|
89
|
+
/** A registered agent in the Ophir registry. */
|
|
90
|
+
RegisteredAgent,
|
|
91
|
+
/** Query parameters for finding agents in the registry. */
|
|
92
|
+
RegistryQuery, } from './registry.js';
|
|
79
93
|
/** Convert agreements to x402 HTTP payment headers and parse responses. */
|
|
80
94
|
export { agreementToX402Headers, parseX402Response } from './x402.js';
|
|
81
95
|
/** All protocol-level types re-exported from @ophirai/protocol for convenience. */
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,12 @@ export { discoverAgents, parseAgentCard } from './discovery.js';
|
|
|
38
38
|
// ── Lockstep verification ───────────────────────────────────────────
|
|
39
39
|
/** Convert agreements to Lockstep specs and monitor SLA compliance. */
|
|
40
40
|
export { agreementToLockstepSpec, LockstepMonitor } from './lockstep.js';
|
|
41
|
+
// ── Metric collection & Lockstep monitoring ────────────────────────
|
|
42
|
+
/** Collect metric observations and evaluate SLA compliance locally. */
|
|
43
|
+
export { MetricCollector } from './metrics.js';
|
|
44
|
+
// ── Agent registry & auto-discovery ────────────────────────────────
|
|
45
|
+
/** Client for the Ophir Agent Registry and autonomous agent discovery. */
|
|
46
|
+
export { OphirRegistry, autoDiscover, BOOTSTRAP_REGISTRIES } from './registry.js';
|
|
41
47
|
// ── x402 payment headers ───────────────────────────────────────────
|
|
42
48
|
/** Convert agreements to x402 HTTP payment headers and parse responses. */
|
|
43
49
|
export { agreementToX402Headers, parseX402Response } from './x402.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { LockstepVerificationSpec } from './sla.js';
|
|
2
|
+
/** A single metric observation recorded by the buyer. */
|
|
3
|
+
export interface MetricSample {
|
|
4
|
+
metric: string;
|
|
5
|
+
value: number;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
agreement_id: string;
|
|
8
|
+
measurement_window: string;
|
|
9
|
+
sample_count: number;
|
|
10
|
+
}
|
|
11
|
+
/** A detected SLA violation with evidence. */
|
|
12
|
+
export interface Violation {
|
|
13
|
+
violation_id: string;
|
|
14
|
+
agreement_id: string;
|
|
15
|
+
agreement_hash: string;
|
|
16
|
+
metric: string;
|
|
17
|
+
threshold: number;
|
|
18
|
+
observed: number;
|
|
19
|
+
operator: string;
|
|
20
|
+
measurement_window: string;
|
|
21
|
+
sample_count: number;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
consecutive_count: number;
|
|
24
|
+
samples: MetricSample[];
|
|
25
|
+
evidence_hash: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Collects metric observations and evaluates behavioral checks from a Lockstep spec.
|
|
29
|
+
* Runs locally — no external service required.
|
|
30
|
+
*/
|
|
31
|
+
export declare class MetricCollector {
|
|
32
|
+
private buffers;
|
|
33
|
+
private agreementId;
|
|
34
|
+
private agreementHash;
|
|
35
|
+
private retentionMs;
|
|
36
|
+
constructor(agreement: {
|
|
37
|
+
agreement_id: string;
|
|
38
|
+
agreement_hash: string;
|
|
39
|
+
}, retentionHours?: number);
|
|
40
|
+
/** Record a raw observation for a metric. */
|
|
41
|
+
record(metric: string, value: number): void;
|
|
42
|
+
/** Evict observations older than the retention window. */
|
|
43
|
+
private evict;
|
|
44
|
+
/** Compute the aggregate value for a metric over a measurement window. */
|
|
45
|
+
aggregate(metric: string, method: string, windowMs: number): {
|
|
46
|
+
value: number;
|
|
47
|
+
sampleCount: number;
|
|
48
|
+
} | null;
|
|
49
|
+
/** Evaluate all behavioral checks in a Lockstep spec and return violations. */
|
|
50
|
+
evaluate(spec: LockstepVerificationSpec): Violation[];
|
|
51
|
+
/** Get the number of observations recorded for a metric. */
|
|
52
|
+
getObservationCount(metric: string): number;
|
|
53
|
+
/** Clear all recorded observations. */
|
|
54
|
+
clear(): void;
|
|
55
|
+
}
|
package/dist/metrics.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects metric observations and evaluates behavioral checks from a Lockstep spec.
|
|
3
|
+
* Runs locally — no external service required.
|
|
4
|
+
*/
|
|
5
|
+
export class MetricCollector {
|
|
6
|
+
buffers = new Map();
|
|
7
|
+
agreementId;
|
|
8
|
+
agreementHash;
|
|
9
|
+
retentionMs;
|
|
10
|
+
constructor(agreement, retentionHours = 168) {
|
|
11
|
+
this.agreementId = agreement.agreement_id;
|
|
12
|
+
this.agreementHash = agreement.agreement_hash;
|
|
13
|
+
this.retentionMs = retentionHours * 3600_000;
|
|
14
|
+
}
|
|
15
|
+
/** Record a raw observation for a metric. */
|
|
16
|
+
record(metric, value) {
|
|
17
|
+
let buf = this.buffers.get(metric);
|
|
18
|
+
if (!buf) {
|
|
19
|
+
buf = { observations: [], consecutiveFailures: 0, lastViolationTime: 0 };
|
|
20
|
+
this.buffers.set(metric, buf);
|
|
21
|
+
}
|
|
22
|
+
buf.observations.push({ value, timestamp: Date.now() });
|
|
23
|
+
this.evict(buf);
|
|
24
|
+
}
|
|
25
|
+
/** Evict observations older than the retention window. */
|
|
26
|
+
evict(buf) {
|
|
27
|
+
const cutoff = Date.now() - this.retentionMs;
|
|
28
|
+
while (buf.observations.length > 0 && buf.observations[0].timestamp < cutoff) {
|
|
29
|
+
buf.observations.shift();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Compute the aggregate value for a metric over a measurement window. */
|
|
33
|
+
aggregate(metric, method, windowMs) {
|
|
34
|
+
const buf = this.buffers.get(metric);
|
|
35
|
+
if (!buf || buf.observations.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
const cutoff = Date.now() - windowMs;
|
|
38
|
+
const windowObs = buf.observations.filter((o) => o.timestamp >= cutoff);
|
|
39
|
+
if (windowObs.length === 0)
|
|
40
|
+
return null;
|
|
41
|
+
const values = windowObs.map((o) => o.value);
|
|
42
|
+
let value;
|
|
43
|
+
switch (method) {
|
|
44
|
+
case 'percentile':
|
|
45
|
+
// For p99, p50 etc — sort and pick percentile
|
|
46
|
+
values.sort((a, b) => a - b);
|
|
47
|
+
value = values[Math.floor(values.length * 0.99)] ?? values[values.length - 1];
|
|
48
|
+
break;
|
|
49
|
+
case 'count':
|
|
50
|
+
value = values.length;
|
|
51
|
+
break;
|
|
52
|
+
case 'rate':
|
|
53
|
+
value = values.reduce((s, v) => s + v, 0) / (windowMs / 60_000);
|
|
54
|
+
break;
|
|
55
|
+
case 'instant':
|
|
56
|
+
value = values[values.length - 1];
|
|
57
|
+
break;
|
|
58
|
+
case 'rolling_average':
|
|
59
|
+
default:
|
|
60
|
+
value = values.reduce((s, v) => s + v, 0) / values.length;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
return { value, sampleCount: windowObs.length };
|
|
64
|
+
}
|
|
65
|
+
/** Evaluate all behavioral checks in a Lockstep spec and return violations. */
|
|
66
|
+
evaluate(spec) {
|
|
67
|
+
const violations = [];
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
for (const check of spec.behavioral_checks) {
|
|
70
|
+
const windowMs = parseDuration(check.measurement_window);
|
|
71
|
+
const agg = this.aggregate(check.metric, check.measurement_method, windowMs);
|
|
72
|
+
const minSamples = check.min_samples ?? 10;
|
|
73
|
+
if (!agg || agg.sampleCount < minSamples) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
let failed = false;
|
|
77
|
+
switch (check.operator) {
|
|
78
|
+
case 'lte':
|
|
79
|
+
failed = agg.value > check.threshold;
|
|
80
|
+
break;
|
|
81
|
+
case 'gte':
|
|
82
|
+
failed = agg.value < check.threshold;
|
|
83
|
+
break;
|
|
84
|
+
case 'eq':
|
|
85
|
+
failed = agg.value !== check.threshold;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
const buf = this.buffers.get(check.metric);
|
|
89
|
+
if (!buf)
|
|
90
|
+
continue;
|
|
91
|
+
if (failed) {
|
|
92
|
+
buf.consecutiveFailures++;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
buf.consecutiveFailures = 0;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const policy = check.violation_policy;
|
|
99
|
+
const requiredFailures = policy?.consecutive_failures ?? 3;
|
|
100
|
+
const cooldownMs = (policy?.cooldown_seconds ?? 300) * 1000;
|
|
101
|
+
if (buf.consecutiveFailures < requiredFailures)
|
|
102
|
+
continue;
|
|
103
|
+
if (now - buf.lastViolationTime < cooldownMs)
|
|
104
|
+
continue;
|
|
105
|
+
buf.lastViolationTime = now;
|
|
106
|
+
const cutoff = now - windowMs;
|
|
107
|
+
const windowObs = buf.observations.filter((o) => o.timestamp >= cutoff);
|
|
108
|
+
const samples = windowObs.map((o) => ({
|
|
109
|
+
metric: check.metric,
|
|
110
|
+
value: o.value,
|
|
111
|
+
timestamp: new Date(o.timestamp).toISOString(),
|
|
112
|
+
agreement_id: this.agreementId,
|
|
113
|
+
measurement_window: check.measurement_window,
|
|
114
|
+
sample_count: 1,
|
|
115
|
+
}));
|
|
116
|
+
violations.push({
|
|
117
|
+
violation_id: crypto.randomUUID(),
|
|
118
|
+
agreement_id: this.agreementId,
|
|
119
|
+
agreement_hash: this.agreementHash,
|
|
120
|
+
metric: check.metric,
|
|
121
|
+
threshold: check.threshold,
|
|
122
|
+
observed: agg.value,
|
|
123
|
+
operator: check.operator,
|
|
124
|
+
measurement_window: check.measurement_window,
|
|
125
|
+
sample_count: agg.sampleCount,
|
|
126
|
+
timestamp: new Date(now).toISOString(),
|
|
127
|
+
consecutive_count: buf.consecutiveFailures,
|
|
128
|
+
samples,
|
|
129
|
+
evidence_hash: '', // Computed by caller via agreementHash(samples)
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return violations;
|
|
133
|
+
}
|
|
134
|
+
/** Get the number of observations recorded for a metric. */
|
|
135
|
+
getObservationCount(metric) {
|
|
136
|
+
return this.buffers.get(metric)?.observations.length ?? 0;
|
|
137
|
+
}
|
|
138
|
+
/** Clear all recorded observations. */
|
|
139
|
+
clear() {
|
|
140
|
+
this.buffers.clear();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Parse an ISO 8601 duration string to milliseconds. Supports PT{n}H, PT{n}M, PT{n}S, P{n}D. */
|
|
144
|
+
function parseDuration(iso) {
|
|
145
|
+
const match = iso.match(/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/);
|
|
146
|
+
if (!match)
|
|
147
|
+
return 3600_000; // default 1 hour
|
|
148
|
+
const days = parseInt(match[1] || '0', 10);
|
|
149
|
+
const hours = parseInt(match[2] || '0', 10);
|
|
150
|
+
const minutes = parseInt(match[3] || '0', 10);
|
|
151
|
+
const seconds = parseInt(match[4] || '0', 10);
|
|
152
|
+
return ((days * 24 + hours) * 3600 + minutes * 60 + seconds) * 1000;
|
|
153
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ServiceOffering } from './types.js';
|
|
2
|
+
import type { AgentCard, NegotiationCapability } from './discovery.js';
|
|
3
|
+
/** A registered agent in the Ophir registry. */
|
|
4
|
+
export interface RegisteredAgent {
|
|
5
|
+
agentId: string;
|
|
6
|
+
endpoint: string;
|
|
7
|
+
services: ServiceOffering[];
|
|
8
|
+
capabilities: NegotiationCapability;
|
|
9
|
+
registeredAt: string;
|
|
10
|
+
lastHeartbeat: string;
|
|
11
|
+
reputation?: {
|
|
12
|
+
score: number;
|
|
13
|
+
total_agreements: number;
|
|
14
|
+
disputes_won: number;
|
|
15
|
+
disputes_lost: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Query parameters for finding agents in the registry. */
|
|
19
|
+
export interface RegistryQuery {
|
|
20
|
+
category?: string;
|
|
21
|
+
maxPrice?: string;
|
|
22
|
+
currency?: string;
|
|
23
|
+
minReputation?: number;
|
|
24
|
+
limit?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Client for the Ophir Agent Registry — a lightweight discovery service
|
|
28
|
+
* where agents register their services and buyers find sellers.
|
|
29
|
+
*
|
|
30
|
+
* The registry is optional. Agents can also discover each other via
|
|
31
|
+
* A2A Agent Cards at /.well-known/agent.json, or via direct endpoints.
|
|
32
|
+
*
|
|
33
|
+
* Supports multiple registry endpoints for redundancy.
|
|
34
|
+
*/
|
|
35
|
+
export declare class OphirRegistry {
|
|
36
|
+
private endpoints;
|
|
37
|
+
private agentId?;
|
|
38
|
+
private signature?;
|
|
39
|
+
constructor(endpoints?: string[]);
|
|
40
|
+
/** Authenticate with the registry using a did:key and signed challenge. */
|
|
41
|
+
authenticate(agentId: string, signature: string): void;
|
|
42
|
+
/** Register this agent's services with the registry. */
|
|
43
|
+
register(card: AgentCard): Promise<{
|
|
44
|
+
success: boolean;
|
|
45
|
+
agentId: string;
|
|
46
|
+
}>;
|
|
47
|
+
/** Send a heartbeat to keep the registration alive. */
|
|
48
|
+
heartbeat(agentId: string): Promise<boolean>;
|
|
49
|
+
/** Find agents matching a query. */
|
|
50
|
+
find(query: RegistryQuery): Promise<RegisteredAgent[]>;
|
|
51
|
+
/** Unregister this agent from the registry. */
|
|
52
|
+
unregister(agentId: string): Promise<boolean>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Well-known bootstrap endpoints for Ophir agent discovery.
|
|
56
|
+
* These can be embedded in agent frameworks so that any agent
|
|
57
|
+
* with the Ophir SDK can automatically find and negotiate with
|
|
58
|
+
* service providers without prior configuration.
|
|
59
|
+
*/
|
|
60
|
+
export declare const BOOTSTRAP_REGISTRIES: readonly ["https://registry.ophir.ai/v1"];
|
|
61
|
+
/**
|
|
62
|
+
* Auto-discover Ophir sellers for a service category.
|
|
63
|
+
* Tries the registry first, falls back to A2A discovery via known endpoints.
|
|
64
|
+
*/
|
|
65
|
+
export declare function autoDiscover(category: string, options?: {
|
|
66
|
+
registries?: string[];
|
|
67
|
+
fallbackEndpoints?: string[];
|
|
68
|
+
maxResults?: number;
|
|
69
|
+
}): Promise<RegisteredAgent[]>;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the Ophir Agent Registry — a lightweight discovery service
|
|
3
|
+
* where agents register their services and buyers find sellers.
|
|
4
|
+
*
|
|
5
|
+
* The registry is optional. Agents can also discover each other via
|
|
6
|
+
* A2A Agent Cards at /.well-known/agent.json, or via direct endpoints.
|
|
7
|
+
*
|
|
8
|
+
* Supports multiple registry endpoints for redundancy.
|
|
9
|
+
*/
|
|
10
|
+
export class OphirRegistry {
|
|
11
|
+
endpoints;
|
|
12
|
+
agentId;
|
|
13
|
+
signature;
|
|
14
|
+
constructor(endpoints) {
|
|
15
|
+
this.endpoints = endpoints ?? ['https://registry.ophir.ai/v1'];
|
|
16
|
+
}
|
|
17
|
+
/** Authenticate with the registry using a did:key and signed challenge. */
|
|
18
|
+
authenticate(agentId, signature) {
|
|
19
|
+
this.agentId = agentId;
|
|
20
|
+
this.signature = signature;
|
|
21
|
+
}
|
|
22
|
+
/** Register this agent's services with the registry. */
|
|
23
|
+
async register(card) {
|
|
24
|
+
for (const endpoint of this.endpoints) {
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(`${endpoint}/agents`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
...(this.agentId ? { 'X-Agent-Id': this.agentId } : {}),
|
|
31
|
+
...(this.signature ? { 'X-Agent-Signature': this.signature } : {}),
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify(card),
|
|
34
|
+
});
|
|
35
|
+
if (res.ok) {
|
|
36
|
+
const data = await res.json();
|
|
37
|
+
return { success: true, agentId: data.agent_id };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { success: false, agentId: '' };
|
|
45
|
+
}
|
|
46
|
+
/** Send a heartbeat to keep the registration alive. */
|
|
47
|
+
async heartbeat(agentId) {
|
|
48
|
+
for (const endpoint of this.endpoints) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(`${endpoint}/agents/${encodeURIComponent(agentId)}/heartbeat`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
...(this.agentId ? { 'X-Agent-Id': this.agentId } : {}),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
if (res.ok)
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/** Find agents matching a query. */
|
|
66
|
+
async find(query) {
|
|
67
|
+
const params = new URLSearchParams();
|
|
68
|
+
if (query.category)
|
|
69
|
+
params.set('category', query.category);
|
|
70
|
+
if (query.maxPrice)
|
|
71
|
+
params.set('max_price', query.maxPrice);
|
|
72
|
+
if (query.currency)
|
|
73
|
+
params.set('currency', query.currency);
|
|
74
|
+
if (query.minReputation !== undefined)
|
|
75
|
+
params.set('min_reputation', String(query.minReputation));
|
|
76
|
+
if (query.limit !== undefined)
|
|
77
|
+
params.set('limit', String(query.limit));
|
|
78
|
+
for (const endpoint of this.endpoints) {
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(`${endpoint}/agents?${params.toString()}`);
|
|
81
|
+
if (res.ok) {
|
|
82
|
+
const data = await res.json();
|
|
83
|
+
return data.agents;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
/** Unregister this agent from the registry. */
|
|
93
|
+
async unregister(agentId) {
|
|
94
|
+
for (const endpoint of this.endpoints) {
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(`${endpoint}/agents/${encodeURIComponent(agentId)}`, {
|
|
97
|
+
method: 'DELETE',
|
|
98
|
+
headers: {
|
|
99
|
+
...(this.agentId ? { 'X-Agent-Id': this.agentId } : {}),
|
|
100
|
+
...(this.signature ? { 'X-Agent-Signature': this.signature } : {}),
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
if (res.ok)
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Well-known bootstrap endpoints for Ophir agent discovery.
|
|
115
|
+
* These can be embedded in agent frameworks so that any agent
|
|
116
|
+
* with the Ophir SDK can automatically find and negotiate with
|
|
117
|
+
* service providers without prior configuration.
|
|
118
|
+
*/
|
|
119
|
+
export const BOOTSTRAP_REGISTRIES = [
|
|
120
|
+
'https://registry.ophir.ai/v1',
|
|
121
|
+
];
|
|
122
|
+
/**
|
|
123
|
+
* Auto-discover Ophir sellers for a service category.
|
|
124
|
+
* Tries the registry first, falls back to A2A discovery via known endpoints.
|
|
125
|
+
*/
|
|
126
|
+
export async function autoDiscover(category, options) {
|
|
127
|
+
const registry = new OphirRegistry(options?.registries);
|
|
128
|
+
const results = await registry.find({
|
|
129
|
+
category,
|
|
130
|
+
limit: options?.maxResults ?? 10,
|
|
131
|
+
});
|
|
132
|
+
if (results.length > 0)
|
|
133
|
+
return results;
|
|
134
|
+
// Fallback: A2A discovery via known endpoints
|
|
135
|
+
if (options?.fallbackEndpoints) {
|
|
136
|
+
const { discoverAgents, parseAgentCard } = await import('./discovery.js');
|
|
137
|
+
const cards = await discoverAgents(options.fallbackEndpoints);
|
|
138
|
+
return cards
|
|
139
|
+
.map((card) => {
|
|
140
|
+
const neg = card.capabilities?.negotiation;
|
|
141
|
+
if (!neg?.supported)
|
|
142
|
+
return null;
|
|
143
|
+
const services = (neg.services ?? [])
|
|
144
|
+
.filter((s) => !category || s.category === category)
|
|
145
|
+
.map((s) => ({
|
|
146
|
+
category: s.category,
|
|
147
|
+
description: s.description,
|
|
148
|
+
base_price: s.base_price,
|
|
149
|
+
currency: s.currency,
|
|
150
|
+
unit: s.unit,
|
|
151
|
+
}));
|
|
152
|
+
if (services.length === 0)
|
|
153
|
+
return null;
|
|
154
|
+
return {
|
|
155
|
+
agentId: card.url,
|
|
156
|
+
endpoint: neg.endpoint,
|
|
157
|
+
services,
|
|
158
|
+
capabilities: neg,
|
|
159
|
+
registeredAt: new Date().toISOString(),
|
|
160
|
+
lastHeartbeat: new Date().toISOString(),
|
|
161
|
+
};
|
|
162
|
+
})
|
|
163
|
+
.filter((a) => a !== null);
|
|
164
|
+
}
|
|
165
|
+
return [];
|
|
166
|
+
}
|
package/package.json
CHANGED