@rovn-ai/agent 1.1.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/README.md +495 -0
- package/dist/index.d.ts +303 -0
- package/dist/index.js +586 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# @rovn-platform/sdk
|
|
2
|
+
|
|
3
|
+
**AI Agent Governance SDK** -- manage, monitor, and govern your AI agents.
|
|
4
|
+
|
|
5
|
+
The official TypeScript/JavaScript SDK for [Rovn](https://rovn.io), the AI Agent Command Center. Zero runtime dependencies -- uses only the Fetch API.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @rovn-platform/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Works with Node.js 18+ (Fetch API required), Deno, Bun, and modern browsers.
|
|
14
|
+
|
|
15
|
+
## Quick Start (5 minutes)
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { RovnAgent, RovnError } from '@rovn-platform/sdk';
|
|
19
|
+
|
|
20
|
+
// 1. Register your agent
|
|
21
|
+
const { agent, id, apiKey } = await RovnAgent.register(
|
|
22
|
+
'https://your-rovn-instance.com',
|
|
23
|
+
{
|
|
24
|
+
name: 'my-data-pipeline',
|
|
25
|
+
description: 'Processes and analyzes customer data',
|
|
26
|
+
type: 'data_pipeline',
|
|
27
|
+
capabilities: ['read_database', 'write_reports', 'send_email'],
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
// Save apiKey -- you will need it to reconnect later
|
|
31
|
+
|
|
32
|
+
// 2. Send an activity
|
|
33
|
+
await agent.logActivity('Processed 1,200 records', {
|
|
34
|
+
type: 'data_processing',
|
|
35
|
+
metadata: { records: 1200, duration_ms: 4500 },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// 3. Check if an action is allowed (pre-flight)
|
|
39
|
+
const check = await agent.checkAction('send_email', {
|
|
40
|
+
urgency: 'high',
|
|
41
|
+
cost: 0.05,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (check.allowed) {
|
|
45
|
+
console.log('Go ahead!');
|
|
46
|
+
} else if (check.needs_approval) {
|
|
47
|
+
// 4. Request approval from the owner
|
|
48
|
+
const approvalId = await agent.requestApproval({
|
|
49
|
+
type: 'action',
|
|
50
|
+
title: 'Send 500 marketing emails',
|
|
51
|
+
description: 'Campaign targeting new signups from last week',
|
|
52
|
+
urgency: 'high',
|
|
53
|
+
});
|
|
54
|
+
console.log(`Waiting for approval: ${approvalId}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 5. Get your report card
|
|
58
|
+
const report = await agent.getReportCard({ days: 7 });
|
|
59
|
+
console.log('Trust:', report.trust);
|
|
60
|
+
console.log('Recommendations:', report.recommendations);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## All Available Methods
|
|
64
|
+
|
|
65
|
+
### Registration & Info
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Register a new agent (static method, no API key needed)
|
|
69
|
+
const { agent, id, apiKey } = await RovnAgent.register(
|
|
70
|
+
baseUrl: string,
|
|
71
|
+
options: {
|
|
72
|
+
name: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
type?: string;
|
|
75
|
+
capabilities?: string[];
|
|
76
|
+
owner_email?: string;
|
|
77
|
+
metadata?: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Connect with an existing API key
|
|
82
|
+
const agent = new RovnAgent({
|
|
83
|
+
baseUrl: 'https://...',
|
|
84
|
+
apiKey: 'rovn_...',
|
|
85
|
+
fireAndForget: false, // optional, default false
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Get agent info (auto-discovers agentId on first call)
|
|
89
|
+
const info: AgentInfo = await agent.getInfo();
|
|
90
|
+
// info.id, info.name, info.description, info.status, info.type,
|
|
91
|
+
// info.approved, info.capabilities, info.metadata, info.created_at,
|
|
92
|
+
// info.updated_at, info.last_seen_at
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Events & Activities
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// Log an activity
|
|
99
|
+
await agent.logActivity(
|
|
100
|
+
title: string,
|
|
101
|
+
options?: { type?: string; description?: string; metadata?: Record<string, unknown> }
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Update agent status
|
|
105
|
+
await agent.updateStatus(status); // 'active' | 'idle' | 'busy' | 'offline' | 'error'
|
|
106
|
+
|
|
107
|
+
// Share structured data with the owner
|
|
108
|
+
await agent.shareData(title: string, content: Record<string, unknown>, type?: string);
|
|
109
|
+
|
|
110
|
+
// Send a raw event (low-level)
|
|
111
|
+
await agent.sendEvent(event: WebhookEvent, data: Record<string, unknown>);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Tasks
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Get assigned tasks
|
|
118
|
+
const tasks: Task[] = await agent.getTasks({ status?: string, limit?: number });
|
|
119
|
+
// Each task has: id, agent_id, owner_id, title, description, status,
|
|
120
|
+
// priority, result, scheduled_at, started_at, completed_at
|
|
121
|
+
|
|
122
|
+
// Update a task's status
|
|
123
|
+
await agent.updateTaskStatus(taskId: string, status: string, result?: Record<string, unknown>);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Messages & Chat
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// Send a message to the owner
|
|
130
|
+
await agent.sendMessage(
|
|
131
|
+
content: string,
|
|
132
|
+
options?: { message_type?: string; metadata?: Record<string, unknown> }
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Respond to an owner command
|
|
136
|
+
await agent.respondToCommand(
|
|
137
|
+
commandId: string, status: string, response?: Record<string, unknown>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Send a message to another agent
|
|
141
|
+
await agent.sendPeerMessage(
|
|
142
|
+
toAgentId: string,
|
|
143
|
+
content: string,
|
|
144
|
+
options?: { message_type?: string; metadata?: Record<string, unknown> }
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Get peer messages
|
|
148
|
+
const messages: PeerMessage[] = await agent.getPeerMessages({
|
|
149
|
+
direction?: 'inbox' | 'outbox' | 'all',
|
|
150
|
+
limit?: number,
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Approvals
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// Request approval from the owner
|
|
158
|
+
const approvalId: string | undefined = await agent.requestApproval({
|
|
159
|
+
type: string,
|
|
160
|
+
title: string,
|
|
161
|
+
description?: string,
|
|
162
|
+
urgency?: 'low' | 'medium' | 'high' | 'critical',
|
|
163
|
+
metadata?: Record<string, unknown>,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Poll all approvals
|
|
167
|
+
const approvals: ApprovalRequest[] = await agent.getApprovals({
|
|
168
|
+
status?: string,
|
|
169
|
+
limit?: number,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Poll a specific approval by ID
|
|
173
|
+
const approval: ApprovalRequest = await agent.pollApproval(approvalId: string);
|
|
174
|
+
// approval.id, approval.status, approval.decided_at, approval.decision_note
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Guardrails & Constraints
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Get all guardrails set by the owner
|
|
181
|
+
const guardrails: Guardrail[] = await agent.getGuardrails();
|
|
182
|
+
// Each guardrail: id, metric, limit_value, current_value, window, action, enabled
|
|
183
|
+
|
|
184
|
+
// Check remaining budget for a specific metric (cached for 60s)
|
|
185
|
+
const remaining: number | null = await agent.getGuardrailRemaining(metric: string);
|
|
186
|
+
|
|
187
|
+
// Clear the guardrail cache to force a fresh fetch
|
|
188
|
+
agent.invalidateGuardrailCache();
|
|
189
|
+
|
|
190
|
+
// Declare self-constraints before starting a task
|
|
191
|
+
const constraint: Constraint = await agent.declareConstraint(
|
|
192
|
+
task: string,
|
|
193
|
+
constraints: Record<string, unknown>, // e.g. { max_api_calls: 100, max_cost_usd: 5.0 }
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Update actual usage against a declared constraint
|
|
197
|
+
const result = await agent.updateConstraint(
|
|
198
|
+
constraintId: string,
|
|
199
|
+
actualUsage: Record<string, unknown>, // e.g. { api_calls: 45, cost_usd: 2.10 }
|
|
200
|
+
completed: boolean = false,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Get all constraints for this agent
|
|
204
|
+
const constraints: Constraint[] = await agent.getConstraints();
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Trust Score
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
const trust: TrustScoreResult = await agent.getTrustScore();
|
|
211
|
+
// trust.score (0-100), trust.grade ('A' through 'F'), trust.breakdown, trust.computed_at
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Pre-flight Check
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// "Can I do this?" -- checks policies, guardrails, and earned autonomy
|
|
218
|
+
const check: CheckResult = await agent.checkAction(
|
|
219
|
+
action: string,
|
|
220
|
+
options?: {
|
|
221
|
+
urgency?: string;
|
|
222
|
+
cost?: number;
|
|
223
|
+
data_fields?: string[];
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
// check.allowed -- true if the action can proceed
|
|
227
|
+
// check.needs_approval -- true if the action requires owner approval
|
|
228
|
+
// check.would_auto_approve -- true if earned autonomy would auto-approve
|
|
229
|
+
// check.checks -- array of individual check results
|
|
230
|
+
// check.summary -- human-readable summary
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Report Card
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const report: ReportCard = await agent.getReportCard({ days?: number });
|
|
237
|
+
// report.agent -- { id, name }
|
|
238
|
+
// report.period -- time period string
|
|
239
|
+
// report.productivity -- productivity metrics
|
|
240
|
+
// report.reliability -- reliability metrics
|
|
241
|
+
// report.compliance -- compliance metrics
|
|
242
|
+
// report.trust -- trust metrics
|
|
243
|
+
// report.recommendations -- array of improvement suggestions
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### SSE Connection (Real-Time)
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
agent.connect(
|
|
250
|
+
handler: (event: SSEEventType, data: Record<string, unknown>) => void,
|
|
251
|
+
options?: {
|
|
252
|
+
agentId?: string; // override auto-discovered ID
|
|
253
|
+
onConnect?: () => void;
|
|
254
|
+
onDisconnect?: () => void;
|
|
255
|
+
reconnect?: boolean; // default true, auto-reconnect on disconnect
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Example handler
|
|
260
|
+
agent.connect((event, data) => {
|
|
261
|
+
// event is one of: 'connected', 'command', 'approval_response',
|
|
262
|
+
// 'interrupt', 'agent_updated', 'peer_message'
|
|
263
|
+
switch (event) {
|
|
264
|
+
case 'command':
|
|
265
|
+
console.log('Owner command:', data);
|
|
266
|
+
break;
|
|
267
|
+
case 'approval_response':
|
|
268
|
+
console.log(`Approval ${data.id}: ${data.status}`);
|
|
269
|
+
break;
|
|
270
|
+
case 'interrupt':
|
|
271
|
+
agent.disconnect();
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Stop listening
|
|
277
|
+
agent.disconnect();
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Flush & Close
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// Flush all queued events (blocks until drained)
|
|
284
|
+
await agent.flush();
|
|
285
|
+
|
|
286
|
+
// Graceful shutdown: disconnect SSE + flush pending events
|
|
287
|
+
await agent.close();
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Advanced Features
|
|
291
|
+
|
|
292
|
+
### Fire-and-Forget Mode
|
|
293
|
+
|
|
294
|
+
Events are queued in memory and sent asynchronously with automatic retry. Your code never awaits HTTP calls for event delivery.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
const agent = new RovnAgent({
|
|
298
|
+
baseUrl: 'https://...',
|
|
299
|
+
apiKey: 'rovn_...',
|
|
300
|
+
fireAndForget: true,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await agent.logActivity('Instant return, sent in background');
|
|
304
|
+
await agent.sendMessage('Also queued');
|
|
305
|
+
await agent.updateStatus('busy');
|
|
306
|
+
|
|
307
|
+
// Flush before shutdown to guarantee delivery
|
|
308
|
+
await agent.flush();
|
|
309
|
+
|
|
310
|
+
// Or use close() for full graceful shutdown
|
|
311
|
+
await agent.close();
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Even in synchronous mode, transient failures (5xx, network errors) are automatically queued for retry instead of throwing.
|
|
315
|
+
|
|
316
|
+
### Error Handling
|
|
317
|
+
|
|
318
|
+
All API errors throw `RovnError` with structured information.
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { RovnError } from '@rovn-platform/sdk';
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
await agent.logActivity('test');
|
|
325
|
+
} catch (err) {
|
|
326
|
+
if (err instanceof RovnError) {
|
|
327
|
+
console.error(err.message); // human-readable message
|
|
328
|
+
console.error(err.statusCode); // HTTP status code (0 for network errors)
|
|
329
|
+
console.error(err.errorCode); // server error code (e.g. 'rate_limited')
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Common error scenarios:
|
|
335
|
+
- **`statusCode === 0`** -- Network error (connection refused, DNS failure)
|
|
336
|
+
- **`statusCode === 401`** -- Invalid or expired API key
|
|
337
|
+
- **`statusCode === 429`** -- Rate limited (auto-retried)
|
|
338
|
+
- **`statusCode >= 500`** -- Server error (auto-retried)
|
|
339
|
+
- **`errorCode === 'AGENT_ID_MISSING'`** -- Call `getInfo()` or `register()` first
|
|
340
|
+
|
|
341
|
+
### Retry Behavior
|
|
342
|
+
|
|
343
|
+
The SDK automatically retries on:
|
|
344
|
+
- Network errors (fetch failures, connection reset)
|
|
345
|
+
- HTTP 429 (rate limit)
|
|
346
|
+
- HTTP 5xx (server errors)
|
|
347
|
+
|
|
348
|
+
Retry uses exponential backoff: 1s, 2s, 4s, 8s, ... up to 30s max. Non-retryable errors (4xx except 429) cause the event to be dropped from the queue.
|
|
349
|
+
|
|
350
|
+
In fire-and-forget mode, failed events stay at the front of the queue and are retried with backoff. In synchronous mode, retryable failures are automatically queued for background retry instead of throwing.
|
|
351
|
+
|
|
352
|
+
The event queue holds up to 10,000 events. When full, the oldest event is dropped to make room.
|
|
353
|
+
|
|
354
|
+
### Guardrail Cache
|
|
355
|
+
|
|
356
|
+
`getGuardrailRemaining()` caches the guardrail list for 60 seconds to avoid excessive API calls. The cache is automatically refreshed when stale.
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
// First call fetches from API
|
|
360
|
+
const remaining = await agent.getGuardrailRemaining('api_calls'); // -> 950
|
|
361
|
+
|
|
362
|
+
// Subsequent calls within 60s use the cache
|
|
363
|
+
const remaining2 = await agent.getGuardrailRemaining('api_calls'); // -> 950 (cached)
|
|
364
|
+
|
|
365
|
+
// Force a fresh fetch
|
|
366
|
+
agent.invalidateGuardrailCache();
|
|
367
|
+
const remaining3 = await agent.getGuardrailRemaining('api_calls'); // -> 947 (fresh)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Complete Workflow Example
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
import { RovnAgent, RovnError } from '@rovn-platform/sdk';
|
|
374
|
+
|
|
375
|
+
const ROVN_URL = 'https://your-rovn-instance.com';
|
|
376
|
+
const API_KEY = 'rovn_abc123...';
|
|
377
|
+
|
|
378
|
+
async function main() {
|
|
379
|
+
// Connect to Rovn
|
|
380
|
+
const agent = new RovnAgent({ baseUrl: ROVN_URL, apiKey: API_KEY });
|
|
381
|
+
const info = await agent.getInfo();
|
|
382
|
+
console.log(`Agent: ${info.name} (status: ${info.status})`);
|
|
383
|
+
|
|
384
|
+
// Check trust score
|
|
385
|
+
const trust = await agent.getTrustScore();
|
|
386
|
+
console.log(`Trust: ${trust.grade} (${trust.score}/100)`);
|
|
387
|
+
|
|
388
|
+
// Check if we're allowed to send emails
|
|
389
|
+
const check = await agent.checkAction('send_email', { cost: 2.5 });
|
|
390
|
+
|
|
391
|
+
if (!check.allowed) {
|
|
392
|
+
if (check.needs_approval) {
|
|
393
|
+
// Request approval and wait
|
|
394
|
+
const approvalId = await agent.requestApproval({
|
|
395
|
+
type: 'action',
|
|
396
|
+
title: 'Send weekly report emails',
|
|
397
|
+
description: '150 emails to subscribers, estimated cost $2.50',
|
|
398
|
+
urgency: 'medium',
|
|
399
|
+
});
|
|
400
|
+
console.log(`Approval requested: ${approvalId}`);
|
|
401
|
+
|
|
402
|
+
// Poll until decided
|
|
403
|
+
let approval;
|
|
404
|
+
do {
|
|
405
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
406
|
+
approval = await agent.pollApproval(approvalId!);
|
|
407
|
+
} while (approval.status === 'pending');
|
|
408
|
+
|
|
409
|
+
if (approval.status !== 'approved') {
|
|
410
|
+
console.log(`Denied: ${approval.decision_note}`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
console.log(`Blocked: ${check.summary}`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Declare constraints for the task
|
|
420
|
+
const constraint = await agent.declareConstraint(
|
|
421
|
+
'send_weekly_emails',
|
|
422
|
+
{ max_emails: 200, max_cost_usd: 5.0 },
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// Do the work
|
|
426
|
+
await agent.updateStatus('busy');
|
|
427
|
+
await agent.logActivity('Sending weekly report emails', { type: 'email_campaign' });
|
|
428
|
+
|
|
429
|
+
const emailsSent = 150;
|
|
430
|
+
const cost = 2.35;
|
|
431
|
+
|
|
432
|
+
// Report actual usage
|
|
433
|
+
await agent.updateConstraint(
|
|
434
|
+
constraint.id,
|
|
435
|
+
{ emails: emailsSent, cost_usd: cost },
|
|
436
|
+
true, // completed
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
// Notify owner
|
|
440
|
+
await agent.sendMessage(
|
|
441
|
+
`Weekly emails sent: ${emailsSent} emails, $${cost.toFixed(2)}`,
|
|
442
|
+
{ metadata: { emails_sent: emailsSent, cost_usd: cost } },
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
await agent.updateStatus('idle');
|
|
446
|
+
|
|
447
|
+
// Check report card
|
|
448
|
+
const report = await agent.getReportCard({ days: 7 });
|
|
449
|
+
console.log('Recommendations:', report.recommendations);
|
|
450
|
+
|
|
451
|
+
// Graceful shutdown
|
|
452
|
+
await agent.close();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
main().catch(console.error);
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Exported Types
|
|
459
|
+
|
|
460
|
+
All types are importable from the package:
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
import {
|
|
464
|
+
RovnAgent, // Client class
|
|
465
|
+
RovnError, // Error class
|
|
466
|
+
type RovnConfig, // Constructor config
|
|
467
|
+
type AgentInfo, // Agent profile
|
|
468
|
+
type Task, // Assigned task
|
|
469
|
+
type PeerMessage, // Inter-agent message
|
|
470
|
+
type Guardrail, // Usage limit
|
|
471
|
+
type Constraint, // Self-constraint declaration
|
|
472
|
+
type ApprovalRequest, // Approval request/response
|
|
473
|
+
type TrustScoreResult, // Trust score result
|
|
474
|
+
type CheckResult, // Pre-flight check result
|
|
475
|
+
type ReportCard, // Performance report card
|
|
476
|
+
type AgentStatus, // 'active' | 'idle' | 'busy' | 'offline' | 'error'
|
|
477
|
+
type TaskStatus, // 'pending' | 'in_progress' | 'completed' | 'cancelled' | 'failed'
|
|
478
|
+
type TaskPriority, // 'low' | 'medium' | 'high' | 'urgent'
|
|
479
|
+
type WebhookEvent, // Event type union
|
|
480
|
+
type SSEEventType, // SSE event type union
|
|
481
|
+
type SSEHandler, // SSE handler function type
|
|
482
|
+
type GuardrailWindow, // 'hourly' | 'daily' | 'weekly' | 'monthly'
|
|
483
|
+
type GuardrailAction, // 'warn' | 'block' | 'approval_required'
|
|
484
|
+
} from '@rovn-platform/sdk';
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## Requirements
|
|
488
|
+
|
|
489
|
+
- Node.js 18+ (Fetch API), Deno, Bun, or modern browsers
|
|
490
|
+
- TypeScript 5+ (for type definitions)
|
|
491
|
+
- Zero runtime dependencies
|
|
492
|
+
|
|
493
|
+
## License
|
|
494
|
+
|
|
495
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
export type RovnConfig = {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** When true, sendEvent queues events and returns immediately without awaiting the HTTP call. */
|
|
5
|
+
fireAndForget?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type WebhookEvent = 'activity' | 'task_update' | 'message' | 'status' | 'share_data' | 'command_response' | 'approval_request' | 'peer_message';
|
|
8
|
+
export type SSEEventType = 'connected' | 'command' | 'approval_response' | 'interrupt' | 'agent_updated' | 'peer_message';
|
|
9
|
+
export type SSEHandler = (event: SSEEventType, data: Record<string, unknown>) => void;
|
|
10
|
+
export type AgentStatus = 'active' | 'idle' | 'busy' | 'offline' | 'error';
|
|
11
|
+
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled' | 'failed';
|
|
12
|
+
export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
|
|
13
|
+
export type GuardrailWindow = 'hourly' | 'daily' | 'weekly' | 'monthly';
|
|
14
|
+
export type GuardrailAction = 'warn' | 'block' | 'approval_required';
|
|
15
|
+
export interface AgentInfo {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description: string | null;
|
|
19
|
+
status: AgentStatus;
|
|
20
|
+
type: string;
|
|
21
|
+
approved: boolean;
|
|
22
|
+
capabilities: string[] | null;
|
|
23
|
+
metadata: Record<string, unknown> | null;
|
|
24
|
+
created_at: string;
|
|
25
|
+
updated_at: string;
|
|
26
|
+
last_seen_at: string | null;
|
|
27
|
+
}
|
|
28
|
+
export interface Task {
|
|
29
|
+
id: string;
|
|
30
|
+
agent_id: string;
|
|
31
|
+
owner_id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
description: string | null;
|
|
34
|
+
status: TaskStatus;
|
|
35
|
+
priority: TaskPriority;
|
|
36
|
+
result: Record<string, unknown> | null;
|
|
37
|
+
scheduled_at: string | null;
|
|
38
|
+
started_at: string | null;
|
|
39
|
+
completed_at: string | null;
|
|
40
|
+
created_at: string;
|
|
41
|
+
updated_at: string;
|
|
42
|
+
}
|
|
43
|
+
export interface PeerMessage {
|
|
44
|
+
id: string;
|
|
45
|
+
from_agent_id: string;
|
|
46
|
+
to_agent_id: string;
|
|
47
|
+
content: string;
|
|
48
|
+
message_type: string;
|
|
49
|
+
metadata: Record<string, unknown> | null;
|
|
50
|
+
read_at: string | null;
|
|
51
|
+
created_at: string;
|
|
52
|
+
from_agent_name?: string;
|
|
53
|
+
to_agent_name?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface Guardrail {
|
|
56
|
+
id: string;
|
|
57
|
+
agent_id: string;
|
|
58
|
+
owner_id: string;
|
|
59
|
+
metric: string;
|
|
60
|
+
limit_value: number;
|
|
61
|
+
current_value: number;
|
|
62
|
+
window: GuardrailWindow;
|
|
63
|
+
action: GuardrailAction;
|
|
64
|
+
enabled: boolean;
|
|
65
|
+
created_at: string;
|
|
66
|
+
updated_at: string;
|
|
67
|
+
}
|
|
68
|
+
export interface Constraint {
|
|
69
|
+
id: string;
|
|
70
|
+
agent_id: string;
|
|
71
|
+
task: string;
|
|
72
|
+
constraints: Record<string, unknown>;
|
|
73
|
+
actual_usage: Record<string, unknown> | null;
|
|
74
|
+
compliance: string;
|
|
75
|
+
started_at: string;
|
|
76
|
+
completed_at: string | null;
|
|
77
|
+
}
|
|
78
|
+
export interface ApprovalRequest {
|
|
79
|
+
id: string;
|
|
80
|
+
agent_id: string;
|
|
81
|
+
type: string;
|
|
82
|
+
title: string;
|
|
83
|
+
status: string;
|
|
84
|
+
urgency: string;
|
|
85
|
+
description: string | null;
|
|
86
|
+
decided_at: string | null;
|
|
87
|
+
decided_by: string | null;
|
|
88
|
+
decision_note: string | null;
|
|
89
|
+
created_at: string;
|
|
90
|
+
}
|
|
91
|
+
export interface TrustScoreResult {
|
|
92
|
+
agent_id: string;
|
|
93
|
+
score: number;
|
|
94
|
+
grade: string;
|
|
95
|
+
breakdown: Record<string, unknown>;
|
|
96
|
+
computed_at: string;
|
|
97
|
+
}
|
|
98
|
+
export interface CheckResult {
|
|
99
|
+
action: string;
|
|
100
|
+
allowed: boolean;
|
|
101
|
+
needs_approval: boolean;
|
|
102
|
+
would_auto_approve: boolean;
|
|
103
|
+
checks: Array<{
|
|
104
|
+
check: string;
|
|
105
|
+
passed: boolean;
|
|
106
|
+
detail: string;
|
|
107
|
+
}>;
|
|
108
|
+
summary: string;
|
|
109
|
+
}
|
|
110
|
+
export interface ReportCard {
|
|
111
|
+
agent: {
|
|
112
|
+
id: string;
|
|
113
|
+
name: string;
|
|
114
|
+
};
|
|
115
|
+
period: string;
|
|
116
|
+
productivity: Record<string, unknown>;
|
|
117
|
+
reliability: Record<string, unknown>;
|
|
118
|
+
compliance: Record<string, unknown>;
|
|
119
|
+
trust: Record<string, unknown>;
|
|
120
|
+
recommendations: string[];
|
|
121
|
+
}
|
|
122
|
+
export declare class RovnError extends Error {
|
|
123
|
+
readonly statusCode: number;
|
|
124
|
+
readonly errorCode: string | undefined;
|
|
125
|
+
constructor(message: string, statusCode: number, errorCode?: string);
|
|
126
|
+
}
|
|
127
|
+
export declare class RovnAgent {
|
|
128
|
+
private baseUrl;
|
|
129
|
+
private apiKey;
|
|
130
|
+
private agentId;
|
|
131
|
+
private sseController;
|
|
132
|
+
private fireAndForget;
|
|
133
|
+
private eventQueue;
|
|
134
|
+
private flushTimer;
|
|
135
|
+
private flushing;
|
|
136
|
+
private guardrailCache;
|
|
137
|
+
constructor(config: RovnConfig);
|
|
138
|
+
private headers;
|
|
139
|
+
/**
|
|
140
|
+
* Throws RovnError if agentId has not been set yet.
|
|
141
|
+
* Call register(), getInfo(), or connect({ agentId }) first.
|
|
142
|
+
*/
|
|
143
|
+
private ensureAgentId;
|
|
144
|
+
private request;
|
|
145
|
+
/**
|
|
146
|
+
* Returns true if the error looks like a network / transient failure
|
|
147
|
+
* (as opposed to a 4xx client error that retrying won't fix).
|
|
148
|
+
*/
|
|
149
|
+
private isRetryable;
|
|
150
|
+
private enqueue;
|
|
151
|
+
private scheduleFlush;
|
|
152
|
+
/**
|
|
153
|
+
* Internal drain loop — sends queued events one by one.
|
|
154
|
+
* On failure, re-queues the event at the front with incremented retry count
|
|
155
|
+
* and waits with exponential backoff before trying again.
|
|
156
|
+
*/
|
|
157
|
+
private drainQueue;
|
|
158
|
+
static register(baseUrl: string, options: {
|
|
159
|
+
name: string;
|
|
160
|
+
description?: string;
|
|
161
|
+
type?: string;
|
|
162
|
+
capabilities?: string[];
|
|
163
|
+
owner_email?: string;
|
|
164
|
+
metadata?: Record<string, unknown>;
|
|
165
|
+
}): Promise<{
|
|
166
|
+
agent: RovnAgent;
|
|
167
|
+
id: string;
|
|
168
|
+
apiKey: string;
|
|
169
|
+
}>;
|
|
170
|
+
getInfo(): Promise<AgentInfo>;
|
|
171
|
+
sendEvent(event: WebhookEvent, data: Record<string, unknown>): Promise<Record<string, unknown> | void>;
|
|
172
|
+
logActivity(title: string, options?: {
|
|
173
|
+
type?: string;
|
|
174
|
+
description?: string;
|
|
175
|
+
metadata?: Record<string, unknown>;
|
|
176
|
+
}): Promise<void>;
|
|
177
|
+
updateTaskStatus(taskId: string, status: string, result?: Record<string, unknown>): Promise<void>;
|
|
178
|
+
sendMessage(content: string, options?: {
|
|
179
|
+
message_type?: string;
|
|
180
|
+
metadata?: Record<string, unknown>;
|
|
181
|
+
}): Promise<void>;
|
|
182
|
+
updateStatus(status: AgentStatus): Promise<void>;
|
|
183
|
+
shareData(title: string, content: Record<string, unknown>, type?: string): Promise<void>;
|
|
184
|
+
respondToCommand(commandId: string, status: string, response?: Record<string, unknown>): Promise<void>;
|
|
185
|
+
requestApproval(options: {
|
|
186
|
+
type: string;
|
|
187
|
+
title: string;
|
|
188
|
+
description?: string;
|
|
189
|
+
urgency?: 'low' | 'medium' | 'high' | 'critical';
|
|
190
|
+
metadata?: Record<string, unknown>;
|
|
191
|
+
}): Promise<string | undefined>;
|
|
192
|
+
sendPeerMessage(toAgentId: string, content: string, options?: {
|
|
193
|
+
message_type?: string;
|
|
194
|
+
metadata?: Record<string, unknown>;
|
|
195
|
+
}): Promise<void>;
|
|
196
|
+
/**
|
|
197
|
+
* Sends all queued events. Resolves when the queue is drained.
|
|
198
|
+
* Useful before shutdown or when you need to guarantee delivery.
|
|
199
|
+
*/
|
|
200
|
+
flush(): Promise<void>;
|
|
201
|
+
/**
|
|
202
|
+
* Graceful shutdown: disconnects SSE, flushes pending events.
|
|
203
|
+
*/
|
|
204
|
+
close(): Promise<void>;
|
|
205
|
+
connect(handler: SSEHandler, options?: {
|
|
206
|
+
agentId?: string;
|
|
207
|
+
onConnect?: () => void;
|
|
208
|
+
onDisconnect?: () => void;
|
|
209
|
+
reconnect?: boolean;
|
|
210
|
+
}): void;
|
|
211
|
+
disconnect(): void;
|
|
212
|
+
getTasks(options?: {
|
|
213
|
+
status?: string;
|
|
214
|
+
limit?: number;
|
|
215
|
+
}): Promise<Task[]>;
|
|
216
|
+
getPeerMessages(options?: {
|
|
217
|
+
direction?: 'inbox' | 'outbox' | 'all';
|
|
218
|
+
limit?: number;
|
|
219
|
+
}): Promise<PeerMessage[]>;
|
|
220
|
+
getGuardrails(): Promise<Guardrail[]>;
|
|
221
|
+
/**
|
|
222
|
+
* Returns how many units remain before hitting the guardrail limit for the
|
|
223
|
+
* given metric. Returns `null` if no guardrail matches.
|
|
224
|
+
*
|
|
225
|
+
* Results are cached for 60 seconds to reduce API calls.
|
|
226
|
+
*/
|
|
227
|
+
getGuardrailRemaining(metric: string): Promise<number | null>;
|
|
228
|
+
/** Clears the cached guardrail data so the next call fetches fresh values. */
|
|
229
|
+
invalidateGuardrailCache(): void;
|
|
230
|
+
declareConstraint(task: string, constraints: Record<string, unknown>): Promise<Constraint>;
|
|
231
|
+
updateConstraint(constraintId: string, actualUsage: Record<string, unknown>, completed?: boolean): Promise<Record<string, unknown>>;
|
|
232
|
+
getConstraints(): Promise<Constraint[]>;
|
|
233
|
+
getTrustScore(): Promise<TrustScoreResult>;
|
|
234
|
+
getApprovals(options?: {
|
|
235
|
+
status?: string;
|
|
236
|
+
limit?: number;
|
|
237
|
+
}): Promise<ApprovalRequest[]>;
|
|
238
|
+
pollApproval(approvalId: string): Promise<ApprovalRequest>;
|
|
239
|
+
checkAction(action: string, options?: {
|
|
240
|
+
urgency?: string;
|
|
241
|
+
cost?: number;
|
|
242
|
+
data_fields?: string[];
|
|
243
|
+
}): Promise<CheckResult>;
|
|
244
|
+
getReportCard(options?: {
|
|
245
|
+
days?: number;
|
|
246
|
+
}): Promise<ReportCard>;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Wraps any function with Rovn governance: pre-flight policy check + activity reporting.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* import { RovnAgent, createGovernedTool } from 'rovn-sdk';
|
|
254
|
+
*
|
|
255
|
+
* const agent = new RovnAgent({ baseUrl: '...', apiKey: '...' });
|
|
256
|
+
* await agent.getInfo();
|
|
257
|
+
*
|
|
258
|
+
* const search = createGovernedTool(agent, {
|
|
259
|
+
* name: 'search',
|
|
260
|
+
* actionName: 'db_read',
|
|
261
|
+
* fn: async (query: string) => `Results for ${query}`,
|
|
262
|
+
* });
|
|
263
|
+
*
|
|
264
|
+
* const result = await search('my query');
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
export declare function createGovernedTool<TArgs extends unknown[], TResult>(agent: RovnAgent, options: {
|
|
268
|
+
name: string;
|
|
269
|
+
actionName?: string;
|
|
270
|
+
fn: (...args: TArgs) => TResult | Promise<TResult>;
|
|
271
|
+
}): (...args: TArgs) => Promise<TResult>;
|
|
272
|
+
/**
|
|
273
|
+
* Creates a middleware-style wrapper for Vercel AI SDK that reports
|
|
274
|
+
* each generation to Rovn and checks policies.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```ts
|
|
278
|
+
* import { RovnAgent, createVercelAIMiddleware } from 'rovn-sdk';
|
|
279
|
+
*
|
|
280
|
+
* const agent = new RovnAgent({ baseUrl: '...', apiKey: '...' });
|
|
281
|
+
* await agent.getInfo();
|
|
282
|
+
*
|
|
283
|
+
* const middleware = createVercelAIMiddleware(agent);
|
|
284
|
+
*
|
|
285
|
+
* // Wrap your doGenerate or doStream calls:
|
|
286
|
+
* const result = await middleware.wrapGenerate(async () => {
|
|
287
|
+
* return await model.doGenerate({ prompt: '...' });
|
|
288
|
+
* });
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
export declare function createVercelAIMiddleware(agent: RovnAgent, options?: {
|
|
292
|
+
actionName?: string;
|
|
293
|
+
}): {
|
|
294
|
+
/**
|
|
295
|
+
* Wraps a doGenerate call with policy check + activity reporting.
|
|
296
|
+
*/
|
|
297
|
+
wrapGenerate<T>(fn: () => Promise<T>): Promise<T>;
|
|
298
|
+
/**
|
|
299
|
+
* Wraps a doStream call with policy check + activity reporting.
|
|
300
|
+
*/
|
|
301
|
+
wrapStream<T>(fn: () => Promise<T>): Promise<T>;
|
|
302
|
+
};
|
|
303
|
+
export default RovnAgent;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ==================== Types ====================
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.RovnAgent = exports.RovnError = void 0;
|
|
5
|
+
exports.createGovernedTool = createGovernedTool;
|
|
6
|
+
exports.createVercelAIMiddleware = createVercelAIMiddleware;
|
|
7
|
+
// ==================== Error ====================
|
|
8
|
+
class RovnError extends Error {
|
|
9
|
+
statusCode;
|
|
10
|
+
errorCode;
|
|
11
|
+
constructor(message, statusCode, errorCode) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'RovnError';
|
|
14
|
+
this.statusCode = statusCode;
|
|
15
|
+
this.errorCode = errorCode;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.RovnError = RovnError;
|
|
19
|
+
// ==================== Client ====================
|
|
20
|
+
const MAX_QUEUE_SIZE = 10_000;
|
|
21
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
22
|
+
const GUARDRAIL_CACHE_TTL_MS = 60_000;
|
|
23
|
+
class RovnAgent {
|
|
24
|
+
baseUrl;
|
|
25
|
+
apiKey;
|
|
26
|
+
agentId = null;
|
|
27
|
+
sseController = null;
|
|
28
|
+
fireAndForget;
|
|
29
|
+
// Event queue for fire-and-forget and offline resilience
|
|
30
|
+
eventQueue = [];
|
|
31
|
+
flushTimer = null;
|
|
32
|
+
flushing = false;
|
|
33
|
+
// Guardrail cache
|
|
34
|
+
guardrailCache = null;
|
|
35
|
+
constructor(config) {
|
|
36
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
37
|
+
this.apiKey = config.apiKey;
|
|
38
|
+
this.fireAndForget = config.fireAndForget ?? false;
|
|
39
|
+
}
|
|
40
|
+
// ==================== Private Helpers ====================
|
|
41
|
+
headers() {
|
|
42
|
+
return {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Throws RovnError if agentId has not been set yet.
|
|
49
|
+
* Call register(), getInfo(), or connect({ agentId }) first.
|
|
50
|
+
*/
|
|
51
|
+
ensureAgentId() {
|
|
52
|
+
if (!this.agentId) {
|
|
53
|
+
throw new RovnError('agentId is required. Call register(), getInfo(), or connect({ agentId }) first.', 0, 'AGENT_ID_MISSING');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async request(method, path, body) {
|
|
57
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
58
|
+
method,
|
|
59
|
+
headers: this.headers(),
|
|
60
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok && res.headers.get('content-type')?.includes('text/html')) {
|
|
63
|
+
throw new RovnError(`Server error: ${res.status} ${res.statusText}`, res.status);
|
|
64
|
+
}
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
if (!data.success) {
|
|
67
|
+
throw new RovnError(data.error || `Request failed: ${method} ${path}`, res.status, data.code);
|
|
68
|
+
}
|
|
69
|
+
return data.data;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns true if the error looks like a network / transient failure
|
|
73
|
+
* (as opposed to a 4xx client error that retrying won't fix).
|
|
74
|
+
*/
|
|
75
|
+
isRetryable(err) {
|
|
76
|
+
if (err instanceof RovnError) {
|
|
77
|
+
// Retry on 5xx and 429; don't retry on 4xx client errors
|
|
78
|
+
return err.statusCode >= 500 || err.statusCode === 429;
|
|
79
|
+
}
|
|
80
|
+
// Network errors (TypeError from fetch), AbortError, etc. are retryable
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
// ==================== Event Queue Internals ====================
|
|
84
|
+
enqueue(event, data) {
|
|
85
|
+
if (this.eventQueue.length >= MAX_QUEUE_SIZE) {
|
|
86
|
+
// Drop the oldest event to make room
|
|
87
|
+
this.eventQueue.shift();
|
|
88
|
+
}
|
|
89
|
+
this.eventQueue.push({ event, data, retries: 0 });
|
|
90
|
+
this.scheduleFlush();
|
|
91
|
+
}
|
|
92
|
+
scheduleFlush() {
|
|
93
|
+
if (this.flushTimer || this.flushing)
|
|
94
|
+
return;
|
|
95
|
+
this.flushTimer = setTimeout(() => {
|
|
96
|
+
this.flushTimer = null;
|
|
97
|
+
this.drainQueue();
|
|
98
|
+
}, 0);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Internal drain loop — sends queued events one by one.
|
|
102
|
+
* On failure, re-queues the event at the front with incremented retry count
|
|
103
|
+
* and waits with exponential backoff before trying again.
|
|
104
|
+
*/
|
|
105
|
+
async drainQueue() {
|
|
106
|
+
if (this.flushing)
|
|
107
|
+
return;
|
|
108
|
+
this.flushing = true;
|
|
109
|
+
try {
|
|
110
|
+
while (this.eventQueue.length > 0) {
|
|
111
|
+
const item = this.eventQueue[0];
|
|
112
|
+
try {
|
|
113
|
+
await this.request('POST', '/api/webhook/agent', { event: item.event, data: item.data });
|
|
114
|
+
// Success — remove from queue
|
|
115
|
+
this.eventQueue.shift();
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (this.isRetryable(err)) {
|
|
119
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, …, max 30s
|
|
120
|
+
const delayMs = Math.min(1000 * Math.pow(2, item.retries), MAX_BACKOFF_MS);
|
|
121
|
+
item.retries++;
|
|
122
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
123
|
+
// The item is still at the front of the queue; loop will retry it
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Non-retryable error — drop the event and move on
|
|
127
|
+
this.eventQueue.shift();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
this.flushing = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ==================== Registration ====================
|
|
137
|
+
static async register(baseUrl, options) {
|
|
138
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/api/agents/register`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
body: JSON.stringify(options),
|
|
142
|
+
});
|
|
143
|
+
const data = await res.json();
|
|
144
|
+
if (!data.success)
|
|
145
|
+
throw new RovnError(data.error || 'Registration failed', res.status);
|
|
146
|
+
const agent = new RovnAgent({ baseUrl, apiKey: data.data.api_key });
|
|
147
|
+
agent.agentId = data.data.id;
|
|
148
|
+
return { agent, id: data.data.id, apiKey: data.data.api_key };
|
|
149
|
+
}
|
|
150
|
+
// ==================== Agent Info ====================
|
|
151
|
+
async getInfo() {
|
|
152
|
+
if (!this.agentId) {
|
|
153
|
+
const info = await this.request('GET', '/api/agents/me');
|
|
154
|
+
this.agentId = info.id;
|
|
155
|
+
return info;
|
|
156
|
+
}
|
|
157
|
+
return this.request('GET', `/api/agents/${this.agentId}`);
|
|
158
|
+
}
|
|
159
|
+
// ==================== Webhook (unified event endpoint) ====================
|
|
160
|
+
async sendEvent(event, data) {
|
|
161
|
+
if (this.fireAndForget) {
|
|
162
|
+
this.enqueue(event, data);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
return await this.request('POST', '/api/webhook/agent', { event, data });
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
if (this.isRetryable(err)) {
|
|
170
|
+
// Queue for retry with backoff
|
|
171
|
+
this.enqueue(event, data);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async logActivity(title, options) {
|
|
178
|
+
await this.sendEvent('activity', { title, ...options });
|
|
179
|
+
}
|
|
180
|
+
async updateTaskStatus(taskId, status, result) {
|
|
181
|
+
await this.sendEvent('task_update', { task_id: taskId, status, result });
|
|
182
|
+
}
|
|
183
|
+
async sendMessage(content, options) {
|
|
184
|
+
await this.sendEvent('message', { content, ...options });
|
|
185
|
+
}
|
|
186
|
+
async updateStatus(status) {
|
|
187
|
+
await this.sendEvent('status', { status });
|
|
188
|
+
}
|
|
189
|
+
async shareData(title, content, type) {
|
|
190
|
+
await this.sendEvent('share_data', { title, content, type });
|
|
191
|
+
}
|
|
192
|
+
async respondToCommand(commandId, status, response) {
|
|
193
|
+
await this.sendEvent('command_response', { command_id: commandId, status, response });
|
|
194
|
+
}
|
|
195
|
+
async requestApproval(options) {
|
|
196
|
+
// Always send synchronously to guarantee approval_id, even in fire-and-forget mode
|
|
197
|
+
const result = await this.request('POST', '/api/webhook/agent', { event: 'approval_request', data: options });
|
|
198
|
+
return result?.approval_id;
|
|
199
|
+
}
|
|
200
|
+
async sendPeerMessage(toAgentId, content, options) {
|
|
201
|
+
await this.sendEvent('peer_message', { to_agent_id: toAgentId, content, ...options });
|
|
202
|
+
}
|
|
203
|
+
// ==================== Flush ====================
|
|
204
|
+
/**
|
|
205
|
+
* Sends all queued events. Resolves when the queue is drained.
|
|
206
|
+
* Useful before shutdown or when you need to guarantee delivery.
|
|
207
|
+
*/
|
|
208
|
+
async flush() {
|
|
209
|
+
// Cancel any pending scheduled flush
|
|
210
|
+
if (this.flushTimer) {
|
|
211
|
+
clearTimeout(this.flushTimer);
|
|
212
|
+
this.flushTimer = null;
|
|
213
|
+
}
|
|
214
|
+
// If already flushing, wait for it to finish
|
|
215
|
+
if (this.flushing) {
|
|
216
|
+
// Spin-wait until the current drain completes
|
|
217
|
+
while (this.flushing) {
|
|
218
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
219
|
+
}
|
|
220
|
+
// If items were added during the wait, drain again
|
|
221
|
+
if (this.eventQueue.length > 0) {
|
|
222
|
+
await this.drainQueue();
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (this.eventQueue.length > 0) {
|
|
227
|
+
await this.drainQueue();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// ==================== Close ====================
|
|
231
|
+
/**
|
|
232
|
+
* Graceful shutdown: disconnects SSE, flushes pending events.
|
|
233
|
+
*/
|
|
234
|
+
async close() {
|
|
235
|
+
this.disconnect();
|
|
236
|
+
await this.flush();
|
|
237
|
+
}
|
|
238
|
+
// ==================== SSE Stream ====================
|
|
239
|
+
connect(handler, options) {
|
|
240
|
+
const targetId = options?.agentId ?? this.agentId;
|
|
241
|
+
if (targetId)
|
|
242
|
+
this.agentId = targetId;
|
|
243
|
+
if (!this.agentId) {
|
|
244
|
+
throw new RovnError('agentId is required. Call register() or getInfo() first, or pass agentId in options.', 0);
|
|
245
|
+
}
|
|
246
|
+
const reconnect = options?.reconnect !== false;
|
|
247
|
+
let lastEventId = null;
|
|
248
|
+
const doConnect = async () => {
|
|
249
|
+
this.sseController = new AbortController();
|
|
250
|
+
try {
|
|
251
|
+
const headers = { ...this.headers() };
|
|
252
|
+
if (lastEventId)
|
|
253
|
+
headers['Last-Event-ID'] = lastEventId;
|
|
254
|
+
const response = await fetch(`${this.baseUrl}/api/agents/${this.agentId}/stream`, {
|
|
255
|
+
headers,
|
|
256
|
+
signal: this.sseController.signal,
|
|
257
|
+
});
|
|
258
|
+
if (!response.ok || !response.body) {
|
|
259
|
+
throw new RovnError(`SSE connection failed: ${response.status}`, response.status);
|
|
260
|
+
}
|
|
261
|
+
options?.onConnect?.();
|
|
262
|
+
const reader = response.body.getReader();
|
|
263
|
+
const decoder = new TextDecoder();
|
|
264
|
+
let buffer = '';
|
|
265
|
+
while (true) {
|
|
266
|
+
const { done, value } = await reader.read();
|
|
267
|
+
if (done)
|
|
268
|
+
break;
|
|
269
|
+
buffer += decoder.decode(value, { stream: true });
|
|
270
|
+
const lines = buffer.split('\n');
|
|
271
|
+
buffer = lines.pop() || '';
|
|
272
|
+
let eventType = '';
|
|
273
|
+
let eventData = '';
|
|
274
|
+
for (const line of lines) {
|
|
275
|
+
if (line.startsWith('id: ')) {
|
|
276
|
+
lastEventId = line.slice(4);
|
|
277
|
+
}
|
|
278
|
+
else if (line.startsWith('event: ')) {
|
|
279
|
+
eventType = line.slice(7);
|
|
280
|
+
}
|
|
281
|
+
else if (line.startsWith('data: ')) {
|
|
282
|
+
eventData = line.slice(6);
|
|
283
|
+
}
|
|
284
|
+
else if (line === '' && eventType && eventData) {
|
|
285
|
+
try {
|
|
286
|
+
const parsed = JSON.parse(eventData);
|
|
287
|
+
handler(eventType, parsed);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// ignore parse errors
|
|
291
|
+
}
|
|
292
|
+
eventType = '';
|
|
293
|
+
eventData = '';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
if (err.name === 'AbortError')
|
|
300
|
+
return;
|
|
301
|
+
options?.onDisconnect?.();
|
|
302
|
+
if (reconnect) {
|
|
303
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
304
|
+
doConnect();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
doConnect();
|
|
309
|
+
}
|
|
310
|
+
disconnect() {
|
|
311
|
+
this.sseController?.abort();
|
|
312
|
+
this.sseController = null;
|
|
313
|
+
}
|
|
314
|
+
// ==================== Tasks ====================
|
|
315
|
+
async getTasks(options) {
|
|
316
|
+
this.ensureAgentId();
|
|
317
|
+
const params = new URLSearchParams();
|
|
318
|
+
if (options?.status)
|
|
319
|
+
params.set('status', options.status);
|
|
320
|
+
if (options?.limit)
|
|
321
|
+
params.set('limit', String(options.limit));
|
|
322
|
+
const qs = params.toString();
|
|
323
|
+
return this.request('GET', `/api/agents/${this.agentId}/tasks${qs ? '?' + qs : ''}`);
|
|
324
|
+
}
|
|
325
|
+
// ==================== Peer Messages ====================
|
|
326
|
+
async getPeerMessages(options) {
|
|
327
|
+
this.ensureAgentId();
|
|
328
|
+
const params = new URLSearchParams();
|
|
329
|
+
if (options?.direction)
|
|
330
|
+
params.set('direction', options.direction);
|
|
331
|
+
if (options?.limit)
|
|
332
|
+
params.set('limit', String(options.limit));
|
|
333
|
+
const qs = params.toString();
|
|
334
|
+
return this.request('GET', `/api/agents/${this.agentId}/peer${qs ? '?' + qs : ''}`);
|
|
335
|
+
}
|
|
336
|
+
// ==================== Guardrails ====================
|
|
337
|
+
async getGuardrails() {
|
|
338
|
+
this.ensureAgentId();
|
|
339
|
+
return this.request('GET', `/api/agents/${this.agentId}/guardrails`);
|
|
340
|
+
}
|
|
341
|
+
// ==================== Guardrail Helper ====================
|
|
342
|
+
/**
|
|
343
|
+
* Returns how many units remain before hitting the guardrail limit for the
|
|
344
|
+
* given metric. Returns `null` if no guardrail matches.
|
|
345
|
+
*
|
|
346
|
+
* Results are cached for 60 seconds to reduce API calls.
|
|
347
|
+
*/
|
|
348
|
+
async getGuardrailRemaining(metric) {
|
|
349
|
+
this.ensureAgentId();
|
|
350
|
+
const now = Date.now();
|
|
351
|
+
if (!this.guardrailCache || now - this.guardrailCache.cachedAt > GUARDRAIL_CACHE_TTL_MS) {
|
|
352
|
+
this.guardrailCache = {
|
|
353
|
+
data: await this.request('GET', `/api/agents/${this.agentId}/guardrails`),
|
|
354
|
+
cachedAt: now,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const guardrail = this.guardrailCache.data.find(g => g.metric === metric);
|
|
358
|
+
if (!guardrail)
|
|
359
|
+
return null;
|
|
360
|
+
return guardrail.limit_value - guardrail.current_value;
|
|
361
|
+
}
|
|
362
|
+
/** Clears the cached guardrail data so the next call fetches fresh values. */
|
|
363
|
+
invalidateGuardrailCache() {
|
|
364
|
+
this.guardrailCache = null;
|
|
365
|
+
}
|
|
366
|
+
// ==================== Constraints (Self-Constraint Declaration) ====================
|
|
367
|
+
async declareConstraint(task, constraints) {
|
|
368
|
+
this.ensureAgentId();
|
|
369
|
+
return this.request('POST', `/api/agents/${this.agentId}/constraints`, {
|
|
370
|
+
task,
|
|
371
|
+
constraints,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
async updateConstraint(constraintId, actualUsage, completed = false) {
|
|
375
|
+
this.ensureAgentId();
|
|
376
|
+
return this.request('PATCH', `/api/agents/${this.agentId}/constraints`, {
|
|
377
|
+
constraint_id: constraintId,
|
|
378
|
+
actual_usage: actualUsage,
|
|
379
|
+
completed,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async getConstraints() {
|
|
383
|
+
this.ensureAgentId();
|
|
384
|
+
return this.request('GET', `/api/agents/${this.agentId}/constraints`);
|
|
385
|
+
}
|
|
386
|
+
// ==================== Trust Score ====================
|
|
387
|
+
async getTrustScore() {
|
|
388
|
+
this.ensureAgentId();
|
|
389
|
+
return this.request('GET', `/api/agents/${this.agentId}/trust-score`);
|
|
390
|
+
}
|
|
391
|
+
// ==================== Approvals (Polling) ====================
|
|
392
|
+
async getApprovals(options) {
|
|
393
|
+
const params = new URLSearchParams();
|
|
394
|
+
if (options?.status)
|
|
395
|
+
params.set('status', options.status);
|
|
396
|
+
if (options?.limit)
|
|
397
|
+
params.set('limit', String(options.limit));
|
|
398
|
+
const qs = params.toString();
|
|
399
|
+
const data = await this.request('GET', `/api/approvals${qs ? '?' + qs : ''}`);
|
|
400
|
+
return data.approvals ?? data;
|
|
401
|
+
}
|
|
402
|
+
async pollApproval(approvalId) {
|
|
403
|
+
return this.request('GET', `/api/approvals/${approvalId}`);
|
|
404
|
+
}
|
|
405
|
+
// ==================== Pre-flight Check ("Can I Do This?") ====================
|
|
406
|
+
async checkAction(action, options) {
|
|
407
|
+
this.ensureAgentId();
|
|
408
|
+
const params = new URLSearchParams({ action });
|
|
409
|
+
if (options?.urgency)
|
|
410
|
+
params.set('urgency', options.urgency);
|
|
411
|
+
if (options?.cost !== undefined)
|
|
412
|
+
params.set('cost', String(options.cost));
|
|
413
|
+
if (options?.data_fields)
|
|
414
|
+
params.set('data_fields', options.data_fields.join(','));
|
|
415
|
+
return this.request('GET', `/api/agents/${this.agentId}/check?${params}`);
|
|
416
|
+
}
|
|
417
|
+
// ==================== Report Card ====================
|
|
418
|
+
async getReportCard(options) {
|
|
419
|
+
this.ensureAgentId();
|
|
420
|
+
const params = new URLSearchParams();
|
|
421
|
+
if (options?.days)
|
|
422
|
+
params.set('days', String(options.days));
|
|
423
|
+
const qs = params.toString();
|
|
424
|
+
return this.request('GET', `/api/agents/${this.agentId}/report-card${qs ? '?' + qs : ''}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
exports.RovnAgent = RovnAgent;
|
|
428
|
+
// ==================== LangChain-style Tool Wrapper ====================
|
|
429
|
+
/**
|
|
430
|
+
* Wraps any function with Rovn governance: pre-flight policy check + activity reporting.
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* ```ts
|
|
434
|
+
* import { RovnAgent, createGovernedTool } from 'rovn-sdk';
|
|
435
|
+
*
|
|
436
|
+
* const agent = new RovnAgent({ baseUrl: '...', apiKey: '...' });
|
|
437
|
+
* await agent.getInfo();
|
|
438
|
+
*
|
|
439
|
+
* const search = createGovernedTool(agent, {
|
|
440
|
+
* name: 'search',
|
|
441
|
+
* actionName: 'db_read',
|
|
442
|
+
* fn: async (query: string) => `Results for ${query}`,
|
|
443
|
+
* });
|
|
444
|
+
*
|
|
445
|
+
* const result = await search('my query');
|
|
446
|
+
* ```
|
|
447
|
+
*/
|
|
448
|
+
function createGovernedTool(agent, options) {
|
|
449
|
+
const actionName = options.actionName ?? options.name;
|
|
450
|
+
return async (...args) => {
|
|
451
|
+
// Pre-flight check
|
|
452
|
+
try {
|
|
453
|
+
const check = await agent.checkAction(actionName);
|
|
454
|
+
if (!check.allowed) {
|
|
455
|
+
await agent.logActivity(`Blocked: ${actionName}`, {
|
|
456
|
+
type: 'policy_block',
|
|
457
|
+
description: check.summary,
|
|
458
|
+
});
|
|
459
|
+
throw new RovnError(`Action '${actionName}' blocked by policy: ${check.summary}`, 403, 'POLICY_BLOCKED');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
if (err instanceof RovnError)
|
|
464
|
+
throw err;
|
|
465
|
+
// If check fails (network, etc.), allow execution but continue
|
|
466
|
+
}
|
|
467
|
+
// Execute
|
|
468
|
+
try {
|
|
469
|
+
const result = await options.fn(...args);
|
|
470
|
+
await agent.logActivity(`Executed: ${actionName}`, {
|
|
471
|
+
type: 'tool_execution',
|
|
472
|
+
description: `Tool '${options.name}' completed successfully`,
|
|
473
|
+
});
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
if (err instanceof RovnError)
|
|
478
|
+
throw err;
|
|
479
|
+
await agent.logActivity(`Failed: ${actionName}`, {
|
|
480
|
+
type: 'tool_error',
|
|
481
|
+
description: `Tool '${options.name}' failed: ${err}`,
|
|
482
|
+
});
|
|
483
|
+
throw err;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
// ==================== Vercel AI SDK Middleware ====================
|
|
488
|
+
/**
|
|
489
|
+
* Creates a middleware-style wrapper for Vercel AI SDK that reports
|
|
490
|
+
* each generation to Rovn and checks policies.
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```ts
|
|
494
|
+
* import { RovnAgent, createVercelAIMiddleware } from 'rovn-sdk';
|
|
495
|
+
*
|
|
496
|
+
* const agent = new RovnAgent({ baseUrl: '...', apiKey: '...' });
|
|
497
|
+
* await agent.getInfo();
|
|
498
|
+
*
|
|
499
|
+
* const middleware = createVercelAIMiddleware(agent);
|
|
500
|
+
*
|
|
501
|
+
* // Wrap your doGenerate or doStream calls:
|
|
502
|
+
* const result = await middleware.wrapGenerate(async () => {
|
|
503
|
+
* return await model.doGenerate({ prompt: '...' });
|
|
504
|
+
* });
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
function createVercelAIMiddleware(agent, options) {
|
|
508
|
+
const actionName = options?.actionName ?? 'ai_generate';
|
|
509
|
+
return {
|
|
510
|
+
/**
|
|
511
|
+
* Wraps a doGenerate call with policy check + activity reporting.
|
|
512
|
+
*/
|
|
513
|
+
async wrapGenerate(fn) {
|
|
514
|
+
// Pre-flight check
|
|
515
|
+
try {
|
|
516
|
+
const check = await agent.checkAction(actionName);
|
|
517
|
+
if (!check.allowed) {
|
|
518
|
+
await agent.logActivity(`Blocked: ${actionName}`, {
|
|
519
|
+
type: 'policy_block',
|
|
520
|
+
description: check.summary,
|
|
521
|
+
});
|
|
522
|
+
throw new RovnError(`Action '${actionName}' blocked by policy: ${check.summary}`, 403, 'POLICY_BLOCKED');
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
if (err instanceof RovnError)
|
|
527
|
+
throw err;
|
|
528
|
+
}
|
|
529
|
+
const start = Date.now();
|
|
530
|
+
try {
|
|
531
|
+
const result = await fn();
|
|
532
|
+
const duration = Date.now() - start;
|
|
533
|
+
await agent.logActivity(`AI Generate completed`, {
|
|
534
|
+
type: 'ai_generate',
|
|
535
|
+
description: `Generation took ${duration}ms`,
|
|
536
|
+
metadata: { duration_ms: duration },
|
|
537
|
+
});
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
if (err instanceof RovnError)
|
|
542
|
+
throw err;
|
|
543
|
+
const duration = Date.now() - start;
|
|
544
|
+
await agent.logActivity(`AI Generate failed`, {
|
|
545
|
+
type: 'ai_error',
|
|
546
|
+
description: `Generation failed after ${duration}ms: ${err}`,
|
|
547
|
+
});
|
|
548
|
+
throw err;
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
/**
|
|
552
|
+
* Wraps a doStream call with policy check + activity reporting.
|
|
553
|
+
*/
|
|
554
|
+
async wrapStream(fn) {
|
|
555
|
+
try {
|
|
556
|
+
const check = await agent.checkAction(actionName);
|
|
557
|
+
if (!check.allowed) {
|
|
558
|
+
await agent.logActivity(`Blocked: ${actionName} (stream)`, {
|
|
559
|
+
type: 'policy_block',
|
|
560
|
+
description: check.summary,
|
|
561
|
+
});
|
|
562
|
+
throw new RovnError(`Action '${actionName}' blocked by policy: ${check.summary}`, 403, 'POLICY_BLOCKED');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
catch (err) {
|
|
566
|
+
if (err instanceof RovnError)
|
|
567
|
+
throw err;
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
const result = await fn();
|
|
571
|
+
await agent.logActivity(`AI Stream started`, { type: 'ai_stream' });
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
if (err instanceof RovnError)
|
|
576
|
+
throw err;
|
|
577
|
+
await agent.logActivity(`AI Stream failed`, {
|
|
578
|
+
type: 'ai_error',
|
|
579
|
+
description: `Stream failed: ${err}`,
|
|
580
|
+
});
|
|
581
|
+
throw err;
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
exports.default = RovnAgent;
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rovn-ai/agent",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "TypeScript SDK for Rovn Agent OS",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["ai-agent", "agent-os", "rovn"],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^5"
|
|
26
|
+
}
|
|
27
|
+
}
|