@nexrall/code-core 1.4.27 → 1.4.28
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/agent/loop.d.ts +23 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +105 -33
- package/package.json +1 -1
package/dist/agent/loop.d.ts
CHANGED
|
@@ -19,6 +19,29 @@ export declare const _stallLimits: {
|
|
|
19
19
|
STALL_LIMIT: number;
|
|
20
20
|
REPEAT_STALL_LIMIT: number;
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Why an agent run stopped.
|
|
24
|
+
*
|
|
25
|
+
* `'clean'` and `'aborted'` are the two silent-by-design outcomes: the model gave a
|
|
26
|
+
* final answer, or the user pressed Ctrl+C and already knows why it stopped.
|
|
27
|
+
* EVERYTHING else owes the user an explanation, which is what stopReasonNotice covers.
|
|
28
|
+
*/
|
|
29
|
+
export type StopReason = 'clean' | 'aborted' | 'reported-elsewhere' | 'empty-response' | 'no-balance' | 'stalled' | 'stalled-repeat' | 'budget' | 'unknown';
|
|
30
|
+
/**
|
|
31
|
+
* The message shown when a run ends for any reason other than a clean finish.
|
|
32
|
+
*
|
|
33
|
+
* Pure and exported so every branch is testable: reaching some of these for real needs
|
|
34
|
+
* an empty wallet, a dead upstream, or hundreds of iterations. Returns null only for
|
|
35
|
+
* the two outcomes that are deliberately silent.
|
|
36
|
+
*
|
|
37
|
+
* `'unknown'` deliberately produces a message rather than nothing. If a future `break`
|
|
38
|
+
* forgets to set a reason, the symptom should be a visible "ended unexpectedly" line —
|
|
39
|
+
* annoying and reportable — not the silent stop that made this refactor necessary.
|
|
40
|
+
*/
|
|
41
|
+
export declare function stopReasonNotice(reason: StopReason, ctx?: {
|
|
42
|
+
budget?: number;
|
|
43
|
+
repeatError?: string | null;
|
|
44
|
+
}): string | null;
|
|
22
45
|
export declare function resolveMaxIterations(optionValue: number | undefined, settingsRaw: Record<string, unknown>): number;
|
|
23
46
|
/**
|
|
24
47
|
* Minimal concurrency gate. Hand-rolled rather than pulling in `p-limit` because
|
package/dist/agent/loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AAuKlB;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,GAC9C,MAAM,CAKR;AAED,+CAA+C;AAC/C,eAAO,MAAM,YAAY;;;CAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports._stallLimits = void 0;
|
|
37
37
|
exports.errorRoundSignature = errorRoundSignature;
|
|
38
|
+
exports.stopReasonNotice = stopReasonNotice;
|
|
38
39
|
exports.resolveMaxIterations = resolveMaxIterations;
|
|
39
40
|
exports.createLimiter = createLimiter;
|
|
40
41
|
exports.extractSubTaskText = extractSubTaskText;
|
|
@@ -214,6 +215,50 @@ function errorRoundSignature(errored) {
|
|
|
214
215
|
}
|
|
215
216
|
/** Runaway-guard limits, exposed for tests. */
|
|
216
217
|
exports._stallLimits = { STALL_LIMIT, REPEAT_STALL_LIMIT };
|
|
218
|
+
/**
|
|
219
|
+
* The message shown when a run ends for any reason other than a clean finish.
|
|
220
|
+
*
|
|
221
|
+
* Pure and exported so every branch is testable: reaching some of these for real needs
|
|
222
|
+
* an empty wallet, a dead upstream, or hundreds of iterations. Returns null only for
|
|
223
|
+
* the two outcomes that are deliberately silent.
|
|
224
|
+
*
|
|
225
|
+
* `'unknown'` deliberately produces a message rather than nothing. If a future `break`
|
|
226
|
+
* forgets to set a reason, the symptom should be a visible "ended unexpectedly" line —
|
|
227
|
+
* annoying and reportable — not the silent stop that made this refactor necessary.
|
|
228
|
+
*/
|
|
229
|
+
function stopReasonNotice(reason, ctx = {}) {
|
|
230
|
+
switch (reason) {
|
|
231
|
+
case 'clean':
|
|
232
|
+
case 'aborted':
|
|
233
|
+
// A dedicated channel (e.g. the zero-balance bubble via onBalanceStatus) has already
|
|
234
|
+
// told the user why this stopped. Naming this case explicitly — rather than reusing
|
|
235
|
+
// 'aborted' — keeps "the user cancelled" from silently coming to mean two things.
|
|
236
|
+
case 'reported-elsewhere':
|
|
237
|
+
return null;
|
|
238
|
+
case 'empty-response':
|
|
239
|
+
return `\n\u26a0\ufe0f The model returned an empty response, so nothing was done. This is usually a transient ` +
|
|
240
|
+
`upstream hiccup \u2014 send "continue" to retry.\n`;
|
|
241
|
+
case 'no-balance':
|
|
242
|
+
return `\n\ud83d\udcb3 Stopped: your balance is empty, so the request was rejected before it started. ` +
|
|
243
|
+
`Top up and send "continue" \u2014 no tokens were used for this turn.\n`;
|
|
244
|
+
case 'stalled':
|
|
245
|
+
return `\n\ud83d\uded1 Stopped: the last ${STALL_LIMIT} tool rounds all failed, so the agent looked stuck. ` +
|
|
246
|
+
`Fix the underlying error (or grant the needed permission) and send "continue".\n`;
|
|
247
|
+
case 'stalled-repeat':
|
|
248
|
+
return `\n\ud83d\uded1 Stopped: the same tool error repeated ${REPEAT_STALL_LIMIT} rounds in a row, so the agent ` +
|
|
249
|
+
`was looping without making progress.` +
|
|
250
|
+
(ctx.repeatError ? ` The recurring error was:\n${ctx.repeatError}\n` : '\n') +
|
|
251
|
+
`Fix that underlying cause (or grant the needed permission) and send "continue".\n`;
|
|
252
|
+
case 'budget':
|
|
253
|
+
return `\n\u23f8\ufe0f Stopped at the ${ctx.budget}-step safety limit \u2014 the task may be incomplete. ` +
|
|
254
|
+
`Send "continue" to resume, or raise the limit via "maxIterations" in .nexrall/settings.json ` +
|
|
255
|
+
`(or the NEXRALL_MAX_ITERATIONS env var). Auto-continue can be disabled with "autoContinue": false.\n`;
|
|
256
|
+
case 'unknown':
|
|
257
|
+
default:
|
|
258
|
+
return `\n\u26a0\ufe0f The run ended unexpectedly without completing. Your work so far is preserved \u2014 ` +
|
|
259
|
+
`send "continue" to resume.\n`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
217
262
|
// Resolve the soft iteration budget. Precedence:
|
|
218
263
|
// options.maxIterations → env NEXRALL_MAX_ITERATIONS → settings.maxIterations → default
|
|
219
264
|
//
|
|
@@ -1535,16 +1580,28 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1535
1580
|
// condition where one sub-agent's chdir overwrites another's, making every tool
|
|
1536
1581
|
// that falls back to process.cwd() resolve paths against the wrong directory.
|
|
1537
1582
|
// workDir is now threaded explicitly through executeTool → resolvePath instead.
|
|
1538
|
-
//
|
|
1539
|
-
//
|
|
1540
|
-
//
|
|
1541
|
-
|
|
1583
|
+
// ─── Why did this run stop? ───────────────────────────────────────────────────
|
|
1584
|
+
//
|
|
1585
|
+
// The epilogue used to INFER the reason by re-testing state (`stalledOut`, then
|
|
1586
|
+
// `iteration >= budget`). Inference only covers the cases someone thought to test,
|
|
1587
|
+
// so any `break` that matched none of them fell through and the run ended in total
|
|
1588
|
+
// silence — the worst possible outcome after an hour of work, because the user
|
|
1589
|
+
// cannot tell "finished" from "died". Two such holes existed:
|
|
1590
|
+
//
|
|
1591
|
+
// • the pre-flight 402 (wallet empty) broke out with no flag set at all, leaving
|
|
1592
|
+
// the optional onBalanceStatus callback as the ONLY signal;
|
|
1593
|
+
// • a thinking-only/empty model turn set completedCleanly = true, actively
|
|
1594
|
+
// claiming success for a turn that produced nothing.
|
|
1595
|
+
//
|
|
1596
|
+
// So the reason is now RECORDED at each exit instead of reconstructed after it.
|
|
1597
|
+
// Anything that isn't an explicit clean finish or a user abort must name itself,
|
|
1598
|
+
// and `default` in the switch below means a future `break` cannot go silent.
|
|
1599
|
+
let stopReason = 'unknown';
|
|
1542
1600
|
// Tool rounds actually completed in this call. Counted explicitly rather than derived
|
|
1543
1601
|
// from `messages.length` because auto-compaction splices the history SHORTER mid-run:
|
|
1544
1602
|
// a long session that compacted and then lost its connection would show a negative
|
|
1545
1603
|
// length delta and be judged "no progress", discarding the work it most needs to keep.
|
|
1546
1604
|
let completedRounds = 0;
|
|
1547
|
-
let stalledOut = false; // tripped the runaway guard (all-failed rounds)
|
|
1548
1605
|
let consecutiveErrorRounds = 0; // rounds where every tool call errored
|
|
1549
1606
|
// Repeated-identical-failure guard: see the runaway guards below. Tracked
|
|
1550
1607
|
// separately from consecutiveErrorRounds because a round can contain a succeeding
|
|
@@ -1605,8 +1662,10 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1605
1662
|
let compactDisabled = false;
|
|
1606
1663
|
try {
|
|
1607
1664
|
for (; iteration < budget; iteration++) {
|
|
1608
|
-
if (options.abortSignal?.aborted)
|
|
1665
|
+
if (options.abortSignal?.aborted) {
|
|
1666
|
+
stopReason = 'aborted';
|
|
1609
1667
|
break;
|
|
1668
|
+
}
|
|
1610
1669
|
// Auto-compact: keep the resent prompt small BEFORE the next stream so we
|
|
1611
1670
|
// never hit the context-window wall — or the backend body-size limit — and,
|
|
1612
1671
|
// just as importantly, so we stop paying cache-read on an ever-growing prefix
|
|
@@ -1802,8 +1861,10 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1802
1861
|
}, onEvent);
|
|
1803
1862
|
}
|
|
1804
1863
|
catch (err) {
|
|
1805
|
-
if (options.abortSignal?.aborted || err.name === 'AbortError')
|
|
1864
|
+
if (options.abortSignal?.aborted || err.name === 'AbortError') {
|
|
1865
|
+
stopReason = 'aborted';
|
|
1806
1866
|
break;
|
|
1867
|
+
}
|
|
1807
1868
|
// Pre-flight 402 (routes/code.js — wallet already empty, the turn couldn't even
|
|
1808
1869
|
// start) is tagged with `status`/`balance` by client.ts. Surface it as the same
|
|
1809
1870
|
// low/zero-balance nudge a mid-turn 'balance_status' event would, instead of a
|
|
@@ -1811,7 +1872,16 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1811
1872
|
const status = err.status;
|
|
1812
1873
|
if (status === 402) {
|
|
1813
1874
|
const balance = err.balance ?? 0;
|
|
1875
|
+
// Prefer the rich, actionable UI (a persistent bubble with a link to Billing)
|
|
1876
|
+
// where the client implements it…
|
|
1814
1877
|
options.onBalanceStatus?.(balance, true);
|
|
1878
|
+
// …but do not DEPEND on it. The callback is optional, so a client without it
|
|
1879
|
+
// used to show the user absolutely nothing — the run simply stopped mid-task.
|
|
1880
|
+
//
|
|
1881
|
+
// Set the reason ONLY when there is no richer channel, so clients that do
|
|
1882
|
+
// render the bubble don't also get a redundant plain-text line saying the same
|
|
1883
|
+
// thing. Either way the user is told something, which is the actual guarantee.
|
|
1884
|
+
stopReason = options.onBalanceStatus ? 'reported-elsewhere' : 'no-balance';
|
|
1815
1885
|
break;
|
|
1816
1886
|
}
|
|
1817
1887
|
runSimpleHooks(hooks.OnError, options.workDir);
|
|
@@ -1824,8 +1894,10 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1824
1894
|
// assistant→tool_result pair here, so it is valid to resend as-is.
|
|
1825
1895
|
throw new types_1.AgentTurnError(`Stream failed: ${err.message}`, trimToResumableBoundary(messages), completedRounds, err);
|
|
1826
1896
|
}
|
|
1827
|
-
if (options.abortSignal?.aborted)
|
|
1897
|
+
if (options.abortSignal?.aborted) {
|
|
1898
|
+
stopReason = 'aborted';
|
|
1828
1899
|
break;
|
|
1900
|
+
}
|
|
1829
1901
|
// 2. Strip thinking blocks — API rejects them in message history
|
|
1830
1902
|
assistantMessage.content = assistantMessage.content.filter((b) => b.type !== 'thinking' && b.type !== 'redacted_thinking');
|
|
1831
1903
|
// If the model returned nothing usable (e.g. thinking-only, then stopped), the
|
|
@@ -1841,8 +1913,12 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1841
1913
|
messages.push({ role: 'user', content: [{ type: 'text', text }] });
|
|
1842
1914
|
continue;
|
|
1843
1915
|
}
|
|
1916
|
+
// NOT a clean finish. This used to set completedCleanly = true, which told the
|
|
1917
|
+
// caller the task succeeded when the model had in fact produced nothing at all
|
|
1918
|
+
// — indistinguishable, to the user, from a finished job with no summary. An
|
|
1919
|
+
// empty turn is a (usually transient) upstream failure, so name it and say so.
|
|
1844
1920
|
runSimpleHooks(hooks.PostMessageComplete, options.workDir);
|
|
1845
|
-
|
|
1921
|
+
stopReason = 'empty-response';
|
|
1846
1922
|
break;
|
|
1847
1923
|
}
|
|
1848
1924
|
// stopReason is transient, single-turn metadata for the truncated-tool-call guard
|
|
@@ -1990,7 +2066,7 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1990
2066
|
}
|
|
1991
2067
|
}
|
|
1992
2068
|
runSimpleHooks(hooks.PostMessageComplete, options.workDir);
|
|
1993
|
-
|
|
2069
|
+
stopReason = 'clean';
|
|
1994
2070
|
break;
|
|
1995
2071
|
}
|
|
1996
2072
|
// 5. Execute all tool uses in parallel
|
|
@@ -2235,7 +2311,7 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2235
2311
|
const allErrored = toolResults.length > 0 && errored.length === toolResults.length;
|
|
2236
2312
|
consecutiveErrorRounds = allErrored ? consecutiveErrorRounds + 1 : 0;
|
|
2237
2313
|
if (consecutiveErrorRounds >= STALL_LIMIT) {
|
|
2238
|
-
|
|
2314
|
+
stopReason = 'stalled';
|
|
2239
2315
|
break;
|
|
2240
2316
|
}
|
|
2241
2317
|
// Signature of this round's failures, order-independent and truncated so a long
|
|
@@ -2249,7 +2325,7 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2249
2325
|
lastErrorSignature = errSignature;
|
|
2250
2326
|
}
|
|
2251
2327
|
if (repeatedErrorRounds >= REPEAT_STALL_LIMIT) {
|
|
2252
|
-
|
|
2328
|
+
stopReason = 'stalled-repeat';
|
|
2253
2329
|
stalledRepeatError = errored[0] ? String(errored[0].result.error).slice(0, 300) : null;
|
|
2254
2330
|
break;
|
|
2255
2331
|
}
|
|
@@ -2264,27 +2340,23 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2264
2340
|
}
|
|
2265
2341
|
// Loop back to step 1
|
|
2266
2342
|
}
|
|
2267
|
-
// Explain why we stopped
|
|
2268
|
-
//
|
|
2269
|
-
//
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
`Send "continue" to resume, or raise the limit via "maxIterations" in .nexrall/settings.json ` +
|
|
2285
|
-
`(or the NEXRALL_MAX_ITERATIONS env var). Auto-continue can be disabled with "autoContinue": false.\n`);
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2343
|
+
// Explain why we stopped, so a long run never just goes silent. History ends on a
|
|
2344
|
+
// tool_result turn, so "continue" resumes exactly where it left off.
|
|
2345
|
+
//
|
|
2346
|
+
// The budget case is resolved HERE rather than at a break: exhausting the `for`
|
|
2347
|
+
// condition is a normal loop exit, not a branch we can annotate. Everything else
|
|
2348
|
+
// named itself on the way out, and 'unknown' is the loud fallback for a break that
|
|
2349
|
+
// forgot to.
|
|
2350
|
+
if (stopReason === 'unknown' && iteration >= budget)
|
|
2351
|
+
stopReason = 'budget';
|
|
2352
|
+
if (options.abortSignal?.aborted)
|
|
2353
|
+
stopReason = 'aborted';
|
|
2354
|
+
// Routed through onNotice (falling back to onText) like every other housekeeping
|
|
2355
|
+
// message in this file — these are statements from the harness, not from the model,
|
|
2356
|
+
// and splicing them into the assistant's own bubble reads as if it said them.
|
|
2357
|
+
const notice = stopReasonNotice(stopReason, { budget, repeatError: stalledRepeatError });
|
|
2358
|
+
if (notice)
|
|
2359
|
+
(options.onNotice ?? options.onText)(notice);
|
|
2288
2360
|
}
|
|
2289
2361
|
catch (err) {
|
|
2290
2362
|
// Every other failure path (a tool executor blowing up, a hook throwing, an
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.28",
|
|
4
4
|
"description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nexrall <support@nexrall.com> (https://nexrall.com)",
|