miii-agent 0.1.20 → 0.1.22

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/cli.js +104 -12
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -89,8 +89,8 @@ var init_config = __esm({
89
89
  "src/config.ts"() {
90
90
  "use strict";
91
91
  EFFORT_OPTIONS = {
92
- low: { temperature: 0.2, num_predict: 1024 },
93
- medium: { temperature: 0.7, num_predict: 2048 },
92
+ low: { temperature: 0.2, num_predict: 8192 },
93
+ medium: { temperature: 0.7, num_predict: 16384 },
94
94
  high: { temperature: 1, num_predict: -1 }
95
95
  };
96
96
  CONFIG_DIR = join(homedir(), ".miii");
@@ -249,6 +249,7 @@ async function* chat(entry, model, messages, tools, opts) {
249
249
  content: stripHarmony(chunk.message.content),
250
250
  thinking: stripHarmony(chunk.message.thinking),
251
251
  done: chunk.done,
252
+ done_reason: chunk.done_reason,
252
253
  tool_calls: chunk.message.tool_calls,
253
254
  prompt_eval_count: chunk.prompt_eval_count,
254
255
  eval_count: chunk.eval_count
@@ -384,6 +385,7 @@ async function* chat2(entry, model, messages, tools, opts) {
384
385
  if (oaTools) body.tools = oaTools;
385
386
  if (opts?.num_predict && opts.num_predict > 0) body.max_tokens = opts.num_predict;
386
387
  const toolCallAccum = /* @__PURE__ */ new Map();
388
+ let lastFinishReason;
387
389
  const TIMEOUT_MS = 18e4;
388
390
  const timeoutSignal = AbortSignal.timeout(TIMEOUT_MS);
389
391
  const combinedSignal = opts?.signal && typeof AbortSignal.any === "function" ? AbortSignal.any([opts.signal, timeoutSignal]) : opts?.signal ?? timeoutSignal;
@@ -423,6 +425,7 @@ async function* chat2(entry, model, messages, tools, opts) {
423
425
  if (!choices || choices.length === 0) continue;
424
426
  const delta = choices[0].delta ?? {};
425
427
  const finishReason = choices[0].finish_reason;
428
+ if (finishReason) lastFinishReason = finishReason;
426
429
  if (delta.content) {
427
430
  yield { content: delta.content, done: false };
428
431
  }
@@ -487,6 +490,9 @@ async function* chat2(entry, model, messages, tools, opts) {
487
490
  yield {
488
491
  content: "",
489
492
  done: true,
493
+ // OpenAI signals a hit token cap as finish_reason 'length'; normalize to the
494
+ // Ollama spelling so the agent loop can detect truncation uniformly.
495
+ done_reason: lastFinishReason === "length" ? "length" : lastFinishReason ?? void 0,
490
496
  tool_calls: toolCalls.length > 0 ? toolCalls : void 0
491
497
  };
492
498
  }
@@ -749,7 +755,10 @@ var init_edit_file = __esm({
749
755
  handler: ({ path, old_str, new_str, replace_all }) => {
750
756
  try {
751
757
  if (old_str === new_str) {
752
- return { content: `old_str and new_str are identical \u2014 nothing to change in ${path}.`, is_error: true };
758
+ return {
759
+ content: `old_str and new_str are identical \u2014 nothing to change in ${path}. If the file is already correct, do NOT edit again: finish with the respond action and tell the user it is done.`,
760
+ is_error: true
761
+ };
753
762
  }
754
763
  const abs = confinePath(path);
755
764
  const src = readFileSync3(abs, "utf-8");
@@ -1218,6 +1227,31 @@ function toZod(schema) {
1218
1227
  }
1219
1228
  return z.object(shape).passthrough();
1220
1229
  }
1230
+ function exampleValue(spec) {
1231
+ if (spec.enum && spec.enum.length) return spec.enum[0];
1232
+ switch (spec.type) {
1233
+ case "number":
1234
+ case "integer":
1235
+ return 0;
1236
+ case "boolean":
1237
+ return false;
1238
+ case "array":
1239
+ return [];
1240
+ case "object":
1241
+ return {};
1242
+ default:
1243
+ return "...";
1244
+ }
1245
+ }
1246
+ function exampleInput(schema) {
1247
+ const required = schema.required ?? [];
1248
+ const obj = {};
1249
+ for (const key of required) {
1250
+ const spec = schema.properties[key];
1251
+ if (spec) obj[key] = exampleValue(spec);
1252
+ }
1253
+ return JSON.stringify(obj);
1254
+ }
1221
1255
  function validateInput(schema, input) {
1222
1256
  const result = toZod(schema).safeParse(input ?? {});
1223
1257
  if (result.success) return null;
@@ -1614,8 +1648,22 @@ function parseGrammarAction(content, knownToolNames) {
1614
1648
  }
1615
1649
  }
1616
1650
  const name = typeof obj.name === "string" ? obj.name : void 0;
1617
- const args2 = obj.arguments ?? {};
1618
1651
  if (!name) return null;
1652
+ let args2;
1653
+ const wrapped = obj.arguments ?? obj.parameters ?? obj.input ?? obj.args;
1654
+ if (typeof wrapped === "string") {
1655
+ try {
1656
+ const parsed = JSON.parse(wrapped);
1657
+ args2 = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1658
+ } catch {
1659
+ args2 = {};
1660
+ }
1661
+ } else if (wrapped && typeof wrapped === "object" && !Array.isArray(wrapped)) {
1662
+ args2 = wrapped;
1663
+ } else {
1664
+ const { name: _n, ...rest } = obj;
1665
+ args2 = rest;
1666
+ }
1619
1667
  if (name === "respond") {
1620
1668
  const message = typeof args2.message === "string" ? args2.message : "";
1621
1669
  return { kind: "respond", message };
@@ -1707,6 +1755,30 @@ function readGuard(name, input, seen) {
1707
1755
  const verb = name === "edit_file" ? "edit" : "overwrite";
1708
1756
  return `Refusing to ${verb} ${p}: you have not read it this turn. Call read_file on ${p} first, then retry the ${name}.`;
1709
1757
  }
1758
+ function unwrapEnvelope(name, input) {
1759
+ if (!("arguments" in input)) return input;
1760
+ if ("name" in input && input.name !== name) return input;
1761
+ let args2 = input.arguments;
1762
+ if (typeof args2 === "string") {
1763
+ try {
1764
+ args2 = JSON.parse(args2);
1765
+ } catch {
1766
+ return input;
1767
+ }
1768
+ }
1769
+ if (args2 && typeof args2 === "object" && !Array.isArray(args2)) return args2;
1770
+ return input;
1771
+ }
1772
+ function splitWriteHint(name, cause) {
1773
+ const lead = cause === "truncated" ? `Your response was cut off at the output token limit, so this ${name} call is incomplete and was NOT run.` : `Your ${name} call arrived with missing or garbled arguments \u2014 usually the response was cut off or mangled while writing a large value. It was NOT run.`;
1774
+ return `${lead} Do not resend the whole file in one call. Instead create the file with write_file containing only the first portion, then append the rest with successive edit_file calls. Keep each call small.`;
1775
+ }
1776
+ function looksTruncatedWrite(name, input) {
1777
+ if (!BIG_WRITE_TOOLS.has(name)) return false;
1778
+ if (typeof input.path !== "string" || !input.path) return true;
1779
+ if (name === "write_file" && typeof input.content !== "string") return true;
1780
+ return false;
1781
+ }
1710
1782
  function markSeen(name, input, seen) {
1711
1783
  if (name !== "read_file" && name !== "edit_file" && name !== "write_file") return;
1712
1784
  const p = input.path;
@@ -1743,9 +1815,11 @@ async function* runAgent(opts) {
1743
1815
  let tool_calls;
1744
1816
  let respondEmitted = 0;
1745
1817
  let streamedRespond = false;
1818
+ let emittedText = false;
1746
1819
  let lastTail = "";
1747
1820
  let tailRepeats = 0;
1748
1821
  let streamLooped = false;
1822
+ let truncated = false;
1749
1823
  const ac = new AbortController();
1750
1824
  const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
1751
1825
  if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
@@ -1755,12 +1829,14 @@ async function* runAgent(opts) {
1755
1829
  if (chunk.content) {
1756
1830
  text += chunk.content;
1757
1831
  if (!useGrammar) {
1832
+ emittedText = true;
1758
1833
  yield { type: "text-delta", text: chunk.content };
1759
1834
  } else {
1760
1835
  const r = streamRespondMessage(text);
1761
1836
  if (r) {
1762
1837
  streamedRespond = true;
1763
1838
  if (r.message.length > respondEmitted) {
1839
+ emittedText = true;
1764
1840
  yield { type: "text-delta", text: r.message.slice(respondEmitted) };
1765
1841
  respondEmitted = r.message.length;
1766
1842
  }
@@ -1781,7 +1857,7 @@ async function* runAgent(opts) {
1781
1857
  }
1782
1858
  }
1783
1859
  }
1784
- if (chunk.thinking) {
1860
+ if (chunk.thinking && !emittedText) {
1785
1861
  yield { type: "thinking-delta", text: chunk.thinking };
1786
1862
  }
1787
1863
  if (chunk.tool_calls && chunk.tool_calls.length > 0) {
@@ -1790,6 +1866,7 @@ async function* runAgent(opts) {
1790
1866
  if (chunk.done) {
1791
1867
  promptTokens += chunk.prompt_eval_count ?? 0;
1792
1868
  evalTokens += chunk.eval_count ?? 0;
1869
+ if (chunk.done_reason === "length") truncated = true;
1793
1870
  }
1794
1871
  }
1795
1872
  } catch (err) {
@@ -1828,6 +1905,19 @@ async function* runAgent(opts) {
1828
1905
  }
1829
1906
  const tool_uses = blocks.filter((b) => b.type === "tool_use");
1830
1907
  history.push({ role: "assistant", content: blocks });
1908
+ if (truncated && tool_uses.length > 0) {
1909
+ const results2 = tool_uses.map((use) => ({
1910
+ type: "tool_result",
1911
+ tool_use_id: use.id,
1912
+ content: splitWriteHint(use.name, "truncated"),
1913
+ is_error: true
1914
+ }));
1915
+ for (const u of tool_uses) yield { type: "tool-use", block: u };
1916
+ for (const r of results2) yield { type: "tool-result", block: r };
1917
+ history.push({ role: "user", content: results2 });
1918
+ yield { type: "turn-end", stop_reason: "tool_use" };
1919
+ continue;
1920
+ }
1831
1921
  if (tool_uses.length === 0) {
1832
1922
  yield { type: "turn-end", stop_reason: "end_turn" };
1833
1923
  break;
@@ -1862,12 +1952,14 @@ async function* runAgent(opts) {
1862
1952
  yield { type: "tool-result", block: r2 };
1863
1953
  continue;
1864
1954
  }
1955
+ use.input = unwrapEnvelope(use.name, use.input);
1865
1956
  const invalid = validateInput(tool.input_schema, use.input);
1866
1957
  if (invalid) {
1958
+ const content = looksTruncatedWrite(use.name, use.input) ? splitWriteHint(use.name, "garbled") : `${invalid} for ${use.name}. Pass the arguments directly as the tool input \u2014 do NOT wrap them in {"name":...,"arguments":...}. Correct shape: ${exampleInput(tool.input_schema)}. Retry with all required fields.`;
1867
1959
  const r2 = {
1868
1960
  type: "tool_result",
1869
1961
  tool_use_id: use.id,
1870
- content: `${invalid} for ${use.name}.`,
1962
+ content,
1871
1963
  is_error: true
1872
1964
  };
1873
1965
  results.push(r2);
@@ -1934,7 +2026,7 @@ async function* runAgent(opts) {
1934
2026
  yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
1935
2027
  return history;
1936
2028
  }
1937
- var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL, GRAMMAR_MAX_PARAMS_B;
2029
+ var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL, GRAMMAR_MAX_PARAMS_B, BIG_WRITE_TOOLS;
1938
2030
  var init_loop = __esm({
1939
2031
  "src/agent/loop.ts"() {
1940
2032
  "use strict";
@@ -1952,6 +2044,7 @@ var init_loop = __esm({
1952
2044
  REPEAT_TAIL = 120;
1953
2045
  REPEAT_KILL = 4;
1954
2046
  GRAMMAR_MAX_PARAMS_B = 14;
2047
+ BIG_WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file"]);
1955
2048
  }
1956
2049
  });
1957
2050
 
@@ -2670,7 +2763,7 @@ import { Box as Box12, Text as Text12, Static } from "ink";
2670
2763
  // src/ui/markdown.ts
2671
2764
  import { Marked } from "marked";
2672
2765
  import { markedTerminal } from "marked-terminal";
2673
- import { highlight } from "cli-highlight";
2766
+ import { highlight, supportsLanguage } from "cli-highlight";
2674
2767
 
2675
2768
  // node_modules/chalk/source/vendor/ansi-styles/index.js
2676
2769
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -3194,7 +3287,7 @@ var theme = {
3194
3287
  // faint rule
3195
3288
  };
3196
3289
  function highlightCode(code, lang) {
3197
- if (!lang) return code;
3290
+ if (!lang || !supportsLanguage(lang)) return code;
3198
3291
  try {
3199
3292
  return highlight(code, { language: lang, ignoreIllegals: true });
3200
3293
  } catch {
@@ -3292,7 +3385,7 @@ import { Box as Box10, Text as Text10 } from "ink";
3292
3385
 
3293
3386
  // src/ui/ToolBlock.tsx
3294
3387
  import { Box as Box9, Text as Text9 } from "ink";
3295
- import { highlight as highlight2 } from "cli-highlight";
3388
+ import { highlight as highlight2, supportsLanguage as supportsLanguage2 } from "cli-highlight";
3296
3389
 
3297
3390
  // src/ui/toolExpand.ts
3298
3391
  import { useState as useState3, useEffect as useEffect3 } from "react";
@@ -3427,7 +3520,7 @@ function langFromPath(path) {
3427
3520
  return ext ? EXT_LANG[ext] : void 0;
3428
3521
  }
3429
3522
  function highlightLine(text, lang) {
3430
- if (!lang) return text;
3523
+ if (!lang || !supportsLanguage2(lang)) return text;
3431
3524
  try {
3432
3525
  return highlight2(text, { language: lang, ignoreIllegals: true });
3433
3526
  } catch {
@@ -4628,7 +4721,6 @@ function App() {
4628
4721
  header: /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd })
4629
4722
  }
4630
4723
  ),
4631
- updateAvailable && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: miii --update` }) }),
4632
4724
  input.startsWith("/") && /* @__PURE__ */ jsx13(CommandPalette, { filter: input, cursor: paletteCursor }),
4633
4725
  contextWarning !== null && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: `\u26A0 context ${contextWarning}% full \u2014 run /clear and start fresh` }) }),
4634
4726
  !input.startsWith("/") && (() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-agent",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Cursor / Claude Code, but local. An offline AI pair-programmer in your terminal, powered by Ollama. Private by default, free forever.",
5
5
  "type": "module",
6
6
  "bin": {