open-agents-ai 0.15.6 → 0.15.7

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 (2) hide show
  1. package/dist/index.js +209 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8188,7 +8188,18 @@ Integrate this guidance into your current approach. Continue working on the task
8188
8188
  maxTokens: this.options.maxTokens,
8189
8189
  timeoutMs: this.options.requestTimeoutMs
8190
8190
  };
8191
- const response = this.options.streamEnabled && this.hasStreamingSupport() ? await this.streamingRequest(chatRequest, turn) : await this.backend.chatCompletion(chatRequest);
8191
+ let response;
8192
+ try {
8193
+ response = this.options.streamEnabled && this.hasStreamingSupport() ? await this.streamingRequest(chatRequest, turn) : await this.backend.chatCompletion(chatRequest);
8194
+ } catch (reqErr) {
8195
+ const recovered = await this.retryOnTransient(reqErr, chatRequest, turn);
8196
+ if (!recovered) {
8197
+ this.emit({ type: "error", content: `Backend error: ${reqErr instanceof Error ? reqErr.message : String(reqErr)}`, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8198
+ messages.push({ role: "user", content: "[System: backend request failed, retrying on next turn. The previous request was lost.]" });
8199
+ continue;
8200
+ }
8201
+ response = recovered;
8202
+ }
8192
8203
  totalTokens += response.usage?.totalTokens ?? 0;
8193
8204
  promptTokens += response.usage?.promptTokens ?? 0;
8194
8205
  completionTokens += response.usage?.completionTokens ?? 0;
@@ -8405,7 +8416,18 @@ Integrate this guidance into your current approach. Continue working on the task
8405
8416
  }
8406
8417
  const compactedMsgs = this.compactMessages(messages);
8407
8418
  const chatRequest = { messages: compactedMsgs, tools: toolDefs, temperature: this.options.temperature, maxTokens: this.options.maxTokens, timeoutMs: this.options.requestTimeoutMs };
8408
- const response = this.options.streamEnabled && this.hasStreamingSupport() ? await this.streamingRequest(chatRequest, turn) : await this.backend.chatCompletion(chatRequest);
8419
+ let response;
8420
+ try {
8421
+ response = this.options.streamEnabled && this.hasStreamingSupport() ? await this.streamingRequest(chatRequest, turn) : await this.backend.chatCompletion(chatRequest);
8422
+ } catch (reqErr) {
8423
+ const recovered = await this.retryOnTransient(reqErr, chatRequest, turn);
8424
+ if (!recovered) {
8425
+ this.emit({ type: "error", content: `Backend error: ${reqErr instanceof Error ? reqErr.message : String(reqErr)}`, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8426
+ messages.push({ role: "user", content: "[System: backend request failed, retrying on next turn. The previous request was lost.]" });
8427
+ continue;
8428
+ }
8429
+ response = recovered;
8430
+ }
8409
8431
  totalTokens += response.usage?.totalTokens ?? 0;
8410
8432
  promptTokens += response.usage?.promptTokens ?? 0;
8411
8433
  completionTokens += response.usage?.completionTokens ?? 0;
@@ -8809,6 +8831,58 @@ ${newerSummary}` : newerSummary;
8809
8831
  }));
8810
8832
  }
8811
8833
  // -------------------------------------------------------------------------
8834
+ // Transient error recovery — retry on 502, fetch failed, timeouts
8835
+ // -------------------------------------------------------------------------
8836
+ /** Detect whether an error is transient (worth retrying) */
8837
+ isTransientError(err) {
8838
+ const msg = err instanceof Error ? err.message : String(err);
8839
+ if (/Backend HTTP (502|503|504)/i.test(msg))
8840
+ return true;
8841
+ if (/fetch failed|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EPIPE|socket hang up/i.test(msg))
8842
+ return true;
8843
+ if (/received HTML error page/i.test(msg))
8844
+ return true;
8845
+ if (/model is loading|server busy|overloaded/i.test(msg))
8846
+ return true;
8847
+ return false;
8848
+ }
8849
+ /**
8850
+ * Retry a failed model request up to 3 times with exponential backoff.
8851
+ * Returns the response on success, or null if all retries failed.
8852
+ */
8853
+ async retryOnTransient(initialErr, chatRequest, turn) {
8854
+ if (!this.isTransientError(initialErr))
8855
+ return null;
8856
+ const maxRetries = 3;
8857
+ const baseDelayMs = 3e3;
8858
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
8859
+ if (this.aborted)
8860
+ return null;
8861
+ const delay = baseDelayMs * Math.pow(2, attempt - 1);
8862
+ this.emit({
8863
+ type: "compaction",
8864
+ content: `Backend error \u2014 retrying in ${(delay / 1e3).toFixed(0)}s (attempt ${attempt}/${maxRetries})`,
8865
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
8866
+ });
8867
+ await new Promise((r) => setTimeout(r, delay));
8868
+ if (this.aborted)
8869
+ return null;
8870
+ try {
8871
+ const response = this.options.streamEnabled && this.hasStreamingSupport() ? await this.streamingRequest(chatRequest, turn) : await this.backend.chatCompletion(chatRequest);
8872
+ this.emit({
8873
+ type: "compaction",
8874
+ content: `Backend recovered on attempt ${attempt}`,
8875
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
8876
+ });
8877
+ return response;
8878
+ } catch (retryErr) {
8879
+ if (!this.isTransientError(retryErr))
8880
+ return null;
8881
+ }
8882
+ }
8883
+ return null;
8884
+ }
8885
+ // -------------------------------------------------------------------------
8812
8886
  // Streaming support — parallel path that emits token events
8813
8887
  // -------------------------------------------------------------------------
8814
8888
  /** Check whether the backend supports SSE streaming */
@@ -10809,6 +10883,106 @@ async function runSetupWizard(config) {
10809
10883
  rl.close();
10810
10884
  }
10811
10885
  }
10886
+ async function promptForCustomEndpoint(config, rl) {
10887
+ process.stdout.write(`
10888
+ ${c2.cyan("\u25CF")} Enter an OpenAI-compatible inference endpoint.
10889
+ `);
10890
+ process.stdout.write(` ${c2.dim("Examples:")}
10891
+ `);
10892
+ process.stdout.write(` ${c2.dim(" https://chutes.ai/v1")}
10893
+ `);
10894
+ process.stdout.write(` ${c2.dim(" http://10.0.0.5:11434")}
10895
+ `);
10896
+ process.stdout.write(` ${c2.dim(" https://api.together.xyz/v1")}
10897
+
10898
+ `);
10899
+ const endpoint = await ask(rl, ` ${c2.bold("Endpoint URL:")} `);
10900
+ if (!endpoint) {
10901
+ process.stdout.write(` ${c2.dim("No endpoint entered.")}
10902
+ `);
10903
+ const startAnyway = await ask(rl, `
10904
+ ${c2.bold("Start anyway without a backend?")} (y/n) `);
10905
+ if (startAnyway.toLowerCase() === "y" || startAnyway.toLowerCase() === "yes") {
10906
+ return config.model;
10907
+ }
10908
+ return config.model;
10909
+ }
10910
+ const cleanUrl = endpoint.replace(/\/+$/, "");
10911
+ const needsKey = await ask(rl, `
10912
+ ${c2.bold("Does this endpoint require an API key?")} (y/n) `);
10913
+ let apiKey = "";
10914
+ if (needsKey.toLowerCase() === "y" || needsKey.toLowerCase() === "yes") {
10915
+ apiKey = await ask(rl, ` ${c2.bold("API key:")} `);
10916
+ }
10917
+ process.stdout.write(`
10918
+ ${c2.cyan("\u25CF")} Enter the model name for this endpoint.
10919
+ `);
10920
+ process.stdout.write(` ${c2.dim("Examples: qwen3.5:122b, meta-llama/Llama-3.3-70B, etc.")}
10921
+
10922
+ `);
10923
+ const modelName = await ask(rl, ` ${c2.bold("Model name")} (Enter for ${c2.dim(config.model)}): `);
10924
+ const chosenModel = modelName || config.model;
10925
+ process.stdout.write(`
10926
+ ${c2.cyan("\u25CF")} Testing endpoint ${c2.bold(cleanUrl)}...
10927
+ `);
10928
+ let testOk = false;
10929
+ try {
10930
+ const testUrl = cleanUrl.endsWith("/v1") ? `${cleanUrl}/models` : cleanUrl.includes("/v1/") ? `${cleanUrl.replace(/\/v1\/.*/, "/v1/models")}` : `${cleanUrl}/v1/models`;
10931
+ const headers = { "Content-Type": "application/json" };
10932
+ if (apiKey)
10933
+ headers["Authorization"] = `Bearer ${apiKey}`;
10934
+ const resp = await fetch(testUrl, { headers, signal: AbortSignal.timeout(1e4) });
10935
+ if (resp.ok) {
10936
+ process.stdout.write(` ${c2.green("\u2714")} Endpoint reachable.
10937
+ `);
10938
+ testOk = true;
10939
+ } else {
10940
+ try {
10941
+ const ollamaResp = await fetch(`${cleanUrl}/api/tags`, { signal: AbortSignal.timeout(1e4) });
10942
+ if (ollamaResp.ok) {
10943
+ process.stdout.write(` ${c2.green("\u2714")} Ollama endpoint detected.
10944
+ `);
10945
+ testOk = true;
10946
+ }
10947
+ } catch {
10948
+ }
10949
+ if (!testOk) {
10950
+ process.stdout.write(` ${c2.yellow("\u26A0")} Endpoint returned HTTP ${resp.status}
10951
+ `);
10952
+ }
10953
+ }
10954
+ } catch (err) {
10955
+ process.stdout.write(` ${c2.yellow("\u26A0")} Could not reach endpoint: ${err instanceof Error ? err.message : String(err)}
10956
+ `);
10957
+ }
10958
+ if (!testOk) {
10959
+ const startAnyway = await ask(rl, `
10960
+ ${c2.bold("Endpoint unreachable. Start anyway?")} (y/n) `);
10961
+ if (startAnyway.toLowerCase() !== "y" && startAnyway.toLowerCase() !== "yes") {
10962
+ process.stdout.write(` ${c2.dim("You can configure the endpoint later with /endpoint")}
10963
+
10964
+ `);
10965
+ return config.model;
10966
+ }
10967
+ }
10968
+ setConfigValue("backendUrl", cleanUrl);
10969
+ setConfigValue("model", chosenModel);
10970
+ if (apiKey) {
10971
+ setConfigValue("apiKey", apiKey);
10972
+ }
10973
+ const backendType = cleanUrl.includes("/v1") ? "vllm" : "ollama";
10974
+ setConfigValue("backendType", backendType);
10975
+ process.stdout.write(`
10976
+ ${c2.green("\u2714")} Configured: ${c2.bold(chosenModel)} at ${c2.bold(cleanUrl)}
10977
+ `);
10978
+ if (apiKey)
10979
+ process.stdout.write(` ${c2.green("\u2714")} API key saved.
10980
+ `);
10981
+ process.stdout.write(` ${c2.green("\u2714")} Backend type: ${c2.bold(backendType)}
10982
+
10983
+ `);
10984
+ return chosenModel;
10985
+ }
10812
10986
  async function doSetup(config, rl) {
10813
10987
  process.stdout.write(`
10814
10988
  ${c2.bold(c2.cyan("open-agents"))}
@@ -10832,16 +11006,42 @@ async function doSetup(config, rl) {
10832
11006
  }
10833
11007
  process.stdout.write("\n");
10834
11008
  let models = [];
11009
+ let usingCustomEndpoint = false;
10835
11010
  try {
10836
11011
  models = await fetchOllamaModels(config.backendUrl);
10837
11012
  } catch {
10838
- renderError(`Cannot reach Ollama at ${config.backendUrl}`);
10839
- renderInfo("Start Ollama with: ollama serve");
10840
- renderInfo("Or use /endpoint to configure a remote backend after startup.");
10841
- const answer = await ask(rl, `
10842
- ${c2.bold("Continue without Ollama?")} (y/n) `);
10843
- if (answer.toLowerCase() !== "y")
10844
- return null;
11013
+ process.stdout.write(` ${c2.yellow("\u26A0")} Cannot reach Ollama at ${c2.bold(config.backendUrl)}
11014
+
11015
+ `);
11016
+ const useWithout = await ask(rl, ` ${c2.bold("Use without Ollama?")} (y/n) `);
11017
+ if (useWithout.toLowerCase() === "y" || useWithout.toLowerCase() === "yes") {
11018
+ const endpointResult = await promptForCustomEndpoint(config, rl);
11019
+ if (endpointResult) {
11020
+ return endpointResult;
11021
+ }
11022
+ usingCustomEndpoint = true;
11023
+ } else {
11024
+ process.stdout.write(`
11025
+ ${c2.cyan("\u25CF")} Install Ollama: ${c2.bold(c2.cyan("https://ollama.com"))}
11026
+ `);
11027
+ process.stdout.write(` ${c2.dim("Linux:")} curl -fsSL https://ollama.com/install.sh | sh
11028
+ `);
11029
+ process.stdout.write(` ${c2.dim("macOS:")} brew install ollama
11030
+ `);
11031
+ process.stdout.write(` ${c2.dim("Then:")} ollama serve
11032
+
11033
+ `);
11034
+ const startAnyway = await ask(rl, ` ${c2.bold("Start anyway?")} (y/n) `);
11035
+ if (startAnyway.toLowerCase() !== "y" && startAnyway.toLowerCase() !== "yes") {
11036
+ process.stdout.write(`
11037
+ ${c2.dim("You can always configure an endpoint later with /endpoint")}
11038
+
11039
+ `);
11040
+ }
11041
+ return config.model;
11042
+ }
11043
+ }
11044
+ if (usingCustomEndpoint) {
10845
11045
  return config.model;
10846
11046
  }
10847
11047
  const currentModel = findModel(models, config.model);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.15.6",
3
+ "version": "0.15.7",
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",