miii-agent 0.1.32 → 0.1.34
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/dist/cli.js +177 -43
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -184,7 +184,7 @@ async function modelContext(entry, model) {
|
|
|
184
184
|
}
|
|
185
185
|
const msg = err instanceof Error ? err.message : String(err);
|
|
186
186
|
if (msg.toLowerCase().includes("not found") || msg.toLowerCase().includes("unknown model")) {
|
|
187
|
-
throw new Error(`
|
|
187
|
+
throw new Error(`I don't see a model called "${model}" on Ollama. Pull it first with: ollama pull ${model}`);
|
|
188
188
|
}
|
|
189
189
|
throw err;
|
|
190
190
|
}
|
|
@@ -240,13 +240,17 @@ async function* chat(entry, model, messages, tools, opts) {
|
|
|
240
240
|
}
|
|
241
241
|
const msg = err instanceof Error ? err.message : String(err);
|
|
242
242
|
if (msg.toLowerCase().includes("not found") || msg.toLowerCase().includes("unknown model")) {
|
|
243
|
-
throw new Error(`
|
|
243
|
+
throw new Error(`I don't see a model called "${model}" on Ollama. Pull it first with: ollama pull ${model}`);
|
|
244
244
|
}
|
|
245
245
|
throw err;
|
|
246
246
|
}
|
|
247
|
+
let sawOutput = false;
|
|
247
248
|
try {
|
|
248
249
|
for await (const chunk of stream) {
|
|
249
250
|
if (signal?.aborted) break;
|
|
251
|
+
if (chunk.message.content || chunk.message.thinking || (chunk.message.tool_calls?.length ?? 0) > 0) {
|
|
252
|
+
sawOutput = true;
|
|
253
|
+
}
|
|
250
254
|
yield {
|
|
251
255
|
content: stripHarmony(chunk.message.content),
|
|
252
256
|
thinking: stripHarmony(chunk.message.thinking),
|
|
@@ -266,8 +270,13 @@ async function* chat(entry, model, messages, tools, opts) {
|
|
|
266
270
|
}
|
|
267
271
|
const msg = err instanceof Error ? err.message : String(err);
|
|
268
272
|
if (msg.includes("Did not receive done or success response in stream")) {
|
|
269
|
-
|
|
270
|
-
|
|
273
|
+
if (sawOutput) {
|
|
274
|
+
yield { content: "", done: true, prompt_eval_count: 0, eval_count: 0 };
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
throw new Error(
|
|
278
|
+
"Ollama closed the stream before sending any output \u2014 the model likely unloaded, ran out of memory, or crashed mid-request. Check `ollama ps`, then retry or switch to a smaller model."
|
|
279
|
+
);
|
|
271
280
|
}
|
|
272
281
|
throw err;
|
|
273
282
|
} finally {
|
|
@@ -278,8 +287,8 @@ var NOT_INSTALLED, NOT_RUNNING, LOCAL_HOST_RE, HARMONY_RE, CHANNEL_LABEL_RE;
|
|
|
278
287
|
var init_ollama = __esm({
|
|
279
288
|
"src/llm/ollama.ts"() {
|
|
280
289
|
"use strict";
|
|
281
|
-
NOT_INSTALLED = "Ollama
|
|
282
|
-
NOT_RUNNING = "Ollama
|
|
290
|
+
NOT_INSTALLED = "I can't find Ollama on this machine. Install it with: npm i -g ollama\nOr download it from https://ollama.com/download";
|
|
291
|
+
NOT_RUNNING = "Ollama isn't running, so I can't reach it. Start it with: ollama serve";
|
|
283
292
|
LOCAL_HOST_RE = /localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]/;
|
|
284
293
|
HARMONY_RE = /<\|?\/?(?:channel|message|start|end|return|constrain|assistant|user|system|developer|tool|tool_call|tool_response|final|analysis|commentary)\|?>/gi;
|
|
285
294
|
CHANNEL_LABEL_RE = /^(?:analysis|commentary|final)\s*(?=\w)/i;
|
|
@@ -288,7 +297,7 @@ var init_ollama = __esm({
|
|
|
288
297
|
|
|
289
298
|
// src/llm/openai.ts
|
|
290
299
|
function notAvailable(entry) {
|
|
291
|
-
return `
|
|
300
|
+
return `I couldn't reach the provider at ${entry.baseUrl}. Check that the server is running and the baseUrl is correct.`;
|
|
292
301
|
}
|
|
293
302
|
function headers(entry) {
|
|
294
303
|
const h = { "Content-Type": "application/json" };
|
|
@@ -314,7 +323,7 @@ async function listModels2(entry) {
|
|
|
314
323
|
detail = await res.text();
|
|
315
324
|
} catch {
|
|
316
325
|
}
|
|
317
|
-
throw new Error(`
|
|
326
|
+
throw new Error(`The provider came back with an error (HTTP ${res.status}): ${detail || res.statusText}`);
|
|
318
327
|
}
|
|
319
328
|
const body = await res.json();
|
|
320
329
|
return body.data.map((m) => m.id);
|
|
@@ -406,12 +415,12 @@ async function* chat2(entry, model, messages, tools, opts) {
|
|
|
406
415
|
} catch {
|
|
407
416
|
}
|
|
408
417
|
if (res.status === 404) {
|
|
409
|
-
throw new Error(`
|
|
418
|
+
throw new Error(`The provider at ${entry.baseUrl} has no model called "${model}". Double-check the name, or pick another with /models.`);
|
|
410
419
|
}
|
|
411
|
-
throw new Error(`
|
|
420
|
+
throw new Error(`The provider came back with an error (HTTP ${res.status}): ${detail || res.statusText}`);
|
|
412
421
|
}
|
|
413
422
|
const reader = res.body?.getReader();
|
|
414
|
-
if (!reader) throw new Error("
|
|
423
|
+
if (!reader) throw new Error("The provider answered but sent no body to read.");
|
|
415
424
|
const decoder = new TextDecoder();
|
|
416
425
|
let buffer = "";
|
|
417
426
|
try {
|
|
@@ -470,7 +479,7 @@ async function* chat2(entry, model, messages, tools, opts) {
|
|
|
470
479
|
throw new Error(notAvailable(entry));
|
|
471
480
|
}
|
|
472
481
|
if (timeoutSignal.aborted && !opts?.signal?.aborted) {
|
|
473
|
-
throw new Error(`
|
|
482
|
+
throw new Error(`The provider didn't respond within ${TIMEOUT_MS / 1e3}s, so I stopped waiting. The model may still be loading or thinking \u2014 give it another try.`);
|
|
474
483
|
}
|
|
475
484
|
throw err;
|
|
476
485
|
}
|
|
@@ -678,14 +687,14 @@ function isUnder(parent, child) {
|
|
|
678
687
|
}
|
|
679
688
|
function confinePath(p) {
|
|
680
689
|
if (typeof p !== "string" || p.length === 0) {
|
|
681
|
-
throw new Error("
|
|
690
|
+
throw new Error("I need a file path here, but none was given.");
|
|
682
691
|
}
|
|
683
692
|
const root = process.cwd();
|
|
684
693
|
const abs = resolve(root, p);
|
|
685
694
|
if (isUnder(root, abs) || isUnder(SPILL_DIR, abs)) {
|
|
686
695
|
return abs;
|
|
687
696
|
}
|
|
688
|
-
throw new Error(`
|
|
697
|
+
throw new Error(`"${p}" sits outside the working directory (${root}), so I can't touch it. Stay within the project folder.`);
|
|
689
698
|
}
|
|
690
699
|
var SPILL_DIR;
|
|
691
700
|
var init_paths = __esm({
|
|
@@ -787,38 +796,38 @@ function nearMiss(src, old_str) {
|
|
|
787
796
|
const width = String(to).length;
|
|
788
797
|
const ctx = srcLines.slice(from, to).map((l, i) => `${String(from + i + 1).padStart(width, " ")} ${l}`).join("\n");
|
|
789
798
|
return `
|
|
790
|
-
|
|
799
|
+
Here's the closest text I could find (lines ${from + 1}-${to}) \u2014 copy it exactly:
|
|
791
800
|
${ctx}`;
|
|
792
801
|
}
|
|
793
802
|
function locate(src, old_str) {
|
|
794
803
|
const first = src.indexOf(old_str);
|
|
795
804
|
if (first !== -1) {
|
|
796
805
|
if (src.indexOf(old_str, first + 1) !== -1) {
|
|
797
|
-
return { error: `
|
|
806
|
+
return { error: `That text shows up in more than one place, so I can't tell which one you mean. Add a line or two around it to make it unique.` };
|
|
798
807
|
}
|
|
799
808
|
return [first, first + old_str.length];
|
|
800
809
|
}
|
|
801
810
|
const fuzzy = fuzzyRange(src, old_str);
|
|
802
811
|
if (fuzzy) return fuzzy;
|
|
803
|
-
return { error: `
|
|
812
|
+
return { error: `I couldn't find that text \u2014 it may differ by whitespace or a stray character.${nearMiss(src, old_str)}` };
|
|
804
813
|
}
|
|
805
814
|
function applyBatch(src, edits) {
|
|
806
815
|
const ranges = [];
|
|
807
816
|
for (let i = 0; i < edits.length; i++) {
|
|
808
817
|
const { old_str, new_str } = edits[i];
|
|
809
818
|
if (typeof old_str !== "string" || typeof new_str !== "string") {
|
|
810
|
-
return { error: `
|
|
819
|
+
return { error: `Edit #${i + 1} is missing text \u2014 each edit needs both an old_str and a new_str string.` };
|
|
811
820
|
}
|
|
812
|
-
if (old_str === "") return { error: `
|
|
813
|
-
if (old_str === new_str) return { error: `
|
|
821
|
+
if (old_str === "") return { error: `Edit #${i + 1} has an empty old_str, so there's nothing to look for. Give it the exact text you want to replace.` };
|
|
822
|
+
if (old_str === new_str) return { error: `Edit #${i + 1} has the same old_str and new_str, so it wouldn't change anything.` };
|
|
814
823
|
const r = locate(src, old_str);
|
|
815
|
-
if (!Array.isArray(r)) return { error: `
|
|
824
|
+
if (!Array.isArray(r)) return { error: `Edit #${i + 1}: ${r.error}` };
|
|
816
825
|
ranges.push({ start: r[0], end: r[1], new_str });
|
|
817
826
|
}
|
|
818
827
|
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
819
828
|
for (let i = 1; i < sorted.length; i++) {
|
|
820
829
|
if (sorted[i].start < sorted[i - 1].end) {
|
|
821
|
-
return { error: `edits
|
|
830
|
+
return { error: `Two of these edits touch the same spot, so I can't apply them together. Split them into separate calls, or widen each old_str so they don't overlap.` };
|
|
822
831
|
}
|
|
823
832
|
}
|
|
824
833
|
let out = src;
|
|
@@ -869,11 +878,11 @@ var init_edit_file = __esm({
|
|
|
869
878
|
return { content: `Edited ${path} (${res.count} edits).${verifyHint(path)}` };
|
|
870
879
|
}
|
|
871
880
|
if (typeof old_str !== "string" || typeof new_str !== "string") {
|
|
872
|
-
return { content: `
|
|
881
|
+
return { content: `To edit ${path} I need either an old_str/new_str pair or an edits[] array \u2014 neither came through.`, is_error: true };
|
|
873
882
|
}
|
|
874
883
|
if (old_str === new_str) {
|
|
875
884
|
return {
|
|
876
|
-
content: `old_str and new_str are
|
|
885
|
+
content: `old_str and new_str are the same, so there's nothing to change in ${path}. If the file is already correct, don't edit it again \u2014 finish with the respond action and tell the user it's done.`,
|
|
877
886
|
is_error: true
|
|
878
887
|
};
|
|
879
888
|
}
|
|
@@ -890,12 +899,12 @@ var init_edit_file = __esm({
|
|
|
890
899
|
return { content: `Edited ${path} (whitespace-tolerant match).${verifyHint(path)}` };
|
|
891
900
|
}
|
|
892
901
|
}
|
|
893
|
-
return { content: `
|
|
902
|
+
return { content: `I couldn't find that text in ${path} \u2014 it may differ by whitespace or a stray character.${nearMiss(src, old_str)}`, is_error: true };
|
|
894
903
|
}
|
|
895
904
|
const all = replace_all === true;
|
|
896
905
|
if (!all && src.indexOf(old_str, first + 1) !== -1) {
|
|
897
906
|
return {
|
|
898
|
-
content: `
|
|
907
|
+
content: `That text appears more than once in ${path}, so I don't know which one to change. Add a line or two around it to single it out, or set replace_all to change them all.`,
|
|
899
908
|
is_error: true
|
|
900
909
|
};
|
|
901
910
|
}
|
|
@@ -953,7 +962,7 @@ var init_read_file = __esm({
|
|
|
953
962
|
if (IMAGE_EXT.has(ext) || looksImage(buf)) {
|
|
954
963
|
if (buf.length > MAX_IMAGE_BYTES) {
|
|
955
964
|
return {
|
|
956
|
-
content: `${path} is an image but
|
|
965
|
+
content: `${path} is an image, but at ${buf.length} bytes it's too big to attach (limit is ${MAX_IMAGE_BYTES}). Resize it and try again.`,
|
|
957
966
|
is_error: true
|
|
958
967
|
};
|
|
959
968
|
}
|
|
@@ -963,7 +972,7 @@ var init_read_file = __esm({
|
|
|
963
972
|
};
|
|
964
973
|
}
|
|
965
974
|
if (buf.subarray(0, 8e3).includes(0)) {
|
|
966
|
-
return { content: `${path} looks binary (${buf.length} bytes)
|
|
975
|
+
return { content: `${path} looks like a binary file (${buf.length} bytes), so I'm not reading it as text.`, is_error: true };
|
|
967
976
|
}
|
|
968
977
|
const raw = buf.toString("utf-8").replace(/\r\n/g, "\n");
|
|
969
978
|
const allLines = raw.split("\n");
|
|
@@ -975,7 +984,7 @@ var init_read_file = __esm({
|
|
|
975
984
|
let body = numbered(slice, start);
|
|
976
985
|
if (body.length > MAX_CHARS) {
|
|
977
986
|
body = body.slice(0, MAX_CHARS) + `
|
|
978
|
-
[
|
|
987
|
+
[There's more \u2014 this hit the ${MAX_CHARS}-char limit. Use offset/limit to read the rest.]`;
|
|
979
988
|
}
|
|
980
989
|
if (ranged) {
|
|
981
990
|
const end = start - 1 + slice.length;
|
|
@@ -1047,7 +1056,7 @@ function spillIfLarge(full, label = "output", budget = INLINE_BUDGET) {
|
|
|
1047
1056
|
const head = Math.floor(budget * HEAD_FRACTION);
|
|
1048
1057
|
const tail = budget - head;
|
|
1049
1058
|
const preview = full.slice(0, head) + "\n\u2026\n" + full.slice(-tail);
|
|
1050
|
-
const notice = path ? `[${label}
|
|
1059
|
+
const notice = path ? `[This ${label} was long (${full.length} bytes), so I'm showing the start and end. The full text is saved at ${path} \u2014 read it with read_file offset/limit to see the middle.]` : `[This ${label} was too long to show in full, and I couldn't save the rest to disk, so the middle is missing.]`;
|
|
1051
1060
|
return `${preview}
|
|
1052
1061
|
${notice}`;
|
|
1053
1062
|
}
|
|
@@ -1130,8 +1139,8 @@ var init_run_bash = __esm({
|
|
|
1130
1139
|
const out = all ?? "";
|
|
1131
1140
|
const is_error = aborted || timedOut || exitCode !== 0;
|
|
1132
1141
|
const note = timedOut ? `
|
|
1133
|
-
[
|
|
1134
|
-
[
|
|
1142
|
+
[Timed out after ${timeout}ms, so I stopped the command and everything it started. Raise timeout_ms for long builds or test suites.]` : aborted ? `
|
|
1143
|
+
[Cancelled \u2014 I stopped the command and everything it started.]` : "";
|
|
1135
1144
|
const body = out || (is_error ? `(no output)` : "");
|
|
1136
1145
|
const content = `${spillIfLarge(body, "command output")}
|
|
1137
1146
|
[exit ${exitCode ?? "killed"}]${note}`;
|
|
@@ -1240,8 +1249,9 @@ var init_grep = __esm({
|
|
|
1240
1249
|
const lines = (res.stdout ?? "").split("\n").slice(0, limit);
|
|
1241
1250
|
const out = lines.join("\n");
|
|
1242
1251
|
const code = res.exitCode ?? 0;
|
|
1243
|
-
|
|
1244
|
-
|
|
1252
|
+
const noMatch = `Nothing matched "${pattern}". Try a looser pattern, case_insensitive, or a wider path.`;
|
|
1253
|
+
if (!out && code === 1) return { content: noMatch };
|
|
1254
|
+
return { content: out || res.stderr || noMatch, is_error: code > 1 };
|
|
1245
1255
|
} catch (err) {
|
|
1246
1256
|
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
1247
1257
|
}
|
|
@@ -1316,7 +1326,7 @@ var init_glob = __esm({
|
|
|
1316
1326
|
res = await tryFind();
|
|
1317
1327
|
}
|
|
1318
1328
|
const all = (res.stdout ?? "").split("\n").filter(Boolean);
|
|
1319
|
-
if (all.length === 0) return { content:
|
|
1329
|
+
if (all.length === 0) return { content: `Nothing matched "${pattern}". Try a broader pattern or a different path.` };
|
|
1320
1330
|
const lines = byMtimeDesc(all).slice(0, limit);
|
|
1321
1331
|
return { content: lines.join("\n") };
|
|
1322
1332
|
} catch (err) {
|
|
@@ -1327,6 +1337,70 @@ var init_glob = __esm({
|
|
|
1327
1337
|
}
|
|
1328
1338
|
});
|
|
1329
1339
|
|
|
1340
|
+
// src/tools/write_todos.ts
|
|
1341
|
+
function render(todos) {
|
|
1342
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
1343
|
+
const header = `Todos (${done}/${todos.length} done):`;
|
|
1344
|
+
const lines = todos.map((t) => `${MARK[t.status]} ${t.content}`);
|
|
1345
|
+
return [header, ...lines].join("\n");
|
|
1346
|
+
}
|
|
1347
|
+
var STATUSES, MARK, write_todos;
|
|
1348
|
+
var init_write_todos = __esm({
|
|
1349
|
+
"src/tools/write_todos.ts"() {
|
|
1350
|
+
"use strict";
|
|
1351
|
+
STATUSES = ["pending", "in_progress", "completed"];
|
|
1352
|
+
MARK = {
|
|
1353
|
+
completed: "[x]",
|
|
1354
|
+
in_progress: "[~]",
|
|
1355
|
+
pending: "[ ]"
|
|
1356
|
+
};
|
|
1357
|
+
write_todos = {
|
|
1358
|
+
name: "write_todos",
|
|
1359
|
+
description: "Create and maintain a task list for the current session, shown to the user as a live checklist. Pass the FULL list every call \u2014 it replaces the previous one (all or nothing), so include finished items too. Each todo is {content, status} where status is pending | in_progress | completed. Keep exactly one item in_progress at a time and mark it completed before starting the next. Use for multi-step work (features, refactors, multi-file changes) so the user can see what is done, in progress, and left. Skip it for single trivial actions.",
|
|
1360
|
+
input_schema: {
|
|
1361
|
+
type: "object",
|
|
1362
|
+
properties: {
|
|
1363
|
+
todos: {
|
|
1364
|
+
type: "array",
|
|
1365
|
+
description: "The full, ordered task list. Replaces the prior list entirely.",
|
|
1366
|
+
items: {
|
|
1367
|
+
type: "object",
|
|
1368
|
+
properties: {
|
|
1369
|
+
content: { type: "string", description: 'Short task description (imperative, e.g. "Add webfetch tool").' },
|
|
1370
|
+
status: { type: "string", enum: STATUSES, description: "pending | in_progress | completed" }
|
|
1371
|
+
},
|
|
1372
|
+
required: ["content", "status"]
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
},
|
|
1376
|
+
required: ["todos"]
|
|
1377
|
+
},
|
|
1378
|
+
handler: ({ todos }) => {
|
|
1379
|
+
if (!Array.isArray(todos)) {
|
|
1380
|
+
return { content: "I need a todos array here \u2014 pass the full list of tasks.", is_error: true };
|
|
1381
|
+
}
|
|
1382
|
+
if (todos.length === 0) {
|
|
1383
|
+
return { content: "The todo list is empty \u2014 give me at least one task.", is_error: true };
|
|
1384
|
+
}
|
|
1385
|
+
for (let i = 0; i < todos.length; i++) {
|
|
1386
|
+
const t = todos[i];
|
|
1387
|
+
if (!t || typeof t.content !== "string" || t.content.trim() === "") {
|
|
1388
|
+
return { content: `Task #${i + 1} needs some text \u2014 its content can't be empty.`, is_error: true };
|
|
1389
|
+
}
|
|
1390
|
+
if (!STATUSES.includes(t.status)) {
|
|
1391
|
+
return { content: `Task #${i + 1} has an unknown status. Use one of: ${STATUSES.join(", ")}.`, is_error: true };
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
const active2 = todos.filter((t) => t.status === "in_progress").length;
|
|
1395
|
+
if (active2 > 1) {
|
|
1396
|
+
return { content: `Keep just one task in progress at a time \u2014 I see ${active2}. Finish one before starting the next.`, is_error: true };
|
|
1397
|
+
}
|
|
1398
|
+
return { content: render(todos) };
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1330
1404
|
// src/tools/registry.ts
|
|
1331
1405
|
function getTool(name) {
|
|
1332
1406
|
return TOOLS.find((t) => t.name === name);
|
|
@@ -1355,13 +1429,15 @@ var init_registry = __esm({
|
|
|
1355
1429
|
init_run_bash();
|
|
1356
1430
|
init_grep();
|
|
1357
1431
|
init_glob();
|
|
1432
|
+
init_write_todos();
|
|
1358
1433
|
TOOLS = [
|
|
1359
1434
|
edit_file,
|
|
1360
1435
|
read_file,
|
|
1361
1436
|
write_file,
|
|
1362
1437
|
run_bash,
|
|
1363
1438
|
grep,
|
|
1364
|
-
glob
|
|
1439
|
+
glob,
|
|
1440
|
+
write_todos
|
|
1365
1441
|
];
|
|
1366
1442
|
}
|
|
1367
1443
|
});
|
|
@@ -1424,7 +1500,7 @@ function validateInput(schema, input) {
|
|
|
1424
1500
|
const result = toZod(schema).safeParse(input ?? {});
|
|
1425
1501
|
if (result.success) return null;
|
|
1426
1502
|
const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
1427
|
-
return `
|
|
1503
|
+
return `Some arguments didn't look right: ${issues}`;
|
|
1428
1504
|
}
|
|
1429
1505
|
var init_validate = __esm({
|
|
1430
1506
|
"src/tools/validate.ts"() {
|
|
@@ -1563,6 +1639,14 @@ Ask in a numbered list. One round of questions per turn. Then wait.
|
|
|
1563
1639
|
You have access to the following tools. Call them via the function-calling interface.
|
|
1564
1640
|
${toolLines}
|
|
1565
1641
|
|
|
1642
|
+
# Task list (write_todos)
|
|
1643
|
+
For multi-step work \u2014 a feature, a refactor, anything touching multiple files or more than ~3 steps \u2014 track it with write_todos so the user sees a live checklist of what is done, in progress, and left.
|
|
1644
|
+
- Create the list right after your plan, before the first real action. Each item is {content, status}: short imperative content, status pending | in_progress | completed.
|
|
1645
|
+
- Pass the FULL list every call; it replaces the previous one. Include already-finished items so nothing disappears.
|
|
1646
|
+
- Mark exactly ONE item in_progress at a time. Set it in_progress when you start it, completed the moment it is done, then move the next to in_progress. Never leave a finished task pending or two tasks in_progress.
|
|
1647
|
+
- Keep items outcome-sized, not tool-sized \u2014 "Add webfetch tool and wire it in", not "call edit_file". Aim for a handful, not twenty.
|
|
1648
|
+
- Do NOT use write_todos for trivial single-step work \u2014 a lone read, one edit, a quick search. It is overhead there.
|
|
1649
|
+
|
|
1566
1650
|
# Loop semantics
|
|
1567
1651
|
- When you need to act on the filesystem or run a command, emit a tool call.
|
|
1568
1652
|
- After each tool result, decide: more tool calls, or a final plain-text answer.
|
|
@@ -1858,7 +1942,7 @@ function readGuard(name, input, seen) {
|
|
|
1858
1942
|
if (seen.has(abs)) return null;
|
|
1859
1943
|
if (name === "write_file" && !existsSync5(abs)) return null;
|
|
1860
1944
|
const verb = name === "edit_file" ? "edit" : "overwrite";
|
|
1861
|
-
return `
|
|
1945
|
+
return `I won't ${verb} ${p} without seeing it first \u2014 I don't want to clobber something. Read it with read_file, then retry the ${name}.`;
|
|
1862
1946
|
}
|
|
1863
1947
|
function unwrapEnvelope(name, input) {
|
|
1864
1948
|
if (!("arguments" in input)) return input;
|
|
@@ -2028,6 +2112,14 @@ async function* runAgent(opts) {
|
|
|
2028
2112
|
yield { type: "turn-end", stop_reason: "tool_use" };
|
|
2029
2113
|
continue;
|
|
2030
2114
|
}
|
|
2115
|
+
if (assistantText.trim() === "") {
|
|
2116
|
+
yield {
|
|
2117
|
+
type: "error",
|
|
2118
|
+
message: "The model returned an empty response \u2014 likely unloaded/OOM mid-stream or the output cap was consumed by thinking before any answer. Retry, switch models, or lower the context/effort."
|
|
2119
|
+
};
|
|
2120
|
+
yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
|
|
2121
|
+
return history;
|
|
2122
|
+
}
|
|
2031
2123
|
endedCleanly = true;
|
|
2032
2124
|
yield { type: "turn-end", stop_reason: "end_turn" };
|
|
2033
2125
|
break;
|
|
@@ -2040,7 +2132,7 @@ async function* runAgent(opts) {
|
|
|
2040
2132
|
const r2 = {
|
|
2041
2133
|
type: "tool_result",
|
|
2042
2134
|
tool_use_id: use.id,
|
|
2043
|
-
content: `Unknown tool: ${use.name}
|
|
2135
|
+
content: `Unknown tool: ${use.name}. There's no tool by that name \u2014 check the spelling against the tools you were given.`,
|
|
2044
2136
|
is_error: true
|
|
2045
2137
|
};
|
|
2046
2138
|
results.push(r2);
|
|
@@ -2066,7 +2158,7 @@ async function* runAgent(opts) {
|
|
|
2066
2158
|
const r2 = {
|
|
2067
2159
|
type: "tool_result",
|
|
2068
2160
|
tool_use_id: use.id,
|
|
2069
|
-
content: `Permission denied
|
|
2161
|
+
content: `Permission denied \u2014 the user chose not to run ${use.name}. Try a different approach, or ask them what they'd prefer.`,
|
|
2070
2162
|
is_error: true
|
|
2071
2163
|
};
|
|
2072
2164
|
results.push(r2);
|
|
@@ -2368,7 +2460,7 @@ var init_run = __esm({
|
|
|
2368
2460
|
});
|
|
2369
2461
|
|
|
2370
2462
|
// src/cli.tsx
|
|
2371
|
-
import { render } from "ink";
|
|
2463
|
+
import { render as render2 } from "ink";
|
|
2372
2464
|
import { createElement } from "react";
|
|
2373
2465
|
|
|
2374
2466
|
// src/ui/App.tsx
|
|
@@ -3658,7 +3750,8 @@ var TOOL_LABEL = {
|
|
|
3658
3750
|
read_file: "Read",
|
|
3659
3751
|
run_bash: "Bash",
|
|
3660
3752
|
glob: "Glob",
|
|
3661
|
-
grep: "Grep"
|
|
3753
|
+
grep: "Grep",
|
|
3754
|
+
write_todos: "Todos"
|
|
3662
3755
|
};
|
|
3663
3756
|
var EXT_LANG = {
|
|
3664
3757
|
ts: "typescript",
|
|
@@ -3760,6 +3853,43 @@ function FileEditBlock({
|
|
|
3760
3853
|
] }) })
|
|
3761
3854
|
] });
|
|
3762
3855
|
}
|
|
3856
|
+
function TodoBlock({ todos }) {
|
|
3857
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
3858
|
+
const doing = todos.filter((t) => t.status === "in_progress").length;
|
|
3859
|
+
const glyph = { completed: "\u2714", in_progress: "\u25B6", pending: "\u25CB" };
|
|
3860
|
+
const color = { completed: "green", in_progress: "yellow", pending: "gray" };
|
|
3861
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
3862
|
+
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
3863
|
+
/* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u25CF " }),
|
|
3864
|
+
/* @__PURE__ */ jsx9(Text9, { color: "white", children: "Todos " }),
|
|
3865
|
+
/* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
3866
|
+
"(",
|
|
3867
|
+
done,
|
|
3868
|
+
"/",
|
|
3869
|
+
todos.length,
|
|
3870
|
+
" done",
|
|
3871
|
+
doing > 0 ? `, ${doing} in progress` : "",
|
|
3872
|
+
")"
|
|
3873
|
+
] })
|
|
3874
|
+
] }),
|
|
3875
|
+
todos.map((t, i) => /* @__PURE__ */ jsxs9(Box9, { marginLeft: 4, children: [
|
|
3876
|
+
/* @__PURE__ */ jsxs9(Text9, { color: color[t.status], children: [
|
|
3877
|
+
glyph[t.status],
|
|
3878
|
+
" "
|
|
3879
|
+
] }),
|
|
3880
|
+
/* @__PURE__ */ jsx9(
|
|
3881
|
+
Text9,
|
|
3882
|
+
{
|
|
3883
|
+
color: t.status === "in_progress" ? "yellow" : void 0,
|
|
3884
|
+
dimColor: t.status !== "in_progress",
|
|
3885
|
+
strikethrough: t.status === "completed",
|
|
3886
|
+
bold: t.status === "in_progress",
|
|
3887
|
+
children: t.content
|
|
3888
|
+
}
|
|
3889
|
+
)
|
|
3890
|
+
] }, i))
|
|
3891
|
+
] });
|
|
3892
|
+
}
|
|
3763
3893
|
function toolHeader(use) {
|
|
3764
3894
|
const label = TOOL_LABEL[use.name] ?? use.name;
|
|
3765
3895
|
const input = use.input ?? {};
|
|
@@ -3839,6 +3969,10 @@ function ToolResultBlock({ result, toolName }) {
|
|
|
3839
3969
|
] });
|
|
3840
3970
|
}
|
|
3841
3971
|
function ToolUseLine({ use, result }) {
|
|
3972
|
+
if (use.name === "write_todos" && !result?.is_error) {
|
|
3973
|
+
const todos = use.input.todos;
|
|
3974
|
+
if (Array.isArray(todos) && todos.length > 0) return /* @__PURE__ */ jsx9(TodoBlock, { todos });
|
|
3975
|
+
}
|
|
3842
3976
|
if (use.name === "write_file" && !result?.is_error) {
|
|
3843
3977
|
const input = use.input;
|
|
3844
3978
|
const content = input.content ?? "";
|
|
@@ -5196,5 +5330,5 @@ if (cmd === "version" || cmd === "--version" || cmd === "-v") {
|
|
|
5196
5330
|
process.on("exit", () => {
|
|
5197
5331
|
if (process.stdout.isTTY) process.stdout.write("\x1B]2;\x07");
|
|
5198
5332
|
});
|
|
5199
|
-
|
|
5333
|
+
render2(createElement(App));
|
|
5200
5334
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miii-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"description": "Local AI coding agent for your terminal — an open-source, offline alternative to Claude Code, Cursor, and GitHub Copilot, powered by Ollama. Private by default, free forever.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|