@nexrall/code-core 1.4.3 → 1.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.
@@ -1,4 +1,4 @@
1
- import type { Message, AgentLoopOptions } from '../types';
1
+ import type { Message, AgentLoopOptions, EnvContext } from '../types';
2
2
  export declare function resolveMaxIterations(optionValue: number | undefined, settingsRaw: Record<string, unknown>): number;
3
3
  /** Approximate serialised request-body size (bytes) for the messages array. */
4
4
  export declare function estimateBodyBytes(messages: Message[]): number;
@@ -73,5 +73,24 @@ export declare function ledgerSummary(ledger: ProgressLedger): string;
73
73
  * Returns the number of bytes reclaimed (0 if nothing was prunable).
74
74
  */
75
75
  export declare function pruneOldToolResults(messages: Message[]): number;
76
+ /** Rough token estimate for a resumed transcript — no API round-trip needed. */
77
+ export declare function estimateTokensRough(messages: Message[]): number;
78
+ /**
79
+ * Proactively compact `messages` in place if resuming this session would blow
80
+ * past the auto-compact threshold on the very first turn. Returns true if any
81
+ * compaction happened (so the caller can surface a one-line notice to the
82
+ * user). Safe to call on any message array, including empty/small ones (no-op).
83
+ *
84
+ * `model` picks the right context window (mirrors runAgentLoop's own lookup);
85
+ * `onNotice` is optional — pass it to show the same "auto-compacted" message
86
+ * the in-loop path shows, so the behaviour is visually consistent.
87
+ */
88
+ export declare function compactMessagesForResume(messages: Message[], opts: {
89
+ model?: 'turbo' | 'pro' | 'ultra';
90
+ workDir: string;
91
+ clientType?: string;
92
+ env?: EnvContext;
93
+ onNotice?: (text: string) => void;
94
+ }): Promise<boolean>;
76
95
  export declare function runAgentLoop(initialMessages: Message[], options: AgentLoopOptions): Promise<Message[]>;
77
96
  //# sourceMappingURL=loop.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EACjB,MAAM,UAAU,CAAC;AAsKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AA8RD,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAM7D;AAsBD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,aAA+G,CAAC;AAC7I,gGAAgG;AAChG,eAAO,MAAM,aAAa,QAA2J,CAAC;AAEtL;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AAUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAwBxD;AAoBD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,qFAAqF;IACrF,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,IAAI,cAAc,CAE7C;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,EAAE,EAAE,OAAO,EACX,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CAoDN;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA6B5D;AAmBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAmB/D;AA2FD,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CA2kBpB"}
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;AAsKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AA8RD,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAM7D;AAsBD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,aAA+G,CAAC;AAC7I,gGAAgG;AAChG,eAAO,MAAM,aAAa,QAA2J,CAAC;AAEtL;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AAUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAwBxD;AAoBD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,qFAAqF;IACrF,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,IAAI,cAAc,CAE7C;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,EAAE,EAAE,OAAO,EACX,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CAoDN;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA6B5D;AAmBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAmB/D;AA+GD,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAE/D;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,GACA,OAAO,CAAC,OAAO,CAAC,CA2DlB;AAID,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CA+kBpB"}
@@ -42,6 +42,8 @@ exports.createLedger = createLedger;
42
42
  exports.ledgerRecord = ledgerRecord;
43
43
  exports.ledgerSummary = ledgerSummary;
44
44
  exports.pruneOldToolResults = pruneOldToolResults;
45
+ exports.estimateTokensRough = estimateTokensRough;
46
+ exports.compactMessagesForResume = compactMessagesForResume;
45
47
  exports.runAgentLoop = runAgentLoop;
46
48
  const client_1 = require("../api/client");
47
49
  const executor_1 = require("../tools/executor");
@@ -772,6 +774,95 @@ async function autoCompactMessages(messages, options, ledger) {
772
774
  void kept;
773
775
  return true;
774
776
  }
777
+ // ─── Resume-time proactive compaction ─────────────────────────────────────────
778
+ //
779
+ // The in-loop auto-compact above only reacts to `lastPromptTokens`, which is
780
+ // populated from the PREVIOUS turn's usage event. On a freshly-resumed session
781
+ // (opening an old chat from history and sending the first new message) there is
782
+ // no previous turn in this process — `lastPromptTokens` starts at 0 — so the
783
+ // token-pressure trigger never fires for turn 0, and the byte-pressure trigger
784
+ // only catches truly huge sessions (MAX_BODY_BYTES is sized to stay under the
785
+ // backend's 25 MB body limit, not to bound cost — 8 MB of tool-heavy JSON is
786
+ // already on the order of the 1M-token context window itself). The result: a
787
+ // resumed session comfortably under both guards, but still hundreds of
788
+ // thousands of tokens, gets sent to the model at FULL PRICE on the very first
789
+ // message after resume, silently, every time.
790
+ //
791
+ // This function closes that gap: call it once, right after loading a stored
792
+ // session and BEFORE the user's next message is sent, so the expensive
793
+ // resend is compacted proactively instead of being missed by both in-loop
794
+ // guards. It reuses the exact same threshold/mechanics as the in-loop guard
795
+ // (cheap prune first, then summarising compaction) so behaviour stays
796
+ // consistent whether compaction happens at resume-time or mid-run.
797
+ const RESUME_CHARS_PER_TOKEN = 4; // rough, conservative estimate for JSON/code-heavy transcripts
798
+ /** Rough token estimate for a resumed transcript — no API round-trip needed. */
799
+ function estimateTokensRough(messages) {
800
+ return Math.ceil(estimateBodyBytes(messages) / RESUME_CHARS_PER_TOKEN);
801
+ }
802
+ /**
803
+ * Proactively compact `messages` in place if resuming this session would blow
804
+ * past the auto-compact threshold on the very first turn. Returns true if any
805
+ * compaction happened (so the caller can surface a one-line notice to the
806
+ * user). Safe to call on any message array, including empty/small ones (no-op).
807
+ *
808
+ * `model` picks the right context window (mirrors runAgentLoop's own lookup);
809
+ * `onNotice` is optional — pass it to show the same "auto-compacted" message
810
+ * the in-loop path shows, so the behaviour is visually consistent.
811
+ */
812
+ async function compactMessagesForResume(messages, opts) {
813
+ if (messages.length <= COMPACT_KEEP_MIN + 2)
814
+ return false;
815
+ const settings = (0, rules_1.loadSettings)(opts.workDir);
816
+ if (!resolveAutoCompact(undefined, settings.raw))
817
+ return false;
818
+ const contextWindow = MODEL_CONTEXT_TOKENS[opts.model ?? 'turbo'] ?? 1000000;
819
+ let bodyBytes = estimateBodyBytes(messages);
820
+ let tokenGuess = estimateTokensRough(messages);
821
+ const overThreshold = () => tokenGuess > contextWindow * AUTO_COMPACT_THRESHOLD || bodyBytes > MAX_BODY_BYTES;
822
+ if (!overThreshold())
823
+ return false;
824
+ let compacted = false;
825
+ // Cheap pass first — shrinks old tool_result blocks with no model call.
826
+ if (messages.length > PRUNE_KEEP_RECENT + 2) {
827
+ const reclaimed = pruneOldToolResults(messages);
828
+ if (reclaimed > 0) {
829
+ bodyBytes = estimateBodyBytes(messages);
830
+ tokenGuess = estimateTokensRough(messages);
831
+ compacted = true;
832
+ opts.onNotice?.(`\n\u267b\ufe0f Trimmed ~${(reclaimed / (1024 * 1024)).toFixed(1)}MB of older tool output before resuming this chat.\n`);
833
+ }
834
+ }
835
+ // If still over threshold, fall through to summarising compaction — same
836
+ // mechanism the in-loop guard uses, so this can safely loop (a single
837
+ // summarisation pass may still leave a very long session over threshold).
838
+ let guard = 0;
839
+ while (overThreshold() && messages.length > COMPACT_KEEP_MIN + 2 && guard < 5) {
840
+ guard += 1;
841
+ // No ledger at resume time — the ledger is per-run, in-memory, and would
842
+ // have been created fresh anyway since this is a new process/run. The
843
+ // ORIGINAL TASK verbatim pin (inside autoCompactMessages) still applies.
844
+ const did = await autoCompactMessages(messages, {
845
+ workDir: opts.workDir,
846
+ model: opts.model,
847
+ clientType: opts.clientType,
848
+ env: opts.env,
849
+ onText: () => { },
850
+ onToolUse: () => { },
851
+ onToolResult: () => { },
852
+ onUsage: () => { },
853
+ requestPermission: async () => false,
854
+ });
855
+ if (!did)
856
+ break;
857
+ compacted = true;
858
+ bodyBytes = estimateBodyBytes(messages);
859
+ tokenGuess = estimateTokensRough(messages);
860
+ }
861
+ if (compacted) {
862
+ opts.onNotice?.(`\n\u267b\ufe0f Auto-compacted this chat's earlier history before resuming, to avoid resending it at full cost.\n`);
863
+ }
864
+ return compacted;
865
+ }
775
866
  // ─── Agent Loop ───────────────────────────────────────────────────────────────
776
867
  async function runAgentLoop(initialMessages, options) {
777
868
  const messages = [...initialMessages];
@@ -970,7 +1061,11 @@ async function runAgentLoop(initialMessages, options) {
970
1061
  completedCleanly = true;
971
1062
  break;
972
1063
  }
973
- messages.push(assistantMessage);
1064
+ // stopReason is transient, single-turn metadata for the truncated-tool-call guard
1065
+ // below — it must never be replayed to the API as part of message history (Anthropic
1066
+ // rejects unknown fields on a request message with a 400).
1067
+ const { stopReason: _stopReason, ...historyMessage } = assistantMessage;
1068
+ messages.push(historyMessage);
974
1069
  // 3. Collect tool_use blocks from the completed message.
975
1070
  // Server-side tools (e.g. web_search_20260209) are executed by Anthropic and their
976
1071
  // tool_result blocks appear in the SAME assistant message. We must skip those —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexrall/code-core",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
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)",