open-agents-ai 0.187.371 → 0.187.372

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 +227 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -81,6 +81,7 @@ function loadConfigFile() {
81
81
  if (typeof parsed.dryRun === "boolean") result.dryRun = parsed.dryRun;
82
82
  if (typeof parsed.verbose === "boolean") result.verbose = parsed.verbose;
83
83
  if (typeof parsed.dbPath === "string") result.dbPath = parsed.dbPath;
84
+ if (typeof parsed.thinking === "boolean") result.thinking = parsed.thinking;
84
85
  return result;
85
86
  } catch {
86
87
  return {};
@@ -515672,18 +515673,38 @@ function stripThinkBlocks(s2) {
515672
515673
  function computeEffectiveThink(params) {
515673
515674
  if (process.env["OA_FORCE_NO_THINK"] === "1")
515674
515675
  return false;
515676
+ if (params.suppressed)
515677
+ return false;
515675
515678
  if (params.hasTools)
515676
515679
  return false;
515677
515680
  if (typeof params.requestThink === "boolean")
515678
515681
  return params.requestThink;
515679
- if (process.env["OA_THINK_AUTO"] === "1" && Array.isArray(params.messages)) {
515682
+ if (process.env["OA_THINK_AUTO"] !== "0" && Array.isArray(params.messages)) {
515680
515683
  const blob = params.messages.filter((m2) => m2.role === "user" || m2.role === "system").map((m2) => typeof m2.content === "string" ? m2.content : "").join("\n").toLowerCase();
515681
- if (/\b(plan|decompose|analyze(?:\s+complex)?|step\s*by\s*step|reason through|think through)\b/.test(blob)) {
515684
+ if (/\b(plan|decompose|analyze(?:\s+complex)?|step\s*by\s*step|reason through|think through|reason step)\b/.test(blob)) {
515682
515685
  return true;
515683
515686
  }
515684
515687
  }
515685
515688
  return params.defaultThink;
515686
515689
  }
515690
+ function classifyThinkOutcome(raw) {
515691
+ if (!raw)
515692
+ return "empty_after_strip";
515693
+ const hasOpen = /<think>/i.test(raw);
515694
+ const hasClose = /<\/think>/i.test(raw);
515695
+ if (hasOpen && !hasClose)
515696
+ return "unclosed_think";
515697
+ const stripped = stripThinkBlocks(raw);
515698
+ if (stripped.trim().length < 2)
515699
+ return "empty_after_strip";
515700
+ if (hasOpen && hasClose) {
515701
+ const thinkLen = raw.length - stripped.length;
515702
+ if (thinkLen > raw.length * 0.9 && stripped.trim().length < 40) {
515703
+ return "runaway_think";
515704
+ }
515705
+ }
515706
+ return null;
515707
+ }
515687
515708
  var SYSTEM_PROMPT, SYSTEM_PROMPT_MEDIUM, SYSTEM_PROMPT_SMALL, VISUAL_TOOLS, AUDIO_TOOLS, SOCIAL_TOOLS, SPATIAL_TOOLS, CODE_TOOLS, AgenticRunner, OllamaAgenticBackend;
515688
515709
  var init_agenticRunner = __esm({
515689
515710
  "packages/orchestrator/dist/agenticRunner.js"() {
@@ -516506,6 +516527,40 @@ ${body}`;
516506
516527
  }
516507
516528
  }
516508
516529
  }
516530
+ /**
516531
+ * Think-loop-guard runner hook. Called once per turn at the top of the
516532
+ * agentic loop. Responsibilities:
516533
+ * 1. Consume OA_THINK_GUARD_RESET env var (written by /think reset) to
516534
+ * clear a prior suppression — the CLI can't talk to the backend
516535
+ * directly, so it drops a timestamp in the env and we pick it up.
516536
+ * 2. Emit a one-shot user-visible warning the first turn after the
516537
+ * guard trips, so the user knows why answers suddenly look different.
516538
+ */
516539
+ _lastThinkGuardResetAt = 0;
516540
+ _maybeApplyThinkGuard() {
516541
+ const resetRaw = process.env["OA_THINK_GUARD_RESET"];
516542
+ if (resetRaw) {
516543
+ const ts = Number(resetRaw);
516544
+ if (Number.isFinite(ts) && ts > this._lastThinkGuardResetAt) {
516545
+ this._lastThinkGuardResetAt = ts;
516546
+ if (typeof this.backend.resetThinkGuard === "function") {
516547
+ this.backend.resetThinkGuard();
516548
+ this.emit({
516549
+ type: "status",
516550
+ content: "🧠 Think-guard cleared — reasoning mode will re-enable on the next eligible request.",
516551
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
516552
+ });
516553
+ }
516554
+ }
516555
+ }
516556
+ if (typeof this.backend.consumeSuppressionNotice === "function" && this.backend.consumeSuppressionNotice()) {
516557
+ this.emit({
516558
+ type: "status",
516559
+ content: "⚠ Think-mode auto-suppressed — two consecutive empty/unclosed-<think> responses detected. Continuing with direct answers. Use `/think reset` to retry.",
516560
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
516561
+ });
516562
+ }
516563
+ }
516509
516564
  /**
516510
516565
  * Detect repetition in recent tool calls.
516511
516566
  * Returns a score 0-1 where 1 = fully repetitive (stuck in a loop).
@@ -516784,6 +516839,7 @@ TASK: ${task}` : task;
516784
516839
  }
516785
516840
  for (let turn = 0; turn < this.options.maxTurns; turn++) {
516786
516841
  clearTurnState(this._appState);
516842
+ this._maybeApplyThinkGuard();
516787
516843
  if (this._paused) {
516788
516844
  const shouldContinue = await this.waitIfPaused();
516789
516845
  if (!shouldContinue) {
@@ -518264,6 +518320,7 @@ You have ${this.options.maxTurns} more turns. Continue making progress. Call tas
518264
518320
  messages2.push(...compacted);
518265
518321
  }
518266
518322
  for (let turn = 0; turn < this.options.maxTurns; turn++) {
518323
+ this._maybeApplyThinkGuard();
518267
518324
  if (this._paused) {
518268
518325
  const shouldContinue = await this.waitIfPaused();
518269
518326
  if (!shouldContinue) {
@@ -521120,13 +521177,23 @@ ${description}`
521120
521177
  return resp;
521121
521178
  }
521122
521179
  };
521123
- OllamaAgenticBackend = class {
521180
+ OllamaAgenticBackend = class _OllamaAgenticBackend {
521124
521181
  baseUrl;
521125
521182
  model;
521126
521183
  apiKey;
521127
521184
  thinking;
521128
521185
  /** Abort signal — set by the runner so /stop can cancel in-flight requests */
521129
521186
  _abortSignal = null;
521187
+ // ── Think-loop guard (0.187.372) ──────────────────────────────────────
521188
+ // If the model keeps producing empty / unclosed-think responses, we
521189
+ // assume Qwen3 dual-mode is looping and start suppressing think for
521190
+ // this backend instance. User can clear via /think reset.
521191
+ _thinkFailStreak = 0;
521192
+ _thinkSuccessStreak = 0;
521193
+ _thinkSuppressed = false;
521194
+ _thinkSuppressedNotified = false;
521195
+ static _thinkFailThreshold = 2;
521196
+ static _thinkRecoveryThreshold = 6;
521130
521197
  /** Multi-key pool — round-robin rotation per request for load distribution */
521131
521198
  _keyPool = [];
521132
521199
  _keyIndex = 0;
@@ -521148,6 +521215,61 @@ ${description}`
521148
521215
  setAbortSignal(signal) {
521149
521216
  this._abortSignal = signal;
521150
521217
  }
521218
+ /** Is think currently auto-suppressed by the loop-guard? */
521219
+ isThinkSuppressed() {
521220
+ return this._thinkSuppressed;
521221
+ }
521222
+ /** Clear the loop-guard — lets think re-enable on the next eligible request. */
521223
+ resetThinkGuard() {
521224
+ this._thinkFailStreak = 0;
521225
+ this._thinkSuccessStreak = 0;
521226
+ this._thinkSuppressed = false;
521227
+ this._thinkSuppressedNotified = false;
521228
+ }
521229
+ /**
521230
+ * Feed a completed assistant response into the loop-guard. We only
521231
+ * update counters on responses that WERE think=true — otherwise
521232
+ * think-off responses (the vast majority) would drive the counters
521233
+ * and mask the failure signal we're trying to detect.
521234
+ *
521235
+ * Failure classes (per classifyThinkOutcome) bump the fail streak.
521236
+ * Healthy think-mode responses bump the success streak and, past a
521237
+ * threshold, clear a prior suppression so think can come back on if
521238
+ * the model is behaving again.
521239
+ *
521240
+ * Returns the classification so callers can decide whether to
521241
+ * emit a warning / retry.
521242
+ */
521243
+ recordThinkOutcome(raw, wasThinkRequested) {
521244
+ if (!wasThinkRequested)
521245
+ return null;
521246
+ const cls = classifyThinkOutcome(raw);
521247
+ if (cls !== null) {
521248
+ this._thinkFailStreak++;
521249
+ this._thinkSuccessStreak = 0;
521250
+ if (this._thinkFailStreak >= _OllamaAgenticBackend._thinkFailThreshold && !this._thinkSuppressed) {
521251
+ this._thinkSuppressed = true;
521252
+ }
521253
+ } else {
521254
+ this._thinkSuccessStreak++;
521255
+ this._thinkFailStreak = 0;
521256
+ if (this._thinkSuppressed && this._thinkSuccessStreak >= _OllamaAgenticBackend._thinkRecoveryThreshold) {
521257
+ this._thinkSuppressed = false;
521258
+ this._thinkSuppressedNotified = false;
521259
+ }
521260
+ }
521261
+ return cls;
521262
+ }
521263
+ /** Pick up the one-shot "notify about suppression" flag. Returns true
521264
+ * the first time it's called after a trip; false thereafter until
521265
+ * the guard resets. Used by the runner to emit a single warning. */
521266
+ consumeSuppressionNotice() {
521267
+ if (this._thinkSuppressed && !this._thinkSuppressedNotified) {
521268
+ this._thinkSuppressedNotified = true;
521269
+ return true;
521270
+ }
521271
+ return false;
521272
+ }
521151
521273
  /** Build auth headers — adapts to provider (Bearer for most, x-api-key for Anthropic).
521152
521274
  * When a key pool is set, round-robins through keys per request. */
521153
521275
  authHeaders() {
@@ -521176,7 +521298,8 @@ ${description}`
521176
521298
  requestThink: request.think,
521177
521299
  defaultThink: this.thinking,
521178
521300
  hasTools: Array.isArray(request.tools) && request.tools.length > 0,
521179
- messages: cleanedMessages
521301
+ messages: cleanedMessages,
521302
+ suppressed: this._thinkSuppressed
521180
521303
  });
521181
521304
  let effectiveMaxTokens = request.maxTokens;
521182
521305
  if (effectiveThink === true && (effectiveMaxTokens ?? 0) < 4096) {
@@ -521207,6 +521330,71 @@ ${description}`
521207
521330
  const data = await resp.json();
521208
521331
  const choices = data.choices ?? [];
521209
521332
  const usage = data.usage;
521333
+ const firstChoice = choices[0];
521334
+ const responseText = firstChoice ? String(firstChoice.message?.content ?? "") : "";
521335
+ const outcome = this.recordThinkOutcome(responseText, effectiveThink === true);
521336
+ if (outcome !== null && effectiveThink === true) {
521337
+ const justSuppressed = this._thinkSuppressed && this._thinkFailStreak === _OllamaAgenticBackend._thinkFailThreshold;
521338
+ if (justSuppressed || outcome === "empty_after_strip" || outcome === "unclosed_think") {
521339
+ const retryBody = {
521340
+ model: this.model,
521341
+ messages: cleanedMessages,
521342
+ tools: request.tools,
521343
+ temperature: request.temperature,
521344
+ max_tokens: request.maxTokens,
521345
+ think: false
521346
+ };
521347
+ try {
521348
+ const retryOpts = {
521349
+ method: "POST",
521350
+ headers: this.authHeaders(),
521351
+ body: JSON.stringify(retryBody)
521352
+ };
521353
+ if (this._abortSignal)
521354
+ retryOpts.signal = this._abortSignal;
521355
+ const retryResp = await fetch(`${this.baseUrl}/v1/chat/completions`, retryOpts);
521356
+ if (retryResp.ok) {
521357
+ const retryData = await retryResp.json();
521358
+ const retryChoices = retryData.choices ?? [];
521359
+ const retryUsage = retryData.usage;
521360
+ if (retryChoices.length > 0) {
521361
+ return {
521362
+ choices: retryChoices.map((c8) => {
521363
+ const msg = c8.message;
521364
+ const toolCalls = msg.tool_calls ?? [];
521365
+ return {
521366
+ message: {
521367
+ content: msg.content || null,
521368
+ toolCalls: toolCalls.length > 0 ? toolCalls.map((tc) => {
521369
+ const fn = tc.function;
521370
+ let args;
521371
+ try {
521372
+ args = typeof fn.arguments === "string" ? JSON.parse(fn.arguments) : fn.arguments ?? {};
521373
+ } catch {
521374
+ const repaired = repairJson(fn.arguments);
521375
+ args = repaired ?? { _raw: fn.arguments };
521376
+ }
521377
+ return {
521378
+ id: tc.id || crypto.randomUUID(),
521379
+ name: fn.name,
521380
+ arguments: args
521381
+ };
521382
+ }) : void 0
521383
+ }
521384
+ };
521385
+ }),
521386
+ usage: retryUsage ? {
521387
+ totalTokens: retryUsage.total_tokens ?? 0,
521388
+ promptTokens: retryUsage.prompt_tokens,
521389
+ completionTokens: retryUsage.completion_tokens
521390
+ } : void 0
521391
+ };
521392
+ }
521393
+ }
521394
+ } catch {
521395
+ }
521396
+ }
521397
+ }
521210
521398
  return {
521211
521399
  choices: choices.map((c8) => {
521212
521400
  const msg = c8.message;
@@ -521350,7 +521538,8 @@ ${description}`
521350
521538
  requestThink: request.think,
521351
521539
  defaultThink: this.thinking,
521352
521540
  hasTools: Array.isArray(request.tools) && request.tools.length > 0,
521353
- messages: cleanedMessages
521541
+ messages: cleanedMessages,
521542
+ suppressed: this._thinkSuppressed
521354
521543
  });
521355
521544
  let effectiveMaxTokens = request.maxTokens;
521356
521545
  if (effectiveThink === true && (effectiveMaxTokens ?? 0) < 4096) {
@@ -521382,6 +521571,9 @@ ${description}`
521382
521571
  }
521383
521572
  let sseBuffer = "";
521384
521573
  const decoder = new TextDecoder();
521574
+ let accumulatedContent = "";
521575
+ let accumulatedThinking = "";
521576
+ let sawReasoningTokens = false;
521385
521577
  for await (const rawChunk of resp.body) {
521386
521578
  sseBuffer += decoder.decode(rawChunk, { stream: true });
521387
521579
  const parts = sseBuffer.split("\n\n");
@@ -521390,8 +521582,10 @@ ${description}`
521390
521582
  const line = part.trim();
521391
521583
  if (!line)
521392
521584
  continue;
521393
- if (line === "data: [DONE]")
521585
+ if (line === "data: [DONE]") {
521586
+ this._finalizeStreamGuard(effectiveThink, accumulatedContent, accumulatedThinking, sawReasoningTokens);
521394
521587
  return;
521588
+ }
521395
521589
  if (!line.startsWith("data: "))
521396
521590
  continue;
521397
521591
  try {
@@ -521415,9 +521609,12 @@ ${description}`
521415
521609
  const finishReason = choice.finish_reason;
521416
521610
  const reasoningToken = delta?.reasoning ?? delta?.reasoning_content;
521417
521611
  if (reasoningToken) {
521612
+ sawReasoningTokens = true;
521613
+ accumulatedThinking += reasoningToken;
521418
521614
  yield { type: "content", content: reasoningToken, thinking: true };
521419
521615
  }
521420
521616
  if (delta?.content) {
521617
+ accumulatedContent += delta.content;
521421
521618
  yield { type: "content", content: delta.content };
521422
521619
  }
521423
521620
  const tcDeltas = delta?.tool_calls;
@@ -521451,6 +521648,23 @@ ${description}`
521451
521648
  }
521452
521649
  }
521453
521650
  }
521651
+ this._finalizeStreamGuard(effectiveThink, accumulatedContent, accumulatedThinking, sawReasoningTokens);
521652
+ }
521653
+ /** Reconstruct a raw-looking assistant response from the streamed
521654
+ * parts, then feed it into the loop-guard. Used at stream end (both
521655
+ * the [DONE] case and the fell-off-the-end case). */
521656
+ _finalizeStreamGuard(thinkRequested, content, thinking, hadReasoningTokens) {
521657
+ if (!thinkRequested) {
521658
+ this.recordThinkOutcome(content, false);
521659
+ return;
521660
+ }
521661
+ let rawLike;
521662
+ if (hadReasoningTokens && thinking) {
521663
+ rawLike = `<think>${thinking}</think>${content}`;
521664
+ } else {
521665
+ rawLike = content;
521666
+ }
521667
+ this.recordThinkOutcome(rawLike, true);
521454
521668
  }
521455
521669
  };
521456
521670
  }
@@ -546170,13 +546384,18 @@ Clone a new voice: /voice clone <wav-file> [name]`);
546170
546384
  if (token === "status" || token === "?") {
546171
546385
  const cur = ctx3.config.thinking ?? false;
546172
546386
  renderInfo2(`Thinking mode: ${cur ? "on" : "off"} — ${desc(cur)}`);
546173
- if (process.env["OA_THINK_AUTO"] === "1") renderInfo2("Auto-heuristic active (OA_THINK_AUTO=1)");
546387
+ if (process.env["OA_THINK_AUTO"] !== "0") renderInfo2("Auto-heuristic active (set OA_THINK_AUTO=0 to disable) — user messages with plan/decompose/analyze/step-by-step/reason-through auto-flip to think=on, tool calls stay off.");
546174
546388
  if (process.env["OA_FORCE_NO_THINK"] === "1") renderWarning2("OA_FORCE_NO_THINK=1 forces off regardless of /think setting");
546175
546389
  return "handled";
546176
546390
  }
546177
546391
  if (token === "auto") {
546178
546392
  process.env["OA_THINK_AUTO"] = "1";
546179
- renderInfo2("Thinking auto-heuristic enabled: /think flips on when user message contains plan/decompose/analyze/step-by-step/reason-through. Tool calls still force off. Unset with OA_THINK_AUTO=0.");
546393
+ renderInfo2("Thinking auto-heuristic enabled (default since 0.187.372). User message containing plan/decompose/analyze/step-by-step/reason-through auto-flips think=on; tool calls still force off. Disable with OA_THINK_AUTO=0.");
546394
+ return "handled";
546395
+ }
546396
+ if (token === "reset" || token === "clear") {
546397
+ process.env["OA_THINK_GUARD_RESET"] = String(Date.now());
546398
+ renderInfo2("Loop-guard reset requested. If think was auto-suppressed after empty/unclosed-think responses, it will re-enable on the next eligible request.");
546180
546399
  return "handled";
546181
546400
  }
546182
546401
  let isOn;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.187.371",
3
+ "version": "0.187.372",
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",