@cjhyy/code-shell-core 0.6.0-rc.8 → 0.6.0-rc.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/context/compaction.d.ts +30 -0
- package/dist/context/compaction.js +93 -0
- package/dist/context/manager.d.ts +18 -0
- package/dist/context/manager.js +156 -44
- package/dist/context/token-counter.js +13 -0
- package/dist/engine/engine.d.ts +21 -11
- package/dist/engine/engine.js +232 -76
- package/dist/engine/model-facade.js +2 -12
- package/dist/engine/query.js +2 -0
- package/dist/engine/session-usage.d.ts +12 -0
- package/dist/engine/session-usage.js +56 -0
- package/dist/engine/steer-queue.d.ts +2 -1
- package/dist/engine/steer-queue.js +2 -2
- package/dist/engine/turn-loop.d.ts +28 -2
- package/dist/engine/turn-loop.js +153 -26
- package/dist/protocol/chat-session.d.ts +2 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/client.d.ts +4 -1
- package/dist/protocol/client.js +8 -2
- package/dist/protocol/server.d.ts +13 -12
- package/dist/protocol/server.js +83 -61
- package/dist/protocol/types.d.ts +4 -0
- package/dist/runtime/safe-spawn.js +74 -11
- package/dist/session/session-manager.js +7 -1
- package/dist/session/transcript.d.ts +4 -0
- package/dist/session/transcript.js +21 -0
- package/dist/tool-system/mcp-manager.js +17 -0
- package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
- package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
- package/dist/types.d.ts +31 -1
- package/package.json +1 -1
package/dist/engine/engine.js
CHANGED
|
@@ -11,7 +11,7 @@ import { applyDynamicToolDef } from "./dynamic-tool-defs.js";
|
|
|
11
11
|
import { getMergedCatalog } from "../model-catalog/index.js";
|
|
12
12
|
import { modelEntriesFromConnections } from "./model-connections-pool.js";
|
|
13
13
|
import { resolveAuxKey } from "./aux-key.js";
|
|
14
|
-
import { foldRunUsage } from "./session-usage.js";
|
|
14
|
+
import { addCumulativeUsage, cumulativeCacheHitRate, foldRunUsage, normalizeCumulativeUsageCounters, } from "./session-usage.js";
|
|
15
15
|
import { enqueueSteerItem, consumeSteerItems, removeSteerItem, } from "./steer-queue.js";
|
|
16
16
|
import { resolveSandboxConfig } from "./sandbox-config.js";
|
|
17
17
|
import { sandboxCacheKey } from "./sandbox-cache-key.js";
|
|
@@ -51,9 +51,9 @@ import { resolveAgentPreset, resolveBuiltinToolNames, } from "../preset/index.js
|
|
|
51
51
|
import { ModelPool } from "../llm/model-pool.js";
|
|
52
52
|
import { AgentDefinitionRegistry } from "../agent/agent-definition-registry.js";
|
|
53
53
|
import { defaultCacheDir } from "../llm/model-cache.js";
|
|
54
|
-
import { detectProviderFromApiKey, buildModelPool
|
|
54
|
+
import { detectProviderFromApiKey, buildModelPool } from "../onboarding.js";
|
|
55
55
|
import { detectPastedNoise } from "../utils/task-sanitizer.js";
|
|
56
|
-
import { parseTaskWithImages
|
|
56
|
+
import { parseTaskWithImages } from "./parse-task.js";
|
|
57
57
|
import { enforceImagePolicy, byteLengthFromBase64, dropOversizedImages, collectAttachedImagePaths, } from "./image-policy.js";
|
|
58
58
|
import { tryCompressImages } from "./image-compression.js";
|
|
59
59
|
import { buildSessionTitle } from "./session-title.js";
|
|
@@ -61,7 +61,7 @@ import { capabilitiesFor } from "../llm/capabilities/index.js";
|
|
|
61
61
|
import { MemoryOrchestrator } from "../services/memory-orchestrator.js";
|
|
62
62
|
import { runDreamConsolidation } from "../services/dream-consolidation.js";
|
|
63
63
|
import { join, isAbsolute } from "node:path";
|
|
64
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync
|
|
64
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
65
65
|
/**
|
|
66
66
|
* Build ScanOptions.compatFileNames from the user's instruction compat toggles.
|
|
67
67
|
* Primary file name stays hard-wired to CODESHELL.md (not exposed). Turning a
|
|
@@ -440,13 +440,15 @@ export class Engine {
|
|
|
440
440
|
// per-turn path can only hide, not add, so a freshly-`on`'d builtin not in
|
|
441
441
|
// the set needs a session restart to appear.
|
|
442
442
|
const builtinLists = effectiveBuiltinLists(config.enabledBuiltinTools ?? [], config.disabledBuiltinTools ?? [], this.readBuiltinOverride(config.cwd));
|
|
443
|
-
this.toolRegistry =
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
443
|
+
this.toolRegistry =
|
|
444
|
+
config.runtime?.toolRegistry ??
|
|
445
|
+
new ToolRegistry({
|
|
446
|
+
builtinTools: resolveBuiltinToolNames({
|
|
447
|
+
preset: this.preset.name,
|
|
448
|
+
enabledBuiltinTools: builtinLists.enabledBuiltinTools,
|
|
449
|
+
disabledBuiltinTools: builtinLists.disabledBuiltinTools,
|
|
450
|
+
}),
|
|
451
|
+
});
|
|
450
452
|
this.hooks = new HookRegistry();
|
|
451
453
|
// Installed-plugin hooks — declared in each plugin's hooks/hooks.json.
|
|
452
454
|
// Registered first (priority 80) so user-authored hooks at lower
|
|
@@ -637,23 +639,44 @@ export class Engine {
|
|
|
637
639
|
* Queue a user message to be spliced into the in-flight run for `sessionId`
|
|
638
640
|
* at the next turn-loop step boundary — the 不打断 steering path (vs cancel +
|
|
639
641
|
* resend). General-purpose: any host path (UI 引导, future agent coordination,
|
|
640
|
-
* external triggers) can call it. If no run is active for
|
|
641
|
-
*
|
|
642
|
-
*
|
|
642
|
+
* external triggers) can call it. If no run is active for this session, reject
|
|
643
|
+
* without queueing so the host can downgrade to a normal run immediately.
|
|
644
|
+
* No-op on blank text.
|
|
643
645
|
*
|
|
644
646
|
* `id` is the host's stable queue-entry id. It rides through to the
|
|
645
647
|
* `steer_injected` event (so the host can match the injected bubble back to
|
|
646
648
|
* the queued draft) and is the handle `unsteer` uses to revoke a still-pending
|
|
647
649
|
* entry. A blank id is tolerated but means the entry can't be revoked.
|
|
648
650
|
*/
|
|
649
|
-
enqueueSteer(sessionId, text, id = "") {
|
|
650
|
-
if (!sessionId)
|
|
651
|
-
return;
|
|
651
|
+
enqueueSteer(sessionId, text, id = "", clientMessageId) {
|
|
652
652
|
const q = this.steerQueueBySid.get(sessionId) ?? [];
|
|
653
|
-
const
|
|
653
|
+
const entryId = id || `steer-${q.length}`;
|
|
654
|
+
if (!sessionId)
|
|
655
|
+
return { accepted: false, id: entryId };
|
|
656
|
+
const activeRunSessionId = this.activeRunSession?.state.sessionId;
|
|
657
|
+
const active = this.activeTurnLoop !== null && activeRunSessionId === sessionId;
|
|
658
|
+
if (!active) {
|
|
659
|
+
logger.info("steer.enqueue.idle_rejected", {
|
|
660
|
+
sessionId,
|
|
661
|
+
id: entryId,
|
|
662
|
+
clientMessageId,
|
|
663
|
+
activeRunSessionId: activeRunSessionId ?? null,
|
|
664
|
+
queueLength: q.length,
|
|
665
|
+
});
|
|
666
|
+
return { accepted: false, id: entryId };
|
|
667
|
+
}
|
|
668
|
+
const next = enqueueSteerItem(q, entryId, text, clientMessageId);
|
|
654
669
|
if (next === q)
|
|
655
|
-
return; // blank text dropped
|
|
670
|
+
return { accepted: false, id: entryId }; // blank text dropped
|
|
656
671
|
this.steerQueueBySid.set(sessionId, next);
|
|
672
|
+
logger.info("steer.enqueue.accepted", {
|
|
673
|
+
sessionId,
|
|
674
|
+
id: entryId,
|
|
675
|
+
clientMessageId,
|
|
676
|
+
activeRunSessionId,
|
|
677
|
+
queueLength: next.length,
|
|
678
|
+
});
|
|
679
|
+
return { accepted: true, id: entryId };
|
|
657
680
|
}
|
|
658
681
|
/**
|
|
659
682
|
* Revoke a still-pending steer entry (the 撤回 path). Returns true if it was
|
|
@@ -670,12 +693,20 @@ export class Engine {
|
|
|
670
693
|
return removed;
|
|
671
694
|
}
|
|
672
695
|
/** Drain + clear the steer queue for a session (turn loop consumes per step). */
|
|
673
|
-
consumeSteer(sessionId) {
|
|
696
|
+
consumeSteer(sessionId, source = "normal_step") {
|
|
674
697
|
const q = this.steerQueueBySid.get(sessionId);
|
|
675
698
|
if (!q || q.length === 0)
|
|
676
699
|
return [];
|
|
677
700
|
const { drained, rest } = consumeSteerItems(q);
|
|
678
701
|
this.steerQueueBySid.set(sessionId, rest);
|
|
702
|
+
logger.info("steer.consume.drained", {
|
|
703
|
+
sessionId,
|
|
704
|
+
source,
|
|
705
|
+
count: drained.length,
|
|
706
|
+
ids: drained.map((item) => item.id),
|
|
707
|
+
clientMessageIds: drained.flatMap((item) => item.clientMessageId ? [item.clientMessageId] : []),
|
|
708
|
+
queueLength: rest.length,
|
|
709
|
+
});
|
|
679
710
|
return drained;
|
|
680
711
|
}
|
|
681
712
|
/** Wire the cookie→browser injection callback (InjectCredential tool). Same
|
|
@@ -916,9 +947,8 @@ export class Engine {
|
|
|
916
947
|
enabledBuiltinTools: childEnabled,
|
|
917
948
|
disabledBuiltinTools: childDisabled,
|
|
918
949
|
customSystemPrompt: this.config.customSystemPrompt,
|
|
919
|
-
appendSystemPrompt: [this.config.appendSystemPrompt, req.appendSystemPrompt]
|
|
920
|
-
|
|
921
|
-
.join("\n\n") || undefined,
|
|
950
|
+
appendSystemPrompt: [this.config.appendSystemPrompt, req.appendSystemPrompt].filter(Boolean).join("\n\n") ||
|
|
951
|
+
undefined,
|
|
922
952
|
responseLanguage: this.config.responseLanguage,
|
|
923
953
|
userProfile: this.config.userProfile,
|
|
924
954
|
instructions: this.config.instructions,
|
|
@@ -1103,6 +1133,23 @@ export class Engine {
|
|
|
1103
1133
|
// call) instead of a try/catch on resume.
|
|
1104
1134
|
let session;
|
|
1105
1135
|
let messages;
|
|
1136
|
+
let freshImageMessage;
|
|
1137
|
+
const claimedClientMessageIds = new Set();
|
|
1138
|
+
const claimClientMessageId = (bundle, clientMessageId, source) => {
|
|
1139
|
+
if (!clientMessageId)
|
|
1140
|
+
return true;
|
|
1141
|
+
if (claimedClientMessageIds.has(clientMessageId) ||
|
|
1142
|
+
bundle.transcript.hasClientMessageId(clientMessageId)) {
|
|
1143
|
+
logger.info("engine.client_message.duplicate_ignored", {
|
|
1144
|
+
sessionId: bundle.state.sessionId,
|
|
1145
|
+
clientMessageId,
|
|
1146
|
+
source,
|
|
1147
|
+
});
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
claimedClientMessageIds.add(clientMessageId);
|
|
1151
|
+
return true;
|
|
1152
|
+
};
|
|
1106
1153
|
if (options?.sessionId && this.sessionManager.exists(options.sessionId)) {
|
|
1107
1154
|
session = this.sessionManager.resume(options.sessionId);
|
|
1108
1155
|
const cachedCompacted = this.compactedMessagesBySession.get(options.sessionId);
|
|
@@ -1126,8 +1173,31 @@ export class Engine {
|
|
|
1126
1173
|
}
|
|
1127
1174
|
// Append new user message
|
|
1128
1175
|
const userMsg = { role: "user", content: userMessageContent };
|
|
1176
|
+
if (!claimClientMessageId(session, options?.clientMessageId, "submit")) {
|
|
1177
|
+
const usage = session.state.tokenUsage ?? {
|
|
1178
|
+
promptTokens: 0,
|
|
1179
|
+
completionTokens: 0,
|
|
1180
|
+
totalTokens: 0,
|
|
1181
|
+
};
|
|
1182
|
+
return {
|
|
1183
|
+
text: "",
|
|
1184
|
+
reason: "completed",
|
|
1185
|
+
sessionId: session.state.sessionId,
|
|
1186
|
+
turnCount: session.state.turnCount ?? 0,
|
|
1187
|
+
usage: {
|
|
1188
|
+
promptTokens: usage.promptTokens ?? 0,
|
|
1189
|
+
completionTokens: usage.completionTokens ?? 0,
|
|
1190
|
+
totalTokens: usage.totalTokens ?? 0,
|
|
1191
|
+
},
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
if (parsedTask.hasImages)
|
|
1195
|
+
freshImageMessage = userMsg;
|
|
1129
1196
|
messages.push(userMsg);
|
|
1130
|
-
session.transcript.appendMessage("user", userMessageContent, {
|
|
1197
|
+
session.transcript.appendMessage("user", userMessageContent, {
|
|
1198
|
+
injected: options?.injected === true,
|
|
1199
|
+
clientMessageId: options?.clientMessageId,
|
|
1200
|
+
});
|
|
1131
1201
|
// Flush "active" status to disk immediately. resume() set it in memory
|
|
1132
1202
|
// (session-manager.ts), but without this write the on-disk state.json
|
|
1133
1203
|
// still shows the previous run's terminal reason — so any external
|
|
@@ -1139,13 +1209,20 @@ export class Engine {
|
|
|
1139
1209
|
// Cold start: shape (2) reuses the host-supplied sid; shape (3)
|
|
1140
1210
|
// lets sessionManager generate one with nanoid.
|
|
1141
1211
|
session = this.sessionManager.create(cwd, this.config.llm.model, this.config.llm.provider, options?.sessionId, this.config.isSubAgent === true ? getCurrentSid() : undefined, this.config.isSubAgent === true ? "subagent" : this.config.origin);
|
|
1142
|
-
|
|
1143
|
-
session
|
|
1212
|
+
const userMsg = { role: "user", content: userMessageContent };
|
|
1213
|
+
claimClientMessageId(session, options?.clientMessageId, "submit");
|
|
1214
|
+
if (parsedTask.hasImages)
|
|
1215
|
+
freshImageMessage = userMsg;
|
|
1216
|
+
messages = [userMsg];
|
|
1217
|
+
session.transcript.appendMessage("user", userMessageContent, {
|
|
1218
|
+
clientMessageId: options?.clientMessageId,
|
|
1219
|
+
});
|
|
1144
1220
|
// Save first user message as session summary — text only. The summary
|
|
1145
1221
|
// shows up in the session list; "[image]" is more informative than a
|
|
1146
1222
|
// truncated `[object Object]` when the prompt was purely visual.
|
|
1147
1223
|
const summarySrc = parsedTask.hasImages
|
|
1148
|
-
? parsedTask.text ||
|
|
1224
|
+
? parsedTask.text ||
|
|
1225
|
+
`[image${parsedTask.images.length > 1 ? `s × ${parsedTask.images.length}` : ""}]`
|
|
1149
1226
|
: taskText;
|
|
1150
1227
|
session.state.summary = summarySrc.slice(0, 80).replace(/\n/g, " ");
|
|
1151
1228
|
this.sessionManager.saveState(session.state);
|
|
@@ -1483,26 +1560,27 @@ export class Engine {
|
|
|
1483
1560
|
// run would be evaluated fresh and might get a different replacement
|
|
1484
1561
|
// string than the one already in the message, breaking idempotency.
|
|
1485
1562
|
contextManager.initReplacementStateFromMessages(messages);
|
|
1486
|
-
//
|
|
1487
|
-
//
|
|
1488
|
-
//
|
|
1489
|
-
//
|
|
1490
|
-
//
|
|
1491
|
-
//
|
|
1563
|
+
// Two summarizers with DIFFERENT quality needs:
|
|
1564
|
+
//
|
|
1565
|
+
// 1. Context-compaction summary (setSummarizeFn) → PRIMARY model. This
|
|
1566
|
+
// condenses many rounds into the running summary that REPLACES the real
|
|
1567
|
+
// history; a dropped decision makes the conversation "forget" and poisons
|
|
1568
|
+
// every subsequent turn. It fires only near the compact ratio (~0.85), so
|
|
1569
|
+
// it's infrequent — quality far outweighs the occasional extra cost of a
|
|
1570
|
+
// primary-model call. (Manual /compact uses the primary for the same
|
|
1571
|
+
// reason; see forceCompact.)
|
|
1572
|
+
//
|
|
1573
|
+
// 2. Tool-use one-liner summaries (modelFacade.summarize below) → AUX model.
|
|
1574
|
+
// These are tiny throwaway outputs ("Wrote design doc") fired every turn;
|
|
1575
|
+
// that high-frequency, low-stakes chore is exactly what aux is for.
|
|
1492
1576
|
const auxSummaryClient = await this.resolveAuxClient(llmClient);
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
// this flips thinking off (~3x faster, fewer tokens); on every other
|
|
1501
|
-
// OpenAI-compatible provider the field is ignored.
|
|
1502
|
-
reasoning: { mode: "off" },
|
|
1503
|
-
});
|
|
1504
|
-
return summaryResponse.text;
|
|
1505
|
-
});
|
|
1577
|
+
Object.assign(session.state, normalizeCumulativeUsageCounters(session.state, session.state.tokenUsage));
|
|
1578
|
+
const recordCumulativeUsage = (usage) => {
|
|
1579
|
+
const next = addCumulativeUsage(session.state, usage);
|
|
1580
|
+
Object.assign(session.state, next);
|
|
1581
|
+
return next;
|
|
1582
|
+
};
|
|
1583
|
+
contextManager.setSummarizeFn(this.buildSummarizeFn(llmClient, recordCumulativeUsage));
|
|
1506
1584
|
// Create components (requires resolved llmClient).
|
|
1507
1585
|
const modelFacade = new ModelFacade(llmClient, session.transcript);
|
|
1508
1586
|
// Session-cumulative usage baseline: the LLM client is recreated per run
|
|
@@ -1676,7 +1754,9 @@ export class Engine {
|
|
|
1676
1754
|
pendingCompactInfo = null;
|
|
1677
1755
|
return info;
|
|
1678
1756
|
},
|
|
1679
|
-
consumeSteer: () => this.consumeSteer(sid),
|
|
1757
|
+
consumeSteer: (source) => this.consumeSteer(sid, source),
|
|
1758
|
+
claimClientMessageId: (clientMessageId, source) => claimClientMessageId(session, clientMessageId, source),
|
|
1759
|
+
recordCumulativeUsage,
|
|
1680
1760
|
// Clear the persisted goal for a self-reported completion / confirmed
|
|
1681
1761
|
// cancel. Clears the in-RAM session's activeGoal (so THIS run's later
|
|
1682
1762
|
// turns don't re-arm) AND persists it, and drops the in-flight stop
|
|
@@ -1715,6 +1795,7 @@ export class Engine {
|
|
|
1715
1795
|
maxToolCallsPerTurn: this.config.maxToolCallsPerTurn ?? 25,
|
|
1716
1796
|
onStream: options?.onStream,
|
|
1717
1797
|
signal: options?.signal,
|
|
1798
|
+
freshImageMessages: freshImageMessage ? [freshImageMessage] : undefined,
|
|
1718
1799
|
// Goal mode: the active goal is surfaced to the on_stop handler via
|
|
1719
1800
|
// ctx.data.goal; the GoalStopHook (registered above) judges it.
|
|
1720
1801
|
goal: normalizedGoal,
|
|
@@ -1727,16 +1808,23 @@ export class Engine {
|
|
|
1727
1808
|
// baseline + this run's running total (idempotent per boundary,
|
|
1728
1809
|
// accumulates across runs; carries cacheRead/cacheCreation too).
|
|
1729
1810
|
session.state.tokenUsage = foldRunUsage(usageBaseline, modelFacade.getUsage());
|
|
1730
|
-
// Surface the session
|
|
1731
|
-
//
|
|
1732
|
-
//
|
|
1733
|
-
const
|
|
1811
|
+
// Surface the whole-session monotonic cache counts to the UI.
|
|
1812
|
+
// Separate from turn-loop's authoritative per-response emit (which
|
|
1813
|
+
// drives the live context reading and single-turn metric).
|
|
1814
|
+
const cumulative = normalizeCumulativeUsageCounters(session.state, session.state.tokenUsage);
|
|
1815
|
+
const cumulativeHitRate = cumulativeCacheHitRate(cumulative);
|
|
1734
1816
|
options?.onStream?.({
|
|
1735
1817
|
type: "usage_update",
|
|
1736
|
-
promptTokens:
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1818
|
+
promptTokens: cumulative.cumulativePromptTokens,
|
|
1819
|
+
cumulativePromptTokens: cumulative.cumulativePromptTokens,
|
|
1820
|
+
cumulativeCacheReadTokens: cumulative.cumulativeCacheReadTokens,
|
|
1821
|
+
cumulativeCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
|
|
1822
|
+
...(cumulativeHitRate !== undefined
|
|
1823
|
+
? { cumulativeCacheHitRate: cumulativeHitRate }
|
|
1824
|
+
: {}),
|
|
1825
|
+
sessionPromptTokens: cumulative.cumulativePromptTokens,
|
|
1826
|
+
sessionCacheReadTokens: cumulative.cumulativeCacheReadTokens,
|
|
1827
|
+
sessionCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
|
|
1740
1828
|
});
|
|
1741
1829
|
if (this.config.costStore) {
|
|
1742
1830
|
session.state.costState = this.config.costStore.serialize();
|
|
@@ -1940,6 +2028,28 @@ export class Engine {
|
|
|
1940
2028
|
* active run's client) when unset, unknown, or on any build failure — aux
|
|
1941
2029
|
* work is best-effort and must never break a run.
|
|
1942
2030
|
*/
|
|
2031
|
+
/**
|
|
2032
|
+
* Build the SummarizeFn used for context compaction. Extracted so both the
|
|
2033
|
+
* run path and forceCompact share one definition of the summarization call.
|
|
2034
|
+
*/
|
|
2035
|
+
buildSummarizeFn(auxSummaryClient, recordCumulativeUsage) {
|
|
2036
|
+
return async (prompt) => {
|
|
2037
|
+
const summaryResponse = await auxSummaryClient.createMessage({
|
|
2038
|
+
systemPrompt: "You are a conversation summarizer. Be concise and factual.",
|
|
2039
|
+
messages: [{ role: "user", content: prompt }],
|
|
2040
|
+
tools: [],
|
|
2041
|
+
maxTokens: 1024,
|
|
2042
|
+
// Auxiliary call — no need to burn reasoning tokens. On DeepSeek V4
|
|
2043
|
+
// this flips thinking off (~3x faster, fewer tokens); on every other
|
|
2044
|
+
// OpenAI-compatible provider the field is ignored.
|
|
2045
|
+
reasoning: { mode: "off" },
|
|
2046
|
+
});
|
|
2047
|
+
if (summaryResponse.usage) {
|
|
2048
|
+
recordCumulativeUsage?.(summaryResponse.usage);
|
|
2049
|
+
}
|
|
2050
|
+
return summaryResponse.text;
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
1943
2053
|
async resolveAuxClient(fallback) {
|
|
1944
2054
|
let auxKey;
|
|
1945
2055
|
try {
|
|
@@ -2005,7 +2115,9 @@ export class Engine {
|
|
|
2005
2115
|
// extraction, which then padded the memory store with low-signal
|
|
2006
2116
|
// entries. 8 messages is roughly "more than a single back-and-forth"
|
|
2007
2117
|
// — substantive enough to be worth a durable note.
|
|
2008
|
-
const messages = transcript
|
|
2118
|
+
const messages = transcript
|
|
2119
|
+
.toMessages()
|
|
2120
|
+
.filter((m) => m.role === "user" || m.role === "assistant");
|
|
2009
2121
|
if (messages.length < 8)
|
|
2010
2122
|
return;
|
|
2011
2123
|
// Memory orchestrator + dream-loop calls are auxiliary LLM calls
|
|
@@ -2107,10 +2219,8 @@ export class Engine {
|
|
|
2107
2219
|
return entry;
|
|
2108
2220
|
}
|
|
2109
2221
|
/**
|
|
2110
|
-
* Zero
|
|
2111
|
-
*
|
|
2112
|
-
* cache-hit stats from the prior model are no longer meaningful. The next
|
|
2113
|
-
* run's baseline (snapshotted from state.tokenUsage) then starts from zero.
|
|
2222
|
+
* Zero the legacy/model-scoped token/cache usage window on disk. The
|
|
2223
|
+
* whole-session cumulative counters are intentionally left alone.
|
|
2114
2224
|
*/
|
|
2115
2225
|
resetSessionUsage(sessionId) {
|
|
2116
2226
|
const zero = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
@@ -2182,7 +2292,9 @@ export class Engine {
|
|
|
2182
2292
|
try {
|
|
2183
2293
|
chmodSync(file, 0o600);
|
|
2184
2294
|
}
|
|
2185
|
-
catch {
|
|
2295
|
+
catch {
|
|
2296
|
+
/* best-effort */
|
|
2297
|
+
}
|
|
2186
2298
|
}
|
|
2187
2299
|
catch (err) {
|
|
2188
2300
|
logger.warn(`persistActiveModel failed: ${err.message}`);
|
|
@@ -2254,8 +2366,14 @@ export class Engine {
|
|
|
2254
2366
|
// The builtin tool SET is ctor-frozen and may be shared via runtime — we
|
|
2255
2367
|
// do NOT rebuild it here. If the new preset implies a different builtin
|
|
2256
2368
|
// tool set, that part of the change only lands on session restart.
|
|
2257
|
-
const prevTools = resolveBuiltinToolNames({ preset: prevPresetName })
|
|
2258
|
-
|
|
2369
|
+
const prevTools = resolveBuiltinToolNames({ preset: prevPresetName })
|
|
2370
|
+
.slice()
|
|
2371
|
+
.sort()
|
|
2372
|
+
.join(",");
|
|
2373
|
+
const nextTools = resolveBuiltinToolNames({ preset: nextPreset.name })
|
|
2374
|
+
.slice()
|
|
2375
|
+
.sort()
|
|
2376
|
+
.join(",");
|
|
2259
2377
|
if (prevTools !== nextTools) {
|
|
2260
2378
|
logger.warn("engine.preset_reload.tool_set_change_needs_restart", {
|
|
2261
2379
|
from: prevPresetName,
|
|
@@ -2354,14 +2472,13 @@ export class Engine {
|
|
|
2354
2472
|
* Force context compaction on a session.
|
|
2355
2473
|
* Returns token stats before/after.
|
|
2356
2474
|
*/
|
|
2357
|
-
forceCompact(sessionId) {
|
|
2475
|
+
async forceCompact(sessionId) {
|
|
2358
2476
|
const effectiveSessionId = sessionId ?? this.lastSessionId;
|
|
2359
2477
|
if (!effectiveSessionId) {
|
|
2360
2478
|
return { before: 0, after: 0, strategy: "none (no active session)" };
|
|
2361
2479
|
}
|
|
2362
2480
|
const session = this.sessionManager.resume(effectiveSessionId);
|
|
2363
|
-
const sourceMessages = this.compactedMessagesBySession.get(effectiveSessionId) ??
|
|
2364
|
-
session.transcript.toMessages();
|
|
2481
|
+
const sourceMessages = this.compactedMessagesBySession.get(effectiveSessionId) ?? session.transcript.toMessages();
|
|
2365
2482
|
const before = estimateTokens(sourceMessages);
|
|
2366
2483
|
let contextManager = this.lastContextManager;
|
|
2367
2484
|
if (!contextManager || this.lastSessionId !== effectiveSessionId) {
|
|
@@ -2373,7 +2490,45 @@ export class Engine {
|
|
|
2373
2490
|
contextManager.initReplacementStateFromMessages(sourceMessages);
|
|
2374
2491
|
this.lastContextManager = contextManager;
|
|
2375
2492
|
}
|
|
2376
|
-
|
|
2493
|
+
// Manual /compact emits its UI boundary at the protocol layer from the
|
|
2494
|
+
// final before/after result. Capture the tier here, but avoid reusing a
|
|
2495
|
+
// stale run callback retained on lastContextManager, which could otherwise
|
|
2496
|
+
// double-emit.
|
|
2497
|
+
let compactStrategy;
|
|
2498
|
+
contextManager.setOnCompact((info) => {
|
|
2499
|
+
if (info.after < info.before)
|
|
2500
|
+
compactStrategy = info.strategy;
|
|
2501
|
+
});
|
|
2502
|
+
// Manual /compact = maximum compaction NOW. The automatic ladder waits for
|
|
2503
|
+
// compactAtRatio (0.85 * window), so on a 1M-window model an 800k text-only
|
|
2504
|
+
// conversation sits under the gate and manage() only runs a no-op micro.
|
|
2505
|
+
// Wire a summarizeFn (the run path does this per-run; a cold forceCompact on
|
|
2506
|
+
// a resumed-but-never-run session has none) and call forceSummarize, which
|
|
2507
|
+
// ignores the ratio gate and always summarizes (falling back to snip/window).
|
|
2508
|
+
//
|
|
2509
|
+
// Use the PRIMARY model, not the aux model. Automatic background compaction
|
|
2510
|
+
// routes to aux to keep the high-frequency path cheap, but summarization is
|
|
2511
|
+
// a high-fidelity task (drop a decision and the conversation "forgets"), and
|
|
2512
|
+
// a manual /compact is a low-frequency, user-initiated request for quality.
|
|
2513
|
+
// The aux model is sized for tiny outputs (titles, memory extraction), so
|
|
2514
|
+
// downgrading the one compaction the user explicitly asked for is backwards.
|
|
2515
|
+
try {
|
|
2516
|
+
const primaryClient = await createLLMClient(this.config.llm, this.config.clientDefaults);
|
|
2517
|
+
Object.assign(session.state, normalizeCumulativeUsageCounters(session.state, session.state.tokenUsage));
|
|
2518
|
+
const recordCompactUsage = (usage) => {
|
|
2519
|
+
const next = addCumulativeUsage(session.state, usage);
|
|
2520
|
+
Object.assign(session.state, next);
|
|
2521
|
+
this.sessionManager.saveState(session.state);
|
|
2522
|
+
return next;
|
|
2523
|
+
};
|
|
2524
|
+
contextManager.setSummarizeFn(this.buildSummarizeFn(primaryClient, recordCompactUsage));
|
|
2525
|
+
}
|
|
2526
|
+
catch (err) {
|
|
2527
|
+
logger.warn("engine.force_compact_client_failed", {
|
|
2528
|
+
error: err.message,
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
const compacted = await contextManager.forceSummarize(sourceMessages);
|
|
2377
2532
|
const after = estimateTokens(compacted);
|
|
2378
2533
|
this.compactedMessagesBySession.set(effectiveSessionId, compacted);
|
|
2379
2534
|
this.lastSessionId = effectiveSessionId;
|
|
@@ -2381,7 +2536,7 @@ export class Engine {
|
|
|
2381
2536
|
return {
|
|
2382
2537
|
before,
|
|
2383
2538
|
after,
|
|
2384
|
-
strategy:
|
|
2539
|
+
strategy: after >= before ? "no compaction needed" : (compactStrategy ?? "compacted"),
|
|
2385
2540
|
};
|
|
2386
2541
|
}
|
|
2387
2542
|
stripUserContextMessage(messages, userContextMsg) {
|
|
@@ -2475,7 +2630,11 @@ export class Engine {
|
|
|
2475
2630
|
backend = interactive;
|
|
2476
2631
|
}
|
|
2477
2632
|
else {
|
|
2478
|
-
backend = new HeadlessApprovalBackend(mode === "bypassPermissions"
|
|
2633
|
+
backend = new HeadlessApprovalBackend(mode === "bypassPermissions"
|
|
2634
|
+
? "approve-all"
|
|
2635
|
+
: mode === "dontAsk"
|
|
2636
|
+
? "deny-all"
|
|
2637
|
+
: "deny-all");
|
|
2479
2638
|
}
|
|
2480
2639
|
}
|
|
2481
2640
|
return { rules, backend };
|
|
@@ -2517,7 +2676,8 @@ export class Engine {
|
|
|
2517
2676
|
* rule set buildPermissionConfig does, without constructing a backend.
|
|
2518
2677
|
*/
|
|
2519
2678
|
getPermissionRules() {
|
|
2520
|
-
return this.buildPermissionConfig(this.getPermissionMode(), this.config.cwd ?? process.cwd())
|
|
2679
|
+
return this.buildPermissionConfig(this.getPermissionMode(), this.config.cwd ?? process.cwd())
|
|
2680
|
+
.rules;
|
|
2521
2681
|
}
|
|
2522
2682
|
/**
|
|
2523
2683
|
* Toggle plan mode directly. Called by the Plan tool (Task 7) via ToolContext.engine.
|
|
@@ -2604,12 +2764,8 @@ export class Engine {
|
|
|
2604
2764
|
getAgentDefinitions(cwd) {
|
|
2605
2765
|
const disabledAgents = this.readDisabledAgents(cwd);
|
|
2606
2766
|
const disabledPlugins = this.readDisabledLists().disabledPlugins;
|
|
2607
|
-
const disabledKey = [...disabledAgents, "::", ...disabledPlugins]
|
|
2608
|
-
|
|
2609
|
-
.sort()
|
|
2610
|
-
.join(" ");
|
|
2611
|
-
if (this.agentDefsCache?.cwd !== cwd ||
|
|
2612
|
-
this.agentDefsCache.disabledKey !== disabledKey) {
|
|
2767
|
+
const disabledKey = [...disabledAgents, "::", ...disabledPlugins].slice().sort().join(" ");
|
|
2768
|
+
if (this.agentDefsCache?.cwd !== cwd || this.agentDefsCache.disabledKey !== disabledKey) {
|
|
2613
2769
|
this.agentDefsCache = {
|
|
2614
2770
|
cwd,
|
|
2615
2771
|
disabledKey,
|
|
@@ -5,6 +5,7 @@ import { logger, getCurrentSid } from "../logging/logger.js";
|
|
|
5
5
|
import { recordLLMError, recordLLMRequest, recordLLMResponse, } from "../logging/session-recorder.js";
|
|
6
6
|
import { sanitizeMessages } from "../logging/sanitize-messages.js";
|
|
7
7
|
import { addAPIDuration, addToModelUsage, addInputTokens, addOutputTokens } from "../state.js";
|
|
8
|
+
import { cacheHitRateFromUsage } from "./session-usage.js";
|
|
8
9
|
let _reqSeq = 0;
|
|
9
10
|
function nextReqId() {
|
|
10
11
|
_reqSeq += 1;
|
|
@@ -20,18 +21,7 @@ function nextReqId() {
|
|
|
20
21
|
* any caching tuning (docs/todo/prompt-cache-optimization.md §四 step 1).
|
|
21
22
|
*/
|
|
22
23
|
export function cacheHitRate(usage) {
|
|
23
|
-
|
|
24
|
-
return undefined;
|
|
25
|
-
const read = usage.cacheReadTokens ?? 0;
|
|
26
|
-
const creation = usage.cacheCreationTokens ?? 0;
|
|
27
|
-
if (read === 0 && creation === 0)
|
|
28
|
-
return undefined;
|
|
29
|
-
const prompt = usage.promptTokens ?? 0;
|
|
30
|
-
const uncached = Math.max(0, prompt - read - creation);
|
|
31
|
-
const denom = read + creation + uncached;
|
|
32
|
-
if (denom === 0)
|
|
33
|
-
return undefined;
|
|
34
|
-
return read / denom;
|
|
24
|
+
return cacheHitRateFromUsage(usage);
|
|
35
25
|
}
|
|
36
26
|
export class ModelFacade {
|
|
37
27
|
client;
|
package/dist/engine/query.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { TurnLoop } from "./turn-loop.js";
|
|
11
11
|
import { logger } from "../logging/logger.js";
|
|
12
|
+
import { messageHasBase64ImagePayload } from "../context/compaction.js";
|
|
12
13
|
// ─── Query generator ────────────────────────────────────────────────
|
|
13
14
|
/**
|
|
14
15
|
* Run an agentic query and yield stream events as they occur.
|
|
@@ -37,6 +38,7 @@ export async function* query(params) {
|
|
|
37
38
|
maxToolCallsPerTurn,
|
|
38
39
|
onStream,
|
|
39
40
|
signal,
|
|
41
|
+
freshImageMessages: messages.filter(messageHasBase64ImagePayload),
|
|
40
42
|
};
|
|
41
43
|
// Local overhead store — query() is a standalone entry point without a real
|
|
42
44
|
// session id, so per-call in-memory state is enough.
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { TokenUsage } from "../types.js";
|
|
2
2
|
import type { LLMUsageTracker } from "../llm/types.js";
|
|
3
|
+
export interface CumulativeUsageCounters {
|
|
4
|
+
cumulativePromptTokens: number;
|
|
5
|
+
cumulativeCacheReadTokens: number;
|
|
6
|
+
cumulativeCacheCreationTokens: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function emptyCumulativeUsageCounters(): CumulativeUsageCounters;
|
|
9
|
+
export declare function normalizeCumulativeUsageCounters(counters: Partial<CumulativeUsageCounters> | undefined, legacyUsage?: TokenUsage): CumulativeUsageCounters;
|
|
10
|
+
export declare function addCumulativeUsage(counters: Partial<CumulativeUsageCounters> | undefined, usage: TokenUsage): CumulativeUsageCounters;
|
|
11
|
+
export declare function addTokenUsage(left: TokenUsage, right: TokenUsage): TokenUsage;
|
|
12
|
+
export declare function cacheHitRateFromTokens(promptTokens: number, cacheReadTokens: number | undefined, cacheCreationTokens: number | undefined): number | undefined;
|
|
13
|
+
export declare function cacheHitRateFromUsage(usage: TokenUsage | undefined): number | undefined;
|
|
14
|
+
export declare function cumulativeCacheHitRate(counters: CumulativeUsageCounters): number | undefined;
|
|
3
15
|
/**
|
|
4
16
|
* Fold one run's cumulative usage onto a session baseline, producing the new
|
|
5
17
|
* session-cumulative TokenUsage.
|
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
export function emptyCumulativeUsageCounters() {
|
|
2
|
+
return {
|
|
3
|
+
cumulativePromptTokens: 0,
|
|
4
|
+
cumulativeCacheReadTokens: 0,
|
|
5
|
+
cumulativeCacheCreationTokens: 0,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function normalizeCumulativeUsageCounters(counters, legacyUsage) {
|
|
9
|
+
return {
|
|
10
|
+
cumulativePromptTokens: typeof counters?.cumulativePromptTokens === "number"
|
|
11
|
+
? counters.cumulativePromptTokens
|
|
12
|
+
: (legacyUsage?.promptTokens ?? 0),
|
|
13
|
+
cumulativeCacheReadTokens: typeof counters?.cumulativeCacheReadTokens === "number"
|
|
14
|
+
? counters.cumulativeCacheReadTokens
|
|
15
|
+
: (legacyUsage?.cacheReadTokens ?? 0),
|
|
16
|
+
cumulativeCacheCreationTokens: typeof counters?.cumulativeCacheCreationTokens === "number"
|
|
17
|
+
? counters.cumulativeCacheCreationTokens
|
|
18
|
+
: (legacyUsage?.cacheCreationTokens ?? 0),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function addCumulativeUsage(counters, usage) {
|
|
22
|
+
const current = normalizeCumulativeUsageCounters(counters);
|
|
23
|
+
return {
|
|
24
|
+
cumulativePromptTokens: current.cumulativePromptTokens + (usage.promptTokens ?? 0),
|
|
25
|
+
cumulativeCacheReadTokens: current.cumulativeCacheReadTokens + (usage.cacheReadTokens ?? 0),
|
|
26
|
+
cumulativeCacheCreationTokens: current.cumulativeCacheCreationTokens + (usage.cacheCreationTokens ?? 0),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function addTokenUsage(left, right) {
|
|
30
|
+
return {
|
|
31
|
+
promptTokens: left.promptTokens + (right.promptTokens ?? 0),
|
|
32
|
+
completionTokens: left.completionTokens + (right.completionTokens ?? 0),
|
|
33
|
+
totalTokens: left.totalTokens + (right.totalTokens ?? 0),
|
|
34
|
+
cacheReadTokens: (left.cacheReadTokens ?? 0) + (right.cacheReadTokens ?? 0),
|
|
35
|
+
cacheCreationTokens: (left.cacheCreationTokens ?? 0) + (right.cacheCreationTokens ?? 0),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function cacheHitRateFromTokens(promptTokens, cacheReadTokens, cacheCreationTokens) {
|
|
39
|
+
const read = cacheReadTokens ?? 0;
|
|
40
|
+
const creation = cacheCreationTokens ?? 0;
|
|
41
|
+
if (read === 0 && creation === 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
const uncached = Math.max(0, promptTokens - read - creation);
|
|
44
|
+
const denom = read + creation + uncached;
|
|
45
|
+
if (denom === 0)
|
|
46
|
+
return undefined;
|
|
47
|
+
return read / denom;
|
|
48
|
+
}
|
|
49
|
+
export function cacheHitRateFromUsage(usage) {
|
|
50
|
+
if (!usage)
|
|
51
|
+
return undefined;
|
|
52
|
+
return cacheHitRateFromTokens(usage.promptTokens ?? 0, usage.cacheReadTokens, usage.cacheCreationTokens);
|
|
53
|
+
}
|
|
54
|
+
export function cumulativeCacheHitRate(counters) {
|
|
55
|
+
return cacheHitRateFromTokens(counters.cumulativePromptTokens, counters.cumulativeCacheReadTokens, counters.cumulativeCacheCreationTokens);
|
|
56
|
+
}
|
|
1
57
|
/**
|
|
2
58
|
* Fold one run's cumulative usage onto a session baseline, producing the new
|
|
3
59
|
* session-cumulative TokenUsage.
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
export interface SteerItem {
|
|
10
10
|
id: string;
|
|
11
11
|
text: string;
|
|
12
|
+
clientMessageId?: string;
|
|
12
13
|
}
|
|
13
14
|
/** Append a steer entry. Blank text is dropped (returns the list unchanged). */
|
|
14
|
-
export declare function enqueueSteerItem(list: SteerItem[], id: string, text: string): SteerItem[];
|
|
15
|
+
export declare function enqueueSteerItem(list: SteerItem[], id: string, text: string, clientMessageId?: string): SteerItem[];
|
|
15
16
|
/**
|
|
16
17
|
* Take everything currently queued and clear the list. Returns the drained
|
|
17
18
|
* entries (in order) and the now-empty remainder. The turn loop calls this at
|