@nanhara/hara 0.109.2 → 0.109.3
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.
- package/CHANGELOG.md +13 -0
- package/dist/agent/loop.js +32 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ All notable changes to `@nanhara/hara`.
|
|
|
5
5
|
> Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
|
|
6
6
|
> **patch** (last) number bumps for **optimizations/fixes of existing features**.
|
|
7
7
|
|
|
8
|
+
## 0.109.3 — an empty model response no longer looks like a hang
|
|
9
|
+
|
|
10
|
+
- **A turn that comes back with no text and no tool calls is no longer silently dropped.** The agent
|
|
11
|
+
loop treated an empty completion as "done" and returned with zero feedback — so after e.g. a "继续"
|
|
12
|
+
the box just sat there, idle, looking frozen for hours (CPU 0%, waiting at the prompt) when really
|
|
13
|
+
the turn had vanished. It now **retries once** with a nudge (an empty response is usually a transient
|
|
14
|
+
hiccup), and if it's still empty says so plainly (`✻ empty response — nothing to do. Rephrase…`)
|
|
15
|
+
instead of disappearing. Matches how Claude Code / codex refuse to end on a blank turn.
|
|
16
|
+
- **Closed a matching spin:** a `tool_use` stop reason carrying an *empty* tool list used to push an
|
|
17
|
+
empty tool round and re-request in a loop; it's now bounded by the same single-retry guard. (The
|
|
18
|
+
120s stall-watchdog already covered a genuinely dead/silent socket — this covers the case where the
|
|
19
|
+
request *succeeds* but returns nothing.)
|
|
20
|
+
|
|
8
21
|
## 0.109.2 — long paste no longer freezes the input box
|
|
9
22
|
|
|
10
23
|
- **The input box now draws a bottom-anchored viewport, not every wrapped row.** A long multi-line
|
package/dist/agent/loop.js
CHANGED
|
@@ -97,6 +97,7 @@ export async function runAgent(history, opts) {
|
|
|
97
97
|
const permRules = loadPermissionRules(ctx.cwd); // command-level allow/ask/deny policy for the bash tool
|
|
98
98
|
let activeProvider = provider; // may switch to a fallback model on a recoverable error (app-failover)
|
|
99
99
|
let triedFallback = false;
|
|
100
|
+
let emptyRetried = false; // one-shot: a genuinely empty model turn gets a single nudge before we give up
|
|
100
101
|
// Stuck/loop guard — only in headless chat (`hara gateway`), where a wrong approach can grind forever with
|
|
101
102
|
// nobody to hit Esc (e.g. screenshots it can't read). Once per run, when the agent keeps repeating one
|
|
102
103
|
// non-read tool or acting blind, we inject a reflection nudge so it steps back instead of spinning.
|
|
@@ -281,7 +282,37 @@ export async function runAgent(history, opts) {
|
|
|
281
282
|
}
|
|
282
283
|
return;
|
|
283
284
|
}
|
|
284
|
-
|
|
285
|
+
// Empty-turn guard. The model returned nothing actionable — no text AND no tool calls (a blank
|
|
286
|
+
// completion, or a "tool_use" stop with an empty tool list). Silently returning here leaves the
|
|
287
|
+
// user at a dead prompt with ZERO feedback: it reads as a 15-hour hang when really the turn just
|
|
288
|
+
// vanished. Retry ONCE with a nudge (usually a transient hiccup), then, if still empty, say so
|
|
289
|
+
// plainly and end — never loop forever, never disappear. (Claude Code / codex both guard this.)
|
|
290
|
+
if (!r.text.trim() && r.toolUses.length === 0) {
|
|
291
|
+
if (!emptyRetried) {
|
|
292
|
+
emptyRetried = true;
|
|
293
|
+
history.pop(); // drop the empty assistant turn before re-asking
|
|
294
|
+
history.push({ role: "user", content: "(Your previous response was empty. Continue the task now — take the next concrete step with a tool, or reply with text. Do not return an empty response.)" });
|
|
295
|
+
if (!opts.quiet) {
|
|
296
|
+
const note = "✻ empty response — retrying once…";
|
|
297
|
+
if (sink)
|
|
298
|
+
sink.notice(note);
|
|
299
|
+
else
|
|
300
|
+
out(c.dim(`${note}\n`));
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const note = "✻ the model returned an empty response — nothing to do. Rephrase your request, or press Enter to try again.";
|
|
305
|
+
if (!opts.quiet) {
|
|
306
|
+
if (sink)
|
|
307
|
+
sink.notice(note);
|
|
308
|
+
else
|
|
309
|
+
out(c.dim(`${note}\n`));
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// A "tool_use" stop with text but no tools (rare) has nothing to execute — end after showing the text
|
|
314
|
+
// rather than pushing an empty tool round and re-requesting in a loop.
|
|
315
|
+
if (r.stop !== "tool_use" || r.toolUses.length === 0)
|
|
285
316
|
return;
|
|
286
317
|
const plans = [];
|
|
287
318
|
// Extra (per-run) tools win over the registry so a run-scoped tool can't be shadowed by a global one.
|