open-agents-ai 0.83.0 → 0.85.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/index.js +471 -9
- package/package.json +2 -2
- package/prompts/agentic/system-large.md +34 -3
- package/prompts/agentic/system-medium.md +1 -1
package/dist/index.js
CHANGED
|
@@ -11223,12 +11223,13 @@ var init_nexus = __esm({
|
|
|
11223
11223
|
/**
|
|
11224
11224
|
* nexus-daemon.mjs \u2014 Standalone nexus process with real TCP/UDP sockets.
|
|
11225
11225
|
* Spawned by the open-agents nexus tool. Communicates via JSON files.
|
|
11226
|
+
* v1.5.0: capability registration, peer blocking, metering, room members.
|
|
11226
11227
|
*
|
|
11227
11228
|
* Usage: node nexus-daemon.mjs <nexus-dir> <agent-name> [agent-type]
|
|
11228
11229
|
*/
|
|
11229
11230
|
|
|
11230
|
-
import { NexusClient } from 'open-agents-nexus';
|
|
11231
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, readdirSync } from 'node:fs';
|
|
11231
|
+
import { NexusClient, createFileAuditHook } from 'open-agents-nexus';
|
|
11232
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, readdirSync, appendFileSync } from 'node:fs';
|
|
11232
11233
|
import { join } from 'node:path';
|
|
11233
11234
|
|
|
11234
11235
|
const nexusDir = process.argv[2];
|
|
@@ -11239,8 +11240,11 @@ const respFile = join(nexusDir, 'resp.json');
|
|
|
11239
11240
|
const statusFile = join(nexusDir, 'status.json');
|
|
11240
11241
|
const inboxDir = join(nexusDir, 'inbox');
|
|
11241
11242
|
const pidFile = join(nexusDir, 'daemon.pid');
|
|
11243
|
+
const invocationsDir = join(nexusDir, 'invocations');
|
|
11244
|
+
const meteringFile = join(nexusDir, 'metering.jsonl');
|
|
11242
11245
|
|
|
11243
11246
|
mkdirSync(inboxDir, { recursive: true });
|
|
11247
|
+
mkdirSync(invocationsDir, { recursive: true });
|
|
11244
11248
|
|
|
11245
11249
|
// Write PID so the agent can kill us
|
|
11246
11250
|
writeFileSync(pidFile, String(process.pid));
|
|
@@ -11257,12 +11261,16 @@ const nexus = new NexusClient({
|
|
|
11257
11261
|
natsServers: ['wss://demo.nats.io:8443'],
|
|
11258
11262
|
enableCircuitRelay: true,
|
|
11259
11263
|
usePublicBootstrap: true,
|
|
11264
|
+
trustPolicy: { denylist: [], allowlist: [] },
|
|
11260
11265
|
});
|
|
11261
11266
|
|
|
11262
11267
|
const rooms = new Map();
|
|
11263
11268
|
let connected = false;
|
|
11269
|
+
const blockedPeers = [];
|
|
11264
11270
|
|
|
11265
11271
|
function writeStatus(extra = {}) {
|
|
11272
|
+
const caps = typeof nexus.getRegisteredCapabilities === 'function'
|
|
11273
|
+
? nexus.getRegisteredCapabilities() : [];
|
|
11266
11274
|
const data = {
|
|
11267
11275
|
pid: process.pid,
|
|
11268
11276
|
connected,
|
|
@@ -11270,6 +11278,8 @@ function writeStatus(extra = {}) {
|
|
|
11270
11278
|
agentName,
|
|
11271
11279
|
agentType,
|
|
11272
11280
|
rooms: [...rooms.keys()],
|
|
11281
|
+
capabilities: caps,
|
|
11282
|
+
blockedPeers,
|
|
11273
11283
|
connectedAt: connected ? new Date().toISOString() : null,
|
|
11274
11284
|
...extra,
|
|
11275
11285
|
};
|
|
@@ -11289,6 +11299,7 @@ async function handleCmd(cmd) {
|
|
|
11289
11299
|
if (rooms.has(roomId)) { writeResp(id, { ok: true, output: 'Already in room: ' + roomId }); return; }
|
|
11290
11300
|
const room = await nexus.joinRoom(roomId);
|
|
11291
11301
|
rooms.set(roomId, room);
|
|
11302
|
+
// Per-room message listener (also captured by client-level event)
|
|
11292
11303
|
room.on('message', (msg) => {
|
|
11293
11304
|
const roomInbox = join(inboxDir, roomId);
|
|
11294
11305
|
mkdirSync(roomInbox, { recursive: true });
|
|
@@ -11302,6 +11313,15 @@ async function handleCmd(cmd) {
|
|
|
11302
11313
|
};
|
|
11303
11314
|
try { writeFileSync(join(roomInbox, fname), JSON.stringify(entry, null, 2)); } catch {}
|
|
11304
11315
|
});
|
|
11316
|
+
// v1.5.0: Room member tracking
|
|
11317
|
+
if (room.on) {
|
|
11318
|
+
room.on('member:join', (member) => {
|
|
11319
|
+
console.log('Member joined ' + roomId + ': ' + (member.agentName || member.peerId));
|
|
11320
|
+
});
|
|
11321
|
+
room.on('member:leave', (member) => {
|
|
11322
|
+
console.log('Member left ' + roomId + ': ' + (member.agentName || member.peerId));
|
|
11323
|
+
});
|
|
11324
|
+
}
|
|
11305
11325
|
writeStatus();
|
|
11306
11326
|
writeResp(id, { ok: true, output: 'Joined room: ' + roomId });
|
|
11307
11327
|
break;
|
|
@@ -11372,6 +11392,366 @@ async function handleCmd(cmd) {
|
|
|
11372
11392
|
writeResp(id, { ok: true, output: JSON.stringify(data, null, 2) });
|
|
11373
11393
|
break;
|
|
11374
11394
|
}
|
|
11395
|
+
|
|
11396
|
+
// \u2500\u2500 v1.5.0: Capability Registration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
11397
|
+
case 'register_capability': {
|
|
11398
|
+
const name = args.capability;
|
|
11399
|
+
if (!name) { writeResp(id, { ok: false, output: 'capability name is required' }); return; }
|
|
11400
|
+
if (typeof nexus.registerCapability !== 'function') {
|
|
11401
|
+
writeResp(id, { ok: false, output: 'registerCapability not available (nexus version too old)' });
|
|
11402
|
+
return;
|
|
11403
|
+
}
|
|
11404
|
+
nexus.registerCapability(name, async (request, stream) => {
|
|
11405
|
+
// Log inbound invocation
|
|
11406
|
+
const logEntry = {
|
|
11407
|
+
ts: Date.now(),
|
|
11408
|
+
from: request.from || 'unknown',
|
|
11409
|
+
capability: request.capability || name,
|
|
11410
|
+
requestId: request.requestId,
|
|
11411
|
+
};
|
|
11412
|
+
const logFile = join(invocationsDir, Date.now() + '-' + name + '.json');
|
|
11413
|
+
try { writeFileSync(logFile, JSON.stringify(logEntry, null, 2)); } catch {}
|
|
11414
|
+
|
|
11415
|
+
// Collect input chunks
|
|
11416
|
+
let inputData = '';
|
|
11417
|
+
stream.onData((msg) => {
|
|
11418
|
+
if (msg.type === 'invoke.chunk') {
|
|
11419
|
+
inputData += (typeof msg.data === 'string' ? msg.data : JSON.stringify(msg.data));
|
|
11420
|
+
}
|
|
11421
|
+
});
|
|
11422
|
+
|
|
11423
|
+
// Accept the invocation
|
|
11424
|
+
await stream.write({
|
|
11425
|
+
type: 'invoke.accept', version: 1,
|
|
11426
|
+
requestId: request.requestId, accepted: true,
|
|
11427
|
+
});
|
|
11428
|
+
|
|
11429
|
+
// Send event with acknowledgment
|
|
11430
|
+
await stream.write({
|
|
11431
|
+
type: 'invoke.event', version: 1,
|
|
11432
|
+
requestId: request.requestId, seq: 0,
|
|
11433
|
+
event: 'ack', data: agentName + ' received invocation for ' + name,
|
|
11434
|
+
});
|
|
11435
|
+
|
|
11436
|
+
// Complete
|
|
11437
|
+
await stream.write({
|
|
11438
|
+
type: 'invoke.done', version: 1,
|
|
11439
|
+
requestId: request.requestId,
|
|
11440
|
+
usage: { inputBytes: inputData.length, outputBytes: 0 },
|
|
11441
|
+
});
|
|
11442
|
+
stream.close();
|
|
11443
|
+
});
|
|
11444
|
+
writeStatus();
|
|
11445
|
+
writeResp(id, { ok: true, output: 'Capability registered: ' + name + '. Now advertised via NATS.' });
|
|
11446
|
+
break;
|
|
11447
|
+
}
|
|
11448
|
+
case 'unregister_capability': {
|
|
11449
|
+
const name = args.capability;
|
|
11450
|
+
if (!name) { writeResp(id, { ok: false, output: 'capability name is required' }); return; }
|
|
11451
|
+
if (typeof nexus.unregisterCapability === 'function') {
|
|
11452
|
+
nexus.unregisterCapability(name);
|
|
11453
|
+
}
|
|
11454
|
+
writeStatus();
|
|
11455
|
+
writeResp(id, { ok: true, output: 'Capability unregistered: ' + name });
|
|
11456
|
+
break;
|
|
11457
|
+
}
|
|
11458
|
+
case 'list_capabilities': {
|
|
11459
|
+
const caps = typeof nexus.getRegisteredCapabilities === 'function'
|
|
11460
|
+
? nexus.getRegisteredCapabilities() : [];
|
|
11461
|
+
writeResp(id, { ok: true, output: caps.length ? 'Capabilities: ' + caps.join(', ') : 'No capabilities registered.' });
|
|
11462
|
+
break;
|
|
11463
|
+
}
|
|
11464
|
+
|
|
11465
|
+
// \u2500\u2500 v1.5.0: Peer Blocking \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
11466
|
+
case 'block_peer': {
|
|
11467
|
+
const peerId = args.target_peer || args.peer_id;
|
|
11468
|
+
if (!peerId) { writeResp(id, { ok: false, output: 'peer_id or target_peer is required' }); return; }
|
|
11469
|
+
if (typeof nexus.blockPeer === 'function') {
|
|
11470
|
+
nexus.blockPeer(peerId);
|
|
11471
|
+
}
|
|
11472
|
+
if (!blockedPeers.includes(peerId)) blockedPeers.push(peerId);
|
|
11473
|
+
writeStatus();
|
|
11474
|
+
writeResp(id, { ok: true, output: 'Blocked peer: ' + peerId.slice(0, 20) + '...' });
|
|
11475
|
+
break;
|
|
11476
|
+
}
|
|
11477
|
+
case 'unblock_peer': {
|
|
11478
|
+
const peerId = args.target_peer || args.peer_id;
|
|
11479
|
+
if (!peerId) { writeResp(id, { ok: false, output: 'peer_id or target_peer is required' }); return; }
|
|
11480
|
+
if (typeof nexus.unblockPeer === 'function') {
|
|
11481
|
+
nexus.unblockPeer(peerId);
|
|
11482
|
+
}
|
|
11483
|
+
const idx = blockedPeers.indexOf(peerId);
|
|
11484
|
+
if (idx >= 0) blockedPeers.splice(idx, 1);
|
|
11485
|
+
writeStatus();
|
|
11486
|
+
writeResp(id, { ok: true, output: 'Unblocked peer: ' + peerId.slice(0, 20) + '...' });
|
|
11487
|
+
break;
|
|
11488
|
+
}
|
|
11489
|
+
|
|
11490
|
+
// \u2500\u2500 v1.5.0: Metering \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
11491
|
+
case 'metering_status': {
|
|
11492
|
+
if (!nexus.metering) {
|
|
11493
|
+
writeResp(id, { ok: true, output: 'Metering not available (nexus version too old).' });
|
|
11494
|
+
return;
|
|
11495
|
+
}
|
|
11496
|
+
const filter = {};
|
|
11497
|
+
if (args.peer_id) filter.peerId = args.peer_id;
|
|
11498
|
+
if (args.capability) filter.service = args.capability;
|
|
11499
|
+
|
|
11500
|
+
// Try per-peer summary first
|
|
11501
|
+
if (args.peer_id && typeof nexus.metering.getSummary === 'function') {
|
|
11502
|
+
const summary = nexus.metering.getSummary(args.peer_id);
|
|
11503
|
+
if (summary) {
|
|
11504
|
+
writeResp(id, { ok: true, output: JSON.stringify(summary, null, 2) });
|
|
11505
|
+
return;
|
|
11506
|
+
}
|
|
11507
|
+
}
|
|
11508
|
+
|
|
11509
|
+
// All summaries
|
|
11510
|
+
if (typeof nexus.metering.getAllSummaries === 'function') {
|
|
11511
|
+
const all = nexus.metering.getAllSummaries();
|
|
11512
|
+
if (all && (all.size > 0 || Object.keys(all).length > 0)) {
|
|
11513
|
+
const entries = all instanceof Map ? Object.fromEntries(all) : all;
|
|
11514
|
+
writeResp(id, { ok: true, output: 'Metering summaries:\\n' + JSON.stringify(entries, null, 2) });
|
|
11515
|
+
return;
|
|
11516
|
+
}
|
|
11517
|
+
}
|
|
11518
|
+
|
|
11519
|
+
// Fall back to raw records
|
|
11520
|
+
if (typeof nexus.metering.getRecords === 'function') {
|
|
11521
|
+
const records = nexus.metering.getRecords(filter);
|
|
11522
|
+
writeResp(id, { ok: true, output: 'Records: ' + records.length + '\\n' + JSON.stringify(records.slice(-10), null, 2) });
|
|
11523
|
+
return;
|
|
11524
|
+
}
|
|
11525
|
+
|
|
11526
|
+
writeResp(id, { ok: true, output: 'No metering data yet.' });
|
|
11527
|
+
break;
|
|
11528
|
+
}
|
|
11529
|
+
|
|
11530
|
+
// \u2500\u2500 v1.5.0: Room Members \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
11531
|
+
case 'room_members': {
|
|
11532
|
+
const roomId = args.room_id;
|
|
11533
|
+
if (!roomId) { writeResp(id, { ok: false, output: 'room_id is required' }); return; }
|
|
11534
|
+
const room = rooms.get(roomId);
|
|
11535
|
+
if (!room) { writeResp(id, { ok: false, output: 'Not in room: ' + roomId }); return; }
|
|
11536
|
+
|
|
11537
|
+
const members = room.members || [];
|
|
11538
|
+
if (members.length === 0) {
|
|
11539
|
+
writeResp(id, { ok: true, output: 'No members tracked in room: ' + roomId });
|
|
11540
|
+
return;
|
|
11541
|
+
}
|
|
11542
|
+
const lines = ['Members in ' + roomId + ' (' + members.length + '):'];
|
|
11543
|
+
for (const m of members) {
|
|
11544
|
+
const name = m.agentName || 'unknown';
|
|
11545
|
+
const type = m.agentType || '?';
|
|
11546
|
+
const caps = m.capabilities?.join(', ') || 'none';
|
|
11547
|
+
const status = m.status || 'active';
|
|
11548
|
+
lines.push(' ' + m.peerId.slice(0, 16) + '... ' + name + ' (' + type + ') [' + status + '] caps: ' + caps);
|
|
11549
|
+
}
|
|
11550
|
+
writeResp(id, { ok: true, output: lines.join('\\n') });
|
|
11551
|
+
break;
|
|
11552
|
+
}
|
|
11553
|
+
|
|
11554
|
+
// \u2500\u2500 Metered Inference Exposure \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
11555
|
+
case 'expose': {
|
|
11556
|
+
// Query Ollama for available models
|
|
11557
|
+
const ollamaUrl = args.ollama_url || process.env.OLLAMA_URL || 'http://localhost:11434';
|
|
11558
|
+
let models = [];
|
|
11559
|
+
try {
|
|
11560
|
+
const tagsResp = await fetch(ollamaUrl + '/api/tags');
|
|
11561
|
+
if (tagsResp.ok) {
|
|
11562
|
+
const tagsData = await tagsResp.json();
|
|
11563
|
+
models = (tagsData.models || []).map(m => ({
|
|
11564
|
+
name: m.name,
|
|
11565
|
+
size: m.size || 0,
|
|
11566
|
+
parameterSize: m.details?.parameter_size || '',
|
|
11567
|
+
family: m.details?.family || '',
|
|
11568
|
+
quantization: m.details?.quantization_level || '',
|
|
11569
|
+
}));
|
|
11570
|
+
}
|
|
11571
|
+
} catch (e) {
|
|
11572
|
+
writeResp(id, { ok: false, output: 'Cannot reach Ollama at ' + ollamaUrl + ': ' + e.message });
|
|
11573
|
+
return;
|
|
11574
|
+
}
|
|
11575
|
+
|
|
11576
|
+
if (models.length === 0) {
|
|
11577
|
+
writeResp(id, { ok: false, output: 'No models found on Ollama. Pull a model first.' });
|
|
11578
|
+
return;
|
|
11579
|
+
}
|
|
11580
|
+
|
|
11581
|
+
// Fetch market rates from OpenRouter (free, no auth)
|
|
11582
|
+
let marketRates = {};
|
|
11583
|
+
try {
|
|
11584
|
+
const orResp = await fetch('https://openrouter.ai/api/v1/models');
|
|
11585
|
+
if (orResp.ok) {
|
|
11586
|
+
const orData = await orResp.json();
|
|
11587
|
+
for (const m of (orData.data || [])) {
|
|
11588
|
+
if (m.pricing) {
|
|
11589
|
+
marketRates[m.id] = {
|
|
11590
|
+
input: parseFloat(m.pricing.prompt || '0') * 1_000_000,
|
|
11591
|
+
output: parseFloat(m.pricing.completion || '0') * 1_000_000,
|
|
11592
|
+
};
|
|
11593
|
+
}
|
|
11594
|
+
}
|
|
11595
|
+
}
|
|
11596
|
+
} catch { /* offline \u2014 use zero rates */ }
|
|
11597
|
+
|
|
11598
|
+
// Build pricing menu \u2014 match local models to market rates
|
|
11599
|
+
const margin = parseFloat(args.margin || '0.5'); // default 50% of market rate
|
|
11600
|
+
const pricingMenu = [];
|
|
11601
|
+
|
|
11602
|
+
for (const model of models) {
|
|
11603
|
+
// Try to match to OpenRouter model ID
|
|
11604
|
+
const baseName = model.name.replace(/:latest$/, '').toLowerCase();
|
|
11605
|
+
let matched = null;
|
|
11606
|
+
for (const [orId, rates] of Object.entries(marketRates)) {
|
|
11607
|
+
const orBase = orId.toLowerCase();
|
|
11608
|
+
if (orBase.includes(baseName) || baseName.includes(orBase.split('/').pop())) {
|
|
11609
|
+
matched = { orId, ...rates };
|
|
11610
|
+
break;
|
|
11611
|
+
}
|
|
11612
|
+
}
|
|
11613
|
+
|
|
11614
|
+
const entry = {
|
|
11615
|
+
model: model.name,
|
|
11616
|
+
parameterSize: model.parameterSize,
|
|
11617
|
+
family: model.family,
|
|
11618
|
+
quantization: model.quantization,
|
|
11619
|
+
pricing: {
|
|
11620
|
+
input_per_1m_tokens: matched ? +(matched.input * margin).toFixed(4) : 0,
|
|
11621
|
+
output_per_1m_tokens: matched ? +(matched.output * margin).toFixed(4) : 0,
|
|
11622
|
+
currency: 'USD',
|
|
11623
|
+
source: matched ? 'openrouter:' + matched.orId : 'self-hosted:free',
|
|
11624
|
+
margin: margin,
|
|
11625
|
+
},
|
|
11626
|
+
};
|
|
11627
|
+
pricingMenu.push(entry);
|
|
11628
|
+
|
|
11629
|
+
// Register as nexus capability
|
|
11630
|
+
const capName = 'inference:' + model.name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
11631
|
+
if (typeof nexus.registerCapability === 'function') {
|
|
11632
|
+
nexus.registerCapability(capName, async (request, stream) => {
|
|
11633
|
+
const logEntry = {
|
|
11634
|
+
ts: Date.now(),
|
|
11635
|
+
from: request.from || 'unknown',
|
|
11636
|
+
capability: capName,
|
|
11637
|
+
model: model.name,
|
|
11638
|
+
requestId: request.requestId,
|
|
11639
|
+
};
|
|
11640
|
+
const logFile = join(invocationsDir, Date.now() + '-' + capName + '.json');
|
|
11641
|
+
try { writeFileSync(logFile, JSON.stringify(logEntry, null, 2)); } catch {}
|
|
11642
|
+
|
|
11643
|
+
// Collect input
|
|
11644
|
+
let prompt = '';
|
|
11645
|
+
stream.onData((msg) => {
|
|
11646
|
+
if (msg.type === 'invoke.chunk') {
|
|
11647
|
+
prompt += (typeof msg.data === 'string' ? msg.data : JSON.stringify(msg.data));
|
|
11648
|
+
}
|
|
11649
|
+
});
|
|
11650
|
+
|
|
11651
|
+
// Accept
|
|
11652
|
+
await stream.write({
|
|
11653
|
+
type: 'invoke.accept', version: 1,
|
|
11654
|
+
requestId: request.requestId, accepted: true,
|
|
11655
|
+
});
|
|
11656
|
+
|
|
11657
|
+
// Forward to Ollama
|
|
11658
|
+
try {
|
|
11659
|
+
const genResp = await fetch(ollamaUrl + '/api/generate', {
|
|
11660
|
+
method: 'POST',
|
|
11661
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11662
|
+
body: JSON.stringify({
|
|
11663
|
+
model: model.name,
|
|
11664
|
+
prompt: prompt || 'Hello',
|
|
11665
|
+
stream: false,
|
|
11666
|
+
}),
|
|
11667
|
+
});
|
|
11668
|
+
const genData = await genResp.json();
|
|
11669
|
+
const output = genData.response || '';
|
|
11670
|
+
const inputTokens = genData.prompt_eval_count || 0;
|
|
11671
|
+
const outputTokens = genData.eval_count || 0;
|
|
11672
|
+
|
|
11673
|
+
// Stream result back
|
|
11674
|
+
await stream.write({
|
|
11675
|
+
type: 'invoke.event', version: 1,
|
|
11676
|
+
requestId: request.requestId, seq: 0,
|
|
11677
|
+
event: 'result',
|
|
11678
|
+
data: JSON.stringify({
|
|
11679
|
+
model: model.name,
|
|
11680
|
+
response: output,
|
|
11681
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens },
|
|
11682
|
+
pricing: entry.pricing,
|
|
11683
|
+
}),
|
|
11684
|
+
});
|
|
11685
|
+
|
|
11686
|
+
await stream.write({
|
|
11687
|
+
type: 'invoke.done', version: 1,
|
|
11688
|
+
requestId: request.requestId,
|
|
11689
|
+
usage: {
|
|
11690
|
+
inputBytes: prompt.length,
|
|
11691
|
+
outputBytes: output.length,
|
|
11692
|
+
tokens: inputTokens + outputTokens,
|
|
11693
|
+
},
|
|
11694
|
+
});
|
|
11695
|
+
} catch (e) {
|
|
11696
|
+
await stream.write({
|
|
11697
|
+
type: 'invoke.event', version: 1,
|
|
11698
|
+
requestId: request.requestId, seq: 0,
|
|
11699
|
+
event: 'error', data: 'Inference failed: ' + e.message,
|
|
11700
|
+
});
|
|
11701
|
+
await stream.write({
|
|
11702
|
+
type: 'invoke.done', version: 1,
|
|
11703
|
+
requestId: request.requestId,
|
|
11704
|
+
usage: { inputBytes: 0, outputBytes: 0 },
|
|
11705
|
+
});
|
|
11706
|
+
}
|
|
11707
|
+
stream.close();
|
|
11708
|
+
});
|
|
11709
|
+
}
|
|
11710
|
+
}
|
|
11711
|
+
|
|
11712
|
+
// Write pricing menu to file
|
|
11713
|
+
const pricingFile = join(nexusDir, 'pricing.json');
|
|
11714
|
+
writeFileSync(pricingFile, JSON.stringify({ updated: new Date().toISOString(), models: pricingMenu }, null, 2));
|
|
11715
|
+
writeStatus({ exposedModels: pricingMenu.length });
|
|
11716
|
+
|
|
11717
|
+
const lines = ['Exposed ' + pricingMenu.length + ' model(s) as nexus capabilities:'];
|
|
11718
|
+
for (const p of pricingMenu) {
|
|
11719
|
+
const cost = p.pricing.input_per_1m_tokens === 0
|
|
11720
|
+
? 'FREE (self-hosted)'
|
|
11721
|
+
: '$' + p.pricing.input_per_1m_tokens + '/$' + p.pricing.output_per_1m_tokens + ' per 1M tokens';
|
|
11722
|
+
lines.push(' inference:' + p.model + ' \u2014 ' + cost);
|
|
11723
|
+
}
|
|
11724
|
+
lines.push('');
|
|
11725
|
+
lines.push('Pricing menu saved to ' + pricingFile);
|
|
11726
|
+
lines.push('Market rates: ' + Object.keys(marketRates).length + ' models from OpenRouter');
|
|
11727
|
+
writeResp(id, { ok: true, output: lines.join('\\n') });
|
|
11728
|
+
break;
|
|
11729
|
+
}
|
|
11730
|
+
|
|
11731
|
+
case 'pricing_menu': {
|
|
11732
|
+
const pricingFile = join(nexusDir, 'pricing.json');
|
|
11733
|
+
if (!existsSync(pricingFile)) {
|
|
11734
|
+
writeResp(id, { ok: false, output: 'No pricing menu. Run expose first.' });
|
|
11735
|
+
return;
|
|
11736
|
+
}
|
|
11737
|
+
try {
|
|
11738
|
+
const menu = JSON.parse(readFileSync(pricingFile, 'utf8'));
|
|
11739
|
+
const lines = ['Inference Pricing Menu (updated: ' + menu.updated + ')'];
|
|
11740
|
+
lines.push('');
|
|
11741
|
+
for (const m of (menu.models || [])) {
|
|
11742
|
+
const cost = m.pricing.input_per_1m_tokens === 0
|
|
11743
|
+
? 'FREE'
|
|
11744
|
+
: '$' + m.pricing.input_per_1m_tokens + ' in / $' + m.pricing.output_per_1m_tokens + ' out per 1M tokens';
|
|
11745
|
+
lines.push(' ' + m.model + ' (' + m.parameterSize + ', ' + m.quantization + ')');
|
|
11746
|
+
lines.push(' ' + cost + ' [' + m.pricing.source + ']');
|
|
11747
|
+
}
|
|
11748
|
+
writeResp(id, { ok: true, output: lines.join('\\n') });
|
|
11749
|
+
} catch (e) {
|
|
11750
|
+
writeResp(id, { ok: false, output: 'Failed to read pricing menu: ' + e.message });
|
|
11751
|
+
}
|
|
11752
|
+
break;
|
|
11753
|
+
}
|
|
11754
|
+
|
|
11375
11755
|
case 'ping': {
|
|
11376
11756
|
writeResp(id, { ok: true, output: 'pong' });
|
|
11377
11757
|
break;
|
|
@@ -11418,6 +11798,41 @@ process.on('unhandledRejection', (reason) => {
|
|
|
11418
11798
|
await node.start();
|
|
11419
11799
|
}
|
|
11420
11800
|
connected = true;
|
|
11801
|
+
|
|
11802
|
+
// v1.5.0: Setup metering audit hook
|
|
11803
|
+
try {
|
|
11804
|
+
if (nexus.metering && typeof nexus.metering.addHook === 'function') {
|
|
11805
|
+
nexus.metering.addHook(createFileAuditHook(meteringFile));
|
|
11806
|
+
}
|
|
11807
|
+
} catch {}
|
|
11808
|
+
|
|
11809
|
+
// v1.5.0: Client-level events for global message/DM/invoke routing
|
|
11810
|
+
try {
|
|
11811
|
+
if (typeof nexus.on === 'function') {
|
|
11812
|
+
nexus.on('message', ({ roomId, message }) => {
|
|
11813
|
+
// Already handled by per-room listener, but log globally
|
|
11814
|
+
console.log('[msg] ' + roomId + ' from ' + (message?.sender || '?').slice(0, 16));
|
|
11815
|
+
});
|
|
11816
|
+
nexus.on('dm', ({ from, content, format, messageId }) => {
|
|
11817
|
+
// Log DMs to inbox/dm/
|
|
11818
|
+
const dmDir = join(inboxDir, '_dm');
|
|
11819
|
+
mkdirSync(dmDir, { recursive: true });
|
|
11820
|
+
const entry = {
|
|
11821
|
+
sender: from,
|
|
11822
|
+
content: content || '',
|
|
11823
|
+
format: format || 'text/plain',
|
|
11824
|
+
timestamp: Date.now(),
|
|
11825
|
+
id: messageId,
|
|
11826
|
+
};
|
|
11827
|
+
try { writeFileSync(join(dmDir, Date.now() + '.json'), JSON.stringify(entry, null, 2)); } catch {}
|
|
11828
|
+
console.log('[dm] from ' + (from || '?').slice(0, 16));
|
|
11829
|
+
});
|
|
11830
|
+
nexus.on('invoke', ({ from, capability, requestId }) => {
|
|
11831
|
+
console.log('[invoke] ' + capability + ' from ' + (from || '?').slice(0, 16) + ' req=' + requestId);
|
|
11832
|
+
});
|
|
11833
|
+
}
|
|
11834
|
+
} catch {}
|
|
11835
|
+
|
|
11421
11836
|
writeStatus();
|
|
11422
11837
|
console.log('Nexus daemon connected as ' + nexus.peerId);
|
|
11423
11838
|
} catch (err) {
|
|
@@ -11445,7 +11860,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
|
|
|
11445
11860
|
];
|
|
11446
11861
|
NexusTool = class {
|
|
11447
11862
|
name = "nexus";
|
|
11448
|
-
description = "Decentralized agent-to-agent communication via open-agents-nexus v1.
|
|
11863
|
+
description = "Decentralized agent-to-agent communication via open-agents-nexus v1.5.0. Spawns a background Node.js process with real network sockets for libp2p + NATS P2P mesh. Simple flow: connect \u2192 join room \u2192 send messages. Auto-installs/updates nexus if needed. EXPOSE: expose action queries Ollama models, fetches OpenRouter market rates, registers metered inference capabilities so peers can invoke your models. pricing_menu shows rates. v1.5.0: register_capability, block_peer/unblock_peer, metering_status, room_members. Also supports direct peer invoke, IPFS storage, DMs, discovery, x402, inference proofs.";
|
|
11449
11864
|
parameters = {
|
|
11450
11865
|
type: "object",
|
|
11451
11866
|
properties: {
|
|
@@ -11468,7 +11883,16 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
|
|
|
11468
11883
|
"retrieve_content",
|
|
11469
11884
|
"wallet_status",
|
|
11470
11885
|
"wallet_create",
|
|
11471
|
-
"inference_proof"
|
|
11886
|
+
"inference_proof",
|
|
11887
|
+
"register_capability",
|
|
11888
|
+
"unregister_capability",
|
|
11889
|
+
"list_capabilities",
|
|
11890
|
+
"block_peer",
|
|
11891
|
+
"unblock_peer",
|
|
11892
|
+
"metering_status",
|
|
11893
|
+
"room_members",
|
|
11894
|
+
"expose",
|
|
11895
|
+
"pricing_menu"
|
|
11472
11896
|
],
|
|
11473
11897
|
description: "The nexus action to perform"
|
|
11474
11898
|
},
|
|
@@ -11515,6 +11939,14 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
|
|
|
11515
11939
|
cid: {
|
|
11516
11940
|
type: "string",
|
|
11517
11941
|
description: "Content ID for retrieve_content"
|
|
11942
|
+
},
|
|
11943
|
+
ollama_url: {
|
|
11944
|
+
type: "string",
|
|
11945
|
+
description: "Ollama API URL for expose (default: http://localhost:11434)"
|
|
11946
|
+
},
|
|
11947
|
+
margin: {
|
|
11948
|
+
type: "string",
|
|
11949
|
+
description: "Price margin multiplier for expose (0.5 = 50% of market rate, 0 = free). Default: 0.5"
|
|
11518
11950
|
}
|
|
11519
11951
|
},
|
|
11520
11952
|
required: ["action"],
|
|
@@ -11588,6 +12020,33 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
|
|
|
11588
12020
|
case "inference_proof":
|
|
11589
12021
|
result = await this.doInferenceProof();
|
|
11590
12022
|
break;
|
|
12023
|
+
case "register_capability":
|
|
12024
|
+
result = await this.sendDaemonCmd("register_capability", args);
|
|
12025
|
+
break;
|
|
12026
|
+
case "unregister_capability":
|
|
12027
|
+
result = await this.sendDaemonCmd("unregister_capability", args);
|
|
12028
|
+
break;
|
|
12029
|
+
case "list_capabilities":
|
|
12030
|
+
result = await this.sendDaemonCmd("list_capabilities", args);
|
|
12031
|
+
break;
|
|
12032
|
+
case "block_peer":
|
|
12033
|
+
result = await this.sendDaemonCmd("block_peer", args);
|
|
12034
|
+
break;
|
|
12035
|
+
case "unblock_peer":
|
|
12036
|
+
result = await this.sendDaemonCmd("unblock_peer", args);
|
|
12037
|
+
break;
|
|
12038
|
+
case "metering_status":
|
|
12039
|
+
result = await this.sendDaemonCmd("metering_status", args);
|
|
12040
|
+
break;
|
|
12041
|
+
case "room_members":
|
|
12042
|
+
result = await this.sendDaemonCmd("room_members", args);
|
|
12043
|
+
break;
|
|
12044
|
+
case "expose":
|
|
12045
|
+
result = await this.sendDaemonCmd("expose", args, 3e4);
|
|
12046
|
+
break;
|
|
12047
|
+
case "pricing_menu":
|
|
12048
|
+
result = await this.sendDaemonCmd("pricing_menu", args);
|
|
12049
|
+
break;
|
|
11591
12050
|
default:
|
|
11592
12051
|
return { success: false, output: "", error: `Unknown nexus action: ${action}`, durationMs: Date.now() - start };
|
|
11593
12052
|
}
|
|
@@ -11611,7 +12070,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
|
|
|
11611
12070
|
return null;
|
|
11612
12071
|
}
|
|
11613
12072
|
}
|
|
11614
|
-
async sendDaemonCmd(action, args) {
|
|
12073
|
+
async sendDaemonCmd(action, args, timeoutMs = 15e3) {
|
|
11615
12074
|
const pid = this.getDaemonPid();
|
|
11616
12075
|
if (!pid)
|
|
11617
12076
|
throw new Error("Nexus daemon not running. Use action 'connect' first.");
|
|
@@ -11622,7 +12081,8 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
|
|
|
11622
12081
|
await unlink(respFile).catch(() => {
|
|
11623
12082
|
});
|
|
11624
12083
|
await writeFile12(cmdFile, JSON.stringify({ id: cmdId, action, args }, null, 2));
|
|
11625
|
-
|
|
12084
|
+
const polls = Math.ceil(timeoutMs / 500);
|
|
12085
|
+
for (let i = 0; i < polls; i++) {
|
|
11626
12086
|
await new Promise((r) => setTimeout(r, 500));
|
|
11627
12087
|
if (!existsSync21(respFile))
|
|
11628
12088
|
continue;
|
|
@@ -11634,7 +12094,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
|
|
|
11634
12094
|
} catch {
|
|
11635
12095
|
}
|
|
11636
12096
|
}
|
|
11637
|
-
return
|
|
12097
|
+
return `Daemon did not respond within ${Math.round(timeoutMs / 1e3)}s. It may still be processing.`;
|
|
11638
12098
|
}
|
|
11639
12099
|
// =========================================================================
|
|
11640
12100
|
// Actions
|
|
@@ -11812,12 +12272,14 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
|
|
|
11812
12272
|
try {
|
|
11813
12273
|
const status = JSON.parse(await readFile13(statusFile, "utf8"));
|
|
11814
12274
|
const lines = [
|
|
11815
|
-
`Nexus Status`,
|
|
12275
|
+
`Nexus Status (v1.5.0)`,
|
|
11816
12276
|
` Connected: ${status.connected}`,
|
|
11817
12277
|
` Peer ID: ${status.peerId || "n/a"}`,
|
|
11818
12278
|
` Agent: ${status.agentName} (${status.agentType})`,
|
|
11819
12279
|
` Daemon PID: ${status.pid}`,
|
|
11820
|
-
` Rooms: ${status.rooms?.length || 0} [${(status.rooms || []).join(", ")}]
|
|
12280
|
+
` Rooms: ${status.rooms?.length || 0} [${(status.rooms || []).join(", ")}]`,
|
|
12281
|
+
` Capabilities: ${(status.capabilities || []).length ? (status.capabilities || []).join(", ") : "none"}`,
|
|
12282
|
+
` Blocked peers: ${(status.blockedPeers || []).length || 0}`
|
|
11821
12283
|
];
|
|
11822
12284
|
const walletPath = join28(this.nexusDir, "wallet.enc");
|
|
11823
12285
|
if (existsSync21(walletPath)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-agents-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.85.0",
|
|
4
4
|
"description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -70,6 +70,6 @@
|
|
|
70
70
|
},
|
|
71
71
|
"optionalDependencies": {
|
|
72
72
|
"moondream": "^0.2.0",
|
|
73
|
-
"open-agents-nexus": "^1.
|
|
73
|
+
"open-agents-nexus": "^1.5.0"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -26,7 +26,7 @@ If a tool fails, try a different approach. If you're unsure, explore with your t
|
|
|
26
26
|
- web_fetch: Fetch a web page and extract text content (for docs, MDN, w3schools.com, etc.)
|
|
27
27
|
- memory_read: Read from persistent memory (learned patterns, solutions)
|
|
28
28
|
- memory_write: Store a fact, pattern, or solution in persistent memory for future tasks
|
|
29
|
-
- nexus: P2P agent networking (libp2p + NATS + IPFS). Connect to other agents, join rooms, send messages, discover peers, invoke capabilities, store content, manage wallet.
|
|
29
|
+
- nexus: P2P agent networking (libp2p + NATS + IPFS). Connect to other agents, join rooms, send messages, discover peers, invoke capabilities, store content, manage wallet. Key actions: connect, join_room, send_message, discover_peers, invoke_capability, expose (metered inference), pricing_menu, register_capability, block_peer, metering_status, room_members. Full list: connect, disconnect, status, join_room, leave_room, send_message, read_messages, discover_peers, list_rooms, send_dm, find_agent, invoke_capability, store_content, retrieve_content, wallet_status, wallet_create, inference_proof, register_capability, unregister_capability, list_capabilities, block_peer, unblock_peer, metering_status, room_members, expose, pricing_menu.
|
|
30
30
|
- task_complete: Signal task completion with a summary
|
|
31
31
|
|
|
32
32
|
## Parallel Execution & Sub-Agents
|
|
@@ -185,7 +185,7 @@ If you notice you're performing the SAME multi-step sequence for the 3rd time or
|
|
|
185
185
|
- Test the tool mentally before creating — ensure the steps would work in order
|
|
186
186
|
- Prefer 'project' scope unless the pattern genuinely applies to all projects
|
|
187
187
|
|
|
188
|
-
## Nexus P2P Networking (v1.
|
|
188
|
+
## Nexus P2P Networking (v1.5.0) — Decentralized Agent Communication
|
|
189
189
|
|
|
190
190
|
You HAVE the nexus tool. It is one of your registered tools. USE IT when asked about connecting, messaging, or networking with other agents.
|
|
191
191
|
|
|
@@ -234,11 +234,42 @@ Use invoke_capability for real work (inference, tool calls) — NOT room message
|
|
|
234
234
|
nexus(action='wallet_create')
|
|
235
235
|
nexus(action='inference_proof')
|
|
236
236
|
|
|
237
|
+
### v1.5.0: Serve Capabilities
|
|
238
|
+
nexus(action='register_capability', capability='text-generation') — register handler for incoming invocations
|
|
239
|
+
nexus(action='unregister_capability', capability='text-generation')
|
|
240
|
+
nexus(action='list_capabilities') — list registered capability names
|
|
241
|
+
|
|
242
|
+
### v1.5.0: Trust & Blocking
|
|
243
|
+
nexus(action='block_peer', target_peer='12D3KooW...') — blocks invoke + DM from peer
|
|
244
|
+
nexus(action='unblock_peer', target_peer='12D3KooW...')
|
|
245
|
+
|
|
246
|
+
### v1.5.0: Usage Metering
|
|
247
|
+
nexus(action='metering_status') — all peer summaries
|
|
248
|
+
nexus(action='metering_status', peer_id='12D3KooW...') — per-peer summary
|
|
249
|
+
nexus(action='metering_status', capability='chat') — filter by service
|
|
250
|
+
|
|
251
|
+
### v1.5.0: Room Members
|
|
252
|
+
nexus(action='room_members', room_id='general') — live member list with capabilities
|
|
253
|
+
|
|
254
|
+
### Metered Inference Exposure
|
|
255
|
+
nexus(action='expose') — expose ALL local Ollama models as nexus capabilities
|
|
256
|
+
nexus(action='expose', margin='0.5') — set pricing at 50% of market rate (default)
|
|
257
|
+
nexus(action='expose', margin='0') — expose for free (self-hosted, no cost)
|
|
258
|
+
nexus(action='expose', margin='1.0') — match market rate
|
|
259
|
+
nexus(action='pricing_menu') — show current pricing menu for exposed models
|
|
260
|
+
|
|
261
|
+
expose queries local Ollama for models, fetches live market rates from OpenRouter
|
|
262
|
+
(https://openrouter.ai/api/v1/models — free, no auth), registers each model as a
|
|
263
|
+
nexus capability (inference:{model_name}), and writes pricing to .oa/nexus/pricing.json.
|
|
264
|
+
Peers can invoke your models via invoke_capability and see metered usage.
|
|
265
|
+
|
|
237
266
|
SECURITY: Wallet private keys are AES-256-GCM encrypted and NEVER accessible to you.
|
|
238
267
|
All outbound messages are scanned for key material leaks.
|
|
239
268
|
|
|
240
269
|
When the user asks about expanding capabilities or connecting with other agents, suggest
|
|
241
|
-
enabling nexus networking. Use
|
|
270
|
+
enabling nexus networking. Use expose to share your models with the network, pricing_menu
|
|
271
|
+
to check rates, register_capability to serve custom invocations, and room_members to
|
|
272
|
+
discover who's online.
|
|
242
273
|
|
|
243
274
|
## Temporal Agency — Scheduling, Reminders & Long-Horizon Tasks
|
|
244
275
|
|
|
@@ -17,7 +17,7 @@ NEVER say "I can't do that". ALWAYS attempt the task using your tools. If a tool
|
|
|
17
17
|
- web_search: Search the web
|
|
18
18
|
- web_fetch: Fetch a web page's text
|
|
19
19
|
- memory_read / memory_write: Persistent memory across sessions
|
|
20
|
-
- nexus: P2P agent networking (connect, join_room, send_message, discover_peers, invoke_capability, wallet, etc.)
|
|
20
|
+
- nexus: P2P agent networking (connect, join_room, send_message, discover_peers, invoke_capability, register_capability, block_peer, metering_status, room_members, wallet, etc.)
|
|
21
21
|
- task_complete: Signal completion with a summary
|
|
22
22
|
- background_run / task_status / task_output / task_stop: Background tasks
|
|
23
23
|
- batch_edit: Multiple edits across files in one call
|