open-agents-ai 0.187.371 → 0.187.373

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 +288 -11
  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,57 @@ 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;
515678
+ const userOptedIn = params.requestThink === true || params.requestThink === void 0 && params.defaultThink === true;
515679
+ if (userOptedIn)
515680
+ return true;
515681
+ if (params.requestThink === false)
515682
+ return false;
515675
515683
  if (params.hasTools)
515676
515684
  return false;
515677
- if (typeof params.requestThink === "boolean")
515678
- return params.requestThink;
515679
- if (process.env["OA_THINK_AUTO"] === "1" && Array.isArray(params.messages)) {
515685
+ if (process.env["OA_THINK_AUTO"] !== "0" && Array.isArray(params.messages)) {
515680
515686
  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)) {
515687
+ if (/\b(plan|decompose|analyze(?:\s+complex)?|step\s*by\s*step|reason through|think through|reason step)\b/.test(blob)) {
515682
515688
  return true;
515683
515689
  }
515684
515690
  }
515685
515691
  return params.defaultThink;
515686
515692
  }
515693
+ function sanitizeHistoryThink(messages2) {
515694
+ let lastAsstIdx = -1;
515695
+ for (let i2 = messages2.length - 1; i2 >= 0; i2--) {
515696
+ if (messages2[i2]?.role === "assistant") {
515697
+ lastAsstIdx = i2;
515698
+ break;
515699
+ }
515700
+ }
515701
+ return messages2.map((m2, i2) => {
515702
+ if (m2.role !== "assistant" || typeof m2.content !== "string")
515703
+ return m2;
515704
+ if (i2 === lastAsstIdx)
515705
+ return m2;
515706
+ return { ...m2, content: stripThinkBlocks(m2.content) };
515707
+ });
515708
+ }
515709
+ function classifyThinkOutcome(raw) {
515710
+ if (!raw)
515711
+ return "empty_after_strip";
515712
+ const hasOpen = /<think>/i.test(raw);
515713
+ const hasClose = /<\/think>/i.test(raw);
515714
+ if (hasOpen && !hasClose)
515715
+ return "unclosed_think";
515716
+ const stripped = stripThinkBlocks(raw);
515717
+ if (stripped.trim().length < 2)
515718
+ return "empty_after_strip";
515719
+ if (hasOpen && hasClose) {
515720
+ const thinkLen = raw.length - stripped.length;
515721
+ if (thinkLen > raw.length * 0.9 && stripped.trim().length < 40) {
515722
+ return "runaway_think";
515723
+ }
515724
+ }
515725
+ return null;
515726
+ }
515687
515727
  var SYSTEM_PROMPT, SYSTEM_PROMPT_MEDIUM, SYSTEM_PROMPT_SMALL, VISUAL_TOOLS, AUDIO_TOOLS, SOCIAL_TOOLS, SPATIAL_TOOLS, CODE_TOOLS, AgenticRunner, OllamaAgenticBackend;
515688
515728
  var init_agenticRunner = __esm({
515689
515729
  "packages/orchestrator/dist/agenticRunner.js"() {
@@ -516506,6 +516546,40 @@ ${body}`;
516506
516546
  }
516507
516547
  }
516508
516548
  }
516549
+ /**
516550
+ * Think-loop-guard runner hook. Called once per turn at the top of the
516551
+ * agentic loop. Responsibilities:
516552
+ * 1. Consume OA_THINK_GUARD_RESET env var (written by /think reset) to
516553
+ * clear a prior suppression — the CLI can't talk to the backend
516554
+ * directly, so it drops a timestamp in the env and we pick it up.
516555
+ * 2. Emit a one-shot user-visible warning the first turn after the
516556
+ * guard trips, so the user knows why answers suddenly look different.
516557
+ */
516558
+ _lastThinkGuardResetAt = 0;
516559
+ _maybeApplyThinkGuard() {
516560
+ const resetRaw = process.env["OA_THINK_GUARD_RESET"];
516561
+ if (resetRaw) {
516562
+ const ts = Number(resetRaw);
516563
+ if (Number.isFinite(ts) && ts > this._lastThinkGuardResetAt) {
516564
+ this._lastThinkGuardResetAt = ts;
516565
+ if (typeof this.backend.resetThinkGuard === "function") {
516566
+ this.backend.resetThinkGuard();
516567
+ this.emit({
516568
+ type: "status",
516569
+ content: "🧠 Think-guard cleared — reasoning mode will re-enable on the next eligible request.",
516570
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
516571
+ });
516572
+ }
516573
+ }
516574
+ }
516575
+ if (typeof this.backend.consumeSuppressionNotice === "function" && this.backend.consumeSuppressionNotice()) {
516576
+ this.emit({
516577
+ type: "status",
516578
+ content: "⚠ Think-mode auto-suppressed — two consecutive empty/unclosed-<think> responses detected. Continuing with direct answers. Use `/think reset` to retry.",
516579
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
516580
+ });
516581
+ }
516582
+ }
516509
516583
  /**
516510
516584
  * Detect repetition in recent tool calls.
516511
516585
  * Returns a score 0-1 where 1 = fully repetitive (stuck in a loop).
@@ -516784,6 +516858,7 @@ TASK: ${task}` : task;
516784
516858
  }
516785
516859
  for (let turn = 0; turn < this.options.maxTurns; turn++) {
516786
516860
  clearTurnState(this._appState);
516861
+ this._maybeApplyThinkGuard();
516787
516862
  if (this._paused) {
516788
516863
  const shouldContinue = await this.waitIfPaused();
516789
516864
  if (!shouldContinue) {
@@ -518264,6 +518339,7 @@ You have ${this.options.maxTurns} more turns. Continue making progress. Call tas
518264
518339
  messages2.push(...compacted);
518265
518340
  }
518266
518341
  for (let turn = 0; turn < this.options.maxTurns; turn++) {
518342
+ this._maybeApplyThinkGuard();
518267
518343
  if (this._paused) {
518268
518344
  const shouldContinue = await this.waitIfPaused();
518269
518345
  if (!shouldContinue) {
@@ -521120,13 +521196,23 @@ ${description}`
521120
521196
  return resp;
521121
521197
  }
521122
521198
  };
521123
- OllamaAgenticBackend = class {
521199
+ OllamaAgenticBackend = class _OllamaAgenticBackend {
521124
521200
  baseUrl;
521125
521201
  model;
521126
521202
  apiKey;
521127
521203
  thinking;
521128
521204
  /** Abort signal — set by the runner so /stop can cancel in-flight requests */
521129
521205
  _abortSignal = null;
521206
+ // ── Think-loop guard (0.187.372) ──────────────────────────────────────
521207
+ // If the model keeps producing empty / unclosed-think responses, we
521208
+ // assume Qwen3 dual-mode is looping and start suppressing think for
521209
+ // this backend instance. User can clear via /think reset.
521210
+ _thinkFailStreak = 0;
521211
+ _thinkSuccessStreak = 0;
521212
+ _thinkSuppressed = false;
521213
+ _thinkSuppressedNotified = false;
521214
+ static _thinkFailThreshold = 2;
521215
+ static _thinkRecoveryThreshold = 6;
521130
521216
  /** Multi-key pool — round-robin rotation per request for load distribution */
521131
521217
  _keyPool = [];
521132
521218
  _keyIndex = 0;
@@ -521148,6 +521234,61 @@ ${description}`
521148
521234
  setAbortSignal(signal) {
521149
521235
  this._abortSignal = signal;
521150
521236
  }
521237
+ /** Is think currently auto-suppressed by the loop-guard? */
521238
+ isThinkSuppressed() {
521239
+ return this._thinkSuppressed;
521240
+ }
521241
+ /** Clear the loop-guard — lets think re-enable on the next eligible request. */
521242
+ resetThinkGuard() {
521243
+ this._thinkFailStreak = 0;
521244
+ this._thinkSuccessStreak = 0;
521245
+ this._thinkSuppressed = false;
521246
+ this._thinkSuppressedNotified = false;
521247
+ }
521248
+ /**
521249
+ * Feed a completed assistant response into the loop-guard. We only
521250
+ * update counters on responses that WERE think=true — otherwise
521251
+ * think-off responses (the vast majority) would drive the counters
521252
+ * and mask the failure signal we're trying to detect.
521253
+ *
521254
+ * Failure classes (per classifyThinkOutcome) bump the fail streak.
521255
+ * Healthy think-mode responses bump the success streak and, past a
521256
+ * threshold, clear a prior suppression so think can come back on if
521257
+ * the model is behaving again.
521258
+ *
521259
+ * Returns the classification so callers can decide whether to
521260
+ * emit a warning / retry.
521261
+ */
521262
+ recordThinkOutcome(raw, wasThinkRequested) {
521263
+ if (!wasThinkRequested)
521264
+ return null;
521265
+ const cls = classifyThinkOutcome(raw);
521266
+ if (cls !== null) {
521267
+ this._thinkFailStreak++;
521268
+ this._thinkSuccessStreak = 0;
521269
+ if (this._thinkFailStreak >= _OllamaAgenticBackend._thinkFailThreshold && !this._thinkSuppressed) {
521270
+ this._thinkSuppressed = true;
521271
+ }
521272
+ } else {
521273
+ this._thinkSuccessStreak++;
521274
+ this._thinkFailStreak = 0;
521275
+ if (this._thinkSuppressed && this._thinkSuccessStreak >= _OllamaAgenticBackend._thinkRecoveryThreshold) {
521276
+ this._thinkSuppressed = false;
521277
+ this._thinkSuppressedNotified = false;
521278
+ }
521279
+ }
521280
+ return cls;
521281
+ }
521282
+ /** Pick up the one-shot "notify about suppression" flag. Returns true
521283
+ * the first time it's called after a trip; false thereafter until
521284
+ * the guard resets. Used by the runner to emit a single warning. */
521285
+ consumeSuppressionNotice() {
521286
+ if (this._thinkSuppressed && !this._thinkSuppressedNotified) {
521287
+ this._thinkSuppressedNotified = true;
521288
+ return true;
521289
+ }
521290
+ return false;
521291
+ }
521151
521292
  /** Build auth headers — adapts to provider (Bearer for most, x-api-key for Anthropic).
521152
521293
  * When a key pool is set, round-robins through keys per request. */
521153
521294
  authHeaders() {
@@ -521171,12 +521312,13 @@ ${description}`
521171
521312
  if (this._isAnthropic) {
521172
521313
  return this._anthropicChatCompletion(request);
521173
521314
  }
521174
- const cleanedMessages = request.messages.map((m2) => m2.role === "assistant" && typeof m2.content === "string" ? { ...m2, content: stripThinkBlocks(m2.content) } : m2);
521315
+ const cleanedMessages = sanitizeHistoryThink(request.messages);
521175
521316
  const effectiveThink = computeEffectiveThink({
521176
521317
  requestThink: request.think,
521177
521318
  defaultThink: this.thinking,
521178
521319
  hasTools: Array.isArray(request.tools) && request.tools.length > 0,
521179
- messages: cleanedMessages
521320
+ messages: cleanedMessages,
521321
+ suppressed: this._thinkSuppressed
521180
521322
  });
521181
521323
  let effectiveMaxTokens = request.maxTokens;
521182
521324
  if (effectiveThink === true && (effectiveMaxTokens ?? 0) < 4096) {
@@ -521207,6 +521349,71 @@ ${description}`
521207
521349
  const data = await resp.json();
521208
521350
  const choices = data.choices ?? [];
521209
521351
  const usage = data.usage;
521352
+ const firstChoice = choices[0];
521353
+ const responseText = firstChoice ? String(firstChoice.message?.content ?? "") : "";
521354
+ const outcome = this.recordThinkOutcome(responseText, effectiveThink === true);
521355
+ if (outcome !== null && effectiveThink === true) {
521356
+ const justSuppressed = this._thinkSuppressed && this._thinkFailStreak === _OllamaAgenticBackend._thinkFailThreshold;
521357
+ if (justSuppressed || outcome === "empty_after_strip" || outcome === "unclosed_think") {
521358
+ const retryBody = {
521359
+ model: this.model,
521360
+ messages: cleanedMessages,
521361
+ tools: request.tools,
521362
+ temperature: request.temperature,
521363
+ max_tokens: request.maxTokens,
521364
+ think: false
521365
+ };
521366
+ try {
521367
+ const retryOpts = {
521368
+ method: "POST",
521369
+ headers: this.authHeaders(),
521370
+ body: JSON.stringify(retryBody)
521371
+ };
521372
+ if (this._abortSignal)
521373
+ retryOpts.signal = this._abortSignal;
521374
+ const retryResp = await fetch(`${this.baseUrl}/v1/chat/completions`, retryOpts);
521375
+ if (retryResp.ok) {
521376
+ const retryData = await retryResp.json();
521377
+ const retryChoices = retryData.choices ?? [];
521378
+ const retryUsage = retryData.usage;
521379
+ if (retryChoices.length > 0) {
521380
+ return {
521381
+ choices: retryChoices.map((c8) => {
521382
+ const msg = c8.message;
521383
+ const toolCalls = msg.tool_calls ?? [];
521384
+ return {
521385
+ message: {
521386
+ content: msg.content || null,
521387
+ toolCalls: toolCalls.length > 0 ? toolCalls.map((tc) => {
521388
+ const fn = tc.function;
521389
+ let args;
521390
+ try {
521391
+ args = typeof fn.arguments === "string" ? JSON.parse(fn.arguments) : fn.arguments ?? {};
521392
+ } catch {
521393
+ const repaired = repairJson(fn.arguments);
521394
+ args = repaired ?? { _raw: fn.arguments };
521395
+ }
521396
+ return {
521397
+ id: tc.id || crypto.randomUUID(),
521398
+ name: fn.name,
521399
+ arguments: args
521400
+ };
521401
+ }) : void 0
521402
+ }
521403
+ };
521404
+ }),
521405
+ usage: retryUsage ? {
521406
+ totalTokens: retryUsage.total_tokens ?? 0,
521407
+ promptTokens: retryUsage.prompt_tokens,
521408
+ completionTokens: retryUsage.completion_tokens
521409
+ } : void 0
521410
+ };
521411
+ }
521412
+ }
521413
+ } catch {
521414
+ }
521415
+ }
521416
+ }
521210
521417
  return {
521211
521418
  choices: choices.map((c8) => {
521212
521419
  const msg = c8.message;
@@ -521350,7 +521557,8 @@ ${description}`
521350
521557
  requestThink: request.think,
521351
521558
  defaultThink: this.thinking,
521352
521559
  hasTools: Array.isArray(request.tools) && request.tools.length > 0,
521353
- messages: cleanedMessages
521560
+ messages: cleanedMessages,
521561
+ suppressed: this._thinkSuppressed
521354
521562
  });
521355
521563
  let effectiveMaxTokens = request.maxTokens;
521356
521564
  if (effectiveThink === true && (effectiveMaxTokens ?? 0) < 4096) {
@@ -521382,6 +521590,9 @@ ${description}`
521382
521590
  }
521383
521591
  let sseBuffer = "";
521384
521592
  const decoder = new TextDecoder();
521593
+ let accumulatedContent = "";
521594
+ let accumulatedThinking = "";
521595
+ let sawReasoningTokens = false;
521385
521596
  for await (const rawChunk of resp.body) {
521386
521597
  sseBuffer += decoder.decode(rawChunk, { stream: true });
521387
521598
  const parts = sseBuffer.split("\n\n");
@@ -521390,8 +521601,10 @@ ${description}`
521390
521601
  const line = part.trim();
521391
521602
  if (!line)
521392
521603
  continue;
521393
- if (line === "data: [DONE]")
521604
+ if (line === "data: [DONE]") {
521605
+ this._finalizeStreamGuard(effectiveThink, accumulatedContent, accumulatedThinking, sawReasoningTokens);
521394
521606
  return;
521607
+ }
521395
521608
  if (!line.startsWith("data: "))
521396
521609
  continue;
521397
521610
  try {
@@ -521415,9 +521628,12 @@ ${description}`
521415
521628
  const finishReason = choice.finish_reason;
521416
521629
  const reasoningToken = delta?.reasoning ?? delta?.reasoning_content;
521417
521630
  if (reasoningToken) {
521631
+ sawReasoningTokens = true;
521632
+ accumulatedThinking += reasoningToken;
521418
521633
  yield { type: "content", content: reasoningToken, thinking: true };
521419
521634
  }
521420
521635
  if (delta?.content) {
521636
+ accumulatedContent += delta.content;
521421
521637
  yield { type: "content", content: delta.content };
521422
521638
  }
521423
521639
  const tcDeltas = delta?.tool_calls;
@@ -521451,6 +521667,23 @@ ${description}`
521451
521667
  }
521452
521668
  }
521453
521669
  }
521670
+ this._finalizeStreamGuard(effectiveThink, accumulatedContent, accumulatedThinking, sawReasoningTokens);
521671
+ }
521672
+ /** Reconstruct a raw-looking assistant response from the streamed
521673
+ * parts, then feed it into the loop-guard. Used at stream end (both
521674
+ * the [DONE] case and the fell-off-the-end case). */
521675
+ _finalizeStreamGuard(thinkRequested, content, thinking, hadReasoningTokens) {
521676
+ if (!thinkRequested) {
521677
+ this.recordThinkOutcome(content, false);
521678
+ return;
521679
+ }
521680
+ let rawLike;
521681
+ if (hadReasoningTokens && thinking) {
521682
+ rawLike = `<think>${thinking}</think>${content}`;
521683
+ } else {
521684
+ rawLike = content;
521685
+ }
521686
+ this.recordThinkOutcome(rawLike, true);
521454
521687
  }
521455
521688
  };
521456
521689
  }
@@ -533035,6 +533268,10 @@ var init_status_bar = __esm({
533035
533268
  setNeovimFocusChecker(checker) {
533036
533269
  this._isNeovimFocused = checker;
533037
533270
  }
533271
+ /** Expose current mouse-tracking state for /mouse status display. */
533272
+ isMouseTrackingEnabled() {
533273
+ return this._mouseTrackingEnabled;
533274
+ }
533038
533275
  /** Enable mouse tracking — respects neovim focus state */
533039
533276
  enableMouseTracking() {
533040
533277
  if (this._mouseTrackingEnabled || isOverlayActive()) return;
@@ -546170,13 +546407,18 @@ Clone a new voice: /voice clone <wav-file> [name]`);
546170
546407
  if (token === "status" || token === "?") {
546171
546408
  const cur = ctx3.config.thinking ?? false;
546172
546409
  renderInfo2(`Thinking mode: ${cur ? "on" : "off"} — ${desc(cur)}`);
546173
- if (process.env["OA_THINK_AUTO"] === "1") renderInfo2("Auto-heuristic active (OA_THINK_AUTO=1)");
546410
+ 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
546411
  if (process.env["OA_FORCE_NO_THINK"] === "1") renderWarning2("OA_FORCE_NO_THINK=1 forces off regardless of /think setting");
546175
546412
  return "handled";
546176
546413
  }
546177
546414
  if (token === "auto") {
546178
546415
  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.");
546416
+ 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.");
546417
+ return "handled";
546418
+ }
546419
+ if (token === "reset" || token === "clear") {
546420
+ process.env["OA_THINK_GUARD_RESET"] = String(Date.now());
546421
+ 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
546422
  return "handled";
546181
546423
  }
546182
546424
  let isOn;
@@ -546197,6 +546439,32 @@ Clone a new voice: /voice clone <wav-file> [name]`);
546197
546439
  }
546198
546440
  return "handled";
546199
546441
  }
546442
+ case "mouse": {
546443
+ const t2 = (arg || "").trim().toLowerCase();
546444
+ const isOnNow = ctx3.isMouseEnabled?.() ?? true;
546445
+ if (t2 === "off" || t2 === "0" || t2 === "false" || t2 === "no") {
546446
+ ctx3.disableMouse?.();
546447
+ renderInfo2("Mouse tracking: off — click-drag to select text natively. Re-enable with /mouse on.");
546448
+ return "handled";
546449
+ }
546450
+ if (t2 === "on" || t2 === "1" || t2 === "true" || t2 === "yes") {
546451
+ ctx3.enableMouse?.();
546452
+ renderInfo2("Mouse tracking: on — header buttons + scroll wheel active. Disable with /mouse off for native text selection.");
546453
+ return "handled";
546454
+ }
546455
+ if (t2 === "status" || t2 === "?") {
546456
+ renderInfo2(`Mouse tracking: ${isOnNow ? "on" : "off"}`);
546457
+ return "handled";
546458
+ }
546459
+ if (isOnNow) {
546460
+ ctx3.disableMouse?.();
546461
+ renderInfo2("Mouse tracking: off — click-drag to select text natively. Re-enable with /mouse on.");
546462
+ } else {
546463
+ ctx3.enableMouse?.();
546464
+ renderInfo2("Mouse tracking: on — header buttons + scroll wheel active.");
546465
+ }
546466
+ return "handled";
546467
+ }
546200
546468
  case "tools": {
546201
546469
  const tools = listCustomToolFiles(ctx3.repoRoot);
546202
546470
  if (tools.length === 0) {
@@ -582407,6 +582675,15 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
582407
582675
  deactivateStatusBar() {
582408
582676
  statusBar.deactivate();
582409
582677
  },
582678
+ disableMouse() {
582679
+ statusBar.disableMouseTracking();
582680
+ },
582681
+ enableMouse() {
582682
+ statusBar.enableMouseTracking();
582683
+ },
582684
+ isMouseEnabled() {
582685
+ return statusBar.isMouseTrackingEnabled?.() ?? true;
582686
+ },
582410
582687
  stopBanner() {
582411
582688
  banner.stop();
582412
582689
  if (carousel.isRunning) carousel.stop();
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.373",
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",