@nexrall/code-core 1.4.7 → 1.4.9
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 +10 -1
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +111 -29
- package/dist/api/client.d.ts +23 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +58 -14
- package/package.json +1 -1
package/dist/agent/loop.d.ts
CHANGED
|
@@ -71,8 +71,17 @@ export declare function ledgerSummary(ledger: ProgressLedger): string;
|
|
|
71
71
|
* serialised body schema-clean while remaining idempotent across repeat calls.
|
|
72
72
|
*
|
|
73
73
|
* Returns the number of bytes reclaimed (0 if nothing was prunable).
|
|
74
|
+
*
|
|
75
|
+
* `minReclaimBytes` (default 0): if the TOTAL prunable amount is below this, the
|
|
76
|
+
* function makes NO changes and returns 0. This is a cache-safety gate — pruning
|
|
77
|
+
* even one old block changes the request prefix and invalidates the message-level
|
|
78
|
+
* prompt cache, so a tiny prune would bust the cache (re-write at 1.25×) for
|
|
79
|
+
* almost no size win. Measuring first, then applying only if worthwhile, keeps
|
|
80
|
+
* the "don't bust cache for a trivial gain" contract truly atomic (the old code
|
|
81
|
+
* mutated first and let the caller decide, which had already invalidated the
|
|
82
|
+
* cache by the time the caller declined).
|
|
74
83
|
*/
|
|
75
|
-
export declare function pruneOldToolResults(messages: Message[]): number;
|
|
84
|
+
export declare function pruneOldToolResults(messages: Message[], minReclaimBytes?: number): number;
|
|
76
85
|
/** Rough token estimate for a resumed transcript — no API round-trip needed. */
|
|
77
86
|
export declare function estimateTokensRough(messages: Message[]): number;
|
|
78
87
|
/**
|
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;AAsKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;
|
|
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;AA4TD,+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;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,SAAI,GAAG,MAAM,CAgCpF;AAsHD,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,CAqElB;AAID,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CAumBpB"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -416,7 +416,37 @@ const MODEL_CONTEXT_TOKENS = {
|
|
|
416
416
|
pro: 1000000,
|
|
417
417
|
ultra: 1000000,
|
|
418
418
|
};
|
|
419
|
-
|
|
419
|
+
// ── Compaction thresholds (cost control) ─────────────────────────────────────
|
|
420
|
+
// Two independent triggers, deliberately at DIFFERENT levels:
|
|
421
|
+
//
|
|
422
|
+
// • PRUNE threshold (cheap, lossy-but-structure-preserving, NO model call):
|
|
423
|
+
// fires EARLY. Every turn a large history is resent, cache-read alone
|
|
424
|
+
// (0.10× input) is still billed on the whole prefix — on a 700K-token
|
|
425
|
+
// session that is real money accruing per turn long before the 1M wall.
|
|
426
|
+
// Anthropic's own server-side compaction defaults its trigger to 150K
|
|
427
|
+
// input tokens (docs: compact_20260112 default trigger 150000). We mirror
|
|
428
|
+
// that intent: start shedding already-consumed tool_result bulk at ~35% of
|
|
429
|
+
// a 1M window (~350K tokens) so the per-turn cache-read bill stops growing,
|
|
430
|
+
// WITHOUT paying for a summariser model call and WITHOUT dropping any turn
|
|
431
|
+
// (pruneOldToolResults keeps every tool_use/tool_result pair intact).
|
|
432
|
+
//
|
|
433
|
+
// • SUMMARISE threshold (expensive: a real model call, lossy: drops whole
|
|
434
|
+
// turns): stays LATE, because summarise-of-summarise is the main cause of an
|
|
435
|
+
// agent "forgetting" earlier work. Only when cheap pruning can't keep the
|
|
436
|
+
// prompt under this line do we fall through to summarisation.
|
|
437
|
+
//
|
|
438
|
+
// Both are overridable via env for power users / tests.
|
|
439
|
+
function envFraction(name, fallback) {
|
|
440
|
+
const v = Number(process.env[name]);
|
|
441
|
+
return Number.isFinite(v) && v > 0 && v < 1 ? v : fallback;
|
|
442
|
+
}
|
|
443
|
+
// Prune early (~35% of window), summarise late (~80% of window).
|
|
444
|
+
const AUTO_PRUNE_THRESHOLD = envFraction('NEXRALL_PRUNE_THRESHOLD', 0.35);
|
|
445
|
+
const AUTO_COMPACT_THRESHOLD = envFraction('NEXRALL_COMPACT_THRESHOLD', 0.8); // summarise when prompt > 80% of the window
|
|
446
|
+
// Only bother pruning if it reclaims a meaningful amount — a tiny prune busts
|
|
447
|
+
// the message-level prompt cache (the pruned prefix changes) for little gain,
|
|
448
|
+
// so we require at least this many bytes reclaimed before accepting a prune.
|
|
449
|
+
const PRUNE_MIN_RECLAIM_BYTES = 256 * 1024; // 256 KB
|
|
420
450
|
const COMPACT_KEEP_MIN = 6; // always keep at least the last N messages verbatim
|
|
421
451
|
// Byte-level safety net, independent of the token estimate.
|
|
422
452
|
//
|
|
@@ -675,12 +705,23 @@ const PRUNE_MARKER_TAIL = ' pruned to conserve context. Re-run the tool if you n
|
|
|
675
705
|
* serialised body schema-clean while remaining idempotent across repeat calls.
|
|
676
706
|
*
|
|
677
707
|
* Returns the number of bytes reclaimed (0 if nothing was prunable).
|
|
708
|
+
*
|
|
709
|
+
* `minReclaimBytes` (default 0): if the TOTAL prunable amount is below this, the
|
|
710
|
+
* function makes NO changes and returns 0. This is a cache-safety gate — pruning
|
|
711
|
+
* even one old block changes the request prefix and invalidates the message-level
|
|
712
|
+
* prompt cache, so a tiny prune would bust the cache (re-write at 1.25×) for
|
|
713
|
+
* almost no size win. Measuring first, then applying only if worthwhile, keeps
|
|
714
|
+
* the "don't bust cache for a trivial gain" contract truly atomic (the old code
|
|
715
|
+
* mutated first and let the caller decide, which had already invalidated the
|
|
716
|
+
* cache by the time the caller declined).
|
|
678
717
|
*/
|
|
679
|
-
function pruneOldToolResults(messages) {
|
|
718
|
+
function pruneOldToolResults(messages, minReclaimBytes = 0) {
|
|
680
719
|
const cutoff = messages.length - PRUNE_KEEP_RECENT;
|
|
681
720
|
if (cutoff <= 1)
|
|
682
721
|
return 0;
|
|
683
|
-
|
|
722
|
+
// Collect prunable blocks + measure the total reclaim WITHOUT mutating yet.
|
|
723
|
+
const targets = [];
|
|
724
|
+
let total = 0;
|
|
684
725
|
for (let i = 0; i < cutoff; i++) {
|
|
685
726
|
const m = messages[i];
|
|
686
727
|
if (!Array.isArray(m.content))
|
|
@@ -695,10 +736,19 @@ function pruneOldToolResults(messages) {
|
|
|
695
736
|
continue; // already small
|
|
696
737
|
const head = text.slice(0, PRUNE_STUB_KEEP_CHARS);
|
|
697
738
|
const omitted = text.length - head.length;
|
|
698
|
-
|
|
699
|
-
|
|
739
|
+
targets.push({ block: b, head, omitted });
|
|
740
|
+
total += omitted;
|
|
700
741
|
}
|
|
701
742
|
}
|
|
743
|
+
// Cache-safety gate: not worth busting the prompt cache for a trivial reclaim.
|
|
744
|
+
if (total < minReclaimBytes)
|
|
745
|
+
return 0;
|
|
746
|
+
// Worthwhile — apply the stubs.
|
|
747
|
+
let reclaimed = 0;
|
|
748
|
+
for (const { block, head, omitted } of targets) {
|
|
749
|
+
block.content = `${head}${PRUNE_MARKER}${omitted} chars of earlier tool output${PRUNE_MARKER_TAIL}`;
|
|
750
|
+
reclaimed += omitted;
|
|
751
|
+
}
|
|
702
752
|
return reclaimed;
|
|
703
753
|
}
|
|
704
754
|
/**
|
|
@@ -736,7 +786,14 @@ async function autoCompactMessages(messages, options, ledger) {
|
|
|
736
786
|
let summary = '';
|
|
737
787
|
try {
|
|
738
788
|
const reply = await (0, client_1.streamChat)([{ role: 'user', content: [{ type: 'text', text: summaryPrompt }] }], {
|
|
739
|
-
|
|
789
|
+
// COST: the summariser is a mechanical "bullet-point this transcript" task —
|
|
790
|
+
// its quality is indistinguishable across model tiers, so there is no reason
|
|
791
|
+
// to run it on the user's (possibly expensive) tier. Force 'turbo' (Sonnet 5):
|
|
792
|
+
// it is the cheapest 1M-context tier (in $2.0/1M vs Opus $2.5, Fable $3.0;
|
|
793
|
+
// out $15/1M vs $25 / $50), so this is always ≤ the user's cost, and its 1M
|
|
794
|
+
// window comfortably holds the transcript (capped at ~170K tokens by
|
|
795
|
+
// transcriptOf) even on the largest sessions.
|
|
796
|
+
model: 'turbo',
|
|
740
797
|
mode: 'ask', // summariser must not call tools; ask-mode discourages action
|
|
741
798
|
env: options.env,
|
|
742
799
|
clientType: options.clientType,
|
|
@@ -818,13 +875,20 @@ async function compactMessagesForResume(messages, opts) {
|
|
|
818
875
|
const contextWindow = MODEL_CONTEXT_TOKENS[opts.model ?? 'turbo'] ?? 1000000;
|
|
819
876
|
let bodyBytes = estimateBodyBytes(messages);
|
|
820
877
|
let tokenGuess = estimateTokensRough(messages);
|
|
821
|
-
|
|
822
|
-
|
|
878
|
+
// Prune fires at the EARLY threshold (mirrors the in-loop guard); summarisation
|
|
879
|
+
// only at the late one. On resume this matters most: a stored session is resent
|
|
880
|
+
// whole on the first turn, so shedding old tool_result bulk up front is exactly
|
|
881
|
+
// what stops that first message being billed at full size.
|
|
882
|
+
const overPruneThreshold = () => tokenGuess > contextWindow * AUTO_PRUNE_THRESHOLD || bodyBytes > MAX_BODY_BYTES;
|
|
883
|
+
const overCompactThreshold = () => tokenGuess > contextWindow * AUTO_COMPACT_THRESHOLD || bodyBytes > MAX_BODY_BYTES;
|
|
884
|
+
if (!overPruneThreshold())
|
|
823
885
|
return false;
|
|
824
886
|
let compacted = false;
|
|
825
|
-
// Cheap pass first — shrinks old tool_result blocks with no model call.
|
|
887
|
+
// Cheap pass first — shrinks old tool_result blocks with no model call. The
|
|
888
|
+
// reclaim floor is enforced atomically inside pruneOldToolResults (measures
|
|
889
|
+
// first, mutates only if worthwhile), so a declined prune leaves the cache intact.
|
|
826
890
|
if (messages.length > PRUNE_KEEP_RECENT + 2) {
|
|
827
|
-
const reclaimed = pruneOldToolResults(messages);
|
|
891
|
+
const reclaimed = pruneOldToolResults(messages, PRUNE_MIN_RECLAIM_BYTES);
|
|
828
892
|
if (reclaimed > 0) {
|
|
829
893
|
bodyBytes = estimateBodyBytes(messages);
|
|
830
894
|
tokenGuess = estimateTokensRough(messages);
|
|
@@ -832,11 +896,13 @@ async function compactMessagesForResume(messages, opts) {
|
|
|
832
896
|
opts.onNotice?.(`\n\u267b\ufe0f Trimmed ~${(reclaimed / (1024 * 1024)).toFixed(1)}MB of older tool output before resuming this chat.\n`);
|
|
833
897
|
}
|
|
834
898
|
}
|
|
835
|
-
// If still over threshold, fall through to summarising
|
|
836
|
-
// mechanism the in-loop guard uses, so this can safely loop
|
|
837
|
-
// summarisation pass may still leave a very long session over
|
|
899
|
+
// If still over the LATE (summarise) threshold, fall through to summarising
|
|
900
|
+
// compaction — same mechanism the in-loop guard uses, so this can safely loop
|
|
901
|
+
// (a single summarisation pass may still leave a very long session over it).
|
|
902
|
+
// A session between the prune and summarise thresholds is left as-is after the
|
|
903
|
+
// cheap prune: no model call needed, prefix already shrunk.
|
|
838
904
|
let guard = 0;
|
|
839
|
-
while (
|
|
905
|
+
while (overCompactThreshold() && messages.length > COMPACT_KEEP_MIN + 2 && guard < 5) {
|
|
840
906
|
guard += 1;
|
|
841
907
|
// No ledger at resume time — the ledger is per-run, in-memory, and would
|
|
842
908
|
// have been created fresh anyway since this is a new process/run. The
|
|
@@ -931,27 +997,43 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
931
997
|
for (; iteration < budget; iteration++) {
|
|
932
998
|
if (options.abortSignal?.aborted)
|
|
933
999
|
break;
|
|
934
|
-
// Auto-compact:
|
|
935
|
-
// hit the context-window wall — or the backend body-size limit —
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
//
|
|
941
|
-
//
|
|
1000
|
+
// Auto-compact: keep the resent prompt small BEFORE the next stream so we
|
|
1001
|
+
// never hit the context-window wall — or the backend body-size limit — and,
|
|
1002
|
+
// just as importantly, so we stop paying cache-read on an ever-growing prefix
|
|
1003
|
+
// every single turn on a long task. Runs at a turn boundary only.
|
|
1004
|
+
//
|
|
1005
|
+
// THREE nested triggers, cheapest/earliest first:
|
|
1006
|
+
// 1. PRUNE pressure — prompt crossed AUTO_PRUNE_THRESHOLD (~35% of window)
|
|
1007
|
+
// OR the body crossed MAX_BODY_BYTES. Handled by the CHEAP, structure-
|
|
1008
|
+
// preserving prune (no model call, keeps every turn). This is the big
|
|
1009
|
+
// cost win: it fires ~2× earlier than summarisation used to, shedding
|
|
1010
|
+
// already-consumed tool_result bulk (read_file/bash/grep output the
|
|
1011
|
+
// model has long since acted on) so the per-turn cache-read bill stops
|
|
1012
|
+
// compounding well before the old 80% wall.
|
|
1013
|
+
// 2. SUMMARISE pressure — prompt crossed AUTO_COMPACT_THRESHOLD (~80%) or
|
|
1014
|
+
// the body is STILL over MAX_BODY_BYTES after pruning. Only then do we
|
|
1015
|
+
// pay for a summariser call + drop whole turns (kept late on purpose:
|
|
1016
|
+
// summarise-of-summarise is what makes an agent "forget" earlier work).
|
|
1017
|
+
// The byte trigger also fires even on turn 0 of a resumed large session,
|
|
1018
|
+
// where lastPromptTokens is 0.
|
|
942
1019
|
let bodyBytes = estimateBodyBytes(messages);
|
|
1020
|
+
const prunePressure = lastPromptTokens > contextWindow * AUTO_PRUNE_THRESHOLD;
|
|
943
1021
|
const tokenPressure = lastPromptTokens > contextWindow * AUTO_COMPACT_THRESHOLD;
|
|
944
1022
|
let bytePressure = bodyBytes > MAX_BODY_BYTES;
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
// is the
|
|
949
|
-
|
|
950
|
-
|
|
1023
|
+
// Cheap prune first — on token OR byte pressure. Require a meaningful reclaim
|
|
1024
|
+
// (PRUNE_MIN_RECLAIM_BYTES): a tiny prune would bust the message-level prompt
|
|
1025
|
+
// cache (the pruned prefix changes) for almost no benefit. pruneOldToolResults
|
|
1026
|
+
// is idempotent, so once the old bulk is stubbed this simply no-ops until new
|
|
1027
|
+
// large tool_results age past PRUNE_KEEP_RECENT.
|
|
1028
|
+
if (autoCompact && !compacting && (prunePressure || bytePressure) && messages.length > PRUNE_KEEP_RECENT + 2) {
|
|
1029
|
+
// The gate lives INSIDE pruneOldToolResults now (atomic: it measures the
|
|
1030
|
+
// total first and mutates nothing if it's below the floor), so a declined
|
|
1031
|
+
// prune never invalidates the prompt cache.
|
|
1032
|
+
const reclaimed = pruneOldToolResults(messages, PRUNE_MIN_RECLAIM_BYTES);
|
|
951
1033
|
if (reclaimed > 0) {
|
|
952
1034
|
bodyBytes = estimateBodyBytes(messages);
|
|
953
1035
|
bytePressure = bodyBytes > MAX_BODY_BYTES;
|
|
954
|
-
options.onText(`\n\u267b\ufe0f Trimmed ~${(reclaimed / (1024 * 1024)).toFixed(1)}MB of
|
|
1036
|
+
options.onText(`\n\u267b\ufe0f Trimmed ~${(reclaimed / (1024 * 1024)).toFixed(1)}MB of already-processed tool output to keep this chat cheap to continue.\n`);
|
|
955
1037
|
}
|
|
956
1038
|
}
|
|
957
1039
|
if (autoCompact && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
|
package/dist/api/client.d.ts
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import type { Message, SSEEvent, AuthConfig, EnvContext, EditorContext } from '../types';
|
|
2
2
|
export declare const API_BASE = "https://api.nexrall.com";
|
|
3
|
+
/**
|
|
4
|
+
* Decide the final assistant `content` array to store in history for a completed
|
|
5
|
+
* turn. Pure + exported so it can be unit-tested without a live SSE stream.
|
|
6
|
+
*
|
|
7
|
+
* - `rebuilt`: the client's own reconstruction from streamed parts (text + tool_use,
|
|
8
|
+
* in stream order). This is what we've always stored.
|
|
9
|
+
* - `rawContent`: Anthropic's authoritative content array for the turn (thinking
|
|
10
|
+
* already stripped by the backend), when available.
|
|
11
|
+
*
|
|
12
|
+
* Rule: if the raw content contains ANY block beyond plain text / tool_use — i.e.
|
|
13
|
+
* server-side blocks like `server_tool_use`, `tool_search_tool_result` (with its
|
|
14
|
+
* nested `tool_reference`s) or `web_search_tool_result` — we MUST preserve the raw
|
|
15
|
+
* array verbatim so those blocks round-trip back to the API on the next request.
|
|
16
|
+
* Dropping them breaks the paired server_tool_use/result the API expects AND, for
|
|
17
|
+
* tool search, makes discovered (deferred) tools be forgotten → re-searched every
|
|
18
|
+
* turn. For an ordinary turn (only text/tool_use) we keep the rebuilt array so the
|
|
19
|
+
* common path is byte-identical to before (zero regression).
|
|
20
|
+
*/
|
|
21
|
+
export declare function chooseFinalContent<T extends {
|
|
22
|
+
type?: string;
|
|
23
|
+
}>(rebuilt: T[], rawContent: Array<{
|
|
24
|
+
type?: string;
|
|
25
|
+
}> | null | undefined): T[];
|
|
3
26
|
export interface StreamChatOptions {
|
|
4
27
|
model: string;
|
|
5
28
|
env?: EnvContext;
|
package/dist/api/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;AAIlD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAML;AAeD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA0BD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CAgclB;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAalD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBhF"}
|
package/dist/api/client.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.API_BASE = void 0;
|
|
7
|
+
exports.chooseFinalContent = chooseFinalContent;
|
|
7
8
|
exports.streamChat = streamChat;
|
|
8
9
|
exports.getBalance = getBalance;
|
|
9
10
|
exports.exchangeVscodeCode = exchangeVscodeCode;
|
|
@@ -14,6 +15,30 @@ const index_1 = require("../auth/index");
|
|
|
14
15
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
15
16
|
exports.API_BASE = 'https://api.nexrall.com';
|
|
16
17
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Decide the final assistant `content` array to store in history for a completed
|
|
20
|
+
* turn. Pure + exported so it can be unit-tested without a live SSE stream.
|
|
21
|
+
*
|
|
22
|
+
* - `rebuilt`: the client's own reconstruction from streamed parts (text + tool_use,
|
|
23
|
+
* in stream order). This is what we've always stored.
|
|
24
|
+
* - `rawContent`: Anthropic's authoritative content array for the turn (thinking
|
|
25
|
+
* already stripped by the backend), when available.
|
|
26
|
+
*
|
|
27
|
+
* Rule: if the raw content contains ANY block beyond plain text / tool_use — i.e.
|
|
28
|
+
* server-side blocks like `server_tool_use`, `tool_search_tool_result` (with its
|
|
29
|
+
* nested `tool_reference`s) or `web_search_tool_result` — we MUST preserve the raw
|
|
30
|
+
* array verbatim so those blocks round-trip back to the API on the next request.
|
|
31
|
+
* Dropping them breaks the paired server_tool_use/result the API expects AND, for
|
|
32
|
+
* tool search, makes discovered (deferred) tools be forgotten → re-searched every
|
|
33
|
+
* turn. For an ordinary turn (only text/tool_use) we keep the rebuilt array so the
|
|
34
|
+
* common path is byte-identical to before (zero regression).
|
|
35
|
+
*/
|
|
36
|
+
function chooseFinalContent(rebuilt, rawContent) {
|
|
37
|
+
if (!Array.isArray(rawContent))
|
|
38
|
+
return rebuilt;
|
|
39
|
+
const hasServerSideBlocks = rawContent.some((b) => b && b.type !== 'text' && b.type !== 'tool_use');
|
|
40
|
+
return hasServerSideBlocks ? rawContent : rebuilt;
|
|
41
|
+
}
|
|
17
42
|
function authHeaders() {
|
|
18
43
|
const token = (0, index_1.getToken)();
|
|
19
44
|
if (!token) {
|
|
@@ -279,28 +304,47 @@ async function streamChat(messages, options, onEvent) {
|
|
|
279
304
|
break;
|
|
280
305
|
}
|
|
281
306
|
case 'message_complete': {
|
|
282
|
-
//
|
|
307
|
+
// Anthropic's stop_reason (and the full raw content array) travel inside
|
|
308
|
+
// the nested `message` object the backend forwards verbatim (routes/code.js's
|
|
309
|
+
// `sendEvent(res, {type:'message_complete', message})` — `message` is the raw
|
|
310
|
+
// Anthropic SDK message with thinking already stripped, which always has
|
|
311
|
+
// stop_reason). 'max_tokens' means the model's output was cut off mid-
|
|
312
|
+
// generation — if the last content block is a tool_use, its `input` may be a
|
|
313
|
+
// truncated JSON object that still happened to parse (e.g. a multi_edit whose
|
|
314
|
+
// `edits` array lost its last, still-in-progress element, or came out empty/
|
|
315
|
+
// missing entirely) without any error at all. The agent loop uses this to
|
|
316
|
+
// refuse executing that block blindly instead of silently applying a partial edit.
|
|
317
|
+
const nestedMessage = evt.message;
|
|
318
|
+
const stopReason = typeof nestedMessage?.stop_reason === 'string' ? nestedMessage.stop_reason : null;
|
|
319
|
+
// Build the complete assistant message from accumulated parts (the common
|
|
320
|
+
// path: text + tool_use, in the client's own order).
|
|
283
321
|
const contentBlocks = [];
|
|
284
322
|
const fullText = textParts.join('');
|
|
285
323
|
if (fullText) {
|
|
286
324
|
contentBlocks.push({ type: 'text', text: fullText });
|
|
287
325
|
}
|
|
288
326
|
contentBlocks.push(...toolUseBlocks);
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
327
|
+
// TOOL-SEARCH / SERVER-SIDE-BLOCK PRESERVATION.
|
|
328
|
+
// The rebuild above keeps ONLY text + tool_use. That is lossy for any turn
|
|
329
|
+
// that also contains server-side blocks — `server_tool_use`,
|
|
330
|
+
// `tool_search_tool_result` (and its nested `tool_reference`s), or a
|
|
331
|
+
// `web_search_tool_result`. Those blocks MUST round-trip back to the API
|
|
332
|
+
// verbatim on the next request or (a) the API rejects the follow-up (it
|
|
333
|
+
// expects the paired server_tool_use/result to be present) and (b) for tool
|
|
334
|
+
// search specifically, the discovered (deferred) tools are FORGOTTEN, forcing
|
|
335
|
+
// Claude to re-search every turn — the exact opposite of the token saving.
|
|
336
|
+
//
|
|
337
|
+
// Preserve server-side blocks (tool_search_tool_result / server_tool_use /
|
|
338
|
+
// web_search_tool_result) verbatim when present; otherwise keep the rebuilt
|
|
339
|
+
// array. See chooseFinalContent for the full rationale. Zero regression for
|
|
340
|
+
// an ordinary text/tool_use turn.
|
|
341
|
+
const rawContent = Array.isArray(nestedMessage?.content)
|
|
342
|
+
? nestedMessage.content
|
|
343
|
+
: null;
|
|
344
|
+
const finalContent = chooseFinalContent(contentBlocks, rawContent);
|
|
301
345
|
completedMessage = {
|
|
302
346
|
role: 'assistant',
|
|
303
|
-
content:
|
|
347
|
+
content: finalContent,
|
|
304
348
|
stopReason,
|
|
305
349
|
};
|
|
306
350
|
onEvent({ type: 'message_complete', message: completedMessage });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.9",
|
|
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)",
|