hostwares-cli 2.4.3 → 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 +29 -146
- package/package.json +1 -1
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,113 +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 \u2014 re-reading it will keep returning the same thing. This call was NOT executed. STOP re-inspecting and DECIDE from what you already know: if a file the task needs does not exist yet, WRITE it now (write_file) \u2014 an empty or missing directory means the file has not been created, not that you should look again. If every file the task needs already exists and has been run/verified, give your final answer now (what you built + how to run it) and end. Do NOT read or list 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
|
-
const wroteThisHop = toRun.some((c2) => WRITE_TOOLS.has(c2.name));
|
|
2002
|
-
if (wroteThisHop) {
|
|
2003
|
-
stalledHops = 0;
|
|
2004
|
-
}
|
|
2005
|
-
const filesWritten = writeCounts.size;
|
|
2006
|
-
const guardHitCeiling = 6 + filesWritten * 3;
|
|
2007
|
-
const stallCeiling = filesWritten > 0 ? 4 : 2;
|
|
2008
|
-
if (stalledHops >= stallCeiling || totalGuardHits >= guardHitCeiling) {
|
|
2009
|
-
const wroteSomething = filesWritten > 0;
|
|
2010
|
-
emit({ type: "error", message: wroteSomething ? `Stopping: I kept re-reading the same files without making new progress. What I built so far is saved. Tell me the specific next change (e.g. "write the UI file" / "start it") and I'll do it directly.` : `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.` });
|
|
2011
|
-
return finish({ conversationId, turnCredits, balance, text, aborted: false });
|
|
2012
|
-
}
|
|
2013
|
-
for (const c2 of toRun) {
|
|
2014
|
-
if (!MUTATION_TOOLS.has(c2.name))
|
|
2015
|
-
continue;
|
|
2016
|
-
const p = typeof c2.input?.path === "string" ? c2.input.path : void 0;
|
|
2017
|
-
if (!p)
|
|
2018
|
-
continue;
|
|
2019
|
-
if (WRITE_TOOLS.has(c2.name))
|
|
2020
|
-
writeCounts.set(p, (writeCounts.get(p) ?? 0) + 1);
|
|
2021
|
-
for (const key of [...callCounts.keys()]) {
|
|
2022
|
-
if (key.includes(JSON.stringify(p)))
|
|
2023
|
-
callCounts.delete(key);
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
let results = shortCircuited;
|
|
2027
|
-
if (toRun.length) {
|
|
2028
|
-
const ran = await executePending(toRun, opts, emit, checkpoint);
|
|
2029
|
-
if (ran === null) {
|
|
2030
|
-
return finish({ conversationId, turnCredits, balance, text, aborted: true });
|
|
2031
|
-
}
|
|
2032
|
-
for (const call of toRun) {
|
|
2033
|
-
if (!WRITE_TOOLS.has(call.name))
|
|
2034
|
-
continue;
|
|
2035
|
-
const res = ran.find((r) => r.id === call.id);
|
|
2036
|
-
if (!res || /^Error:/i.test(res.result))
|
|
2037
|
-
continue;
|
|
2038
|
-
const path = String(call.input?.path ?? "");
|
|
2039
|
-
if (!path)
|
|
2040
|
-
continue;
|
|
2041
|
-
let hookResults;
|
|
2042
|
-
try {
|
|
2043
|
-
hookResults = await runPostWriteHooks(host.cwd ?? process.cwd(), path);
|
|
2044
|
-
} catch {
|
|
2045
|
-
hookResults = [];
|
|
2046
|
-
}
|
|
2047
|
-
for (const h of hookResults) {
|
|
2048
|
-
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` });
|
|
2049
|
-
res.result += h.ok ? `
|
|
2050
|
-
|
|
2051
|
-
[${h.name}] passed on ${path}.` : `
|
|
2052
|
-
|
|
2053
|
-
[${h.name}] FAILED after this write \u2014 fix before continuing:
|
|
2054
|
-
${h.output}`;
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
results = [...shortCircuited, ...ran];
|
|
2058
|
-
}
|
|
2059
|
-
toolResults = results;
|
|
1954
|
+
toolResults = ran;
|
|
2060
1955
|
if (hop === MAX_HOPS - 1) {
|
|
2061
|
-
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.` });
|
|
2062
1957
|
}
|
|
2063
1958
|
}
|
|
2064
1959
|
emit({
|
|
@@ -2097,21 +1992,6 @@ function finish(r) {
|
|
|
2097
1992
|
error: r.error
|
|
2098
1993
|
};
|
|
2099
1994
|
}
|
|
2100
|
-
function isReadOnlyTool(name) {
|
|
2101
|
-
return READ_ONLY_TOOLS.has(name);
|
|
2102
|
-
}
|
|
2103
|
-
function stableStringify(v) {
|
|
2104
|
-
if (v === null || typeof v !== "object")
|
|
2105
|
-
return JSON.stringify(v);
|
|
2106
|
-
if (Array.isArray(v))
|
|
2107
|
-
return `[${v.map(stableStringify).join(",")}]`;
|
|
2108
|
-
const o = v;
|
|
2109
|
-
return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(o[k])}`).join(",")}}`;
|
|
2110
|
-
}
|
|
2111
|
-
function compactArgs(input) {
|
|
2112
|
-
const s = Object.entries(input).map(([k, val]) => `${k}: ${JSON.stringify(val)}`).join(", ");
|
|
2113
|
-
return s.length > 120 ? `${s.slice(0, 117)}...` : s;
|
|
2114
|
-
}
|
|
2115
1995
|
async function executePending(calls, opts, emit, checkpoint) {
|
|
2116
1996
|
const results = [];
|
|
2117
1997
|
for (const call of calls) {
|
|
@@ -2199,7 +2079,25 @@ async function executePending(calls, opts, emit, checkpoint) {
|
|
|
2199
2079
|
}
|
|
2200
2080
|
const bytes = effect.kind === "wrote" || effect.kind === "modified" ? byteLength(call.input?.content) : void 0;
|
|
2201
2081
|
emit({ type: "tool:end", id: call.id, tool: call.name, ok: !failed2, durationMs: Date.now() - started, effect, bytes });
|
|
2202
|
-
|
|
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) });
|
|
2203
2101
|
}
|
|
2204
2102
|
return results;
|
|
2205
2103
|
}
|
|
@@ -2247,7 +2145,7 @@ function targetOf(call) {
|
|
|
2247
2145
|
return void 0;
|
|
2248
2146
|
return s.length > 60 ? `${s.slice(0, 57)}\u2026` : s;
|
|
2249
2147
|
}
|
|
2250
|
-
var MAX_HOPS,
|
|
2148
|
+
var MAX_HOPS, MAX_RESULT_CHARS, MUTATION_TOOLS, WRITE_TOOLS;
|
|
2251
2149
|
var init_loop = __esm({
|
|
2252
2150
|
"node_modules/@hostwares/agent-client/dist/esm/agent/loop.js"() {
|
|
2253
2151
|
init_client();
|
|
@@ -2259,28 +2157,13 @@ var init_loop = __esm({
|
|
|
2259
2157
|
init_subagents();
|
|
2260
2158
|
init_safety();
|
|
2261
2159
|
init_events();
|
|
2262
|
-
MAX_HOPS =
|
|
2263
|
-
MAX_IDENTICAL_CALLS = 2;
|
|
2264
|
-
MAX_WRITES_PER_FILE = 2;
|
|
2160
|
+
MAX_HOPS = 80;
|
|
2265
2161
|
MAX_RESULT_CHARS = 3e4;
|
|
2266
|
-
READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
2267
|
-
"read_file",
|
|
2268
|
-
"list_directory",
|
|
2269
|
-
"search_files",
|
|
2270
|
-
"file_search",
|
|
2271
|
-
"get_system_info",
|
|
2272
|
-
"list_processes",
|
|
2273
|
-
"get_process_output",
|
|
2274
|
-
"check_port",
|
|
2275
|
-
"git_status",
|
|
2276
|
-
"git_log",
|
|
2277
|
-
"git_diff",
|
|
2278
|
-
"docker_ps"
|
|
2279
|
-
]);
|
|
2280
2162
|
MUTATION_TOOLS = /* @__PURE__ */ new Set([
|
|
2281
2163
|
"write_file",
|
|
2282
2164
|
"str_replace_file",
|
|
2283
2165
|
"append_file",
|
|
2166
|
+
"edit_file",
|
|
2284
2167
|
"delete_path",
|
|
2285
2168
|
"git_commit",
|
|
2286
2169
|
"git_add",
|
|
@@ -2290,7 +2173,7 @@ var init_loop = __esm({
|
|
|
2290
2173
|
"stop_process",
|
|
2291
2174
|
"kill_process"
|
|
2292
2175
|
]);
|
|
2293
|
-
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"]);
|
|
2294
2177
|
}
|
|
2295
2178
|
});
|
|
2296
2179
|
|