@nanhara/hara 0.112.1 → 0.112.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 +23 -0
- package/dist/providers/openai.js +33 -13
- package/dist/tui/run.js +29 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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.112.3 — big file write no longer traps the model in a "params not passed" loop
|
|
9
|
+
|
|
10
|
+
- **Fixed the loop where a model (glm-5 / qwen via DashScope) repeats `write_file` / `bash` with empty or
|
|
11
|
+
`undefined` arguments.** Two problems compounded: the OpenAI-compatible path capped output at
|
|
12
|
+
`max_tokens: 8192`, so a large `write_file` (e.g. a 64-hexagram data file) ran PAST the limit and its
|
|
13
|
+
tool-call-arguments JSON was cut off — and hara then **silently swallowed the unparseable JSON into an
|
|
14
|
+
empty `{}`**. So the tool ran with no path/content (`bash` got `command: undefined` →
|
|
15
|
+
`/bin/sh: undefined: command not found`), the model saw the failure, "fixed" it by calling again, and
|
|
16
|
+
looped — never realizing its *output* had been truncated. Now: (1) `max_tokens` is raised to 32000
|
|
17
|
+
(glm-5/qwen accept it), and (2) **truncated/malformed tool arguments surface as an actionable error**
|
|
18
|
+
("the model hit its output-length limit mid tool-call — write the file in smaller parts") instead of a
|
|
19
|
+
silent `{}`, so the loop breaks and the fix is obvious.
|
|
20
|
+
|
|
21
|
+
## 0.112.2 — moving/resizing the window no longer garbles the UI
|
|
22
|
+
|
|
23
|
+
- **Terminal resize no longer stacks the status row + input box into a garble.** ink 6.8's resize
|
|
24
|
+
handler only clears the screen when the terminal gets *narrower*; on a *widen* (or a resize it doesn't
|
|
25
|
+
classify as narrowing) it just re-renders, so the old frame — reflowed at the new width — is never
|
|
26
|
+
erased, and the ~125ms spinner tick stacks a fresh copy each time (the "I moved the window and the UI
|
|
27
|
+
filled up with repeated `waiting for the model…` lines"). hara now hooks the resize event and, on ANY
|
|
28
|
+
resize, resets ink's tracked output (debounced across a drag's burst of events) so the next render
|
|
29
|
+
starts clean and subsequent ticks erase correctly. Not "just the terminal" — a real repaint fix.
|
|
30
|
+
|
|
8
31
|
## 0.112.1 — the background-job indicator is now LIVE (even at idle)
|
|
9
32
|
|
|
10
33
|
- **`⚙ N bg running` updates in real time — including when hara is idle.** In 0.112.0 the indicator only
|
package/dist/providers/openai.js
CHANGED
|
@@ -2,6 +2,35 @@ import OpenAI from "openai";
|
|
|
2
2
|
import { imageToBase64 } from "../images.js";
|
|
3
3
|
import { reasoningParams } from "./reasoning.js";
|
|
4
4
|
import { resolvePlatform } from "./registry.js";
|
|
5
|
+
/** Assemble streamed tool-call fragments into tool uses. CRITICAL: non-empty arguments that don't parse
|
|
6
|
+
* mean the model was cut off MID tool-call (almost always the output-length limit on a big write_file /
|
|
7
|
+
* bash). Silently substituting `{}` makes the model loop forever — it calls write_file with no path, bash
|
|
8
|
+
* with `command: undefined` (`/bin/sh: undefined: command not found`), sees the failure, and retries,
|
|
9
|
+
* never realizing its OUTPUT was truncated. So we surface it as an actionable error instead. Exported for
|
|
10
|
+
* tests. */
|
|
11
|
+
export function assembleToolCalls(entries, finish) {
|
|
12
|
+
let truncated = false;
|
|
13
|
+
const toolUses = entries
|
|
14
|
+
.filter((t) => t.id && t.name)
|
|
15
|
+
.map((t) => {
|
|
16
|
+
let input = {};
|
|
17
|
+
const raw = (t.args || "").trim();
|
|
18
|
+
if (raw) {
|
|
19
|
+
try {
|
|
20
|
+
input = JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
truncated = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { id: t.id, name: t.name, input };
|
|
27
|
+
});
|
|
28
|
+
if (truncated) {
|
|
29
|
+
const why = finish === "length" ? "the model hit its output-length limit mid tool-call" : "the model emitted malformed tool-call arguments";
|
|
30
|
+
return { toolUses: [], error: `Tool call dropped — ${why}, so its arguments were incomplete. For a large file, write it in smaller parts (several write_file/edit calls) rather than one giant call.` };
|
|
31
|
+
}
|
|
32
|
+
return { toolUses };
|
|
33
|
+
}
|
|
5
34
|
// Re-exported for callers that still import it from here (the reasoning-family check now lives in reasoning.ts).
|
|
6
35
|
export { isReasoningModel } from "./reasoning.js";
|
|
7
36
|
/** Build OpenAI chat-completions messages from neutral history. */
|
|
@@ -63,7 +92,7 @@ export function createOpenAIProvider(opts) {
|
|
|
63
92
|
const params = {
|
|
64
93
|
model: opts.model,
|
|
65
94
|
messages: toOpenAI(system, history),
|
|
66
|
-
max_tokens: 8192
|
|
95
|
+
max_tokens: 32000, // was 8192 — too small: a big write_file's args got truncated → unparseable → loop
|
|
67
96
|
stream: true,
|
|
68
97
|
stream_options: { include_usage: true },
|
|
69
98
|
};
|
|
@@ -118,18 +147,9 @@ export function createOpenAIProvider(opts) {
|
|
|
118
147
|
return { text: "", toolUses: [], stop: "error", errorMsg: "interrupted" };
|
|
119
148
|
return { text: "", toolUses: [], stop: "error", errorMsg: `${e?.status ?? ""} ${e?.message ?? e}` };
|
|
120
149
|
}
|
|
121
|
-
const toolUses = [...acc.values()]
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
let input = {};
|
|
125
|
-
try {
|
|
126
|
-
input = JSON.parse(t.args || "{}");
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
input = {};
|
|
130
|
-
}
|
|
131
|
-
return { id: t.id, name: t.name, input };
|
|
132
|
-
});
|
|
150
|
+
const { toolUses, error: argsError } = assembleToolCalls([...acc.values()], finish);
|
|
151
|
+
if (argsError)
|
|
152
|
+
return { text, toolUses: [], stop: "error", errorMsg: argsError, usage };
|
|
133
153
|
const stop = finish === "tool_calls" || toolUses.length ? "tool_use" : "end";
|
|
134
154
|
return { text, toolUses, stop, usage };
|
|
135
155
|
},
|
package/dist/tui/run.js
CHANGED
|
@@ -6,7 +6,35 @@ import { createElement } from "react";
|
|
|
6
6
|
import { App } from "./App.js";
|
|
7
7
|
export async function runTui(props) {
|
|
8
8
|
const instance = render(createElement(App, props));
|
|
9
|
-
|
|
9
|
+
// Resize repaint fix. ink 6.8's own resize handler only clears the screen when the terminal gets
|
|
10
|
+
// NARROWER; on a WIDEN (or a resize it doesn't classify as narrowing) it just re-renders, so the old
|
|
11
|
+
// frame — reflowed at the new width — is never erased, and the ~125ms spinner tick stacks a fresh copy
|
|
12
|
+
// each time (the "moved the window and the UI stacked up" garble). We complement it: on ANY resize,
|
|
13
|
+
// clear ink's tracked output so the next render starts clean. Debounced by a microtask so a burst of
|
|
14
|
+
// resize events during a window drag collapses to one clear.
|
|
15
|
+
const out = process.stdout;
|
|
16
|
+
let pending = false;
|
|
17
|
+
const onResize = () => {
|
|
18
|
+
if (pending)
|
|
19
|
+
return;
|
|
20
|
+
pending = true;
|
|
21
|
+
queueMicrotask(() => {
|
|
22
|
+
pending = false;
|
|
23
|
+
try {
|
|
24
|
+
instance.clear();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
/* best-effort — never let a repaint fix crash the session */
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
out.on("resize", onResize);
|
|
32
|
+
try {
|
|
33
|
+
await instance.waitUntilExit();
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
out.off("resize", onResize);
|
|
37
|
+
}
|
|
10
38
|
}
|
|
11
39
|
// A tiny ink yes/no prompt for pre-TUI confirms (e.g. the first-run "create AGENTS.md?" offer).
|
|
12
40
|
// MUST be ink, NOT readline: a readline question before the main TUI leaves stdin in a state ink
|