@nanhara/hara 0.112.2 → 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 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.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
+
8
21
  ## 0.112.2 — moving/resizing the window no longer garbles the UI
9
22
 
10
23
  - **Terminal resize no longer stacks the status row + input box into a garble.** ink 6.8's resize
@@ -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
- .filter((t) => t.id && t.name)
123
- .map((t) => {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanhara/hara",
3
- "version": "0.112.2",
3
+ "version": "0.112.3",
4
4
  "description": "hara — a coding agent CLI that runs like an engineering org.",
5
5
  "bin": {
6
6
  "hara": "dist/index.js"