miii-agent 0.1.17 → 0.1.19
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 +314 -23
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -158,6 +158,30 @@ async function modelContext(entry, model) {
|
|
|
158
158
|
throw err;
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
+
async function paramCountB(entry, model) {
|
|
162
|
+
try {
|
|
163
|
+
const info = await makeClient(entry).show({ model });
|
|
164
|
+
const details = info.details;
|
|
165
|
+
if (details?.parameter_size) {
|
|
166
|
+
const m = details.parameter_size.match(/([\d.]+)\s*([BM])/i);
|
|
167
|
+
if (m) {
|
|
168
|
+
const n = parseFloat(m[1]);
|
|
169
|
+
if (!isNaN(n)) return m[2].toUpperCase() === "M" ? n / 1e3 : n;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const modelInfo = info.model_info;
|
|
173
|
+
if (modelInfo) {
|
|
174
|
+
const key = Object.keys(modelInfo).find((k) => k.endsWith("parameter_count"));
|
|
175
|
+
if (key) {
|
|
176
|
+
const val = Number(modelInfo[key]);
|
|
177
|
+
if (!isNaN(val) && val > 0) return val / 1e9;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
161
185
|
async function* chat(entry, model, messages, tools, opts) {
|
|
162
186
|
if (opts?.signal?.aborted) return;
|
|
163
187
|
const signal = opts?.signal;
|
|
@@ -177,15 +201,19 @@ async function* chat(entry, model, messages, tools, opts) {
|
|
|
177
201
|
num_ctx: opts?.num_ctx ?? 8192
|
|
178
202
|
};
|
|
179
203
|
if (numPredict !== void 0 && numPredict > 0) options.num_predict = numPredict;
|
|
180
|
-
|
|
204
|
+
const req = {
|
|
181
205
|
model,
|
|
182
206
|
messages,
|
|
183
|
-
tools,
|
|
184
207
|
stream: true,
|
|
185
208
|
think: true,
|
|
186
209
|
keep_alive: opts?.keep_alive ?? "10m",
|
|
187
210
|
options
|
|
188
|
-
}
|
|
211
|
+
};
|
|
212
|
+
if (opts?.format) req.format = opts.format;
|
|
213
|
+
else if (tools) req.tools = tools;
|
|
214
|
+
stream = await client.chat(
|
|
215
|
+
req
|
|
216
|
+
);
|
|
189
217
|
} catch (err) {
|
|
190
218
|
if (signal?.aborted) return;
|
|
191
219
|
if (isConnectionError(err)) {
|
|
@@ -452,6 +480,9 @@ var init_openai = __esm({
|
|
|
452
480
|
function active() {
|
|
453
481
|
return resolveProvider();
|
|
454
482
|
}
|
|
483
|
+
function providerName() {
|
|
484
|
+
return active().name;
|
|
485
|
+
}
|
|
455
486
|
function isAvailable3() {
|
|
456
487
|
const { entry } = active();
|
|
457
488
|
return entry.type === "ollama" ? isAvailable(entry) : isAvailable2(entry);
|
|
@@ -468,6 +499,16 @@ async function modelContext3(model) {
|
|
|
468
499
|
const { entry } = active();
|
|
469
500
|
return entry.type === "ollama" ? modelContext(entry, model) : modelContext2(entry, model);
|
|
470
501
|
}
|
|
502
|
+
async function modelParamCountB(model) {
|
|
503
|
+
const { entry } = active();
|
|
504
|
+
if (entry.type !== "ollama") return null;
|
|
505
|
+
const key = `${entry.baseUrl}:${model}`;
|
|
506
|
+
const cached = paramCountCache.get(key);
|
|
507
|
+
if (cached !== void 0) return cached;
|
|
508
|
+
const params = await paramCountB(entry, model);
|
|
509
|
+
paramCountCache.set(key, params);
|
|
510
|
+
return params;
|
|
511
|
+
}
|
|
471
512
|
async function* chat3(model, messages, tools, opts) {
|
|
472
513
|
const { entry } = active();
|
|
473
514
|
if (entry.type === "ollama") {
|
|
@@ -476,12 +517,70 @@ async function* chat3(model, messages, tools, opts) {
|
|
|
476
517
|
yield* chat2(entry, model, messages, tools, opts);
|
|
477
518
|
}
|
|
478
519
|
}
|
|
520
|
+
var paramCountCache;
|
|
479
521
|
var init_client = __esm({
|
|
480
522
|
"src/llm/client.ts"() {
|
|
481
523
|
"use strict";
|
|
482
524
|
init_config();
|
|
483
525
|
init_ollama();
|
|
484
526
|
init_openai();
|
|
527
|
+
paramCountCache = /* @__PURE__ */ new Map();
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// src/llm/grammar.ts
|
|
532
|
+
function argProperties(props) {
|
|
533
|
+
const out = {};
|
|
534
|
+
for (const [key, spec] of Object.entries(props)) {
|
|
535
|
+
const node = { type: spec.type };
|
|
536
|
+
if (spec.enum && spec.enum.length) node.enum = spec.enum;
|
|
537
|
+
out[key] = node;
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
function toolBranch(tool) {
|
|
542
|
+
const args2 = {
|
|
543
|
+
type: "object",
|
|
544
|
+
additionalProperties: false,
|
|
545
|
+
properties: argProperties(tool.input_schema.properties)
|
|
546
|
+
};
|
|
547
|
+
if (tool.input_schema.required && tool.input_schema.required.length) {
|
|
548
|
+
args2.required = tool.input_schema.required;
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
type: "object",
|
|
552
|
+
additionalProperties: false,
|
|
553
|
+
required: ["name", "arguments"],
|
|
554
|
+
properties: {
|
|
555
|
+
name: { const: tool.name },
|
|
556
|
+
arguments: args2
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function respondBranch() {
|
|
561
|
+
return {
|
|
562
|
+
type: "object",
|
|
563
|
+
additionalProperties: false,
|
|
564
|
+
required: ["name", "arguments"],
|
|
565
|
+
properties: {
|
|
566
|
+
name: { const: RESPOND_ACTION },
|
|
567
|
+
arguments: {
|
|
568
|
+
type: "object",
|
|
569
|
+
additionalProperties: false,
|
|
570
|
+
required: ["message"],
|
|
571
|
+
properties: { message: { type: "string" } }
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function buildToolGrammar(tools) {
|
|
577
|
+
return { oneOf: [...tools.map(toolBranch), respondBranch()] };
|
|
578
|
+
}
|
|
579
|
+
var RESPOND_ACTION;
|
|
580
|
+
var init_grammar = __esm({
|
|
581
|
+
"src/llm/grammar.ts"() {
|
|
582
|
+
"use strict";
|
|
583
|
+
RESPOND_ACTION = "respond";
|
|
485
584
|
}
|
|
486
585
|
});
|
|
487
586
|
|
|
@@ -851,26 +950,45 @@ var init_run_bash = __esm({
|
|
|
851
950
|
|
|
852
951
|
// src/tools/grep.ts
|
|
853
952
|
import { execa as execa2 } from "execa";
|
|
854
|
-
var grep;
|
|
953
|
+
var bool, grep;
|
|
855
954
|
var init_grep = __esm({
|
|
856
955
|
"src/tools/grep.ts"() {
|
|
857
956
|
"use strict";
|
|
858
957
|
init_paths();
|
|
958
|
+
bool = (v) => v === true || String(v) === "true";
|
|
859
959
|
grep = {
|
|
860
960
|
name: "grep",
|
|
861
961
|
description: "Search file contents for a regex pattern. Uses ripgrep if available, falls back to grep -R.",
|
|
862
962
|
input_schema: {
|
|
863
963
|
type: "object",
|
|
864
964
|
properties: {
|
|
865
|
-
pattern: { type: "string", description: "Regex pattern" },
|
|
965
|
+
pattern: { type: "string", description: "Regex pattern (literal when fixed_strings)" },
|
|
866
966
|
path: { type: "string", description: "Root path to search (default cwd)" },
|
|
867
967
|
glob: { type: "string", description: 'File glob filter, e.g. "*.ts"' },
|
|
868
968
|
case_insensitive: { type: "boolean", description: "Case-insensitive match" },
|
|
869
|
-
max_results: { type: "number", description: "Max matching lines (default 200)" }
|
|
969
|
+
max_results: { type: "number", description: "Max matching lines (default 200)" },
|
|
970
|
+
context: { type: "number", description: "Lines of context before & after each match" },
|
|
971
|
+
files_only: { type: "boolean", description: "List matching filenames only" },
|
|
972
|
+
type: { type: "string", description: 'ripgrep file type filter, e.g. "js" (ignored by grep fallback)' },
|
|
973
|
+
multiline: { type: "boolean", description: "Allow matches to span multiple lines" },
|
|
974
|
+
count: { type: "boolean", description: "Print count of matching lines per file" },
|
|
975
|
+
fixed_strings: { type: "boolean", description: "Treat pattern as literal string, not regex" }
|
|
870
976
|
},
|
|
871
977
|
required: ["pattern"]
|
|
872
978
|
},
|
|
873
|
-
handler: async ({
|
|
979
|
+
handler: async ({
|
|
980
|
+
pattern,
|
|
981
|
+
path,
|
|
982
|
+
glob: glob2,
|
|
983
|
+
case_insensitive,
|
|
984
|
+
max_results,
|
|
985
|
+
context,
|
|
986
|
+
files_only,
|
|
987
|
+
type,
|
|
988
|
+
multiline,
|
|
989
|
+
count,
|
|
990
|
+
fixed_strings
|
|
991
|
+
}) => {
|
|
874
992
|
let root;
|
|
875
993
|
try {
|
|
876
994
|
root = confinePath(path ?? ".");
|
|
@@ -878,21 +996,37 @@ var init_grep = __esm({
|
|
|
878
996
|
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
879
997
|
}
|
|
880
998
|
const limit = max_results ?? 200;
|
|
881
|
-
const ci =
|
|
999
|
+
const ci = bool(case_insensitive);
|
|
1000
|
+
const filesOnly = bool(files_only);
|
|
1001
|
+
const ml = bool(multiline);
|
|
1002
|
+
const cnt = bool(count);
|
|
1003
|
+
const fixed = bool(fixed_strings);
|
|
1004
|
+
const ctx = typeof context === "number" && context > 0 ? Math.floor(context) : 0;
|
|
882
1005
|
const tryRg = async () => {
|
|
883
1006
|
const args2 = ["--line-number", "--no-heading", "--color=never", "-m", String(limit)];
|
|
884
1007
|
if (ci) args2.push("-i");
|
|
1008
|
+
if (fixed) args2.push("-F");
|
|
885
1009
|
if (glob2) args2.push("--glob", glob2);
|
|
1010
|
+
if (type) args2.push("-t", type);
|
|
1011
|
+
if (ctx) args2.push("-C", String(ctx));
|
|
1012
|
+
if (ml) args2.push("-U", "--multiline-dotall");
|
|
1013
|
+
if (filesOnly) args2.push("-l");
|
|
1014
|
+
if (cnt) args2.push("-c");
|
|
886
1015
|
args2.push("--", pattern, root);
|
|
887
1016
|
return execa2("rg", args2, { reject: false, timeout: 2e4 });
|
|
888
1017
|
};
|
|
889
1018
|
const tryGrep = async () => {
|
|
890
1019
|
const args2 = ["-R", "-n", "--color=never"];
|
|
891
1020
|
if (ci) args2.push("-i");
|
|
1021
|
+
if (fixed) args2.push("-F");
|
|
892
1022
|
if (glob2) args2.push("--include", glob2);
|
|
1023
|
+
if (ctx) args2.push("-C", String(ctx));
|
|
1024
|
+
if (filesOnly) args2.push("-l");
|
|
1025
|
+
if (cnt) args2.push("-c");
|
|
893
1026
|
args2.push("--", pattern, root);
|
|
894
1027
|
return execa2("grep", args2, { reject: false, timeout: 2e4 });
|
|
895
1028
|
};
|
|
1029
|
+
const missing = (err) => err?.code === "ENOENT" || err?.errno === "ENOENT";
|
|
896
1030
|
try {
|
|
897
1031
|
let res;
|
|
898
1032
|
try {
|
|
@@ -900,7 +1034,8 @@ var init_grep = __esm({
|
|
|
900
1034
|
if (res.exitCode === 127 || (res.stderr ?? "").includes("command not found")) {
|
|
901
1035
|
res = await tryGrep();
|
|
902
1036
|
}
|
|
903
|
-
} catch {
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
if (!missing(err)) throw err;
|
|
904
1039
|
res = await tryGrep();
|
|
905
1040
|
}
|
|
906
1041
|
const lines = (res.stdout ?? "").split("\n").slice(0, limit);
|
|
@@ -1113,8 +1248,15 @@ var init_context = __esm({
|
|
|
1113
1248
|
});
|
|
1114
1249
|
|
|
1115
1250
|
// src/prompt/system.ts
|
|
1116
|
-
function buildSystemPrompt(tools, cwd, project) {
|
|
1251
|
+
function buildSystemPrompt(tools, cwd, project, grammarMode = false) {
|
|
1117
1252
|
const toolLines = tools.map((t) => `- ${t.name}: ${t.description}`).join("\n");
|
|
1253
|
+
const actionProtocol = grammarMode ? `
|
|
1254
|
+
# Action protocol (strict)
|
|
1255
|
+
Every reply is exactly ONE JSON action object, nothing else \u2014 no prose outside it, no markdown, no fences. Decoding is grammar-constrained, so malformed output is impossible; your only job is to choose the right action.
|
|
1256
|
+
To use a tool: {"name": "<tool_name>", "arguments": { ...that tool's args }}
|
|
1257
|
+
To give your final answer to the user: {"name": "respond", "arguments": {"message": "<your full answer here>"}}
|
|
1258
|
+
Call tools until the GOAL is met, then emit a single "respond" action with the complete answer. The "respond" action is the ONLY way to end the turn and talk to the user \u2014 never put your final answer in a tool call.
|
|
1259
|
+
` : "";
|
|
1118
1260
|
const projectSection = project && project.content.trim() ? `
|
|
1119
1261
|
# ${CONTEXT_FILENAME} \u2014 project instructions (authoritative, read first)
|
|
1120
1262
|
The user maintains ${CONTEXT_FILENAME} at ${project.source} to steer how you work in this project: conventions, commands, architecture, do's and don'ts. Treat it as direct instruction from the user, higher priority than your defaults. When it conflicts with a default rule below, ${CONTEXT_FILENAME} wins (except permissions and safety, which you never override).${project.truncated ? `
|
|
@@ -1139,6 +1281,17 @@ If GAPS is non-empty, ask the minimum questions needed to fill them \u2014 one m
|
|
|
1139
1281
|
|
|
1140
1282
|
Re-read GOAL before every tool call. If a tool call does not move toward GOAL, skip it.
|
|
1141
1283
|
|
|
1284
|
+
# Plan summary before acting (conditional)
|
|
1285
|
+
Write a brief plan BEFORE the first tool call ONLY when the work is non-trivial:
|
|
1286
|
+
- Multi-step, OR touches multiple files, OR is destructive/hard to reverse, OR mixes investigation + change.
|
|
1287
|
+
SKIP the plan for trivial work \u2014 a single read, one small edit, a quick search, a direct question. Just act.
|
|
1288
|
+
When you do write one:
|
|
1289
|
+
- One or two plain-text sentences naming what you will do and in what order.
|
|
1290
|
+
- State the intent (the bug/feature/fix and the steps), not a tool-by-tool narration.
|
|
1291
|
+
- Keep it short \u2014 the user reads this to follow along, not a spec.
|
|
1292
|
+
Then begin immediately with the first tool call. Do not wait for approval unless GAPS was non-empty or the work is destructive.
|
|
1293
|
+
This summary is the ONE allowed preamble. It does not override the Tool calls rule below: after this plan, emit tool calls directly with no further narration between them.
|
|
1294
|
+
|
|
1142
1295
|
# Attention: re-attend to goal at each step
|
|
1143
1296
|
After each tool result, answer silently: "Does this result move me toward GOAL?"
|
|
1144
1297
|
YES \u2192 continue
|
|
@@ -1148,9 +1301,19 @@ This prevents drift. Each step attends to the original goal, not just the previo
|
|
|
1148
1301
|
|
|
1149
1302
|
# Output format
|
|
1150
1303
|
- Always reply in plain text. Never use Markdown syntax: no \`#\` headings, no \`**bold**\`, no \`-\` bullet lists, no fenced \`\`\` code blocks, no inline backticks.
|
|
1304
|
+
- This applies to your reasoning/thinking too. Write internal thoughts in plain text \u2014 no Markdown headings, bold, lists, or code fences there either.
|
|
1151
1305
|
- Quote code, paths, and identifiers inline as plain text. Do not wrap them.
|
|
1152
1306
|
- Keep prose terse.
|
|
1153
1307
|
|
|
1308
|
+
# Tone and voice
|
|
1309
|
+
- Sound like a calm, caring teammate who is genuinely invested in the user's goal. Warm, steady, reassuring \u2014 never cold or robotic.
|
|
1310
|
+
- Lead with empathy, especially when the user is stuck, frustrated, or facing a hard bug. Acknowledge the difficulty briefly before diving in: "That's a tricky one \u2014 let's work through it together."
|
|
1311
|
+
- Be encouraging about the goal. Treat it as something worth caring about, and convey quiet confidence that you'll reach it together.
|
|
1312
|
+
- Stay honest and direct. Empathy never means hedging, sugarcoating, or hiding bad news. Deliver hard truths kindly but plainly.
|
|
1313
|
+
- Keep warmth lightweight: a sentence or a few words, not gushing. One genuine, human touch beats a paragraph of pleasantries.
|
|
1314
|
+
- Mind the user's effort and context. If something will take a while or carry risk, say so gently and set expectations.
|
|
1315
|
+
- Celebrate progress in passing \u2014 a fixed bug, a passing test \u2014 without slowing the work down.
|
|
1316
|
+
|
|
1154
1317
|
# Engineering mindset
|
|
1155
1318
|
- Treat every request as one of: bug, feature, or fix. Name which one before you start.
|
|
1156
1319
|
- Apply first principles: decompose unclear tasks into smallest concrete sub-problems, solve each explicitly, compose the result.
|
|
@@ -1178,7 +1341,7 @@ Ask in a numbered list. One round of questions per turn. Then wait.
|
|
|
1178
1341
|
# Tools
|
|
1179
1342
|
You have access to the following tools. Call them via the function-calling interface.
|
|
1180
1343
|
${toolLines}
|
|
1181
|
-
|
|
1344
|
+
${actionProtocol}
|
|
1182
1345
|
# Loop semantics
|
|
1183
1346
|
- When you need to act on the filesystem or run a command, emit a tool call.
|
|
1184
1347
|
- After each tool result, decide: more tool calls, or a final plain-text answer.
|
|
@@ -1189,7 +1352,7 @@ ${toolLines}
|
|
|
1189
1352
|
- Prefer editing existing files over creating new ones.
|
|
1190
1353
|
- For edit_file, make old_str unique by including surrounding context, or set replace_all to change every occurrence.
|
|
1191
1354
|
- Never invent file paths. Read, glob, or grep before editing.
|
|
1192
|
-
- No filler,
|
|
1355
|
+
- No empty filler or robotic boilerplate. A brief, genuine warm touch (see Tone and voice) is welcome; hollow pleasantries and reflexive apologies are not.
|
|
1193
1356
|
|
|
1194
1357
|
# Context discipline
|
|
1195
1358
|
- read_file returns line numbers and accepts offset/limit. For large files, grep or glob to the relevant region first, then read only that range with offset/limit. Do not read a whole large file when you need a few functions \u2014 it wastes the context window.
|
|
@@ -1243,6 +1406,17 @@ function subjectFor(toolName, input) {
|
|
|
1243
1406
|
if (typeof obj.path === "string") return obj.path;
|
|
1244
1407
|
return "";
|
|
1245
1408
|
}
|
|
1409
|
+
function generalizeCommand(command) {
|
|
1410
|
+
const tokens = command.trim().split(/\s+/);
|
|
1411
|
+
if (tokens.length === 0 || tokens[0] === "") return command;
|
|
1412
|
+
const prog = tokens[0];
|
|
1413
|
+
const prefixLen = WRAPPER_PROGRAMS.has(prog) && tokens.length > 1 ? 2 : 1;
|
|
1414
|
+
const prefix = tokens.slice(0, prefixLen).join(" ");
|
|
1415
|
+
return `${prefix} *`;
|
|
1416
|
+
}
|
|
1417
|
+
function patternToPersist(toolName, subject) {
|
|
1418
|
+
return toolName === "run_bash" ? generalizeCommand(subject) : subject;
|
|
1419
|
+
}
|
|
1246
1420
|
function globToRegExp(glob2) {
|
|
1247
1421
|
const escaped = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1248
1422
|
const pattern = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
@@ -1263,15 +1437,28 @@ async function check(toolName, input, ctx) {
|
|
|
1263
1437
|
if (rules.some((r) => matches(r, toolName, subject))) return "allow";
|
|
1264
1438
|
const answer = await ctx.ask(toolName, input);
|
|
1265
1439
|
if (answer === "no") return "deny";
|
|
1266
|
-
if (answer === "always") addRule(toolName, subject);
|
|
1440
|
+
if (answer === "always") addRule(toolName, patternToPersist(toolName, subject));
|
|
1267
1441
|
return "allow";
|
|
1268
1442
|
}
|
|
1269
|
-
var RULES_DIR, RULES_PATH, ALWAYS_ALLOW;
|
|
1443
|
+
var RULES_DIR, RULES_PATH, WRAPPER_PROGRAMS, ALWAYS_ALLOW;
|
|
1270
1444
|
var init_policy = __esm({
|
|
1271
1445
|
"src/permissions/policy.ts"() {
|
|
1272
1446
|
"use strict";
|
|
1273
1447
|
RULES_DIR = join7(homedir5(), ".miii");
|
|
1274
1448
|
RULES_PATH = join7(RULES_DIR, "permissions.json");
|
|
1449
|
+
WRAPPER_PROGRAMS = /* @__PURE__ */ new Set([
|
|
1450
|
+
"npm",
|
|
1451
|
+
"npx",
|
|
1452
|
+
"pnpm",
|
|
1453
|
+
"yarn",
|
|
1454
|
+
"brew",
|
|
1455
|
+
"pip",
|
|
1456
|
+
"pip3",
|
|
1457
|
+
"cargo",
|
|
1458
|
+
"docker",
|
|
1459
|
+
"kubectl",
|
|
1460
|
+
"go"
|
|
1461
|
+
]);
|
|
1275
1462
|
ALWAYS_ALLOW = /* @__PURE__ */ new Set(["read_file", "grep", "glob"]);
|
|
1276
1463
|
}
|
|
1277
1464
|
});
|
|
@@ -1384,6 +1571,75 @@ function extractFirstJsonObject(s) {
|
|
|
1384
1571
|
}
|
|
1385
1572
|
return null;
|
|
1386
1573
|
}
|
|
1574
|
+
function parseGrammarAction(content, knownToolNames) {
|
|
1575
|
+
if (!content) return null;
|
|
1576
|
+
let raw = content.trim();
|
|
1577
|
+
if (!raw.startsWith("{")) {
|
|
1578
|
+
const found = extractFirstJsonObject(raw);
|
|
1579
|
+
if (!found) return null;
|
|
1580
|
+
raw = found.json;
|
|
1581
|
+
}
|
|
1582
|
+
let obj;
|
|
1583
|
+
try {
|
|
1584
|
+
obj = JSON.parse(raw);
|
|
1585
|
+
} catch {
|
|
1586
|
+
const found = extractFirstJsonObject(raw);
|
|
1587
|
+
if (!found) return null;
|
|
1588
|
+
try {
|
|
1589
|
+
obj = JSON.parse(found.json);
|
|
1590
|
+
} catch {
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
1595
|
+
const args2 = obj.arguments ?? {};
|
|
1596
|
+
if (!name) return null;
|
|
1597
|
+
if (name === "respond") {
|
|
1598
|
+
const message = typeof args2.message === "string" ? args2.message : "";
|
|
1599
|
+
return { kind: "respond", message };
|
|
1600
|
+
}
|
|
1601
|
+
if (!knownToolNames.includes(name)) return null;
|
|
1602
|
+
return { kind: "tool", name, arguments: args2 };
|
|
1603
|
+
}
|
|
1604
|
+
function streamRespondMessage(text) {
|
|
1605
|
+
if (!/"name"\s*:\s*"respond"/.test(text)) return null;
|
|
1606
|
+
const m = text.match(/"message"\s*:\s*"/);
|
|
1607
|
+
if (!m || m.index == null) return null;
|
|
1608
|
+
const start = m.index + m[0].length;
|
|
1609
|
+
const escapes = {
|
|
1610
|
+
n: "\n",
|
|
1611
|
+
t: " ",
|
|
1612
|
+
r: "\r",
|
|
1613
|
+
b: "\b",
|
|
1614
|
+
f: "\f",
|
|
1615
|
+
'"': '"',
|
|
1616
|
+
"\\": "\\",
|
|
1617
|
+
"/": "/"
|
|
1618
|
+
};
|
|
1619
|
+
let out = "";
|
|
1620
|
+
let i = start;
|
|
1621
|
+
while (i < text.length) {
|
|
1622
|
+
const ch = text[i];
|
|
1623
|
+
if (ch === '"') return { message: out, complete: true };
|
|
1624
|
+
if (ch === "\\") {
|
|
1625
|
+
const nx = text[i + 1];
|
|
1626
|
+
if (nx === void 0) break;
|
|
1627
|
+
if (nx === "u") {
|
|
1628
|
+
const hex = text.slice(i + 2, i + 6);
|
|
1629
|
+
if (hex.length < 4) break;
|
|
1630
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
1631
|
+
i += 6;
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
out += escapes[nx] ?? nx;
|
|
1635
|
+
i += 2;
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
out += ch;
|
|
1639
|
+
i++;
|
|
1640
|
+
}
|
|
1641
|
+
return { message: out, complete: false };
|
|
1642
|
+
}
|
|
1387
1643
|
function blocksFromOllama(text, tool_calls, knownToolNames = []) {
|
|
1388
1644
|
const blocks = [];
|
|
1389
1645
|
let finalText = text;
|
|
@@ -1441,8 +1697,15 @@ function markSeen(name, input, seen) {
|
|
|
1441
1697
|
async function* runAgent(opts) {
|
|
1442
1698
|
const { model, cwd, permissions, hooks, signal, num_ctx } = opts;
|
|
1443
1699
|
const startTime = Date.now();
|
|
1444
|
-
|
|
1700
|
+
let useGrammar = false;
|
|
1701
|
+
if (providerName() === "ollama") {
|
|
1702
|
+
const params = await modelParamCountB(model);
|
|
1703
|
+
useGrammar = params == null || params <= GRAMMAR_MAX_PARAMS_B;
|
|
1704
|
+
}
|
|
1705
|
+
const system = buildSystemPrompt(TOOLS, cwd, loadProjectContext(cwd), useGrammar);
|
|
1706
|
+
const grammar = useGrammar ? buildToolGrammar(TOOLS) : void 0;
|
|
1445
1707
|
const ollamaTools = toOllamaTools(TOOLS);
|
|
1708
|
+
const toolNames = TOOLS.map((t) => t.name);
|
|
1446
1709
|
const effort = EFFORT_OPTIONS[loadConfig().effort ?? "medium"];
|
|
1447
1710
|
const history = [
|
|
1448
1711
|
...opts.history,
|
|
@@ -1456,6 +1719,8 @@ async function* runAgent(opts) {
|
|
|
1456
1719
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
1457
1720
|
let text = "";
|
|
1458
1721
|
let tool_calls;
|
|
1722
|
+
let respondEmitted = 0;
|
|
1723
|
+
let streamedRespond = false;
|
|
1459
1724
|
let lastTail = "";
|
|
1460
1725
|
let tailRepeats = 0;
|
|
1461
1726
|
let streamLooped = false;
|
|
@@ -1463,11 +1728,22 @@ async function* runAgent(opts) {
|
|
|
1463
1728
|
const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
|
|
1464
1729
|
if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
|
|
1465
1730
|
try {
|
|
1466
|
-
for await (const chunk of chat3(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx, num_predict: effort.num_predict, temperature: effort.temperature })) {
|
|
1731
|
+
for await (const chunk of chat3(model, toOllamaMessages(history, system), useGrammar ? void 0 : ollamaTools, { signal: composedSignal, num_ctx, num_predict: effort.num_predict, temperature: effort.temperature, format: grammar })) {
|
|
1467
1732
|
if (signal?.aborted) break;
|
|
1468
1733
|
if (chunk.content) {
|
|
1469
1734
|
text += chunk.content;
|
|
1470
|
-
|
|
1735
|
+
if (!useGrammar) {
|
|
1736
|
+
yield { type: "text-delta", text: chunk.content };
|
|
1737
|
+
} else {
|
|
1738
|
+
const r = streamRespondMessage(text);
|
|
1739
|
+
if (r) {
|
|
1740
|
+
streamedRespond = true;
|
|
1741
|
+
if (r.message.length > respondEmitted) {
|
|
1742
|
+
yield { type: "text-delta", text: r.message.slice(respondEmitted) };
|
|
1743
|
+
respondEmitted = r.message.length;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1471
1747
|
if (text.length >= REPEAT_TAIL) {
|
|
1472
1748
|
const tail = text.slice(-REPEAT_TAIL);
|
|
1473
1749
|
if (tail === lastTail) {
|
|
@@ -1515,7 +1791,19 @@ async function* runAgent(opts) {
|
|
|
1515
1791
|
};
|
|
1516
1792
|
return history;
|
|
1517
1793
|
}
|
|
1518
|
-
|
|
1794
|
+
let blocks;
|
|
1795
|
+
if (useGrammar) {
|
|
1796
|
+
const action = parseGrammarAction(text, toolNames);
|
|
1797
|
+
if (action?.kind === "tool") {
|
|
1798
|
+
blocks = [{ type: "tool_use", id: mintToolUseId(), name: action.name, input: action.arguments }];
|
|
1799
|
+
} else {
|
|
1800
|
+
const message = action?.kind === "respond" ? action.message : text.trim();
|
|
1801
|
+
if (message && !streamedRespond) yield { type: "text-delta", text: message };
|
|
1802
|
+
blocks = message ? [{ type: "text", text: message }] : [];
|
|
1803
|
+
}
|
|
1804
|
+
} else {
|
|
1805
|
+
blocks = blocksFromOllama(text, tool_calls, toolNames);
|
|
1806
|
+
}
|
|
1519
1807
|
const tool_uses = blocks.filter((b) => b.type === "tool_use");
|
|
1520
1808
|
history.push({ role: "assistant", content: blocks });
|
|
1521
1809
|
if (tool_uses.length === 0) {
|
|
@@ -1624,11 +1912,12 @@ async function* runAgent(opts) {
|
|
|
1624
1912
|
yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
|
|
1625
1913
|
return history;
|
|
1626
1914
|
}
|
|
1627
|
-
var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL;
|
|
1915
|
+
var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL, GRAMMAR_MAX_PARAMS_B;
|
|
1628
1916
|
var init_loop = __esm({
|
|
1629
1917
|
"src/agent/loop.ts"() {
|
|
1630
1918
|
"use strict";
|
|
1631
1919
|
init_client();
|
|
1920
|
+
init_grammar();
|
|
1632
1921
|
init_paths();
|
|
1633
1922
|
init_registry();
|
|
1634
1923
|
init_validate();
|
|
@@ -1640,6 +1929,7 @@ var init_loop = __esm({
|
|
|
1640
1929
|
MAX_TURNS = 25;
|
|
1641
1930
|
REPEAT_TAIL = 120;
|
|
1642
1931
|
REPEAT_KILL = 4;
|
|
1932
|
+
GRAMMAR_MAX_PARAMS_B = 14;
|
|
1643
1933
|
}
|
|
1644
1934
|
});
|
|
1645
1935
|
|
|
@@ -2615,10 +2905,11 @@ function ToolResultBlock({ result, toolName }) {
|
|
|
2615
2905
|
const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
|
|
2616
2906
|
const shown = visible.map((l) => truncate2(l, MAX_LINE_WIDTH));
|
|
2617
2907
|
const extra = lines.length - shown.length;
|
|
2908
|
+
const header = toolName === "grep" || toolName === "glob" ? summarizeResult(result, toolName) : `${lines.length} line${lines.length === 1 ? "" : "s"}`;
|
|
2618
2909
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
2619
2910
|
/* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
|
|
2620
2911
|
"\u23BF ",
|
|
2621
|
-
|
|
2912
|
+
header
|
|
2622
2913
|
] }),
|
|
2623
2914
|
shown.map((ln, i) => /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(Text9, { color: result.is_error ? "red" : void 0, dimColor: true, children: ln || " " }) }, i)),
|
|
2624
2915
|
extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
@@ -2809,7 +3100,7 @@ function useAgentRunner(model, activeCtx) {
|
|
|
2809
3100
|
if (busyRef.current || !model) return;
|
|
2810
3101
|
busyRef.current = true;
|
|
2811
3102
|
setBusy(true);
|
|
2812
|
-
setProcessingLabel("
|
|
3103
|
+
setProcessingLabel("crunching\u2026");
|
|
2813
3104
|
setError(null);
|
|
2814
3105
|
setMessages((prev) => [...prev, { role: "user", content: text }]);
|
|
2815
3106
|
setThinking(true);
|
|
@@ -2891,7 +3182,7 @@ function useAgentRunner(model, activeCtx) {
|
|
|
2891
3182
|
case "thinking-delta": {
|
|
2892
3183
|
thinkingAcc += ev.text;
|
|
2893
3184
|
setThinking(true);
|
|
2894
|
-
setProcessingLabel("
|
|
3185
|
+
setProcessingLabel("crunching\u2026");
|
|
2895
3186
|
flushThink();
|
|
2896
3187
|
break;
|
|
2897
3188
|
}
|
|
@@ -2908,7 +3199,7 @@ function useAgentRunner(model, activeCtx) {
|
|
|
2908
3199
|
is_error: ev.block.is_error
|
|
2909
3200
|
});
|
|
2910
3201
|
setActiveToolResults([...turnResults]);
|
|
2911
|
-
setProcessingLabel("
|
|
3202
|
+
setProcessingLabel("crunching\u2026");
|
|
2912
3203
|
break;
|
|
2913
3204
|
}
|
|
2914
3205
|
case "turn-end": {
|