open-agents-ai 0.103.19 → 0.103.21

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.
Files changed (3) hide show
  1. package/README.md +40 -1
  2. package/dist/index.js +191 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -919,6 +919,7 @@ The TUI features an animated multilingual phrase carousel, live metrics bar with
919
919
  | `/models` | List all available models |
920
920
  | `/endpoint <url>` | Connect to a remote vLLM or OpenAI-compatible API |
921
921
  | `/endpoint <url> --auth <key>` | Set endpoint with Bearer auth |
922
+ | `/endpoint <peerId> --auth <key>` | Connect to a libp2p peer via nexus P2P network |
922
923
  | **Task Control** | |
923
924
  | `/pause` | Pause after current turn finishes (gentle halt) |
924
925
  | `/stop` | Kill current inference immediately, save state |
@@ -952,7 +953,11 @@ The TUI features an animated multilingual phrase carousel, live metrics bar with
952
953
  | `/secrets set <name> <value>` | Register a secret in the vault |
953
954
  | `/secrets list` | List registered secrets (values hidden) |
954
955
  | `/secrets import-env` | Auto-import secrets from environment variables |
955
- | `/expose` | Expose local inference via cloudflared tunnel |
956
+ | `/expose ollama` | Expose local inference via libp2p (default) |
957
+ | `/expose ollama --tunnel` | Expose via cloudflared tunnel |
958
+ | `/expose ollama --full` | Allow full Ollama API access (pull/delete) |
959
+ | `/expose stop` | Stop the expose gateway |
960
+ | `/expose status` | Show expose usage stats |
956
961
  | **Metrics & Updates** | |
957
962
  | `/cost` | Show token cost breakdown for the current session |
958
963
  | `/score` | Show inference capability scorecard (memory, compute, speed, model compatibility) |
@@ -1498,6 +1503,40 @@ Use `/endpoint` in the TUI or pass via CLI:
1498
1503
 
1499
1504
  The agent auto-detects the provider, normalizes the URL (strips `/v1/chat/completions` if pasted), tests connectivity, and saves the configuration. You can paste full endpoint URLs — they'll be cleaned up automatically.
1500
1505
 
1506
+ ### P2P Inference via libp2p
1507
+
1508
+ Expose your local Ollama models to the decentralized nexus network, or consume another peer's models — no port forwarding, DNS, or cloud accounts needed:
1509
+
1510
+ ```bash
1511
+ # Provider: expose local models via libp2p (default transport)
1512
+ /expose ollama
1513
+
1514
+ # Output shows a copy-pasteable command:
1515
+ # /endpoint 12D3KooWSwaCi1J... --auth 5aJ68QuP...
1516
+
1517
+ # Consumer: connect to a remote peer
1518
+ /endpoint 12D3KooWSwaCi1JgXp2f2tQNFZFyMPZVcDe8oyTG672n6ELxSgBt --auth 5aJ68QuPxyz
1519
+
1520
+ # Fallback: expose via cloudflared tunnel instead
1521
+ /expose ollama --tunnel
1522
+
1523
+ # Grant full Ollama API access to consumers (pull, delete, etc.)
1524
+ /expose ollama --full
1525
+ ```
1526
+
1527
+ Transport: DHT + mDNS + NATS relay + circuit relay. Auth key is auto-generated and required for all requests. System metrics (CPU/GPU/memory) are available to consumers via the `system_metrics` capability. Without `--full`, destructive Ollama API endpoints (`/api/pull`, `/api/delete`, `/api/create`) are blocked.
1528
+
1529
+ ### Endpoint Cascade Failover
1530
+
1531
+ When you've used multiple endpoints, the agent automatically builds a failover cascade. If the primary endpoint fails with transient errors (502, connection refused, timeout), it transparently switches to the next endpoint that has the same model — then periodically probes the primary to return when it recovers:
1532
+
1533
+ ```
1534
+ [cascade] Failover → https://api.groq.com/openai: 2 consecutive failures: fetch failed
1535
+ [cascade] Primary recovered: http://localhost:11434
1536
+ ```
1537
+
1538
+ No configuration needed — the cascade is built from your endpoint usage history. Works across local Ollama, cloud providers, and P2P peers.
1539
+
1501
1540
  ## Evaluation Suite
1502
1541
 
1503
1542
  46 evaluation tasks test the agent's autonomous capabilities across coding, web research, SDLC analysis, tool creation, multi-file reasoning, memory systems, and context engineering:
package/dist/index.js CHANGED
@@ -27395,7 +27395,34 @@ async function collectSystemMetricsAsync() {
27395
27395
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
27396
27396
  };
27397
27397
  }
27398
- var HOP_BY_HOP_HEADERS, CF_HEADERS_PREFIX, DEFAULT_TARGETS, STATE_FILE_NAME, ExposeGateway, ExposeP2PGateway;
27398
+ function readP2PExposeState(stateDir) {
27399
+ try {
27400
+ const path = join35(stateDir, P2P_STATE_FILE_NAME);
27401
+ if (!existsSync27(path))
27402
+ return null;
27403
+ const raw = readFileSync19(path, "utf8");
27404
+ const data = JSON.parse(raw);
27405
+ if (!data.peerId || !data.authKey)
27406
+ return null;
27407
+ return data;
27408
+ } catch {
27409
+ return null;
27410
+ }
27411
+ }
27412
+ function writeP2PExposeState(stateDir, state) {
27413
+ try {
27414
+ mkdirSync9(stateDir, { recursive: true });
27415
+ writeFileSync9(join35(stateDir, P2P_STATE_FILE_NAME), JSON.stringify(state, null, 2));
27416
+ } catch {
27417
+ }
27418
+ }
27419
+ function removeP2PExposeState(stateDir) {
27420
+ try {
27421
+ unlinkSync3(join35(stateDir, P2P_STATE_FILE_NAME));
27422
+ } catch {
27423
+ }
27424
+ }
27425
+ var HOP_BY_HOP_HEADERS, CF_HEADERS_PREFIX, DEFAULT_TARGETS, STATE_FILE_NAME, ExposeGateway, P2P_STATE_FILE_NAME, ExposeP2PGateway;
27399
27426
  var init_expose = __esm({
27400
27427
  "packages/cli/dist/tui/expose.js"() {
27401
27428
  "use strict";
@@ -28047,7 +28074,8 @@ ${this.formatConnectionInfo()}`);
28047
28074
  return lines.join("\n");
28048
28075
  }
28049
28076
  };
28050
- ExposeP2PGateway = class extends EventEmitter3 {
28077
+ P2P_STATE_FILE_NAME = "expose-p2p-state.json";
28078
+ ExposeP2PGateway = class _ExposeP2PGateway extends EventEmitter3 {
28051
28079
  _nexusTool;
28052
28080
  // NexusTool instance
28053
28081
  _kind;
@@ -28156,6 +28184,16 @@ ${this.formatConnectionInfo()}`);
28156
28184
  this._stats.status = "active";
28157
28185
  this._stats.startedAt = Date.now();
28158
28186
  this.emitStats();
28187
+ if (this._stateDir && this._peerId) {
28188
+ writeP2PExposeState(this._stateDir, {
28189
+ peerId: this._peerId,
28190
+ authKey: this._authKey,
28191
+ kind: this._kind,
28192
+ targetUrl: this._targetUrl,
28193
+ margin: this._margin,
28194
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
28195
+ });
28196
+ }
28159
28197
  this.startMeteringPoll(nexusDir);
28160
28198
  return this._peerId ?? "connected (peer ID pending)";
28161
28199
  }
@@ -28164,6 +28202,54 @@ ${this.formatConnectionInfo()}`);
28164
28202
  this._stats.status = "standby";
28165
28203
  this._stats.activeConnections = 0;
28166
28204
  this.emitStats();
28205
+ if (this._stateDir)
28206
+ removeP2PExposeState(this._stateDir);
28207
+ }
28208
+ /**
28209
+ * Check for and reconnect to a surviving P2P expose from a previous session.
28210
+ * Verifies the nexus daemon is still running and the peer ID is valid,
28211
+ * then resumes metering poll without re-registering capabilities
28212
+ * (the daemon keeps them registered across OA restarts).
28213
+ */
28214
+ static async checkAndReconnect(stateDir, nexusTool, options) {
28215
+ const state = readP2PExposeState(stateDir);
28216
+ if (!state)
28217
+ return null;
28218
+ const nexusDir = nexusTool.getNexusDir();
28219
+ const statusPath = join35(nexusDir, "status.json");
28220
+ try {
28221
+ if (!existsSync27(statusPath)) {
28222
+ removeP2PExposeState(stateDir);
28223
+ return null;
28224
+ }
28225
+ const status = JSON.parse(readFileSync19(statusPath, "utf8"));
28226
+ if (!status.connected || !status.peerId) {
28227
+ removeP2PExposeState(stateDir);
28228
+ return null;
28229
+ }
28230
+ if (status.peerId !== state.peerId) {
28231
+ removeP2PExposeState(stateDir);
28232
+ return null;
28233
+ }
28234
+ } catch {
28235
+ removeP2PExposeState(stateDir);
28236
+ return null;
28237
+ }
28238
+ const gateway = new _ExposeP2PGateway({
28239
+ kind: state.kind,
28240
+ targetUrl: state.targetUrl,
28241
+ authKey: state.authKey,
28242
+ stateDir,
28243
+ margin: state.margin,
28244
+ ...options
28245
+ }, nexusTool);
28246
+ gateway._peerId = state.peerId;
28247
+ gateway._stats.status = "active";
28248
+ gateway._stats.startedAt = new Date(state.startedAt).getTime();
28249
+ gateway.emitStats();
28250
+ gateway.startMeteringPoll(nexusDir);
28251
+ options.onInfo?.(`P2P expose reconnected: ${state.peerId}`);
28252
+ return gateway;
28167
28253
  }
28168
28254
  /** Poll the daemon's metering.jsonl and status.json for stats */
28169
28255
  startMeteringPoll(nexusDir) {
@@ -32979,10 +33065,20 @@ async function handleSlashCommand(input, ctx) {
32979
33065
  renderWarning("Expose gateway not available in this context.");
32980
33066
  return "handled";
32981
33067
  }
32982
- if (arg === "stop" || arg === "off") {
33068
+ if (arg?.startsWith("stop") || arg === "off") {
33069
+ const stopParts = (arg ?? "").split(/\s+/);
33070
+ let stopWhich;
33071
+ if (stopParts.includes("--tunnel"))
33072
+ stopWhich = "tunnel";
33073
+ else if (stopParts.includes("--libp2p"))
33074
+ stopWhich = "libp2p";
32983
33075
  if (ctx.isExposeActive?.()) {
32984
- await ctx.exposeStop?.();
32985
- renderInfo("Expose gateway stopped.");
33076
+ await ctx.exposeStop?.(stopWhich);
33077
+ if (stopWhich) {
33078
+ renderInfo(`Expose ${stopWhich} gateway stopped.`);
33079
+ } else {
33080
+ renderInfo("All expose gateways stopped.");
33081
+ }
32986
33082
  } else {
32987
33083
  renderWarning("No active expose gateway.");
32988
33084
  }
@@ -33009,7 +33105,9 @@ async function handleSlashCommand(input, ctx) {
33009
33105
  renderInfo(" /expose vllm Expose local vLLM (8000)");
33010
33106
  renderInfo(" /expose llvm Expose local LLVM (8080)");
33011
33107
  renderInfo(" /expose host:port Expose custom address");
33012
- renderInfo(" /expose stop Stop the gateway");
33108
+ renderInfo(" /expose stop Stop all gateways");
33109
+ renderInfo(" /expose stop --tunnel Stop tunnel only");
33110
+ renderInfo(" /expose stop --libp2p Stop libp2p only");
33013
33111
  renderInfo(" /expose status Show usage stats");
33014
33112
  renderInfo(" --key <key> Use specific auth key (tunnel mode)");
33015
33113
  renderInfo(" --key Auto-generate auth key (tunnel mode)");
@@ -33017,9 +33115,6 @@ async function handleSlashCommand(input, ctx) {
33017
33115
  }
33018
33116
  return "handled";
33019
33117
  }
33020
- if (ctx.isExposeActive?.()) {
33021
- await ctx.exposeStop?.();
33022
- }
33023
33118
  const exposeParts = arg.split(/\s+/);
33024
33119
  const kindOrUrl = exposeParts[0];
33025
33120
  let exposeAuthKey;
@@ -44437,11 +44532,15 @@ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce
44437
44532
  }
44438
44533
  try {
44439
44534
  const endpointHistory = loadUsageHistory("endpoint", repoRoot);
44535
+ const primaryIsNexus = config.backendType === "nexus";
44440
44536
  const fallbackEndpoints = [];
44441
44537
  for (const entry of endpointHistory) {
44442
44538
  if (entry.value === config.backendUrl)
44443
44539
  continue;
44444
44540
  const bt = entry.meta?.backendType ?? "ollama";
44541
+ const entryIsNexus = bt === "nexus";
44542
+ if (primaryIsNexus !== entryIsNexus)
44543
+ continue;
44445
44544
  const ep = {
44446
44545
  url: entry.value,
44447
44546
  backendType: bt,
@@ -45104,7 +45203,8 @@ async function startInteractive(config, repoPath) {
45104
45203
  autoUpdateTimer.unref();
45105
45204
  const voiceEngine = new VoiceEngine();
45106
45205
  let voiceSession = null;
45107
- let exposeGateway = null;
45206
+ let tunnelGateway = null;
45207
+ let p2pGateway = null;
45108
45208
  let peerMesh = null;
45109
45209
  let inferenceRouter = null;
45110
45210
  const secretVault = new SecretVault(join50(repoRoot, ".oa", "vault.enc"));
@@ -45456,7 +45556,7 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
45456
45556
  onError: (msg) => writeContent(() => renderWarning(msg))
45457
45557
  });
45458
45558
  if (reconnected) {
45459
- exposeGateway = reconnected;
45559
+ tunnelGateway = reconnected;
45460
45560
  reconnected.on("stats", (stats) => {
45461
45561
  statusBar.setExposeStatus({
45462
45562
  status: stats.status,
@@ -45471,6 +45571,28 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
45471
45571
  }
45472
45572
  } catch {
45473
45573
  }
45574
+ try {
45575
+ const oaDir = join50(repoRoot, ".oa");
45576
+ const reconnectedP2P = await ExposeP2PGateway.checkAndReconnect(oaDir, new NexusTool(repoRoot), {
45577
+ onInfo: (msg) => writeContent(() => renderInfo(msg)),
45578
+ onError: (msg) => writeContent(() => renderWarning(msg))
45579
+ });
45580
+ if (reconnectedP2P) {
45581
+ p2pGateway = reconnectedP2P;
45582
+ reconnectedP2P.on("stats", (stats) => {
45583
+ statusBar.setExposeStatus({
45584
+ status: stats.status,
45585
+ totalRequests: stats.totalRequests,
45586
+ activeConnections: stats.activeConnections,
45587
+ modelUsage: stats.modelUsage
45588
+ });
45589
+ });
45590
+ writeContent(() => {
45591
+ renderInfo(`Reconnected to existing P2P expose: ${reconnectedP2P.peerId}`);
45592
+ });
45593
+ }
45594
+ } catch {
45595
+ }
45474
45596
  try {
45475
45597
  const isPeer = currentConfig.backendUrl.startsWith("peer://");
45476
45598
  const isRemote = !currentConfig.backendUrl.includes("127.0.0.1") && !currentConfig.backendUrl.includes("localhost") && !isPeer;
@@ -46123,14 +46245,18 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
46123
46245
  }
46124
46246
  const effectiveTransport = transport ?? "libp2p";
46125
46247
  if (effectiveTransport === "libp2p") {
46248
+ if (p2pGateway?.isActive) {
46249
+ await p2pGateway.stop();
46250
+ p2pGateway = null;
46251
+ }
46126
46252
  const nexusTool = new NexusTool(repoRoot);
46127
- const p2pGateway = new ExposeP2PGateway({
46253
+ const newP2P = new ExposeP2PGateway({
46128
46254
  kind,
46129
46255
  targetUrl,
46130
46256
  authKey,
46131
46257
  stateDir: join50(repoRoot, ".oa")
46132
46258
  }, nexusTool);
46133
- p2pGateway.on("stats", (stats) => {
46259
+ newP2P.on("stats", (stats) => {
46134
46260
  statusBar.setExposeStatus({
46135
46261
  status: stats.status,
46136
46262
  totalRequests: stats.totalRequests,
@@ -46139,10 +46265,10 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
46139
46265
  });
46140
46266
  });
46141
46267
  try {
46142
- const peerId = await p2pGateway.start();
46143
- exposeGateway = p2pGateway;
46268
+ const peerId = await newP2P.start();
46269
+ p2pGateway = newP2P;
46144
46270
  writeContent(() => {
46145
- process.stdout.write("\n" + p2pGateway.formatConnectionInfo() + "\n\n");
46271
+ process.stdout.write("\n" + newP2P.formatConnectionInfo() + "\n\n");
46146
46272
  });
46147
46273
  return peerId;
46148
46274
  } catch (err) {
@@ -46156,8 +46282,12 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
46156
46282
  }
46157
46283
  }
46158
46284
  }
46159
- const tunnelGateway = new ExposeGateway({ kind, targetUrl, authKey, fullAccess, stateDir: join50(repoRoot, ".oa") });
46160
- tunnelGateway.on("stats", (stats) => {
46285
+ if (tunnelGateway?.isActive) {
46286
+ await tunnelGateway.stop();
46287
+ tunnelGateway = null;
46288
+ }
46289
+ const newTunnel = new ExposeGateway({ kind, targetUrl, authKey, fullAccess, stateDir: join50(repoRoot, ".oa") });
46290
+ newTunnel.on("stats", (stats) => {
46161
46291
  statusBar.setExposeStatus({
46162
46292
  status: stats.status,
46163
46293
  totalRequests: stats.totalRequests,
@@ -46166,41 +46296,67 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
46166
46296
  });
46167
46297
  });
46168
46298
  try {
46169
- const url = await tunnelGateway.start();
46170
- exposeGateway = tunnelGateway;
46299
+ const url = await newTunnel.start();
46300
+ tunnelGateway = newTunnel;
46171
46301
  writeContent(() => {
46172
- process.stdout.write("\n" + tunnelGateway.formatConnectionInfo() + "\n\n");
46302
+ process.stdout.write("\n" + newTunnel.formatConnectionInfo() + "\n\n");
46173
46303
  });
46174
46304
  return url;
46175
46305
  } catch (err) {
46176
- exposeGateway = null;
46177
46306
  throw err;
46178
46307
  }
46179
46308
  },
46180
- async exposeStop() {
46181
- if (exposeGateway) {
46182
- await exposeGateway.stop();
46309
+ async exposeStop(which) {
46310
+ if (!which || which === "tunnel") {
46311
+ if (tunnelGateway) {
46312
+ await tunnelGateway.stop();
46313
+ tunnelGateway = null;
46314
+ }
46315
+ }
46316
+ if (!which || which === "libp2p") {
46317
+ if (p2pGateway) {
46318
+ await p2pGateway.stop();
46319
+ p2pGateway = null;
46320
+ }
46321
+ }
46322
+ if (!tunnelGateway?.isActive && !p2pGateway?.isActive) {
46183
46323
  statusBar.clearExposeStatus();
46184
- exposeGateway = null;
46185
46324
  }
46186
46325
  },
46187
46326
  isExposeActive() {
46188
- return exposeGateway?.isActive ?? false;
46327
+ return (tunnelGateway?.isActive ?? false) || (p2pGateway?.isActive ?? false);
46189
46328
  },
46190
46329
  getExposeInfo() {
46191
- if (!exposeGateway)
46330
+ const infos = [];
46331
+ if (tunnelGateway?.isActive) {
46332
+ infos.push({
46333
+ tunnelUrl: tunnelGateway.tunnelUrl,
46334
+ authKey: tunnelGateway.authKey,
46335
+ stats: tunnelGateway.formatStats(),
46336
+ transport: "tunnel"
46337
+ });
46338
+ }
46339
+ if (p2pGateway?.isActive) {
46340
+ infos.push({
46341
+ tunnelUrl: p2pGateway.peerId,
46342
+ authKey: p2pGateway.authKey,
46343
+ stats: p2pGateway.formatStats(),
46344
+ transport: "libp2p"
46345
+ });
46346
+ }
46347
+ if (infos.length === 0)
46192
46348
  return null;
46193
- if (exposeGateway instanceof ExposeP2PGateway) {
46349
+ if (infos.length === 2) {
46194
46350
  return {
46195
- tunnelUrl: exposeGateway.peerId,
46196
- authKey: exposeGateway.authKey,
46197
- stats: exposeGateway.formatStats()
46351
+ tunnelUrl: infos[0].tunnelUrl,
46352
+ authKey: infos[0].authKey,
46353
+ stats: infos[0].stats + "\n\n" + infos[1].stats
46198
46354
  };
46199
46355
  }
46200
46356
  return {
46201
- tunnelUrl: exposeGateway.tunnelUrl,
46202
- authKey: exposeGateway.authKey,
46203
- stats: exposeGateway.formatStats()
46357
+ tunnelUrl: infos[0].tunnelUrl,
46358
+ authKey: infos[0].authKey,
46359
+ stats: infos[0].stats
46204
46360
  };
46205
46361
  },
46206
46362
  // ── Nexus daemon connect ──────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.103.19",
3
+ "version": "0.103.21",
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",