open-agents-ai 0.87.0 → 0.89.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 CHANGED
@@ -999,6 +999,7 @@ The steering sub-agent uses the same model and backend as the main agent with `m
999
999
  | `nexus:expose` | Expose local Ollama models as metered inference capabilities with OpenRouter-based pricing |
1000
1000
  | `nexus:wallet_create` | Generate secp256k1/EVM wallet (Base mainnet USDC) with AES-256-GCM encryption + x402-wallet.key |
1001
1001
  | `nexus:spend` | Sign EIP-3009 USDC TransferWithAuthorization — budget-checked, gasless for payer |
1002
+ | `nexus:remote_infer` | Route inference to a remote peer's model — auto-discovers peers, budget-checks, invokes, returns result |
1002
1003
  | `nexus:ledger_status` | Transaction history (earned/spent/pending USDC) |
1003
1004
  | `nexus:budget_set` | Configure spending limits — daily cap, per-invoke max, auto-approve threshold |
1004
1005
 
@@ -1077,6 +1078,13 @@ nexus(action='spend', target_address='0x...', amount_usdc='0.10')
1077
1078
  ```
1078
1079
  Signs an EIP-3009 `TransferWithAuthorization`. Budget-checked before signing. The recipient (or any facilitator) submits on-chain — no gas needed from the payer. Proof saved to `.oa/nexus/pending-transfer.json`.
1079
1080
 
1081
+ ### Remote Inference — Tap Into the Mesh
1082
+ ```
1083
+ nexus(action='remote_infer', model='qwen3.5:70b', prompt='Complex analysis task...')
1084
+ nexus(action='remote_infer', model='llama3.3:70b', prompt='...', target_peer='12D3KooW...')
1085
+ ```
1086
+ Route a prompt to a remote peer's model on the P2P mesh. Auto-discovers peers that have the requested model exposed, budget-checks the estimated cost, invokes the inference capability, and returns the response. Use `target_peer` to route to a specific provider, or omit for automatic peer selection. Your 8B laptop can seamlessly tap into a 122B model running on the mesh.
1087
+
1080
1088
  ### Ledger & Budget
1081
1089
  ```
1082
1090
  nexus(action='ledger_status') # Earned/spent/pending history
@@ -1093,6 +1101,7 @@ nexus(action='budget_set', auto_approve_below='0.01') # Auto-approve micropayme
1093
1101
  4. Consumer's daemon auto-signs `payment_proof` → provider validates → invoke proceeds
1094
1102
  5. Metering hook writes payment events to `ledger.jsonl`
1095
1103
  6. **spend** → direct agent-to-agent USDC transfers (EIP-3009, gasless)
1104
+ 7. **remote_infer** → auto-discover + invoke in one action (budget-checked, with ledger entry)
1096
1105
 
1097
1106
  ### Security Model
1098
1107
  - Private keys: AES-256-GCM encrypted in `wallet.enc` (scrypt-derived key)
package/dist/index.js CHANGED
@@ -139,7 +139,7 @@ var init_config = __esm({
139
139
  verbose: false,
140
140
  dbPath: join(homedir(), ".open-agents", "memory.db")
141
141
  });
142
- VALID_BACKEND_TYPES = /* @__PURE__ */ new Set(["ollama", "vllm", "fake"]);
142
+ VALID_BACKEND_TYPES = /* @__PURE__ */ new Set(["ollama", "vllm", "fake", "nexus"]);
143
143
  }
144
144
  });
145
145
 
@@ -11202,7 +11202,7 @@ ${truncated}`, durationMs: performance.now() - start };
11202
11202
  });
11203
11203
 
11204
11204
  // packages/execution/dist/tools/nexus.js
11205
- import { readFile as readFile13, writeFile as writeFile12, mkdir as mkdir8, chmod, unlink, readdir as readdir3 } from "node:fs/promises";
11205
+ import { readFile as readFile13, writeFile as writeFile12, mkdir as mkdir8, chmod, unlink, readdir as readdir3, open as fsOpen } from "node:fs/promises";
11206
11206
  import { existsSync as existsSync21, readFileSync as readFileSync15 } from "node:fs";
11207
11207
  import { resolve as resolve26, join as join28 } from "node:path";
11208
11208
  import { randomBytes as randomBytes6, createCipheriv, createDecipheriv, scryptSync, createHash } from "node:crypto";
@@ -11405,6 +11405,101 @@ async function handleCmd(cmd) {
11405
11405
  }
11406
11406
  break;
11407
11407
  }
11408
+ case 'remote_infer': {
11409
+ var riModel = args.model;
11410
+ var riPrompt = args.prompt || args.input || '';
11411
+ var riMessages = args.messages || null;
11412
+ var riTools = args.tools || null;
11413
+ var riTargetPeer = args.target_peer || '';
11414
+ if (!riModel) { writeResp(id, { ok: false, output: 'model is required' }); return; }
11415
+ if (!riPrompt && !riMessages) { writeResp(id, { ok: false, output: 'prompt or messages is required' }); return; }
11416
+
11417
+ // Build invoke data \u2014 structured (messages+tools) or flat (prompt)
11418
+ var riData;
11419
+ if (riMessages) {
11420
+ var riParsedMsgs = typeof riMessages === 'string' ? JSON.parse(riMessages) : riMessages;
11421
+ riData = { messages: riParsedMsgs };
11422
+ if (riTools) {
11423
+ riData.tools = typeof riTools === 'string' ? JSON.parse(riTools) : riTools;
11424
+ }
11425
+ if (args.temperature !== undefined) riData.temperature = Number(args.temperature);
11426
+ if (args.max_tokens) riData.max_tokens = Number(args.max_tokens);
11427
+ } else {
11428
+ riData = { prompt: riPrompt };
11429
+ }
11430
+
11431
+ // Derive capability name from model
11432
+ var riCapName = 'inference:' + riModel.replace(/[^a-zA-Z0-9._-]/g, '_');
11433
+ dlog('remote_infer: model=' + riModel + ' cap=' + riCapName + ' prompt_len=' + riPrompt.length);
11434
+
11435
+ // If target_peer specified, invoke directly. Otherwise auto-discover.
11436
+ if (riTargetPeer) {
11437
+ // Direct invoke on specified peer
11438
+ dlog('remote_infer: invoking ' + riCapName + ' on explicit peer ' + riTargetPeer.slice(0, 20));
11439
+ try {
11440
+ var RI_TIMEOUT = 120000;
11441
+ var riInvokeP = nexus.invokeCapability(riTargetPeer, riCapName, riData, { stream: false, maxDurationMs: 90000 });
11442
+ var riTimeoutP = new Promise(function(_, reject) { setTimeout(function() { reject(new Error('remote_infer timed out after ' + (RI_TIMEOUT / 1000) + 's')); }, RI_TIMEOUT); });
11443
+ var riResult = await Promise.race([riInvokeP, riTimeoutP]);
11444
+ dlog('remote_infer: SUCCESS (explicit peer)');
11445
+ } catch (riErr) {
11446
+ dlog('remote_infer: ERROR (explicit peer) ' + (riErr.message || String(riErr)));
11447
+ writeResp(id, { ok: false, output: 'Remote inference failed: ' + (riErr.message || String(riErr)) });
11448
+ return;
11449
+ }
11450
+ } else {
11451
+ // Auto-discover: try speculative invoke on each 12D3KooW peer
11452
+ // Skip Qm-prefixed bootstrap/relay nodes \u2014 they never host inference
11453
+ var discStart = Date.now();
11454
+ var riResult = null;
11455
+ try {
11456
+ var riNode = nexus.network ? nexus.network.node : null;
11457
+ var riPeers = riNode && typeof riNode.getPeers === 'function' ? riNode.getPeers() : [];
11458
+ var riCandidates = [];
11459
+ for (var rfi = 0; rfi < riPeers.length; rfi++) {
11460
+ var rpStr = riPeers[rfi].toString ? riPeers[rfi].toString() : String(riPeers[rfi]);
11461
+ if (rpStr.startsWith('12D3KooW')) riCandidates.push(rpStr);
11462
+ }
11463
+ dlog('remote_infer: auto-discover across ' + riCandidates.length + ' peers (of ' + riPeers.length + ' total) for ' + riCapName);
11464
+
11465
+ for (var ri = 0; ri < riCandidates.length && !riResult; ri++) {
11466
+ var rPid = riCandidates[ri];
11467
+ dlog('remote_infer: trying peer ' + rPid.slice(0, 20) + '...');
11468
+ try {
11469
+ var PEER_INVOKE_TIMEOUT = 30000; // 30s per peer attempt
11470
+ var specInvoke = nexus.invokeCapability(rPid, riCapName, riData, { stream: false, maxDurationMs: 25000 });
11471
+ var specTimeout = new Promise(function(_, reject) {
11472
+ setTimeout(function() { reject(new Error('peer invoke timeout')); }, PEER_INVOKE_TIMEOUT);
11473
+ });
11474
+ riResult = await Promise.race([specInvoke, specTimeout]);
11475
+ riTargetPeer = rPid;
11476
+ dlog('remote_infer: SUCCESS via peer ' + rPid.slice(0, 20) + ' (' + (Date.now() - discStart) + 'ms)');
11477
+ } catch (specErr) {
11478
+ dlog('remote_infer: peer ' + rPid.slice(0, 20) + ' failed: ' + (specErr.message || specErr));
11479
+ }
11480
+ }
11481
+ } catch (discErr) {
11482
+ dlog('remote_infer: discovery error: ' + (discErr.message || discErr));
11483
+ }
11484
+ dlog('remote_infer: auto-discover took ' + (Date.now() - discStart) + 'ms');
11485
+
11486
+ if (!riResult) {
11487
+ writeResp(id, { ok: false, output: 'No peer found with capability: ' + riCapName + '. Ensure a provider is running expose with model ' + riModel });
11488
+ return;
11489
+ }
11490
+ }
11491
+
11492
+ // riResult and riTargetPeer are available from either path above
11493
+ var riOutput = {
11494
+ success: true,
11495
+ model: riModel,
11496
+ peer: riTargetPeer,
11497
+ capability: riCapName,
11498
+ result: riResult,
11499
+ };
11500
+ writeResp(id, { ok: true, output: JSON.stringify(riOutput, null, 2) });
11501
+ break;
11502
+ }
11408
11503
  case 'store_content': {
11409
11504
  const data = args.data;
11410
11505
  if (!data) { writeResp(id, { ok: false, output: 'data is required' }); return; }
@@ -11679,11 +11774,17 @@ async function handleCmd(cmd) {
11679
11774
  const logFile = join(invocationsDir, Date.now() + '-' + capName + '.json');
11680
11775
  try { writeFileSync(logFile, JSON.stringify(logEntry, null, 2)); } catch {}
11681
11776
 
11682
- // Collect input
11777
+ // Collect input via stream data events
11683
11778
  let prompt = '';
11779
+ var dataChunks = [];
11780
+ var inputDone = false;
11684
11781
  stream.onData((msg) => {
11685
11782
  if (msg.type === 'invoke.chunk') {
11686
- prompt += (typeof msg.data === 'string' ? msg.data : JSON.stringify(msg.data));
11783
+ var chunk = typeof msg.data === 'string' ? msg.data : JSON.stringify(msg.data);
11784
+ dataChunks.push(chunk);
11785
+ }
11786
+ if (msg.type === 'invoke.done' || msg.type === 'invoke.end' || msg.type === 'invoke.close') {
11787
+ inputDone = true;
11687
11788
  }
11688
11789
  });
11689
11790
 
@@ -11693,33 +11794,100 @@ async function handleCmd(cmd) {
11693
11794
  requestId: request.requestId, accepted: true,
11694
11795
  });
11695
11796
 
11696
- // Forward to Ollama
11797
+ // Wait for input data to arrive (invoke protocol sends data after accept)
11798
+ var waitMs = 0;
11799
+ while (!inputDone && dataChunks.length === 0 && waitMs < 5000) {
11800
+ await new Promise(function(r) { setTimeout(r, 100); });
11801
+ waitMs += 100;
11802
+ }
11803
+ prompt = dataChunks.join('');
11804
+ dlog('expose: received ' + dataChunks.length + ' chunks, prompt_len=' + prompt.length + ' inputDone=' + inputDone);
11805
+
11806
+ // Forward to Ollama \u2014 supports both flat prompt and structured messages
11697
11807
  try {
11698
- const genResp = await fetch(ollamaUrl + '/api/generate', {
11699
- method: 'POST',
11700
- headers: { 'Content-Type': 'application/json' },
11701
- body: JSON.stringify({
11808
+ var parsedReq = null;
11809
+ try { parsedReq = JSON.parse(prompt); } catch (pe) { dlog('expose: JSON parse error: ' + (pe.message || pe)); }
11810
+
11811
+ var genResp, genData, output, inputTokens, outputTokens, responsePayload;
11812
+
11813
+ if (parsedReq && parsedReq.messages && Array.isArray(parsedReq.messages)) {
11814
+ // Structured request \u2014 use /v1/chat/completions (supports tools + multi-turn)
11815
+ var chatBody = {
11702
11816
  model: model.name,
11703
- prompt: prompt || 'Hello',
11817
+ messages: parsedReq.messages,
11704
11818
  stream: false,
11705
- }),
11706
- });
11707
- const genData = await genResp.json();
11708
- const output = genData.response || '';
11709
- const inputTokens = genData.prompt_eval_count || 0;
11710
- const outputTokens = genData.eval_count || 0;
11819
+ };
11820
+ if (parsedReq.tools && parsedReq.tools.length > 0) {
11821
+ chatBody.tools = parsedReq.tools;
11822
+ }
11823
+ if (parsedReq.temperature !== undefined) chatBody.temperature = parsedReq.temperature;
11824
+ if (parsedReq.max_tokens) chatBody.max_tokens = parsedReq.max_tokens;
11825
+
11826
+ dlog('expose: calling /v1/chat/completions with ' + chatBody.messages.length + ' messages, tools=' + (chatBody.tools ? chatBody.tools.length : 0));
11827
+ genResp = await fetch(ollamaUrl + '/v1/chat/completions', {
11828
+ method: 'POST',
11829
+ headers: { 'Content-Type': 'application/json' },
11830
+ body: JSON.stringify(chatBody),
11831
+ });
11832
+ genData = await genResp.json();
11833
+ dlog('expose: ollama response keys=' + Object.keys(genData).join(',') + ' choices=' + (genData.choices ? genData.choices.length : 0));
11834
+
11835
+ var chatChoices = genData.choices || [];
11836
+ var firstMsg = (chatChoices[0] || {}).message || {};
11837
+ // Strip <think>...</think> tags from content (thinking models)
11838
+ var rawContent = firstMsg.content || '';
11839
+ output = rawContent.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();
11840
+ // Fallback: if content is empty but reasoning exists, use reasoning
11841
+ // (happens when max_tokens is too small for thinking models at temp=0)
11842
+ if (!output && firstMsg.reasoning) {
11843
+ output = firstMsg.reasoning;
11844
+ }
11845
+ // Update the choices to reflect final content
11846
+ if (chatChoices[0] && chatChoices[0].message) {
11847
+ chatChoices[0].message.content = output;
11848
+ }
11849
+ dlog('expose: rawContent_len=' + rawContent.length + ' output_len=' + output.length + ' reasoning=' + (firstMsg.reasoning ? firstMsg.reasoning.length : 0) + ' tool_calls=' + (firstMsg.tool_calls ? firstMsg.tool_calls.length : 0));
11850
+ inputTokens = (genData.usage || {}).prompt_tokens || 0;
11851
+ outputTokens = (genData.usage || {}).completion_tokens || 0;
11852
+
11853
+ responsePayload = {
11854
+ model: model.name,
11855
+ response: output,
11856
+ choices: chatChoices,
11857
+ usage: { input_tokens: inputTokens, output_tokens: outputTokens },
11858
+ pricing: entry.pricing,
11859
+ };
11860
+ } else {
11861
+ // Flat prompt \u2014 use /api/generate (original behavior)
11862
+ var flatPrompt = parsedReq && parsedReq.prompt ? parsedReq.prompt : (prompt || 'Hello');
11863
+ genResp = await fetch(ollamaUrl + '/api/generate', {
11864
+ method: 'POST',
11865
+ headers: { 'Content-Type': 'application/json' },
11866
+ body: JSON.stringify({
11867
+ model: model.name,
11868
+ prompt: flatPrompt,
11869
+ stream: false,
11870
+ }),
11871
+ });
11872
+ genData = await genResp.json();
11873
+ output = genData.response || '';
11874
+ inputTokens = genData.prompt_eval_count || 0;
11875
+ outputTokens = genData.eval_count || 0;
11876
+
11877
+ responsePayload = {
11878
+ model: model.name,
11879
+ response: output,
11880
+ usage: { input_tokens: inputTokens, output_tokens: outputTokens },
11881
+ pricing: entry.pricing,
11882
+ };
11883
+ }
11711
11884
 
11712
11885
  // Stream result back
11713
11886
  await stream.write({
11714
11887
  type: 'invoke.event', version: 1,
11715
11888
  requestId: request.requestId, seq: 0,
11716
11889
  event: 'result',
11717
- data: JSON.stringify({
11718
- model: model.name,
11719
- response: output,
11720
- usage: { input_tokens: inputTokens, output_tokens: outputTokens },
11721
- pricing: entry.pricing,
11722
- }),
11890
+ data: JSON.stringify(responsePayload),
11723
11891
  });
11724
11892
 
11725
11893
  await stream.write({
@@ -11727,7 +11895,7 @@ async function handleCmd(cmd) {
11727
11895
  requestId: request.requestId,
11728
11896
  usage: {
11729
11897
  inputBytes: prompt.length,
11730
- outputBytes: output.length,
11898
+ outputBytes: (output || '').length,
11731
11899
  tokens: inputTokens + outputTokens,
11732
11900
  },
11733
11901
  });
@@ -11932,7 +12100,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11932
12100
  ];
11933
12101
  NexusTool = class {
11934
12102
  name = "nexus";
11935
- 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. WALLET: wallet_create generates EVM wallet (secp256k1, Base mainnet USDC). wallet_status shows address, USDC balance, and ledger summary. PAYMENTS: ledger_status tracks earnings/spending. budget_status/budget_set configure spending limits. spend: sign EIP-3009 USDC transfer (target_address + amount_usdc). x402 native payment via daemon.";
12103
+ 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. WALLET: wallet_create generates EVM wallet (secp256k1, Base mainnet USDC). wallet_status shows address, USDC balance, and ledger summary. PAYMENTS: ledger_status tracks earnings/spending. budget_status/budget_set configure spending limits. spend: sign EIP-3009 USDC transfer (target_address + amount_usdc). x402 native payment via daemon. REMOTE INFERENCE: remote_infer routes a prompt to a remote peer's model on the mesh. Auto-discovers peers with the requested model capability, budget-checks, invokes, and returns the result. Use: nexus(action='remote_infer', model='qwen3.5:70b', prompt='...'). Optional: target_peer, temperature, max_tokens.";
11936
12104
  parameters = {
11937
12105
  type: "object",
11938
12106
  properties: {
@@ -11968,7 +12136,8 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11968
12136
  "ledger_status",
11969
12137
  "budget_status",
11970
12138
  "budget_set",
11971
- "spend"
12139
+ "spend",
12140
+ "remote_infer"
11972
12141
  ],
11973
12142
  description: "The nexus action to perform"
11974
12143
  },
@@ -12043,6 +12212,22 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12043
12212
  amount_usdc: {
12044
12213
  type: "string",
12045
12214
  description: "For spend: USDC amount to transfer (e.g. '0.10')"
12215
+ },
12216
+ model: {
12217
+ type: "string",
12218
+ description: "For remote_infer: model to request (e.g. 'qwen3.5:70b', 'nemotron-3-nano:30b')"
12219
+ },
12220
+ prompt: {
12221
+ type: "string",
12222
+ description: "For remote_infer: the prompt text to send to the remote model"
12223
+ },
12224
+ temperature: {
12225
+ type: "string",
12226
+ description: "For remote_infer: sampling temperature (e.g. '0.7'). Default: 0.7"
12227
+ },
12228
+ max_tokens: {
12229
+ type: "string",
12230
+ description: "For remote_infer: maximum tokens to generate (e.g. '4096'). Default: 4096"
12046
12231
  }
12047
12232
  },
12048
12233
  required: ["action"],
@@ -12155,6 +12340,9 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12155
12340
  case "spend":
12156
12341
  result = await this.doSpend(args);
12157
12342
  break;
12343
+ case "remote_infer":
12344
+ result = await this.doRemoteInfer(args);
12345
+ break;
12158
12346
  default:
12159
12347
  return { success: false, output: "", error: `Unknown nexus action: ${action}`, durationMs: Date.now() - start };
12160
12348
  }
@@ -12204,6 +12392,14 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12204
12392
  }
12205
12393
  return `Daemon did not respond within ${Math.round(timeoutMs / 1e3)}s. It may still be processing.`;
12206
12394
  }
12395
+ /** Public interface for daemon commands — used by NexusAgenticBackend */
12396
+ async sendCommand(action, args, timeoutMs) {
12397
+ return this.sendDaemonCmd(action, args, timeoutMs);
12398
+ }
12399
+ /** Get the nexus data directory path */
12400
+ getNexusDir() {
12401
+ return this.nexusDir;
12402
+ }
12207
12403
  // =========================================================================
12208
12404
  // Actions
12209
12405
  // =========================================================================
@@ -12551,8 +12747,9 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12551
12747
  network: "base",
12552
12748
  chainId: 8453
12553
12749
  };
12554
- await writeFile12(walletPath, JSON.stringify(w2, null, 2));
12555
- await chmod(walletPath, 384);
12750
+ const walletFh = await fsOpen(walletPath, "w", 384);
12751
+ await walletFh.writeFile(JSON.stringify(w2, null, 2));
12752
+ await walletFh.close();
12556
12753
  await this.ensureDefaultBudget();
12557
12754
  return `Wallet configured: ${userAddress} (user-managed, Base mainnet, no x402 key \u2014 spend/sign requires self-hosted wallet)`;
12558
12755
  }
@@ -12578,8 +12775,9 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12578
12775
  let enc = cipher.update(privKeyHex, "utf8", "hex");
12579
12776
  enc += cipher.final("hex");
12580
12777
  const x402KeyPath = join28(this.nexusDir, "x402-wallet.key");
12581
- await writeFile12(x402KeyPath, "0x" + privKeyHex);
12582
- await chmod(x402KeyPath, 384);
12778
+ const x402Fh = await fsOpen(x402KeyPath, "w", 384);
12779
+ await x402Fh.writeFile("0x" + privKeyHex);
12780
+ await x402Fh.close();
12583
12781
  privKeyHex = "0".repeat(64);
12584
12782
  const w = {
12585
12783
  version: 2,
@@ -12592,8 +12790,9 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12592
12790
  network: "base",
12593
12791
  chainId: 8453
12594
12792
  };
12595
- await writeFile12(walletPath, JSON.stringify(w, null, 2));
12596
- await chmod(walletPath, 384);
12793
+ const walletFh2 = await fsOpen(walletPath, "w", 384);
12794
+ await walletFh2.writeFile(JSON.stringify(w, null, 2));
12795
+ await walletFh2.close();
12597
12796
  await this.ensureDefaultBudget();
12598
12797
  return [
12599
12798
  `Wallet created (v2, secp256k1/EVM):`,
@@ -12748,7 +12947,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12748
12947
  // $0.50 — warn when low
12749
12948
  circuitBreakerUsdc: 1e5,
12750
12949
  // $0.10 — refuse payments below this balance
12751
- allowedCapabilities: ["inference:*", "transfer:direct"],
12950
+ allowedCapabilities: ["inference:*", "transfer:direct", "remote_infer"],
12752
12951
  deniedPeers: []
12753
12952
  };
12754
12953
  await this.ensureDir();
@@ -12878,7 +13077,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12878
13077
  const amountSmallest = Math.round(parseFloat(amountUsd) * 1e6);
12879
13078
  if (amountSmallest <= 0)
12880
13079
  throw new Error("amount must be positive");
12881
- const budgetResult = await this.checkBudget(amountSmallest, "transfer:direct", "");
13080
+ const budgetResult = await this.checkBudget(amountSmallest, "transfer:direct", targetAddress);
12882
13081
  if (!budgetResult.allowed)
12883
13082
  return `BLOCKED by budget policy: ${budgetResult.reason}`;
12884
13083
  const walletPath = join28(this.nexusDir, "wallet.enc");
@@ -12972,6 +13171,109 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12972
13171
  privKeyHex = "0".repeat(64);
12973
13172
  }
12974
13173
  }
13174
+ // ---------------------------------------------------------------------------
13175
+ // Step 6: Remote Inference — Route inference to remote peers on the mesh
13176
+ // ---------------------------------------------------------------------------
13177
+ async doRemoteInfer(args) {
13178
+ const model = args.model;
13179
+ const prompt = args.prompt || args.input;
13180
+ if (!model)
13181
+ throw new Error("model is required (e.g., 'qwen3.5:70b', 'nemotron-3-nano:30b')");
13182
+ if (!prompt)
13183
+ throw new Error("prompt is required");
13184
+ const estimatedTokens = Math.ceil(prompt.length / 4) + 1e3;
13185
+ let estimatedCostSmallest = 0;
13186
+ const pricingPath = join28(this.nexusDir, "pricing.json");
13187
+ if (existsSync21(pricingPath)) {
13188
+ try {
13189
+ const pricing = JSON.parse(await readFile13(pricingPath, "utf8"));
13190
+ const modelPricing = (pricing.models || []).find((m) => m.model === model || m.model.startsWith(model.split(":")[0]));
13191
+ if (modelPricing?.pricing?.input_per_1m_tokens > 0) {
13192
+ estimatedCostSmallest = Math.ceil(estimatedTokens / 1e6 * modelPricing.pricing.input_per_1m_tokens * 1e6);
13193
+ }
13194
+ } catch {
13195
+ }
13196
+ }
13197
+ if (estimatedCostSmallest > 0) {
13198
+ const budgetResult = await this.checkBudget(estimatedCostSmallest, "remote_infer", args.target_peer || "");
13199
+ if (!budgetResult.allowed) {
13200
+ return `BLOCKED by budget policy: ${budgetResult.reason}`;
13201
+ }
13202
+ }
13203
+ const budget = await this.loadBudget();
13204
+ if (budget && !budget.allowedCapabilities.some((c3) => c3 === "remote_infer" || c3 === "inference:*" || c3 === "*")) {
13205
+ return "BLOCKED: remote_infer not in budget allowedCapabilities. Use budget_set to allow it.";
13206
+ }
13207
+ const daemonArgs = {
13208
+ model,
13209
+ target_peer: args.target_peer,
13210
+ temperature: args.temperature,
13211
+ max_tokens: args.max_tokens
13212
+ };
13213
+ if (args.messages) {
13214
+ daemonArgs.messages = args.messages;
13215
+ if (args.tools)
13216
+ daemonArgs.tools = args.tools;
13217
+ } else {
13218
+ daemonArgs.prompt = prompt;
13219
+ }
13220
+ const result = await this.sendDaemonCmd("remote_infer", daemonArgs, 12e4);
13221
+ let peerUsed = args.target_peer || "auto-selected";
13222
+ try {
13223
+ const parsed = JSON.parse(result);
13224
+ if (parsed.peer)
13225
+ peerUsed = parsed.peer;
13226
+ if (estimatedCostSmallest > 0) {
13227
+ await this.appendLedger({
13228
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
13229
+ type: "spent",
13230
+ amount: String(estimatedCostSmallest),
13231
+ amountUsd: (estimatedCostSmallest / 1e6).toFixed(6),
13232
+ peer: peerUsed,
13233
+ capability: "remote_infer:" + model,
13234
+ note: "remote inference"
13235
+ });
13236
+ }
13237
+ const inferResult = parsed.result;
13238
+ let responseText = "";
13239
+ if (typeof inferResult === "string") {
13240
+ responseText = inferResult;
13241
+ } else if (inferResult) {
13242
+ const events = Array.isArray(inferResult) ? inferResult : [inferResult];
13243
+ for (const evt of events) {
13244
+ if (evt.event === "result" && evt.data) {
13245
+ try {
13246
+ const data = typeof evt.data === "string" ? JSON.parse(evt.data) : evt.data;
13247
+ responseText = data.response || data.content || JSON.stringify(data);
13248
+ } catch {
13249
+ responseText = String(evt.data);
13250
+ }
13251
+ }
13252
+ }
13253
+ if (!responseText) {
13254
+ responseText = JSON.stringify(inferResult, null, 2);
13255
+ }
13256
+ }
13257
+ const lines = [
13258
+ `Remote inference result (${model} via ${peerUsed.slice(0, 20)}...):`,
13259
+ ``,
13260
+ responseText,
13261
+ ``,
13262
+ `--- Metadata ---`,
13263
+ `Model: ${parsed.model || model}`,
13264
+ `Peer: ${peerUsed}`,
13265
+ `Capability: ${parsed.capability || "inference:" + model}`
13266
+ ];
13267
+ if (estimatedCostSmallest > 0) {
13268
+ lines.push(`Estimated cost: $${(estimatedCostSmallest / 1e6).toFixed(6)} USDC`);
13269
+ } else {
13270
+ lines.push(`Cost: FREE (provider margin = 0)`);
13271
+ }
13272
+ return lines.join("\n");
13273
+ } catch {
13274
+ return result;
13275
+ }
13276
+ }
12975
13277
  async doInferenceProof() {
12976
13278
  try {
12977
13279
  const psRaw = execSync21("ollama ps 2>/dev/null", { timeout: 1e4, encoding: "utf8" });
@@ -17924,6 +18226,218 @@ ${transcript}`
17924
18226
  }
17925
18227
  });
17926
18228
 
18229
+ // packages/orchestrator/dist/nexusBackend.js
18230
+ var NexusAgenticBackend;
18231
+ var init_nexusBackend = __esm({
18232
+ "packages/orchestrator/dist/nexusBackend.js"() {
18233
+ "use strict";
18234
+ NexusAgenticBackend = class {
18235
+ sendFn;
18236
+ model;
18237
+ targetPeer;
18238
+ constructor(sendFn, model, targetPeer) {
18239
+ this.sendFn = sendFn;
18240
+ this.model = model;
18241
+ this.targetPeer = targetPeer || "";
18242
+ }
18243
+ async chatCompletion(request) {
18244
+ const daemonArgs = {
18245
+ model: this.model,
18246
+ messages: JSON.stringify(request.messages),
18247
+ tools: JSON.stringify(request.tools),
18248
+ temperature: String(request.temperature),
18249
+ max_tokens: String(request.maxTokens)
18250
+ };
18251
+ if (this.targetPeer) {
18252
+ daemonArgs.target_peer = this.targetPeer;
18253
+ }
18254
+ const rawResult = await this.sendFn("remote_infer", daemonArgs, request.timeoutMs || 12e4);
18255
+ let parsed;
18256
+ try {
18257
+ parsed = JSON.parse(rawResult);
18258
+ } catch {
18259
+ throw new Error(`Invalid response from nexus daemon: ${rawResult.slice(0, 200)}`);
18260
+ }
18261
+ if (parsed.success === false) {
18262
+ throw new Error(`Remote inference failed: ${parsed.output || rawResult}`);
18263
+ }
18264
+ const invokeResult = parsed.result;
18265
+ let responseData = null;
18266
+ if (typeof invokeResult === "string") {
18267
+ try {
18268
+ responseData = JSON.parse(invokeResult);
18269
+ } catch {
18270
+ responseData = { response: invokeResult };
18271
+ }
18272
+ } else if (invokeResult) {
18273
+ const events = Array.isArray(invokeResult) ? invokeResult : [invokeResult];
18274
+ for (const evt of events) {
18275
+ if (evt.event === "result" && evt.data) {
18276
+ const evtData = evt.data;
18277
+ try {
18278
+ responseData = typeof evtData === "string" ? JSON.parse(evtData) : evtData;
18279
+ } catch {
18280
+ responseData = { response: String(evtData) };
18281
+ }
18282
+ }
18283
+ }
18284
+ }
18285
+ if (!responseData) {
18286
+ throw new Error("No response data from remote inference");
18287
+ }
18288
+ const choices = responseData.choices;
18289
+ if (choices && Array.isArray(choices)) {
18290
+ return {
18291
+ choices: choices.map((c3) => {
18292
+ const msg = c3.message || {};
18293
+ const toolCalls = msg.tool_calls || [];
18294
+ return {
18295
+ message: {
18296
+ content: typeof msg.content === "string" ? msg.content : msg.content ?? null,
18297
+ toolCalls: toolCalls.length > 0 ? toolCalls.map((tc) => {
18298
+ const fn = tc.function || {};
18299
+ let args;
18300
+ try {
18301
+ args = typeof fn.arguments === "string" ? JSON.parse(fn.arguments) : fn.arguments ?? {};
18302
+ } catch {
18303
+ args = { _raw: fn.arguments };
18304
+ }
18305
+ return {
18306
+ id: tc.id || crypto.randomUUID(),
18307
+ name: fn.name,
18308
+ arguments: args
18309
+ };
18310
+ }) : void 0
18311
+ }
18312
+ };
18313
+ }),
18314
+ usage: this.extractUsage(responseData)
18315
+ };
18316
+ }
18317
+ const content = responseData.response || JSON.stringify(responseData);
18318
+ return {
18319
+ choices: [{
18320
+ message: {
18321
+ content,
18322
+ toolCalls: void 0
18323
+ }
18324
+ }],
18325
+ usage: this.extractUsage(responseData)
18326
+ };
18327
+ }
18328
+ /**
18329
+ * Simulated streaming: calls chatCompletion() for the full response,
18330
+ * then yields it as word-level StreamChunk objects.
18331
+ * Gives API compatibility with the streaming path in AgenticRunner.
18332
+ */
18333
+ async *chatCompletionStream(request) {
18334
+ const response = await this.chatCompletion(request);
18335
+ const choice = response.choices[0];
18336
+ if (!choice)
18337
+ return;
18338
+ const msg = choice.message;
18339
+ if (msg.content) {
18340
+ const words = msg.content.split(/(\s+)/);
18341
+ for (const word of words) {
18342
+ if (word) {
18343
+ yield { type: "content", content: word };
18344
+ }
18345
+ }
18346
+ }
18347
+ if (msg.toolCalls) {
18348
+ for (let i = 0; i < msg.toolCalls.length; i++) {
18349
+ const tc = msg.toolCalls[i];
18350
+ yield {
18351
+ type: "tool_call_delta",
18352
+ toolCallIndex: i,
18353
+ toolCallId: tc.id,
18354
+ toolCallName: tc.name,
18355
+ toolCallArgs: JSON.stringify(tc.arguments)
18356
+ };
18357
+ }
18358
+ }
18359
+ if (response.usage) {
18360
+ yield {
18361
+ type: "usage",
18362
+ usage: {
18363
+ promptTokens: response.usage.promptTokens ?? 0,
18364
+ completionTokens: response.usage.completionTokens ?? 0,
18365
+ totalTokens: response.usage.totalTokens
18366
+ }
18367
+ };
18368
+ }
18369
+ yield { type: "finish", finishReason: "stop" };
18370
+ }
18371
+ extractUsage(data) {
18372
+ const usage = data.usage;
18373
+ if (!usage)
18374
+ return void 0;
18375
+ const inputTokens = usage.input_tokens || usage.prompt_tokens || 0;
18376
+ const outputTokens = usage.output_tokens || usage.completion_tokens || 0;
18377
+ return {
18378
+ totalTokens: inputTokens + outputTokens,
18379
+ promptTokens: inputTokens,
18380
+ completionTokens: outputTokens
18381
+ };
18382
+ }
18383
+ };
18384
+ }
18385
+ });
18386
+
18387
+ // packages/orchestrator/dist/flowstatePrompt.js
18388
+ var FLOWSTATE_PROMPT;
18389
+ var init_flowstatePrompt = __esm({
18390
+ "packages/orchestrator/dist/flowstatePrompt.js"() {
18391
+ "use strict";
18392
+ FLOWSTATE_PROMPT = `## FlowState Protocol (Active)
18393
+
18394
+ You are operating in FlowState mode. Apply this protocol to every task:
18395
+
18396
+ ### Pre-Task (Engage)
18397
+ - Clear context of noise \u2014 compact if SNR is degraded
18398
+ - Set ridiculously specific sub-goals (not "fix things" but "change line 47 of auth.ts to check for null")
18399
+ - Begin immediately \u2014 first tool call in first response (response inhibition)
18400
+ - Identify the first concrete action before reasoning further
18401
+
18402
+ ### During Task (Struggle -> Release -> Flow)
18403
+ - Execute the highest-impact action first (goal-directedness)
18404
+ - Maintain tight feedback loops: execute, verify, learn (immediate feedback)
18405
+ - Calibrate task complexity: chunk if overwhelming, compress if trivial (challenge-skill balance)
18406
+ - Rotate approaches when stuck: different tools, different angles (oasis effect)
18407
+ - Minimize context pollution: archive large outputs, compact when needed (minimalism)
18408
+ - Never research when you can act; never read when you can execute
18409
+ - Track your own token allocation: maximize flow-tokens (core reasoning), minimize junk-tokens (unnecessary elaboration)
18410
+
18411
+ ### Between Tasks (Recovery)
18412
+ - Compact context aggressively (active recovery, not passive degradation)
18413
+ - Update persistent memory with learnings (consolidation)
18414
+ - Clear the todo list \u2014 identify next highest-impact action (power-down ritual)
18415
+ - Assess context quality and improve if degraded
18416
+
18417
+ ### Flowstate Checklist (verify before each major action)
18418
+ - [ ] Is the goal ridiculously clear? (specific file, line, change \u2014 not vague intent)
18419
+ - [ ] Is context clean? (no irrelevant history cluttering working memory)
18420
+ - [ ] Is the first action identified? (a specific tool call, not more research)
18421
+ - [ ] Is the feedback loop tight? (verification command defined before making changes)
18422
+ - [ ] Is recovery planned? (compaction strategy selected for after task)
18423
+
18424
+ ### The Diagnostic Compass (when stuck)
18425
+ | Symptom | Variable | Fix |
18426
+ |---|---|---|
18427
+ | Working on the wrong thing | Goal-Directedness | Re-target. Find the most direct path. |
18428
+ | Underleveraged | Leverage | Parallelize. Use sub-agents. Build reusable systems. |
18429
+ | Distracted, fractured, unable to sustain depth | Flow | Remove friction. Reduce cognitive load. Create immersion. |
18430
+
18431
+ ### Peak Performance Equation
18432
+ Peak Performance = Goal-Directedness x Leverage x Flow
18433
+ (These MULTIPLY \u2014 a zero in any one collapses the entire equation)
18434
+
18435
+ ### Mantra
18436
+ Flow follows focus. Focus follows clarity. Clarity follows constraint.
18437
+ Constrain your context. Clarify your target. Focus your tools. Flow will follow.`;
18438
+ }
18439
+ });
18440
+
17927
18441
  // packages/orchestrator/dist/costTracker.js
17928
18442
  var DEFAULT_PRICING, CostTracker;
17929
18443
  var init_costTracker = __esm({
@@ -18403,6 +18917,8 @@ var init_dist5 = __esm({
18403
18917
  init_mergeRunner();
18404
18918
  init_retryController();
18405
18919
  init_agenticRunner();
18920
+ init_nexusBackend();
18921
+ init_flowstatePrompt();
18406
18922
  init_costTracker();
18407
18923
  init_workEvaluator();
18408
18924
  init_sessionMetrics();
@@ -23256,6 +23772,7 @@ function renderSlashHelp() {
23256
23772
  ["/compact <strategy>", "Compact with strategy: aggressive, decisions, errors, summary, structured"],
23257
23773
  ["/bruteforce", "Toggle brute-force mode (auto re-engage on turn limit)"],
23258
23774
  ["/deep", "Toggle deep context \u2014 relaxes compaction so large models use 85% of context"],
23775
+ ["/flow", "Toggle FlowState protocol \u2014 immediate action, tight loops, goal clarity"],
23259
23776
  ["/tools", "List agent-created custom tools"],
23260
23777
  ["/skills", "List available AIWG skills"],
23261
23778
  ["/skills <keyword>", "Filter skills by name or trigger"],
@@ -28968,6 +29485,18 @@ async function handleSlashCommand(input, ctx) {
28968
29485
  renderInfo(`Deep context mode: ${isOn ? "ON" : "OFF"}${hasLocal ? " (project-local)" : ""}` + (isOn ? " \u2014 compaction relaxed to 85% of context window. Large models will retain more working memory for complex reasoning." : " \u2014 compaction restored to default thresholds."));
28969
29486
  return "handled";
28970
29487
  }
29488
+ case "flow":
29489
+ case "flowstate": {
29490
+ if (!ctx.flowToggle) {
29491
+ renderWarning("FlowState not available");
29492
+ return "handled";
29493
+ }
29494
+ const isFlowOn = ctx.flowToggle();
29495
+ const flowSave = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
29496
+ flowSave({ flow: isFlowOn });
29497
+ renderInfo(`FlowState protocol: ${isFlowOn ? "ON" : "OFF"}${hasLocal ? " (project-local)" : ""}` + (isFlowOn ? " \u2014 agent will follow FlowState protocol: immediate action, tight feedback loops, goal clarity, challenge-skill calibration." : " \u2014 standard agent behavior restored."));
29498
+ return "handled";
29499
+ }
28971
29500
  case "emojis":
28972
29501
  case "emoji": {
28973
29502
  const current = ctx.getEmojis?.() ?? true;
@@ -39069,7 +39598,13 @@ function createSubAgentTool(config, repoRoot, ctxWindowSize) {
39069
39598
  if (!task) {
39070
39599
  return { success: false, output: "", error: "task is required" };
39071
39600
  }
39072
- const backend = new OllamaAgenticBackend(config.backendUrl, config.model, config.apiKey);
39601
+ let backend;
39602
+ if (config.backendType === "nexus") {
39603
+ const nexusTool = new NexusTool(repoRoot);
39604
+ backend = new NexusAgenticBackend(nexusTool.sendCommand.bind(nexusTool), config.model);
39605
+ } else {
39606
+ backend = new OllamaAgenticBackend(config.backendUrl, config.model, config.apiKey);
39607
+ }
39073
39608
  const subCtxWindow = ctxWindowSize ?? 0;
39074
39609
  const subTier = getModelTier(config.model);
39075
39610
  const subCompaction = subTier === "small" ? 12e3 : subTier === "medium" ? 24e3 : 4e4;
@@ -39206,7 +39741,7 @@ function formatDMNToolArgs(toolName, args) {
39206
39741
  return "";
39207
39742
  }
39208
39743
  }
39209
- function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce, statusBar, sudoCallback, costTracker, onComplete, taskType, contextWindowSize, modelCaps, personality, deepContext, onCompaction, emotionEngine) {
39744
+ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce, statusBar, sudoCallback, costTracker, onComplete, taskType, contextWindowSize, modelCaps, personality, deepContext, onCompaction, emotionEngine, flowEnabled) {
39210
39745
  const voiceStyleMap = {
39211
39746
  concise: 1,
39212
39747
  balanced: 3,
@@ -39220,7 +39755,17 @@ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce
39220
39755
  if (taskType && modelTier !== "small") {
39221
39756
  dynamicContext += "\n\n" + buildTaskContext(taskType);
39222
39757
  }
39223
- const backend = new OllamaAgenticBackend(config.backendUrl, config.model, config.apiKey);
39758
+ if (flowEnabled) {
39759
+ dynamicContext += "\n\n" + FLOWSTATE_PROMPT;
39760
+ }
39761
+ let backend;
39762
+ if (config.backendType === "nexus") {
39763
+ const nexusTool = new NexusTool(repoRoot);
39764
+ const sendFn = nexusTool.sendCommand.bind(nexusTool);
39765
+ backend = new NexusAgenticBackend(sendFn, config.model);
39766
+ } else {
39767
+ backend = new OllamaAgenticBackend(config.backendUrl, config.model, config.apiKey);
39768
+ }
39224
39769
  const compactionThreshold = modelTier === "small" ? 12e3 : modelTier === "medium" ? 24e3 : 4e4;
39225
39770
  const runner = new AgenticRunner(backend, {
39226
39771
  maxTurns: 60,
@@ -39611,6 +40156,7 @@ async function startInteractive(config, repoPath) {
39611
40156
  let bruteForceEnabled = savedSettings.bruteforce ?? true;
39612
40157
  let currentStyle = PRESET_NAMES.includes(savedSettings.style) ? savedSettings.style : "balanced";
39613
40158
  let deepContextEnabled = savedSettings.deepContext ?? false;
40159
+ let flowEnabled = savedSettings.flow === true;
39614
40160
  if (savedSettings.emojis !== void 0)
39615
40161
  setEmojisEnabled(savedSettings.emojis);
39616
40162
  if (savedSettings.colors !== void 0)
@@ -40143,6 +40689,10 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
40143
40689
  deepContextEnabled = !deepContextEnabled;
40144
40690
  return deepContextEnabled;
40145
40691
  },
40692
+ flowToggle() {
40693
+ flowEnabled = !flowEnabled;
40694
+ return flowEnabled;
40695
+ },
40146
40696
  setStyle(preset) {
40147
40697
  currentStyle = preset;
40148
40698
  },
@@ -41116,7 +41666,7 @@ Execute this skill now. Follow the behavioral guidance above.`;
41116
41666
  }, bruteForceEnabled, statusBar, handleSudoRequest, costTracker, (summary, meta) => {
41117
41667
  lastCompletedSummary = summary;
41118
41668
  lastTaskMeta = meta ?? null;
41119
- }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled, compactionSNRCallback(), emotionEngine);
41669
+ }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled, compactionSNRCallback(), emotionEngine, flowEnabled);
41120
41670
  activeTask = task;
41121
41671
  showPrompt();
41122
41672
  await task.promise;
@@ -41333,7 +41883,7 @@ NEW TASK: ${fullInput}`;
41333
41883
  }, bruteForceEnabled, statusBar, handleSudoRequest, costTracker, (summary, meta) => {
41334
41884
  lastCompletedSummary = summary;
41335
41885
  lastTaskMeta = meta ?? null;
41336
- }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled, compactionSNRCallback(), emotionEngine);
41886
+ }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled, compactionSNRCallback(), emotionEngine, flowEnabled);
41337
41887
  activeTask = task;
41338
41888
  showPrompt();
41339
41889
  await task.promise;
@@ -42432,6 +42982,10 @@ async function evalCommand(opts, config) {
42432
42982
  const evalRepoRoot = opts.repoPath ?? createTempEvalRepo();
42433
42983
  let rawBackend;
42434
42984
  if (useLive) {
42985
+ if (config.backendType === "nexus") {
42986
+ printError("Nexus backend is not supported for eval. Use --backend ollama or --backend vllm.");
42987
+ process.exit(1);
42988
+ }
42435
42989
  rawBackend = createBackend({
42436
42990
  type: config.backendType,
42437
42991
  model: config.model,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.87.0",
3
+ "version": "0.89.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",
@@ -288,12 +288,30 @@ Signs an EIP-3009 TransferWithAuthorization for USDC on Base. Budget-checked bef
288
288
  The signed proof is saved to `.oa/nexus/pending-transfer.json` — anyone can submit it on-chain
289
289
  via `USDC.transferWithAuthorization()`. No gas needed from the payer.
290
290
 
291
+ ### Remote Inference — `nexus(action='remote_infer', model='...', prompt='...')`
292
+
293
+ Route a prompt to a remote peer's model on the P2P mesh. The action auto-discovers peers
294
+ that have the requested model exposed, budget-checks the estimated cost, invokes the
295
+ inference capability, and returns the response text.
296
+
297
+ **Parameters**:
298
+ - `model` (required) — model name the provider is running (e.g., `qwen3.5:70b`, `nemotron-3-nano:30b`)
299
+ - `prompt` (required) — the text prompt to send
300
+ - `target_peer` (optional) — specific peer ID; if omitted, auto-selects the first peer with the model
301
+ - `temperature` (optional) — sampling temperature (default: 0.7)
302
+ - `max_tokens` (optional) — max tokens to generate (default: 4096)
303
+
304
+ **When to use**: When a task needs a larger/different model than what's available locally,
305
+ or when you want to offload inference to a remote GPU. The provider must be connected to
306
+ the mesh and have run `expose` to advertise their models.
307
+
291
308
  ### x402 Flow Summary
292
309
  1. wallet_create → generates wallet + x402-wallet.key (plaintext, 0600, for daemon)
293
310
  2. expose with margin > 0 → registers capabilities with USDC pricing
294
311
  3. Peers invoke_capability → daemon auto-handles payment_required/payment_proof
295
312
  4. Metering hook writes payment events to ledger.jsonl
296
313
  5. spend → sign direct USDC transfers (EIP-3009)
314
+ 6. remote_infer → auto-discover peer + invoke inference + budget check + ledger entry
297
315
 
298
316
  SECURITY: Wallet private keys are AES-256-GCM encrypted and NEVER accessible to you.
299
317
  x402-wallet.key is 0600-permissioned for daemon use only. All outbound messages are scanned