miii-agent 0.1.20 → 0.1.21

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 +105 -57
  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");
@@ -163,30 +163,6 @@ async function modelContext(entry, model) {
163
163
  throw err;
164
164
  }
165
165
  }
166
- async function paramCountB(entry, model) {
167
- try {
168
- const info = await makeClient(entry).show({ model });
169
- const details = info.details;
170
- if (details?.parameter_size) {
171
- const m = details.parameter_size.match(/([\d.]+)\s*([BM])/i);
172
- if (m) {
173
- const n = parseFloat(m[1]);
174
- if (!isNaN(n)) return m[2].toUpperCase() === "M" ? n / 1e3 : n;
175
- }
176
- }
177
- const modelInfo = info.model_info;
178
- if (modelInfo) {
179
- const key = Object.keys(modelInfo).find((k) => k.endsWith("parameter_count"));
180
- if (key) {
181
- const val = Number(modelInfo[key]);
182
- if (!isNaN(val) && val > 0) return val / 1e9;
183
- }
184
- }
185
- return null;
186
- } catch {
187
- return null;
188
- }
189
- }
190
166
  async function* chat(entry, model, messages, tools, opts) {
191
167
  if (opts?.signal?.aborted) return;
192
168
  const signal = opts?.signal;
@@ -249,6 +225,7 @@ async function* chat(entry, model, messages, tools, opts) {
249
225
  content: stripHarmony(chunk.message.content),
250
226
  thinking: stripHarmony(chunk.message.thinking),
251
227
  done: chunk.done,
228
+ done_reason: chunk.done_reason,
252
229
  tool_calls: chunk.message.tool_calls,
253
230
  prompt_eval_count: chunk.prompt_eval_count,
254
231
  eval_count: chunk.eval_count
@@ -384,6 +361,7 @@ async function* chat2(entry, model, messages, tools, opts) {
384
361
  if (oaTools) body.tools = oaTools;
385
362
  if (opts?.num_predict && opts.num_predict > 0) body.max_tokens = opts.num_predict;
386
363
  const toolCallAccum = /* @__PURE__ */ new Map();
364
+ let lastFinishReason;
387
365
  const TIMEOUT_MS = 18e4;
388
366
  const timeoutSignal = AbortSignal.timeout(TIMEOUT_MS);
389
367
  const combinedSignal = opts?.signal && typeof AbortSignal.any === "function" ? AbortSignal.any([opts.signal, timeoutSignal]) : opts?.signal ?? timeoutSignal;
@@ -423,6 +401,7 @@ async function* chat2(entry, model, messages, tools, opts) {
423
401
  if (!choices || choices.length === 0) continue;
424
402
  const delta = choices[0].delta ?? {};
425
403
  const finishReason = choices[0].finish_reason;
404
+ if (finishReason) lastFinishReason = finishReason;
426
405
  if (delta.content) {
427
406
  yield { content: delta.content, done: false };
428
407
  }
@@ -487,6 +466,9 @@ async function* chat2(entry, model, messages, tools, opts) {
487
466
  yield {
488
467
  content: "",
489
468
  done: true,
469
+ // OpenAI signals a hit token cap as finish_reason 'length'; normalize to the
470
+ // Ollama spelling so the agent loop can detect truncation uniformly.
471
+ done_reason: lastFinishReason === "length" ? "length" : lastFinishReason ?? void 0,
490
472
  tool_calls: toolCalls.length > 0 ? toolCalls : void 0
491
473
  };
492
474
  }
@@ -502,9 +484,6 @@ var init_openai = __esm({
502
484
  function active() {
503
485
  return resolveProvider();
504
486
  }
505
- function providerName() {
506
- return active().name;
507
- }
508
487
  function isAvailable3() {
509
488
  const { entry } = active();
510
489
  return entry.type === "ollama" ? isAvailable(entry) : isAvailable2(entry);
@@ -521,16 +500,6 @@ async function modelContext3(model) {
521
500
  const { entry } = active();
522
501
  return entry.type === "ollama" ? modelContext(entry, model) : modelContext2(entry, model);
523
502
  }
524
- async function modelParamCountB(model) {
525
- const { entry } = active();
526
- if (entry.type !== "ollama") return null;
527
- const key = `${entry.baseUrl}:${model}`;
528
- const cached = paramCountCache.get(key);
529
- if (cached !== void 0) return cached;
530
- const params = await paramCountB(entry, model);
531
- paramCountCache.set(key, params);
532
- return params;
533
- }
534
503
  async function* chat3(model, messages, tools, opts) {
535
504
  const { entry } = active();
536
505
  if (entry.type === "ollama") {
@@ -539,14 +508,12 @@ async function* chat3(model, messages, tools, opts) {
539
508
  yield* chat2(entry, model, messages, tools, opts);
540
509
  }
541
510
  }
542
- var paramCountCache;
543
511
  var init_client = __esm({
544
512
  "src/llm/client.ts"() {
545
513
  "use strict";
546
514
  init_config();
547
515
  init_ollama();
548
516
  init_openai();
549
- paramCountCache = /* @__PURE__ */ new Map();
550
517
  }
551
518
  });
552
519
 
@@ -749,7 +716,10 @@ var init_edit_file = __esm({
749
716
  handler: ({ path, old_str, new_str, replace_all }) => {
750
717
  try {
751
718
  if (old_str === new_str) {
752
- return { content: `old_str and new_str are identical \u2014 nothing to change in ${path}.`, is_error: true };
719
+ return {
720
+ 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.`,
721
+ is_error: true
722
+ };
753
723
  }
754
724
  const abs = confinePath(path);
755
725
  const src = readFileSync3(abs, "utf-8");
@@ -1218,6 +1188,31 @@ function toZod(schema) {
1218
1188
  }
1219
1189
  return z.object(shape).passthrough();
1220
1190
  }
1191
+ function exampleValue(spec) {
1192
+ if (spec.enum && spec.enum.length) return spec.enum[0];
1193
+ switch (spec.type) {
1194
+ case "number":
1195
+ case "integer":
1196
+ return 0;
1197
+ case "boolean":
1198
+ return false;
1199
+ case "array":
1200
+ return [];
1201
+ case "object":
1202
+ return {};
1203
+ default:
1204
+ return "...";
1205
+ }
1206
+ }
1207
+ function exampleInput(schema) {
1208
+ const required = schema.required ?? [];
1209
+ const obj = {};
1210
+ for (const key of required) {
1211
+ const spec = schema.properties[key];
1212
+ if (spec) obj[key] = exampleValue(spec);
1213
+ }
1214
+ return JSON.stringify(obj);
1215
+ }
1221
1216
  function validateInput(schema, input) {
1222
1217
  const result = toZod(schema).safeParse(input ?? {});
1223
1218
  if (result.success) return null;
@@ -1614,8 +1609,22 @@ function parseGrammarAction(content, knownToolNames) {
1614
1609
  }
1615
1610
  }
1616
1611
  const name = typeof obj.name === "string" ? obj.name : void 0;
1617
- const args2 = obj.arguments ?? {};
1618
1612
  if (!name) return null;
1613
+ let args2;
1614
+ const wrapped = obj.arguments ?? obj.parameters ?? obj.input ?? obj.args;
1615
+ if (typeof wrapped === "string") {
1616
+ try {
1617
+ const parsed = JSON.parse(wrapped);
1618
+ args2 = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1619
+ } catch {
1620
+ args2 = {};
1621
+ }
1622
+ } else if (wrapped && typeof wrapped === "object" && !Array.isArray(wrapped)) {
1623
+ args2 = wrapped;
1624
+ } else {
1625
+ const { name: _n, ...rest } = obj;
1626
+ args2 = rest;
1627
+ }
1619
1628
  if (name === "respond") {
1620
1629
  const message = typeof args2.message === "string" ? args2.message : "";
1621
1630
  return { kind: "respond", message };
@@ -1707,6 +1716,30 @@ function readGuard(name, input, seen) {
1707
1716
  const verb = name === "edit_file" ? "edit" : "overwrite";
1708
1717
  return `Refusing to ${verb} ${p}: you have not read it this turn. Call read_file on ${p} first, then retry the ${name}.`;
1709
1718
  }
1719
+ function unwrapEnvelope(name, input) {
1720
+ if (!("arguments" in input)) return input;
1721
+ if ("name" in input && input.name !== name) return input;
1722
+ let args2 = input.arguments;
1723
+ if (typeof args2 === "string") {
1724
+ try {
1725
+ args2 = JSON.parse(args2);
1726
+ } catch {
1727
+ return input;
1728
+ }
1729
+ }
1730
+ if (args2 && typeof args2 === "object" && !Array.isArray(args2)) return args2;
1731
+ return input;
1732
+ }
1733
+ function splitWriteHint(name, cause) {
1734
+ 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.`;
1735
+ 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.`;
1736
+ }
1737
+ function looksTruncatedWrite(name, input) {
1738
+ if (!BIG_WRITE_TOOLS.has(name)) return false;
1739
+ if (typeof input.path !== "string" || !input.path) return true;
1740
+ if (name === "write_file" && typeof input.content !== "string") return true;
1741
+ return false;
1742
+ }
1710
1743
  function markSeen(name, input, seen) {
1711
1744
  if (name !== "read_file" && name !== "edit_file" && name !== "write_file") return;
1712
1745
  const p = input.path;
@@ -1719,11 +1752,7 @@ function markSeen(name, input, seen) {
1719
1752
  async function* runAgent(opts) {
1720
1753
  const { model, cwd, permissions, hooks, signal, num_ctx } = opts;
1721
1754
  const startTime = Date.now();
1722
- let useGrammar = false;
1723
- if (providerName() === "ollama") {
1724
- const params = await modelParamCountB(model);
1725
- useGrammar = params == null || params <= GRAMMAR_MAX_PARAMS_B;
1726
- }
1755
+ const useGrammar = false;
1727
1756
  const system = buildSystemPrompt(TOOLS, cwd, loadProjectContext(cwd), useGrammar);
1728
1757
  const grammar = useGrammar ? buildToolGrammar(TOOLS) : void 0;
1729
1758
  const ollamaTools = toOllamaTools(TOOLS);
@@ -1743,9 +1772,11 @@ async function* runAgent(opts) {
1743
1772
  let tool_calls;
1744
1773
  let respondEmitted = 0;
1745
1774
  let streamedRespond = false;
1775
+ let emittedText = false;
1746
1776
  let lastTail = "";
1747
1777
  let tailRepeats = 0;
1748
1778
  let streamLooped = false;
1779
+ let truncated = false;
1749
1780
  const ac = new AbortController();
1750
1781
  const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
1751
1782
  if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
@@ -1755,12 +1786,14 @@ async function* runAgent(opts) {
1755
1786
  if (chunk.content) {
1756
1787
  text += chunk.content;
1757
1788
  if (!useGrammar) {
1789
+ emittedText = true;
1758
1790
  yield { type: "text-delta", text: chunk.content };
1759
1791
  } else {
1760
1792
  const r = streamRespondMessage(text);
1761
1793
  if (r) {
1762
1794
  streamedRespond = true;
1763
1795
  if (r.message.length > respondEmitted) {
1796
+ emittedText = true;
1764
1797
  yield { type: "text-delta", text: r.message.slice(respondEmitted) };
1765
1798
  respondEmitted = r.message.length;
1766
1799
  }
@@ -1781,7 +1814,7 @@ async function* runAgent(opts) {
1781
1814
  }
1782
1815
  }
1783
1816
  }
1784
- if (chunk.thinking) {
1817
+ if (chunk.thinking && !emittedText) {
1785
1818
  yield { type: "thinking-delta", text: chunk.thinking };
1786
1819
  }
1787
1820
  if (chunk.tool_calls && chunk.tool_calls.length > 0) {
@@ -1790,6 +1823,7 @@ async function* runAgent(opts) {
1790
1823
  if (chunk.done) {
1791
1824
  promptTokens += chunk.prompt_eval_count ?? 0;
1792
1825
  evalTokens += chunk.eval_count ?? 0;
1826
+ if (chunk.done_reason === "length") truncated = true;
1793
1827
  }
1794
1828
  }
1795
1829
  } catch (err) {
@@ -1828,6 +1862,19 @@ async function* runAgent(opts) {
1828
1862
  }
1829
1863
  const tool_uses = blocks.filter((b) => b.type === "tool_use");
1830
1864
  history.push({ role: "assistant", content: blocks });
1865
+ if (truncated && tool_uses.length > 0) {
1866
+ const results2 = tool_uses.map((use) => ({
1867
+ type: "tool_result",
1868
+ tool_use_id: use.id,
1869
+ content: splitWriteHint(use.name, "truncated"),
1870
+ is_error: true
1871
+ }));
1872
+ for (const u of tool_uses) yield { type: "tool-use", block: u };
1873
+ for (const r of results2) yield { type: "tool-result", block: r };
1874
+ history.push({ role: "user", content: results2 });
1875
+ yield { type: "turn-end", stop_reason: "tool_use" };
1876
+ continue;
1877
+ }
1831
1878
  if (tool_uses.length === 0) {
1832
1879
  yield { type: "turn-end", stop_reason: "end_turn" };
1833
1880
  break;
@@ -1862,12 +1909,14 @@ async function* runAgent(opts) {
1862
1909
  yield { type: "tool-result", block: r2 };
1863
1910
  continue;
1864
1911
  }
1912
+ use.input = unwrapEnvelope(use.name, use.input);
1865
1913
  const invalid = validateInput(tool.input_schema, use.input);
1866
1914
  if (invalid) {
1915
+ 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
1916
  const r2 = {
1868
1917
  type: "tool_result",
1869
1918
  tool_use_id: use.id,
1870
- content: `${invalid} for ${use.name}.`,
1919
+ content,
1871
1920
  is_error: true
1872
1921
  };
1873
1922
  results.push(r2);
@@ -1934,7 +1983,7 @@ async function* runAgent(opts) {
1934
1983
  yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
1935
1984
  return history;
1936
1985
  }
1937
- var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL, GRAMMAR_MAX_PARAMS_B;
1986
+ var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL, BIG_WRITE_TOOLS;
1938
1987
  var init_loop = __esm({
1939
1988
  "src/agent/loop.ts"() {
1940
1989
  "use strict";
@@ -1951,7 +2000,7 @@ var init_loop = __esm({
1951
2000
  MAX_TURNS = 25;
1952
2001
  REPEAT_TAIL = 120;
1953
2002
  REPEAT_KILL = 4;
1954
- GRAMMAR_MAX_PARAMS_B = 14;
2003
+ BIG_WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file"]);
1955
2004
  }
1956
2005
  });
1957
2006
 
@@ -2670,7 +2719,7 @@ import { Box as Box12, Text as Text12, Static } from "ink";
2670
2719
  // src/ui/markdown.ts
2671
2720
  import { Marked } from "marked";
2672
2721
  import { markedTerminal } from "marked-terminal";
2673
- import { highlight } from "cli-highlight";
2722
+ import { highlight, supportsLanguage } from "cli-highlight";
2674
2723
 
2675
2724
  // node_modules/chalk/source/vendor/ansi-styles/index.js
2676
2725
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -3194,7 +3243,7 @@ var theme = {
3194
3243
  // faint rule
3195
3244
  };
3196
3245
  function highlightCode(code, lang) {
3197
- if (!lang) return code;
3246
+ if (!lang || !supportsLanguage(lang)) return code;
3198
3247
  try {
3199
3248
  return highlight(code, { language: lang, ignoreIllegals: true });
3200
3249
  } catch {
@@ -3292,7 +3341,7 @@ import { Box as Box10, Text as Text10 } from "ink";
3292
3341
 
3293
3342
  // src/ui/ToolBlock.tsx
3294
3343
  import { Box as Box9, Text as Text9 } from "ink";
3295
- import { highlight as highlight2 } from "cli-highlight";
3344
+ import { highlight as highlight2, supportsLanguage as supportsLanguage2 } from "cli-highlight";
3296
3345
 
3297
3346
  // src/ui/toolExpand.ts
3298
3347
  import { useState as useState3, useEffect as useEffect3 } from "react";
@@ -3427,7 +3476,7 @@ function langFromPath(path) {
3427
3476
  return ext ? EXT_LANG[ext] : void 0;
3428
3477
  }
3429
3478
  function highlightLine(text, lang) {
3430
- if (!lang) return text;
3479
+ if (!lang || !supportsLanguage2(lang)) return text;
3431
3480
  try {
3432
3481
  return highlight2(text, { language: lang, ignoreIllegals: true });
3433
3482
  } catch {
@@ -4628,7 +4677,6 @@ function App() {
4628
4677
  header: /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd })
4629
4678
  }
4630
4679
  ),
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
4680
  input.startsWith("/") && /* @__PURE__ */ jsx13(CommandPalette, { filter: input, cursor: paletteCursor }),
4633
4681
  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
4682
  !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.21",
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": {