hostwares-cli 2.4.2 → 2.4.4
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/index.js +32 -139
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1877,12 +1877,6 @@ async function runTurn(opts) {
|
|
|
1877
1877
|
let assistantContent = null;
|
|
1878
1878
|
let toolResults = [];
|
|
1879
1879
|
let firstHop = true;
|
|
1880
|
-
const callCounts = /* @__PURE__ */ new Map();
|
|
1881
|
-
const writeCounts = /* @__PURE__ */ new Map();
|
|
1882
|
-
let stalledHops = 0;
|
|
1883
|
-
let totalGuardHits = 0;
|
|
1884
|
-
let emptyHops = 0;
|
|
1885
|
-
const MAX_EMPTY_RETRIES = 1;
|
|
1886
1880
|
const steering = buildSteeringContext(host.cwd ?? process.cwd(), opts.message);
|
|
1887
1881
|
const firstMessage = steering ? `${steering}
|
|
1888
1882
|
|
|
@@ -1897,7 +1891,6 @@ ${opts.message}` : opts.message;
|
|
|
1897
1891
|
throw new AbortedError();
|
|
1898
1892
|
emit({ type: "thinking" });
|
|
1899
1893
|
const pending = [];
|
|
1900
|
-
const textBefore = text.length;
|
|
1901
1894
|
const done = await streamChat(credentials, {
|
|
1902
1895
|
...firstHop ? { message: firstMessage, images: opts.images } : { toolResults, assistantContent },
|
|
1903
1896
|
conversationId,
|
|
@@ -1952,106 +1945,15 @@ ${opts.message}` : opts.message;
|
|
|
1952
1945
|
}
|
|
1953
1946
|
}
|
|
1954
1947
|
firstHop = false;
|
|
1955
|
-
if (!pending.length)
|
|
1956
|
-
const producedText = text.length > textBefore;
|
|
1957
|
-
if (!producedText && hop > 0 && emptyHops < MAX_EMPTY_RETRIES) {
|
|
1958
|
-
emptyHops++;
|
|
1959
|
-
toolResults = [{
|
|
1960
|
-
id: `empty-${hop}`,
|
|
1961
|
-
result: `[continue] Your last turn was empty \u2014 no message and no tool call. Do NOT stop here. Look at what has been done so far in this conversation and take the NEXT concrete step toward the goal: create the next file, run the next command, or if the task is genuinely complete, give a short final answer stating what you built and how to run it. Act now.`
|
|
1962
|
-
}];
|
|
1963
|
-
assistantContent = assistantContent ?? "";
|
|
1964
|
-
continue;
|
|
1965
|
-
}
|
|
1966
|
-
if (!producedText && emptyHops >= MAX_EMPTY_RETRIES) {
|
|
1967
|
-
const wroteSomething = writeCounts.size > 0;
|
|
1968
|
-
emit({ type: "error", message: wroteSomething ? `Stopping: the last steps returned nothing new. What I changed is saved \u2014 tell me the specific next change and I'll make it directly.` : `Stopping: I couldn't make progress on that. Tell me the specific step you want (e.g. "create the server file") and I'll do it directly.` });
|
|
1969
|
-
}
|
|
1948
|
+
if (!pending.length)
|
|
1970
1949
|
break;
|
|
1950
|
+
const ran = await executePending(pending, opts, emit, checkpoint);
|
|
1951
|
+
if (ran === null) {
|
|
1952
|
+
return finish({ conversationId, turnCredits, balance, text, aborted: true });
|
|
1971
1953
|
}
|
|
1972
|
-
|
|
1973
|
-
const toRun = [];
|
|
1974
|
-
const shortCircuited = [];
|
|
1975
|
-
for (const call of pending) {
|
|
1976
|
-
const fp = `${call.name}:${stableStringify(call.input)}`;
|
|
1977
|
-
const seen = (callCounts.get(fp) ?? 0) + 1;
|
|
1978
|
-
callCounts.set(fp, seen);
|
|
1979
|
-
const path = typeof call.input?.path === "string" ? call.input.path : void 0;
|
|
1980
|
-
if (seen > MAX_IDENTICAL_CALLS && isReadOnlyTool(call.name)) {
|
|
1981
|
-
shortCircuited.push({
|
|
1982
|
-
id: call.id,
|
|
1983
|
-
result: `[loop guard] You already ran ${call.name}(${compactArgs(call.input)}) and nothing has changed since. This call was NOT executed. You are looping. If you have already written and verified the work this turn, STOP NOW and give your final answer \u2014 say what you changed, quote the check that passed, and end. Only continue calling tools if there is a concrete NEW change left to make; if so, make it directly instead of reading again.`
|
|
1984
|
-
});
|
|
1985
|
-
emit({ type: "tool:end", id: call.id, tool: call.name, ok: false, durationMs: 0, summary: "loop guard: repeated read skipped" });
|
|
1986
|
-
} else if (WRITE_TOOLS.has(call.name) && path && (writeCounts.get(path) ?? 0) >= MAX_WRITES_PER_FILE) {
|
|
1987
|
-
shortCircuited.push({
|
|
1988
|
-
id: call.id,
|
|
1989
|
-
result: `[loop guard] You have already written ${path} ${writeCounts.get(path)} times this turn \u2014 its content is on disk. This write was NOT executed. Stop rewriting it: verify it (run it / typecheck it) or give your final answer. Do not write this file again unless you are making a specific, different change.`
|
|
1990
|
-
});
|
|
1991
|
-
emit({ type: "tool:end", id: call.id, tool: call.name, ok: false, durationMs: 0, summary: "loop guard: write thrash skipped" });
|
|
1992
|
-
} else {
|
|
1993
|
-
toRun.push(call);
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
totalGuardHits += shortCircuited.length;
|
|
1997
|
-
if (toRun.length === 0 && shortCircuited.length > 0)
|
|
1998
|
-
stalledHops++;
|
|
1999
|
-
else
|
|
2000
|
-
stalledHops = 0;
|
|
2001
|
-
if (stalledHops >= 2 || totalGuardHits >= 6) {
|
|
2002
|
-
const wroteSomething = writeCounts.size > 0;
|
|
2003
|
-
emit({ type: "error", message: wroteSomething ? `Stopping: I kept re-reading or rewriting the same files without making new progress. What I changed is saved; tell me specifically what to adjust next.` : `Stopping: I kept re-reading the same files without writing anything. I have enough context now \u2014 tell me the specific change you want and I'll make it directly.` });
|
|
2004
|
-
return finish({ conversationId, turnCredits, balance, text, aborted: false });
|
|
2005
|
-
}
|
|
2006
|
-
for (const c2 of toRun) {
|
|
2007
|
-
if (!MUTATION_TOOLS.has(c2.name))
|
|
2008
|
-
continue;
|
|
2009
|
-
const p = typeof c2.input?.path === "string" ? c2.input.path : void 0;
|
|
2010
|
-
if (!p)
|
|
2011
|
-
continue;
|
|
2012
|
-
if (WRITE_TOOLS.has(c2.name))
|
|
2013
|
-
writeCounts.set(p, (writeCounts.get(p) ?? 0) + 1);
|
|
2014
|
-
for (const key of [...callCounts.keys()]) {
|
|
2015
|
-
if (key.includes(JSON.stringify(p)))
|
|
2016
|
-
callCounts.delete(key);
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
let results = shortCircuited;
|
|
2020
|
-
if (toRun.length) {
|
|
2021
|
-
const ran = await executePending(toRun, opts, emit, checkpoint);
|
|
2022
|
-
if (ran === null) {
|
|
2023
|
-
return finish({ conversationId, turnCredits, balance, text, aborted: true });
|
|
2024
|
-
}
|
|
2025
|
-
for (const call of toRun) {
|
|
2026
|
-
if (!WRITE_TOOLS.has(call.name))
|
|
2027
|
-
continue;
|
|
2028
|
-
const res = ran.find((r) => r.id === call.id);
|
|
2029
|
-
if (!res || /^Error:/i.test(res.result))
|
|
2030
|
-
continue;
|
|
2031
|
-
const path = String(call.input?.path ?? "");
|
|
2032
|
-
if (!path)
|
|
2033
|
-
continue;
|
|
2034
|
-
let hookResults;
|
|
2035
|
-
try {
|
|
2036
|
-
hookResults = await runPostWriteHooks(host.cwd ?? process.cwd(), path);
|
|
2037
|
-
} catch {
|
|
2038
|
-
hookResults = [];
|
|
2039
|
-
}
|
|
2040
|
-
for (const h of hookResults) {
|
|
2041
|
-
emit({ type: "tool:end", id: `${call.id}:${h.name}`, tool: h.name, ok: h.ok, durationMs: 0, summary: h.ok ? `${h.name} passed` : `${h.name} failed` });
|
|
2042
|
-
res.result += h.ok ? `
|
|
2043
|
-
|
|
2044
|
-
[${h.name}] passed on ${path}.` : `
|
|
2045
|
-
|
|
2046
|
-
[${h.name}] FAILED after this write \u2014 fix before continuing:
|
|
2047
|
-
${h.output}`;
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
results = [...shortCircuited, ...ran];
|
|
2051
|
-
}
|
|
2052
|
-
toolResults = results;
|
|
1954
|
+
toolResults = ran;
|
|
2053
1955
|
if (hop === MAX_HOPS - 1) {
|
|
2054
|
-
emit({ type: "error", message: `Paused after ${MAX_HOPS} steps to check in
|
|
1956
|
+
emit({ type: "error", message: `Paused after ${MAX_HOPS} steps to check in \u2014 this is a long task, not necessarily a finished one. Say "continue" and I'll pick up exactly where I left off.` });
|
|
2055
1957
|
}
|
|
2056
1958
|
}
|
|
2057
1959
|
emit({
|
|
@@ -2090,21 +1992,6 @@ function finish(r) {
|
|
|
2090
1992
|
error: r.error
|
|
2091
1993
|
};
|
|
2092
1994
|
}
|
|
2093
|
-
function isReadOnlyTool(name) {
|
|
2094
|
-
return READ_ONLY_TOOLS.has(name);
|
|
2095
|
-
}
|
|
2096
|
-
function stableStringify(v) {
|
|
2097
|
-
if (v === null || typeof v !== "object")
|
|
2098
|
-
return JSON.stringify(v);
|
|
2099
|
-
if (Array.isArray(v))
|
|
2100
|
-
return `[${v.map(stableStringify).join(",")}]`;
|
|
2101
|
-
const o = v;
|
|
2102
|
-
return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(o[k])}`).join(",")}}`;
|
|
2103
|
-
}
|
|
2104
|
-
function compactArgs(input) {
|
|
2105
|
-
const s = Object.entries(input).map(([k, val]) => `${k}: ${JSON.stringify(val)}`).join(", ");
|
|
2106
|
-
return s.length > 120 ? `${s.slice(0, 117)}...` : s;
|
|
2107
|
-
}
|
|
2108
1995
|
async function executePending(calls, opts, emit, checkpoint) {
|
|
2109
1996
|
const results = [];
|
|
2110
1997
|
for (const call of calls) {
|
|
@@ -2192,7 +2079,25 @@ async function executePending(calls, opts, emit, checkpoint) {
|
|
|
2192
2079
|
}
|
|
2193
2080
|
const bytes = effect.kind === "wrote" || effect.kind === "modified" ? byteLength(call.input?.content) : void 0;
|
|
2194
2081
|
emit({ type: "tool:end", id: call.id, tool: call.name, ok: !failed2, durationMs: Date.now() - started, effect, bytes });
|
|
2195
|
-
|
|
2082
|
+
let resultText = output;
|
|
2083
|
+
if (!failed2 && WRITE_TOOLS.has(call.name) && typeof call.input?.path === "string" && call.input.path) {
|
|
2084
|
+
let hookResults;
|
|
2085
|
+
try {
|
|
2086
|
+
hookResults = await runPostWriteHooks(opts.host.cwd ?? process.cwd(), String(call.input.path));
|
|
2087
|
+
} catch {
|
|
2088
|
+
hookResults = [];
|
|
2089
|
+
}
|
|
2090
|
+
for (const h of hookResults) {
|
|
2091
|
+
emit({ type: "tool:end", id: `${call.id}:${h.name}`, tool: h.name, ok: h.ok, durationMs: 0, summary: h.ok ? `${h.name} passed` : `${h.name} failed` });
|
|
2092
|
+
resultText += h.ok ? `
|
|
2093
|
+
|
|
2094
|
+
[${h.name}] passed on ${call.input.path}.` : `
|
|
2095
|
+
|
|
2096
|
+
[${h.name}] FAILED after this write \u2014 fix before continuing:
|
|
2097
|
+
${h.output}`;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
results.push({ id: call.id, result: truncate(resultText) });
|
|
2196
2101
|
}
|
|
2197
2102
|
return results;
|
|
2198
2103
|
}
|
|
@@ -2240,7 +2145,7 @@ function targetOf(call) {
|
|
|
2240
2145
|
return void 0;
|
|
2241
2146
|
return s.length > 60 ? `${s.slice(0, 57)}\u2026` : s;
|
|
2242
2147
|
}
|
|
2243
|
-
var MAX_HOPS,
|
|
2148
|
+
var MAX_HOPS, MAX_RESULT_CHARS, MUTATION_TOOLS, WRITE_TOOLS;
|
|
2244
2149
|
var init_loop = __esm({
|
|
2245
2150
|
"node_modules/@hostwares/agent-client/dist/esm/agent/loop.js"() {
|
|
2246
2151
|
init_client();
|
|
@@ -2252,28 +2157,13 @@ var init_loop = __esm({
|
|
|
2252
2157
|
init_subagents();
|
|
2253
2158
|
init_safety();
|
|
2254
2159
|
init_events();
|
|
2255
|
-
MAX_HOPS =
|
|
2256
|
-
MAX_IDENTICAL_CALLS = 2;
|
|
2257
|
-
MAX_WRITES_PER_FILE = 2;
|
|
2160
|
+
MAX_HOPS = 80;
|
|
2258
2161
|
MAX_RESULT_CHARS = 3e4;
|
|
2259
|
-
READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
2260
|
-
"read_file",
|
|
2261
|
-
"list_directory",
|
|
2262
|
-
"search_files",
|
|
2263
|
-
"file_search",
|
|
2264
|
-
"get_system_info",
|
|
2265
|
-
"list_processes",
|
|
2266
|
-
"get_process_output",
|
|
2267
|
-
"check_port",
|
|
2268
|
-
"git_status",
|
|
2269
|
-
"git_log",
|
|
2270
|
-
"git_diff",
|
|
2271
|
-
"docker_ps"
|
|
2272
|
-
]);
|
|
2273
2162
|
MUTATION_TOOLS = /* @__PURE__ */ new Set([
|
|
2274
2163
|
"write_file",
|
|
2275
2164
|
"str_replace_file",
|
|
2276
2165
|
"append_file",
|
|
2166
|
+
"edit_file",
|
|
2277
2167
|
"delete_path",
|
|
2278
2168
|
"git_commit",
|
|
2279
2169
|
"git_add",
|
|
@@ -2283,7 +2173,7 @@ var init_loop = __esm({
|
|
|
2283
2173
|
"stop_process",
|
|
2284
2174
|
"kill_process"
|
|
2285
2175
|
]);
|
|
2286
|
-
WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "str_replace_file", "append_file"]);
|
|
2176
|
+
WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "str_replace_file", "append_file", "edit_file"]);
|
|
2287
2177
|
}
|
|
2288
2178
|
});
|
|
2289
2179
|
|
|
@@ -3321,6 +3211,9 @@ async function runCliTurn(opts) {
|
|
|
3321
3211
|
spinner.stop();
|
|
3322
3212
|
error(e.message);
|
|
3323
3213
|
break;
|
|
3214
|
+
case "tool:pending":
|
|
3215
|
+
spinner.stop();
|
|
3216
|
+
break;
|
|
3324
3217
|
}
|
|
3325
3218
|
}
|
|
3326
3219
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hostwares-cli",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.4",
|
|
4
4
|
"description": "Hostwares CLI - AI DevOps in your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"url": "https://github.com/Hostwares/hostwares-cli/issues"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
+
"@hostwares/agent-client": "github:Hostwares/hostwares-agent-client#v1",
|
|
44
45
|
"@types/node": "^20.0.0",
|
|
45
|
-
"typescript": "^5.5.0",
|
|
46
46
|
"esbuild": "^0.28.2",
|
|
47
|
-
"
|
|
47
|
+
"typescript": "^5.5.0"
|
|
48
48
|
}
|
|
49
49
|
}
|