@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.
- package/dist/agent/loop.d.ts +20 -1
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +96 -1
- package/package.json +1 -1
package/dist/agent/loop.d.ts
CHANGED
|
@@ -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
|
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,
|
|
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"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
"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)",
|