open-agents-ai 0.15.6 → 0.15.8

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 +226 -24
  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);
@@ -15222,10 +15422,8 @@ async function startInteractive(config, repoPath) {
15222
15422
  const needsSetup = isFirstRun() || !await isModelAvailable(config);
15223
15423
  if (needsSetup && config.backendType === "ollama") {
15224
15424
  const setupModel = await runSetupWizard(config);
15225
- if (setupModel === null) {
15226
- process.exit(0);
15227
- }
15228
- config = { ...config, model: setupModel };
15425
+ const freshConfig = loadConfig();
15426
+ config = { ...config, ...freshConfig, model: setupModel ?? freshConfig.model };
15229
15427
  }
15230
15428
  }
15231
15429
  if (config.backendType === "ollama" && !config.model.startsWith("open-agents-")) {
@@ -15244,16 +15442,18 @@ async function startInteractive(config, repoPath) {
15244
15442
  if (!isResumed) {
15245
15443
  try {
15246
15444
  const healthUrl = config.backendType === "ollama" ? `${config.backendUrl}/api/tags` : `${config.backendUrl}/v1/models`;
15247
- const resp = await fetch(healthUrl, { signal: AbortSignal.timeout(1e4) });
15445
+ const headers = {};
15446
+ if (config.apiKey)
15447
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
15448
+ const resp = await fetch(healthUrl, { headers, signal: AbortSignal.timeout(1e4) });
15248
15449
  if (!resp.ok)
15249
15450
  throw new Error(`HTTP ${resp.status}`);
15250
15451
  } catch {
15251
- renderError(`Cannot reach ${config.backendType} at ${config.backendUrl}`);
15452
+ renderWarning(`Cannot reach ${config.backendType} at ${config.backendUrl}`);
15252
15453
  if (config.backendType === "ollama") {
15253
15454
  renderInfo("Start Ollama with: ollama serve");
15254
15455
  }
15255
- renderInfo("Use /endpoint to configure a different backend.");
15256
- process.exit(1);
15456
+ renderInfo("Use /endpoint to configure a different backend. Starting anyway...");
15257
15457
  }
15258
15458
  }
15259
15459
  const carousel = new Carousel();
@@ -15757,10 +15957,8 @@ async function runWithTUI(task, config, repoPath) {
15757
15957
  const needsSetup = isFirstRun() || !await isModelAvailable(config);
15758
15958
  if (needsSetup && config.backendType === "ollama") {
15759
15959
  const setupModel = await runSetupWizard(config);
15760
- if (setupModel === null) {
15761
- process.exit(0);
15762
- }
15763
- config = { ...config, model: setupModel };
15960
+ const freshConfig = loadConfig();
15961
+ config = { ...config, ...freshConfig, model: setupModel ?? freshConfig.model };
15764
15962
  }
15765
15963
  if (config.backendType === "ollama" && !config.model.startsWith("open-agents-")) {
15766
15964
  try {
@@ -15773,15 +15971,18 @@ async function runWithTUI(task, config, repoPath) {
15773
15971
  }
15774
15972
  try {
15775
15973
  const healthUrl = config.backendType === "ollama" ? `${config.backendUrl}/api/tags` : `${config.backendUrl}/v1/models`;
15776
- const resp = await fetch(healthUrl, { signal: AbortSignal.timeout(1e4) });
15974
+ const headers = {};
15975
+ if (config.apiKey)
15976
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
15977
+ const resp = await fetch(healthUrl, { headers, signal: AbortSignal.timeout(1e4) });
15777
15978
  if (!resp.ok)
15778
15979
  throw new Error(`HTTP ${resp.status}`);
15779
15980
  } catch {
15780
- renderError(`Cannot reach ${config.backendType} at ${config.backendUrl}`);
15981
+ renderWarning(`Cannot reach ${config.backendType} at ${config.backendUrl}`);
15781
15982
  if (config.backendType === "ollama") {
15782
15983
  renderInfo("Start Ollama with: ollama serve");
15783
15984
  }
15784
- process.exit(1);
15985
+ renderInfo("The agent will retry when you submit a task. Use /endpoint to reconfigure.");
15785
15986
  }
15786
15987
  renderCompactHeader(config.model);
15787
15988
  renderUserMessage(task);
@@ -15800,6 +16001,7 @@ var init_interactive = __esm({
15800
16001
  init_dist5();
15801
16002
  init_dist2();
15802
16003
  init_listen();
16004
+ init_config();
15803
16005
  init_updater();
15804
16006
  init_commands();
15805
16007
  init_setup();
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.8",
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",