@cjhyy/code-shell-core 0.6.0-rc.1 → 0.6.0-rc.11
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 +22 -12
- package/dist/engine/engine.js +263 -81
- package/dist/engine/model-connections-pool.js +1 -0
- package/dist/engine/model-facade.js +2 -12
- package/dist/engine/query.js +2 -0
- package/dist/engine/runtime.d.ts +2 -0
- package/dist/engine/runtime.js +25 -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/git/utils.d.ts +12 -0
- package/dist/git/utils.js +33 -6
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/llm/capabilities/rules.js +1 -1
- package/dist/llm/model-pool.d.ts +7 -0
- package/dist/llm/model-pool.js +8 -1
- package/dist/model-catalog/builtin.js +6 -1
- package/dist/preset/index.d.ts +5 -1
- package/dist/preset/index.js +21 -2
- package/dist/prompt/composer.d.ts +5 -0
- package/dist/prompt/composer.js +10 -2
- package/dist/prompt/sections/base.md +1 -0
- package/dist/protocol/chat-session-manager.d.ts +1 -0
- package/dist/protocol/chat-session-manager.js +2 -0
- package/dist/protocol/chat-session.d.ts +4 -1
- package/dist/protocol/chat-session.js +9 -3
- package/dist/protocol/client.d.ts +5 -1
- package/dist/protocol/client.js +8 -2
- package/dist/protocol/server.d.ts +13 -12
- package/dist/protocol/server.js +199 -67
- package/dist/protocol/types.d.ts +14 -0
- package/dist/runtime/background-shell.js +14 -0
- package/dist/runtime/safe-spawn.js +89 -11
- package/dist/runtime/spawn-common.d.ts +15 -4
- package/dist/runtime/spawn-common.js +113 -12
- 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/builtin/bash.js +3 -2
- package/dist/tool-system/builtin/cron.js +10 -2
- package/dist/tool-system/builtin/edit-model-catalog.js +15 -5
- package/dist/tool-system/builtin/generate-video.js +3 -0
- package/dist/tool-system/builtin/grep.d.ts +9 -0
- package/dist/tool-system/builtin/grep.js +100 -3
- package/dist/tool-system/builtin/index.d.ts +3 -1
- package/dist/tool-system/builtin/index.js +5 -5
- package/dist/tool-system/builtin/powershell.js +4 -1
- package/dist/tool-system/builtin/sleep.js +5 -0
- package/dist/tool-system/context.d.ts +10 -0
- package/dist/tool-system/executor.js +25 -2
- 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/tool-system/permission.d.ts +3 -1
- package/dist/tool-system/permission.js +2 -1
- package/dist/tool-system/sandbox/off.js +7 -1
- package/dist/types.d.ts +35 -1
- package/dist/utils/exec.d.ts +8 -0
- package/dist/utils/exec.js +10 -0
- 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);
|
|
@@ -1323,6 +1400,12 @@ export class Engine {
|
|
|
1323
1400
|
disabledPlugins,
|
|
1324
1401
|
skillAllowlist: this.config.skillAllowlist,
|
|
1325
1402
|
memoriesMaxAgeDays: this.readMemoriesConfig()?.maxAge,
|
|
1403
|
+
goalToolState: {
|
|
1404
|
+
hasGoal: this.config.isSubAgent !== true &&
|
|
1405
|
+
(normalizeGoal(options?.goal) !== undefined ||
|
|
1406
|
+
session.state.activeGoal !== undefined ||
|
|
1407
|
+
normalizeGoal(this.config.goal) !== undefined),
|
|
1408
|
+
},
|
|
1326
1409
|
});
|
|
1327
1410
|
// Connect MCP servers (if configured and not already connected).
|
|
1328
1411
|
// B1: prefer the Runtime-owned MCPManager so all sessions in a
|
|
@@ -1358,6 +1441,14 @@ export class Engine {
|
|
|
1358
1441
|
// cwd. Recomputed every message, so configuring a key takes effect on the
|
|
1359
1442
|
// NEXT message without a restart. Tools with no guard entry are always kept.
|
|
1360
1443
|
const guardCwd = toolCtx.cwd;
|
|
1444
|
+
const toolVisibility = {
|
|
1445
|
+
cwd: guardCwd,
|
|
1446
|
+
hasGoal: this.config.isSubAgent !== true &&
|
|
1447
|
+
(normalizeGoal(options?.goal) !== undefined ||
|
|
1448
|
+
session.state.activeGoal !== undefined ||
|
|
1449
|
+
normalizeGoal(this.config.goal) !== undefined),
|
|
1450
|
+
};
|
|
1451
|
+
toolCtx.toolVisibility = toolVisibility;
|
|
1361
1452
|
// #7: per-turn project builtin override. The toolRegistry's builtin tool
|
|
1362
1453
|
// SET is ctor-frozen (and may be shared via runtime), so a mid-session
|
|
1363
1454
|
// project override of a builtin can't rebuild the registry. But the tool
|
|
@@ -1407,7 +1498,7 @@ export class Engine {
|
|
|
1407
1498
|
.filter((t) => mcpVisible(t.name))
|
|
1408
1499
|
.filter((t) => {
|
|
1409
1500
|
const guard = BUILTIN_TOOL_GUARDS.get(t.name);
|
|
1410
|
-
return guard ? guard(
|
|
1501
|
+
return guard ? guard(toolVisibility) : true;
|
|
1411
1502
|
})
|
|
1412
1503
|
.filter((t) => {
|
|
1413
1504
|
const flag = TOOL_FEATURE_FLAGS.get(t.name);
|
|
@@ -1469,26 +1560,27 @@ export class Engine {
|
|
|
1469
1560
|
// run would be evaluated fresh and might get a different replacement
|
|
1470
1561
|
// string than the one already in the message, breaking idempotency.
|
|
1471
1562
|
contextManager.initReplacementStateFromMessages(messages);
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1474
|
-
//
|
|
1475
|
-
//
|
|
1476
|
-
//
|
|
1477
|
-
//
|
|
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.
|
|
1478
1576
|
const auxSummaryClient = await this.resolveAuxClient(llmClient);
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
// this flips thinking off (~3x faster, fewer tokens); on every other
|
|
1487
|
-
// OpenAI-compatible provider the field is ignored.
|
|
1488
|
-
reasoning: { mode: "off" },
|
|
1489
|
-
});
|
|
1490
|
-
return summaryResponse.text;
|
|
1491
|
-
});
|
|
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));
|
|
1492
1584
|
// Create components (requires resolved llmClient).
|
|
1493
1585
|
const modelFacade = new ModelFacade(llmClient, session.transcript);
|
|
1494
1586
|
// Session-cumulative usage baseline: the LLM client is recreated per run
|
|
@@ -1662,7 +1754,9 @@ export class Engine {
|
|
|
1662
1754
|
pendingCompactInfo = null;
|
|
1663
1755
|
return info;
|
|
1664
1756
|
},
|
|
1665
|
-
consumeSteer: () => this.consumeSteer(sid),
|
|
1757
|
+
consumeSteer: (source) => this.consumeSteer(sid, source),
|
|
1758
|
+
claimClientMessageId: (clientMessageId, source) => claimClientMessageId(session, clientMessageId, source),
|
|
1759
|
+
recordCumulativeUsage,
|
|
1666
1760
|
// Clear the persisted goal for a self-reported completion / confirmed
|
|
1667
1761
|
// cancel. Clears the in-RAM session's activeGoal (so THIS run's later
|
|
1668
1762
|
// turns don't re-arm) AND persists it, and drops the in-flight stop
|
|
@@ -1701,6 +1795,7 @@ export class Engine {
|
|
|
1701
1795
|
maxToolCallsPerTurn: this.config.maxToolCallsPerTurn ?? 25,
|
|
1702
1796
|
onStream: options?.onStream,
|
|
1703
1797
|
signal: options?.signal,
|
|
1798
|
+
freshImageMessages: freshImageMessage ? [freshImageMessage] : undefined,
|
|
1704
1799
|
// Goal mode: the active goal is surfaced to the on_stop handler via
|
|
1705
1800
|
// ctx.data.goal; the GoalStopHook (registered above) judges it.
|
|
1706
1801
|
goal: normalizedGoal,
|
|
@@ -1713,16 +1808,23 @@ export class Engine {
|
|
|
1713
1808
|
// baseline + this run's running total (idempotent per boundary,
|
|
1714
1809
|
// accumulates across runs; carries cacheRead/cacheCreation too).
|
|
1715
1810
|
session.state.tokenUsage = foldRunUsage(usageBaseline, modelFacade.getUsage());
|
|
1716
|
-
// Surface the session
|
|
1717
|
-
//
|
|
1718
|
-
//
|
|
1719
|
-
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);
|
|
1720
1816
|
options?.onStream?.({
|
|
1721
1817
|
type: "usage_update",
|
|
1722
|
-
promptTokens:
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
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,
|
|
1726
1828
|
});
|
|
1727
1829
|
if (this.config.costStore) {
|
|
1728
1830
|
session.state.costState = this.config.costStore.serialize();
|
|
@@ -1926,6 +2028,28 @@ export class Engine {
|
|
|
1926
2028
|
* active run's client) when unset, unknown, or on any build failure — aux
|
|
1927
2029
|
* work is best-effort and must never break a run.
|
|
1928
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
|
+
}
|
|
1929
2053
|
async resolveAuxClient(fallback) {
|
|
1930
2054
|
let auxKey;
|
|
1931
2055
|
try {
|
|
@@ -1991,7 +2115,9 @@ export class Engine {
|
|
|
1991
2115
|
// extraction, which then padded the memory store with low-signal
|
|
1992
2116
|
// entries. 8 messages is roughly "more than a single back-and-forth"
|
|
1993
2117
|
// — substantive enough to be worth a durable note.
|
|
1994
|
-
const messages = transcript
|
|
2118
|
+
const messages = transcript
|
|
2119
|
+
.toMessages()
|
|
2120
|
+
.filter((m) => m.role === "user" || m.role === "assistant");
|
|
1995
2121
|
if (messages.length < 8)
|
|
1996
2122
|
return;
|
|
1997
2123
|
// Memory orchestrator + dream-loop calls are auxiliary LLM calls
|
|
@@ -2093,10 +2219,8 @@ export class Engine {
|
|
|
2093
2219
|
return entry;
|
|
2094
2220
|
}
|
|
2095
2221
|
/**
|
|
2096
|
-
* Zero
|
|
2097
|
-
*
|
|
2098
|
-
* cache-hit stats from the prior model are no longer meaningful. The next
|
|
2099
|
-
* 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.
|
|
2100
2224
|
*/
|
|
2101
2225
|
resetSessionUsage(sessionId) {
|
|
2102
2226
|
const zero = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
@@ -2168,7 +2292,9 @@ export class Engine {
|
|
|
2168
2292
|
try {
|
|
2169
2293
|
chmodSync(file, 0o600);
|
|
2170
2294
|
}
|
|
2171
|
-
catch {
|
|
2295
|
+
catch {
|
|
2296
|
+
/* best-effort */
|
|
2297
|
+
}
|
|
2172
2298
|
}
|
|
2173
2299
|
catch (err) {
|
|
2174
2300
|
logger.warn(`persistActiveModel failed: ${err.message}`);
|
|
@@ -2240,8 +2366,14 @@ export class Engine {
|
|
|
2240
2366
|
// The builtin tool SET is ctor-frozen and may be shared via runtime — we
|
|
2241
2367
|
// do NOT rebuild it here. If the new preset implies a different builtin
|
|
2242
2368
|
// tool set, that part of the change only lands on session restart.
|
|
2243
|
-
const prevTools = resolveBuiltinToolNames({ preset: prevPresetName })
|
|
2244
|
-
|
|
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(",");
|
|
2245
2377
|
if (prevTools !== nextTools) {
|
|
2246
2378
|
logger.warn("engine.preset_reload.tool_set_change_needs_restart", {
|
|
2247
2379
|
from: prevPresetName,
|
|
@@ -2337,25 +2469,74 @@ export class Engine {
|
|
|
2337
2469
|
}
|
|
2338
2470
|
}
|
|
2339
2471
|
/**
|
|
2340
|
-
* Force context compaction on
|
|
2472
|
+
* Force context compaction on a session.
|
|
2341
2473
|
* Returns token stats before/after.
|
|
2342
2474
|
*/
|
|
2343
|
-
forceCompact() {
|
|
2344
|
-
const
|
|
2345
|
-
if (!
|
|
2475
|
+
async forceCompact(sessionId) {
|
|
2476
|
+
const effectiveSessionId = sessionId ?? this.lastSessionId;
|
|
2477
|
+
if (!effectiveSessionId) {
|
|
2346
2478
|
return { before: 0, after: 0, strategy: "none (no active session)" };
|
|
2347
2479
|
}
|
|
2348
|
-
const
|
|
2349
|
-
|
|
2480
|
+
const session = this.sessionManager.resume(effectiveSessionId);
|
|
2481
|
+
const sourceMessages = this.compactedMessagesBySession.get(effectiveSessionId) ?? session.transcript.toMessages();
|
|
2350
2482
|
const before = estimateTokens(sourceMessages);
|
|
2351
|
-
|
|
2483
|
+
let contextManager = this.lastContextManager;
|
|
2484
|
+
if (!contextManager || this.lastSessionId !== effectiveSessionId) {
|
|
2485
|
+
contextManager = new ContextManager({
|
|
2486
|
+
maxTokens: this.resolveMaxContextTokens(),
|
|
2487
|
+
...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
|
|
2488
|
+
});
|
|
2489
|
+
contextManager.setTranscriptPath(session.transcript.getFilePath());
|
|
2490
|
+
contextManager.initReplacementStateFromMessages(sourceMessages);
|
|
2491
|
+
this.lastContextManager = contextManager;
|
|
2492
|
+
}
|
|
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);
|
|
2352
2532
|
const after = estimateTokens(compacted);
|
|
2353
|
-
this.compactedMessagesBySession.set(
|
|
2533
|
+
this.compactedMessagesBySession.set(effectiveSessionId, compacted);
|
|
2534
|
+
this.lastSessionId = effectiveSessionId;
|
|
2354
2535
|
this.lastMessages = compacted;
|
|
2355
2536
|
return {
|
|
2356
2537
|
before,
|
|
2357
2538
|
after,
|
|
2358
|
-
strategy:
|
|
2539
|
+
strategy: after >= before ? "no compaction needed" : (compactStrategy ?? "compacted"),
|
|
2359
2540
|
};
|
|
2360
2541
|
}
|
|
2361
2542
|
stripUserContextMessage(messages, userContextMsg) {
|
|
@@ -2449,7 +2630,11 @@ export class Engine {
|
|
|
2449
2630
|
backend = interactive;
|
|
2450
2631
|
}
|
|
2451
2632
|
else {
|
|
2452
|
-
backend = new HeadlessApprovalBackend(mode === "bypassPermissions"
|
|
2633
|
+
backend = new HeadlessApprovalBackend(mode === "bypassPermissions"
|
|
2634
|
+
? "approve-all"
|
|
2635
|
+
: mode === "dontAsk"
|
|
2636
|
+
? "deny-all"
|
|
2637
|
+
: "deny-all");
|
|
2453
2638
|
}
|
|
2454
2639
|
}
|
|
2455
2640
|
return { rules, backend };
|
|
@@ -2491,7 +2676,8 @@ export class Engine {
|
|
|
2491
2676
|
* rule set buildPermissionConfig does, without constructing a backend.
|
|
2492
2677
|
*/
|
|
2493
2678
|
getPermissionRules() {
|
|
2494
|
-
return this.buildPermissionConfig(this.getPermissionMode(), this.config.cwd ?? process.cwd())
|
|
2679
|
+
return this.buildPermissionConfig(this.getPermissionMode(), this.config.cwd ?? process.cwd())
|
|
2680
|
+
.rules;
|
|
2495
2681
|
}
|
|
2496
2682
|
/**
|
|
2497
2683
|
* Toggle plan mode directly. Called by the Plan tool (Task 7) via ToolContext.engine.
|
|
@@ -2578,12 +2764,8 @@ export class Engine {
|
|
|
2578
2764
|
getAgentDefinitions(cwd) {
|
|
2579
2765
|
const disabledAgents = this.readDisabledAgents(cwd);
|
|
2580
2766
|
const disabledPlugins = this.readDisabledLists().disabledPlugins;
|
|
2581
|
-
const disabledKey = [...disabledAgents, "::", ...disabledPlugins]
|
|
2582
|
-
|
|
2583
|
-
.sort()
|
|
2584
|
-
.join(" ");
|
|
2585
|
-
if (this.agentDefsCache?.cwd !== cwd ||
|
|
2586
|
-
this.agentDefsCache.disabledKey !== disabledKey) {
|
|
2767
|
+
const disabledKey = [...disabledAgents, "::", ...disabledPlugins].slice().sort().join(" ");
|
|
2768
|
+
if (this.agentDefsCache?.cwd !== cwd || this.agentDefsCache.disabledKey !== disabledKey) {
|
|
2587
2769
|
this.agentDefsCache = {
|
|
2588
2770
|
cwd,
|
|
2589
2771
|
disabledKey,
|
|
@@ -51,6 +51,7 @@ export function modelEntriesFromConnections(connections, credentials, catalog) {
|
|
|
51
51
|
const e = {
|
|
52
52
|
key: inst.id,
|
|
53
53
|
provider: clientProvider(entry),
|
|
54
|
+
providerKind: entry.adapterKind,
|
|
54
55
|
model: resolved.model,
|
|
55
56
|
baseUrl: resolved.baseUrl,
|
|
56
57
|
needsKey: resolved.needsKey,
|
|
@@ -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.
|
package/dist/engine/runtime.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare class EngineRuntime {
|
|
|
23
23
|
readonly costTracker: CostTracker;
|
|
24
24
|
private sandboxCache;
|
|
25
25
|
constructor(opts: EngineRuntimeOptions);
|
|
26
|
+
clearModels(): void;
|
|
27
|
+
reloadModelsFromSettings(): void;
|
|
26
28
|
/**
|
|
27
29
|
* Lazily resolve and cache a sandbox backend for `(mode, cwd)`.
|
|
28
30
|
*
|
package/dist/engine/runtime.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { getMergedCatalog } from "../model-catalog/index.js";
|
|
2
|
+
import { modelEntriesFromConnections } from "./model-connections-pool.js";
|
|
3
|
+
import { defaultCacheDir } from "../llm/model-cache.js";
|
|
1
4
|
import { resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
|
|
2
5
|
import { sandboxCacheKey } from "./sandbox-cache-key.js";
|
|
3
6
|
/**
|
|
@@ -22,6 +25,28 @@ export class EngineRuntime {
|
|
|
22
25
|
this.mcpPool = opts.mcpPool;
|
|
23
26
|
this.costTracker = opts.costTracker;
|
|
24
27
|
}
|
|
28
|
+
clearModels() {
|
|
29
|
+
this.modelPool.clear();
|
|
30
|
+
}
|
|
31
|
+
reloadModelsFromSettings() {
|
|
32
|
+
const settings = this.settings.load();
|
|
33
|
+
const connections = settings.modelConnections ?? [];
|
|
34
|
+
const credentials = settings.credentials ?? [];
|
|
35
|
+
const catalog = getMergedCatalog();
|
|
36
|
+
const entries = modelEntriesFromConnections(connections, credentials, catalog);
|
|
37
|
+
this.modelPool.clear();
|
|
38
|
+
if (entries.length === 0)
|
|
39
|
+
return;
|
|
40
|
+
for (const entry of entries)
|
|
41
|
+
this.modelPool.register(entry);
|
|
42
|
+
if (entries.length > 0) {
|
|
43
|
+
const defaultText = settings.defaults?.text;
|
|
44
|
+
const key = defaultText && entries.some((e) => e.key === defaultText) ? defaultText : entries[0].key;
|
|
45
|
+
this.modelPool.switch(key);
|
|
46
|
+
this.modelPool.setCacheDir(defaultCacheDir());
|
|
47
|
+
this.modelPool.reloadCachedContextWindows();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
25
50
|
/**
|
|
26
51
|
* Lazily resolve and cache a sandbox backend for `(mode, cwd)`.
|
|
27
52
|
*
|