@nexrall/code-core 1.4.55 → 1.4.57

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.
@@ -2,6 +2,32 @@ import type { Message, ToolResult, AgentLoopOptions, EnvContext } from '../types
2
2
  import { type AgentType, type AgentMemoryScope } from './agentTypes';
3
3
  import { bashNeedsRepoLock } from '../permissions/bashClassify';
4
4
  export { bashNeedsRepoLock };
5
+ /** Backoff for the Nth (0-based) empty-turn retry. Pure, so the schedule is testable. */
6
+ export declare function emptyTurnBackoffMs(attempt: number): number;
7
+ /**
8
+ * Should an empty assistant turn be retried automatically, rather than surfaced?
9
+ *
10
+ * Split out as a pure function because the two "empty" causes look IDENTICAL at the
11
+ * call site (both are `content.length === 0`) and telling them apart is the entire
12
+ * point — the previous bug in this area was treating a deterministic truncation as a
13
+ * transient hiccup and advising a retry that reproduced it verbatim.
14
+ *
15
+ * • `max_tokens` → the model burned its whole output budget on reasoning and was CUT
16
+ * OFF. Retrying re-runs the same prompt with the same budget and the same reasoning
17
+ * behaviour, so it fails the same way while billing again. Never retried; the user
18
+ * is told to lower effort or split the task (stopReasonNotice 'output-limit').
19
+ * • anything else → a genuinely transient empty turn. Retried, up to the limit.
20
+ *
21
+ * The attempt cap matters as much as the classification: a model that has decided to
22
+ * return nothing (e.g. a prompt it refuses to continue) would otherwise loop forever
23
+ * on a paid endpoint. After the cap we fall back to the old visible notice.
24
+ */
25
+ export declare function shouldRetryEmptyTurn(stopReason: string | null | undefined, attemptsSoFar: number): boolean;
26
+ /** Empty-turn retry limits, exposed for tests. */
27
+ export declare const _emptyTurnRetry: {
28
+ EMPTY_TURN_RETRY_LIMIT: number;
29
+ EMPTY_TURN_RETRY_BASE_MS: number;
30
+ };
5
31
  /**
6
32
  * Fingerprint one round's tool failures, for the repeated-failure runaway guard.
7
33
  *
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
@@ -33,7 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports.AGENT_MEMORY_TOOL_SCHEMA = exports.AGENT_MEMORY_TOOL = exports._stallLimits = exports.bashNeedsRepoLock = void 0;
36
+ exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports.AGENT_MEMORY_TOOL_SCHEMA = exports.AGENT_MEMORY_TOOL = exports._stallLimits = exports._emptyTurnRetry = exports.bashNeedsRepoLock = void 0;
37
+ exports.emptyTurnBackoffMs = emptyTurnBackoffMs;
38
+ exports.shouldRetryEmptyTurn = shouldRetryEmptyTurn;
37
39
  exports.errorRoundSignature = errorRoundSignature;
38
40
  exports.executeAgentMemoryWrite = executeAgentMemoryWrite;
39
41
  exports.stopReasonNotice = stopReasonNotice;
@@ -226,6 +228,60 @@ const STALL_LIMIT = 8; // consecutive all-failed tool rounds → give up (runawa
226
228
  // being stuck than a total failure: legitimately retrying one failing command a few times
227
229
  // while making progress elsewhere is normal, twelve times is not.
228
230
  const REPEAT_STALL_LIMIT = 12;
231
+ // ─── Empty-turn auto-retry ────────────────────────────────────────────────────
232
+ //
233
+ // A model turn can come back STRUCTURALLY FINE (the stream completed, `message_complete`
234
+ // arrived) and still contain nothing usable once thinking blocks are stripped for
235
+ // history. Anthropic models hit this materially more often than the OpenAI-compatible
236
+ // ones, because only they emit `thinking`/`redacted_thinking` blocks — a turn that
237
+ // reasons and then ends without committing text or a tool call strips down to `[]`.
238
+ //
239
+ // The transport layer cannot fix this. client.ts only retries when the stream produced
240
+ // NOTHING; here the stream produced a valid message that happens to be empty, so it
241
+ // resolves normally and the loop used to break immediately and tell the user to type
242
+ // "continue" — asking a human to press a button the loop can press itself.
243
+ //
244
+ // Retrying here is safe for a specific, checkable reason, not a hopeful one:
245
+ // • tools are dispatched from `assistantMessage.content` AFTER this check, and this
246
+ // content is empty, so no tool ran and no file changed;
247
+ // • nothing was pushed to `messages` (the push happens below this branch), so the
248
+ // history is byte-identical to the previous attempt — a retry is a clean re-ask,
249
+ // not a resend of a corrupted turn;
250
+ // • no round was counted, no hook fired, no checkpoint was taken.
251
+ // Contrast the output-limit case, which is NOT retried: see shouldRetryEmptyTurn.
252
+ const EMPTY_TURN_RETRY_LIMIT = 3;
253
+ // Short, escalating pause (1s, 2s, 4s). Long enough to ride out the upstream blip that
254
+ // causes this; short enough that three failures cost ~7s rather than a visible stall.
255
+ const EMPTY_TURN_RETRY_BASE_MS = 1000;
256
+ /** Backoff for the Nth (0-based) empty-turn retry. Pure, so the schedule is testable. */
257
+ function emptyTurnBackoffMs(attempt) {
258
+ return EMPTY_TURN_RETRY_BASE_MS * Math.pow(2, Math.max(0, attempt));
259
+ }
260
+ /**
261
+ * Should an empty assistant turn be retried automatically, rather than surfaced?
262
+ *
263
+ * Split out as a pure function because the two "empty" causes look IDENTICAL at the
264
+ * call site (both are `content.length === 0`) and telling them apart is the entire
265
+ * point — the previous bug in this area was treating a deterministic truncation as a
266
+ * transient hiccup and advising a retry that reproduced it verbatim.
267
+ *
268
+ * • `max_tokens` → the model burned its whole output budget on reasoning and was CUT
269
+ * OFF. Retrying re-runs the same prompt with the same budget and the same reasoning
270
+ * behaviour, so it fails the same way while billing again. Never retried; the user
271
+ * is told to lower effort or split the task (stopReasonNotice 'output-limit').
272
+ * • anything else → a genuinely transient empty turn. Retried, up to the limit.
273
+ *
274
+ * The attempt cap matters as much as the classification: a model that has decided to
275
+ * return nothing (e.g. a prompt it refuses to continue) would otherwise loop forever
276
+ * on a paid endpoint. After the cap we fall back to the old visible notice.
277
+ */
278
+ function shouldRetryEmptyTurn(stopReason, attemptsSoFar) {
279
+ if (stopReason === 'max_tokens')
280
+ return false;
281
+ return attemptsSoFar < EMPTY_TURN_RETRY_LIMIT;
282
+ }
283
+ /** Empty-turn retry limits, exposed for tests. */
284
+ exports._emptyTurnRetry = { EMPTY_TURN_RETRY_LIMIT, EMPTY_TURN_RETRY_BASE_MS };
229
285
  /**
230
286
  * Fingerprint one round's tool failures, for the repeated-failure runaway guard.
231
287
  *
@@ -331,9 +387,14 @@ function stopReasonNotice(reason, ctx = {}) {
331
387
  // 'aborted' — keeps "the user cancelled" from silently coming to mean two things.
332
388
  case 'reported-elsewhere':
333
389
  return null;
390
+ // Reaching this notice now means the loop ALREADY retried automatically and the
391
+ // model came back empty every time (see shouldRetryEmptyTurn). Saying "send
392
+ // continue to retry" without that context reads as if nothing was tried, and the
393
+ // user's manual retry is then the fourth identical attempt — so name the attempts.
334
394
  case 'empty-response':
335
- return `\n\u26a0\ufe0f The model returned an empty response, so nothing was done. This is usually a transient ` +
336
- `upstream hiccup \u2014 send "continue" to retry.\n`;
395
+ return `\n\u26a0\ufe0f The model returned an empty response ${EMPTY_TURN_RETRY_LIMIT} times in a row, so nothing was done. ` +
396
+ `This is usually a transient upstream hiccup that the agent retries by itself; it did not clear this time. ` +
397
+ `Send "continue" to try again, or switch model with /model if it persists.\n`;
337
398
  // Deliberately NOT folded into 'empty-response'. Both arrive as an assistant
338
399
  // message with no content once thinking blocks are stripped, so they used to
339
400
  // be indistinguishable — and the user was told the truncation case was "a
@@ -2565,6 +2626,10 @@ async function runAgentLoop(initialMessages, options) {
2565
2626
  let stalledRepeatError = null;
2566
2627
  let budget = maxIterations; // extended by auto-continue, capped at hardCap
2567
2628
  let iteration = 0;
2629
+ // Consecutive empty (thinking-only) model turns auto-retried in this run. Reset on
2630
+ // any turn that produces content, so a session that hits one blip an hour keeps its
2631
+ // full retry budget each time instead of slowly using it up over a long run.
2632
+ let emptyTurnRetries = 0;
2568
2633
  // ─── Verification nudge (GAP D) ───────────────────────────────────────────────
2569
2634
  // Coding agents commonly claim "done" after editing files without ever running a
2570
2635
  // build/test/lint command to confirm the change actually works (the industry's
@@ -2953,10 +3018,38 @@ async function runAgentLoop(initialMessages, options) {
2953
3018
  // 'max_tokens' means the model was CUT OFF at its output ceiling. Landing here
2954
3019
  // with empty content means the entire budget went to thinking blocks that were
2955
3020
  // just stripped, leaving nothing to show. Deterministic, not a hiccup.
3021
+ //
3022
+ // …and before giving up at all, RETRY IT. Nothing has been pushed to `messages`,
3023
+ // no tool ran (tools are dispatched from this same empty content, below), so the
3024
+ // next attempt re-asks the identical prompt with no duplicated side effects. This
3025
+ // is the fix for users being told to type "continue" for a hiccup the loop can
3026
+ // ride out itself; shouldRetryEmptyTurn keeps the deterministic `max_tokens`
3027
+ // truncation OUT of it, because retrying that reproduces it exactly.
3028
+ if (shouldRetryEmptyTurn(assistantMessage.stopReason, emptyTurnRetries)) {
3029
+ const waitMs = emptyTurnBackoffMs(emptyTurnRetries);
3030
+ emptyTurnRetries++;
3031
+ // Reuse the existing "reconnecting…" channel rather than printing a line into
3032
+ // the transcript: this is the same class of event (a transparent retry the user
3033
+ // does not have to act on), and clients already render + auto-clear it.
3034
+ options.onRetry?.(emptyTurnRetries, EMPTY_TURN_RETRY_LIMIT, 'The model returned an empty response — retrying automatically');
3035
+ await new Promise((r) => setTimeout(r, waitMs));
3036
+ if (options.abortSignal?.aborted) {
3037
+ stopReason = 'aborted';
3038
+ break;
3039
+ }
3040
+ options.onRetryResolved?.();
3041
+ // Does NOT count as an iteration: a turn that produced nothing must not eat
3042
+ // the step budget, or three blips would silently shorten the task by three
3043
+ // steps. `continue` re-enters the loop body without incrementing.
3044
+ iteration--;
3045
+ continue;
3046
+ }
2956
3047
  runSimpleHooks(hooks.PostMessageComplete, options.workDir);
2957
3048
  stopReason = assistantMessage.stopReason === 'max_tokens' ? 'output-limit' : 'empty-response';
2958
3049
  break;
2959
3050
  }
3051
+ // A turn with real content: the blip (if any) is over, restore the full budget.
3052
+ emptyTurnRetries = 0;
2960
3053
  // stopReason is transient, single-turn metadata for the truncated-tool-call guard
2961
3054
  // below — it must never be replayed to the API as part of message history (Anthropic
2962
3055
  // rejects unknown fields on a request message with a 400).
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
@@ -213,6 +213,42 @@ function isExpiredTokenResponse(status, body) {
213
213
  const MAX_RETRIES = 5;
214
214
  const RETRY_BASE_MS = 1000;
215
215
  const RETRY_MAX_MS = 30000;
216
+ /**
217
+ * SSE frames that are backend HOUSEKEEPING, not model output.
218
+ *
219
+ * These decide which STALL LIMIT a turn is held to: the generous time-to-first-token
220
+ * allowance (300 s) while the upstream is still processing the prompt, or the tighter
221
+ * mid-stream one (150 s) once tokens are flowing. See the `sawModelEvent` assignment
222
+ * in the parser for the full story.
223
+ *
224
+ * They are emitted by routes/code.js BEFORE (or independently of) the upstream model
225
+ * call, so their arrival proves only that the BACKEND is alive — never that the MODEL
226
+ * has started responding:
227
+ *
228
+ * resumable — turn registered + frame numbering announced. Emitted at
229
+ * routes/code.js:1151; openStream() is not called until :1318.
230
+ * This one alone held every production turn to the 150 s
231
+ * mid-stream limit during prompt processing.
232
+ * context_warning — server-side history trim/compact, already handled server-side.
233
+ * tool_progress — a liveness ping during long tool-argument generation.
234
+ *
235
+ * DELIBERATELY NOT LISTED (they mean the model really has started responding, and MUST
236
+ * count): text, thinking, thinking_delta, thinking_progress, tool_use,
237
+ * tool_result_info, message_complete, usage, balance_status, done, error.
238
+ *
239
+ * `thinking_progress` counts on purpose: it is derived from tokens the model has
240
+ * actually emitted, so time-to-first-token is genuinely over by then.
241
+ *
242
+ * Kept as an explicit ALLOWLIST of things to ignore rather than a denylist of things
243
+ * that count: a new backend frame type then defaults to "the model is responding",
244
+ * whose worst case is holding a turn to the tighter stall limit — visible and
245
+ * self-correcting, unlike the silent 300 s→150 s downgrade this list exists to stop.
246
+ */
247
+ const HOUSEKEEPING_FRAMES = new Set([
248
+ 'resumable',
249
+ 'context_warning',
250
+ 'tool_progress',
251
+ ]);
216
252
  /**
217
253
  * Wall-clock ceiling on ALL reconnect effort for a single streamChat() call.
218
254
  *
@@ -437,7 +473,21 @@ async function streamChat(messages, options, onEvent) {
437
473
  // see it. We detect it in the parser and retry the WHOLE attempt with exponential backoff
438
474
  // — but only while nothing has been emitted to the caller yet, so a retry can never
439
475
  // duplicate already-rendered text or tool calls.
440
- const isRetryableStreamMsg = (m) => /overloaded|rate.?limit|temporarily|unavailable|try again|internal server error/i.test(String(m ?? ''));
476
+ const isRetryableStreamMsg = (m) => {
477
+ const s = String(m ?? '');
478
+ // Anthropic's intermittent upstream refusal, reproduced verbatim through the AI
479
+ // Gateway passthrough:
480
+ // 403 {"error":{"type":"forbidden","message":"Request not allowed"}}
481
+ // Sporadic and gone on retry rather than a real permissions failure. A modern
482
+ // backend now flags this explicitly (`retryable` on the error frame), which is the
483
+ // reliable signal; this pattern is the FALLBACK for older backends that don't,
484
+ // and is deliberately narrow — it requires the upstream's own `"type":"forbidden"`
485
+ // envelope, so Nexrall's own auth 403s (a completely different body) stay terminal
486
+ // and a genuinely revoked key is still bounded by the capped retry budget.
487
+ if (/403[\s\S]*"type"\s*:\s*"forbidden"/i.test(s))
488
+ return true;
489
+ return /overloaded|rate.?limit|temporarily|unavailable|try again|internal server error/i.test(s);
490
+ };
441
491
  // Tracks whether a "reconnecting…" signal has been shown to the caller so we know to
442
492
  // clear it (retry_resolved) once real data starts flowing again. Shared across both
443
493
  // the connect-time retry loop and the outer stream-retry loop below, and across a
@@ -1056,10 +1106,35 @@ async function streamChat(messages, options, onEvent) {
1056
1106
  return;
1057
1107
  }
1058
1108
  const evt = parsed;
1059
- // Reaching here means a well-formed model event was parsed (": ping" keepalive
1109
+ // Reaching here means a well-formed frame was parsed (": ping" keepalive
1060
1110
  // comments never produce a ParsedEvent with data) — count it as real progress.
1111
+ //
1112
+ // `lastProgressAt` is bumped for EVERY frame, including the housekeeping ones
1113
+ // below: they prove the backend is alive and talking to us, which is exactly
1114
+ // what the stall watchdog measures.
1061
1115
  lastProgressAt = Date.now();
1062
- sawModelEvent = true;
1116
+ // …but `sawModelEvent` means something STRICTLY NARROWER: "the upstream model
1117
+ // has started responding". It selects which stall limit applies — the generous
1118
+ // time-to-first-token allowance (FIRST_EVENT_TIMEOUT_MS, 300 s) or the tighter
1119
+ // mid-stream one (PROGRESS_TIMEOUT_MS, 150 s) — so it must not be flipped by a
1120
+ // frame the BACKEND sent on its own initiative.
1121
+ //
1122
+ // routes/code.js emits `resumable` as soon as the turn is registered
1123
+ // (line ~1151) — BEFORE it calls openStream() (line ~1318), i.e. before the
1124
+ // model has been asked anything at all. Counting that as "the model is
1125
+ // responding" downgraded every turn to the 150 s mid-stream limit while the
1126
+ // upstream was still doing prompt processing, which is exactly the phase that
1127
+ // legitimately needs 300 s on a large cold-cache context. The result was
1128
+ // spurious stalls on big conversations that then "worked on retry" — because
1129
+ // the first attempt had warmed the prompt cache.
1130
+ //
1131
+ // NOTE: this flag deliberately no longer gates the empty-turn recovery in
1132
+ // `stream.on('end')`. That check now asks what the turn actually PRODUCED
1133
+ // (see `producedNothing` there), because "a frame arrived" and "content
1134
+ // exists" are different questions — conflating them is what made that guard
1135
+ // dead code in production.
1136
+ if (!HOUSEKEEPING_FRAMES.has(evt.type))
1137
+ sawModelEvent = true;
1063
1138
  // Real data is flowing again — clear any "reconnecting…" indicator the caller
1064
1139
  // may be showing from an earlier retry on THIS same attempt.
1065
1140
  clearRetryIfNeeded();
@@ -1250,6 +1325,26 @@ async function streamChat(messages, options, onEvent) {
1250
1325
  // — so this always needs a fresh turnId, which the outer loop mints via
1251
1326
  // `forceRestart`.
1252
1327
  const notResumable = evt.notResumable === true;
1328
+ // Does the BACKEND say this is worth retrying? Set by routes/code.js from
1329
+ // the upstream HTTP STATUS (403/408/409/429/5xx and bare transport
1330
+ // failures — see isTransientUpstreamError there), which is information
1331
+ // only the server has and which no amount of reading the message text can
1332
+ // reliably recover.
1333
+ //
1334
+ // This closes a real hole. `res.flushHeaders()` runs before the model is
1335
+ // called, so the HTTP status is pinned at 200 and an upstream failure can
1336
+ // only arrive as this frame — making the connect-time 403 retry logic
1337
+ // below unreachable in production. The only remaining hook was
1338
+ // `isRetryableStreamMsg`, whose regex does NOT match the string Anthropic
1339
+ // actually emits (`403 {"error":{"type":"forbidden",…}}`), so an
1340
+ // intermittent upstream refusal killed the run with a raw `⚠️ 403 …`
1341
+ // instead of being retried. Same lesson as `notResumable` directly above:
1342
+ // read a flag the server sets deliberately, never English prose.
1343
+ //
1344
+ // OR'd with the legacy text match rather than replacing it, so a client
1345
+ // running against an older backend that doesn't send the flag keeps
1346
+ // exactly its current behaviour.
1347
+ const serverSaysRetryable = evt.retryable === true;
1253
1348
  // Transient upstream failure before any output → let the outer loop retry it
1254
1349
  // transparently instead of killing the turn (this is the "Overloaded" case).
1255
1350
  // But never retry once the turn's message has already been delivered in
@@ -1266,7 +1361,7 @@ async function streamChat(messages, options, onEvent) {
1266
1361
  stream.destroy?.();
1267
1362
  reject(Object.assign(tagTransient(new Error(message)), { forceRestart: true }));
1268
1363
  }
1269
- else if (isRetryableStreamMsg(message) && (!emittedToCaller || allowRestartAfterRender)) {
1364
+ else if ((serverSaysRetryable || isRetryableStreamMsg(message)) && (!emittedToCaller || allowRestartAfterRender)) {
1270
1365
  clearInterval(heartbeatWatchdog);
1271
1366
  stream.destroy?.();
1272
1367
  reject(tagTransient(new Error(message)));
@@ -1299,12 +1394,34 @@ async function streamChat(messages, options, onEvent) {
1299
1394
  });
1300
1395
  stream.on('end', () => {
1301
1396
  clearInterval(heartbeatWatchdog);
1302
- // A clean 'end' with ZERO model events means the connection was dropped before
1303
- // the backend produced anything (deploy restart, proxy reset). Without this,
1304
- // the fallback below returns an EMPTY assistant message and the agent loop
1305
- // treats it as a silent no-op the turn just vanishes. Nothing was emitted,
1306
- // so a retry cannot duplicate output tag retryable.
1307
- if (!sawModelEvent && !completedMessage) {
1397
+ // A clean 'end' that produced NOTHING USABLE means the turn died before the
1398
+ // model committed any content (deploy restart, proxy reset, upstream refusal).
1399
+ // Without this, the fallback below returns an EMPTY assistant message and the
1400
+ // agent loop reports "⚠️ The model returned an empty response" telling the
1401
+ // user to send "continue" by hand to do the retry that belongs here.
1402
+ //
1403
+ // The condition is deliberately "produced no usable content", NOT "saw no
1404
+ // frames". Those are different, and the difference is the whole bug:
1405
+ //
1406
+ // • `resumable` / `context_warning` / `tool_progress` are emitted by
1407
+ // routes/code.js BEFORE (or independently of) the model call, so keying on
1408
+ // "any frame arrived" made this guard dead code on every production turn —
1409
+ // `resumable` alone is sent at routes/code.js:1151, while openStream() is
1410
+ // not reached until :1318.
1411
+ // • `thinking_progress` is the subtler half: it IS real evidence the model is
1412
+ // working (it counts tokens the model emitted), so classifying frames by
1413
+ // name would keep it "counting" — yet thinking tokens are stripped from
1414
+ // history, so a turn that dies there still has zero content and still
1415
+ // surfaces as an empty response. Frame-name classification cannot express
1416
+ // that; asking what the turn actually PRODUCED can.
1417
+ //
1418
+ // Retrying is safe precisely because there is no content: with nothing
1419
+ // accumulated, a fresh attempt cannot duplicate anything on screen. Anything
1420
+ // already rendered (text/tool_use) leaves `textParts`/`toolUseBlocks` non-empty
1421
+ // and takes the normal resolve path, so a real partial answer is never
1422
+ // discarded or re-billed.
1423
+ const producedNothing = !completedMessage && textParts.length === 0 && toolUseBlocks.length === 0;
1424
+ if (producedNothing) {
1308
1425
  reject(Object.assign(new Error('Connection closed before the model responded. Retrying…'), { retryable: true }));
1309
1426
  return;
1310
1427
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexrall/code-core",
3
- "version": "1.4.55",
3
+ "version": "1.4.57",
4
4
  "description": "Core agent loop, tools, and extension primitives for Nexrall Code \u2014 embed an AI coding agent in any Node.js application.",
5
5
  "license": "MIT",
6
6
  "author": "Nexrall <support@nexrall.com> (https://nexrall.com)",