@cjhyy/code-shell-core 0.6.0-rc.8 → 0.7.0-beta.1
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/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +38 -1
- package/dist/context/compaction.js +138 -0
- package/dist/context/manager.d.ts +34 -2
- package/dist/context/manager.js +254 -58
- package/dist/context/token-counter.js +13 -0
- package/dist/credentials/access.d.ts +56 -0
- package/dist/credentials/access.js +183 -0
- package/dist/credentials/index.d.ts +1 -0
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/types.d.ts +2 -2
- package/dist/credentials/use-credential-tool.d.ts +9 -1
- package/dist/credentials/use-credential-tool.js +58 -45
- package/dist/engine/engine.d.ts +38 -12
- package/dist/engine/engine.js +477 -163
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +255 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +6 -16
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- 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/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +34 -2
- package/dist/engine/turn-loop.js +269 -52
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -84
- package/dist/git/worktree.js +5 -230
- package/dist/index.d.ts +31 -28
- package/dist/index.js +27 -25
- package/dist/logging/logger.js +6 -6
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +16 -9
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.d.ts +2 -0
- package/dist/protocol/chat-session-manager.js +38 -2
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +2 -0
- package/dist/protocol/client.d.ts +13 -5
- package/dist/protocol/client.js +26 -3
- package/dist/protocol/server.d.ts +43 -12
- package/dist/protocol/server.js +326 -104
- package/dist/protocol/types.d.ts +51 -0
- package/dist/protocol/types.js +6 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- package/dist/runtime/safe-spawn.js +74 -11
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +197 -4
- package/dist/session/transcript.d.ts +5 -1
- package/dist/session/transcript.js +38 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +87 -37
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
- package/dist/tool-system/builtin/background-jobs.js +12 -1
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
- package/dist/tool-system/builtin/drive-claude-code.js +301 -47
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +37 -21
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +118 -6
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +109 -19
- package/dist/tool-system/builtin/worktree.d.ts +4 -4
- package/dist/tool-system/builtin/worktree.js +297 -74
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +23 -1
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +18 -5
- package/dist/tool-system/mcp-manager.js +157 -74
- 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/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +41 -12
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +86 -5
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +4 -3
|
@@ -18,6 +18,144 @@ import { estimateMessagesTokens } from "./token-counter.js";
|
|
|
18
18
|
export function estimateTokens(messages) {
|
|
19
19
|
return Math.ceil(estimateMessagesTokens(messages) * (4 / 3));
|
|
20
20
|
}
|
|
21
|
+
export const IMAGE_HISTORY_PLACEHOLDER_PREFIX = "[image #";
|
|
22
|
+
export const IMAGE_HISTORY_PLACEHOLDER_SUFFIX = ", 已处理 / already provided earlier]";
|
|
23
|
+
export function collectBase64Images(messages) {
|
|
24
|
+
const entries = [];
|
|
25
|
+
let nextImageNumber = 1;
|
|
26
|
+
const visitBlocks = (blocks) => {
|
|
27
|
+
for (const block of blocks) {
|
|
28
|
+
const placeholderNumber = imageHistoryPlaceholderNumber(block);
|
|
29
|
+
if (placeholderNumber !== undefined) {
|
|
30
|
+
nextImageNumber = Math.max(nextImageNumber, placeholderNumber + 1);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (isBase64ImageBlock(block)) {
|
|
34
|
+
entries.push({ imageNumber: nextImageNumber++, block });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (block.type === "tool_result" && Array.isArray(block.content)) {
|
|
38
|
+
visitBlocks(block.content);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
for (const msg of messages) {
|
|
43
|
+
if (Array.isArray(msg.content))
|
|
44
|
+
visitBlocks(msg.content);
|
|
45
|
+
}
|
|
46
|
+
return entries;
|
|
47
|
+
}
|
|
48
|
+
export function findImageByNumber(messages, imageNumber) {
|
|
49
|
+
if (!Number.isSafeInteger(imageNumber) || imageNumber <= 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
return collectBase64Images(messages).find((entry) => entry.imageNumber === imageNumber);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Replace already-consumed image payload blocks with compact text markers.
|
|
55
|
+
*
|
|
56
|
+
* The transcript may retain the full image bytes for rendering/resume, but the
|
|
57
|
+
* working message history sent to the model should not re-send base64 after the
|
|
58
|
+
* model has seen it once. This handles both our internal Anthropic-style image
|
|
59
|
+
* blocks and OpenAI-style data-url image blocks defensively, including images
|
|
60
|
+
* nested inside tool_result.content arrays (view_image / browser screenshots).
|
|
61
|
+
*/
|
|
62
|
+
export function downgradeImagePayloadsInHistory(messages, options = {}) {
|
|
63
|
+
let replacedCount = 0;
|
|
64
|
+
let changed = false;
|
|
65
|
+
const imageNumberQueues = new WeakMap();
|
|
66
|
+
for (const entry of collectBase64Images(messages)) {
|
|
67
|
+
const key = entry.block;
|
|
68
|
+
const existing = imageNumberQueues.get(key);
|
|
69
|
+
if (existing) {
|
|
70
|
+
existing.push(entry.imageNumber);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
imageNumberQueues.set(key, [entry.imageNumber]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const takeImageNumber = (block) => {
|
|
77
|
+
const queue = imageNumberQueues.get(block);
|
|
78
|
+
return queue?.shift();
|
|
79
|
+
};
|
|
80
|
+
const placeholderFor = (imageNumber) => ({
|
|
81
|
+
type: "text",
|
|
82
|
+
text: `${IMAGE_HISTORY_PLACEHOLDER_PREFIX}${imageNumber}${IMAGE_HISTORY_PLACEHOLDER_SUFFIX}`,
|
|
83
|
+
});
|
|
84
|
+
const transformBlocks = (blocks, preserve) => {
|
|
85
|
+
let blocksChanged = false;
|
|
86
|
+
const out = blocks.map((block) => {
|
|
87
|
+
const placeholderNumber = imageHistoryPlaceholderNumber(block);
|
|
88
|
+
if (placeholderNumber !== undefined) {
|
|
89
|
+
return block;
|
|
90
|
+
}
|
|
91
|
+
if (isBase64ImageBlock(block)) {
|
|
92
|
+
const imageNumber = takeImageNumber(block);
|
|
93
|
+
if (imageNumber === undefined)
|
|
94
|
+
return block;
|
|
95
|
+
if (preserve)
|
|
96
|
+
return block;
|
|
97
|
+
replacedCount++;
|
|
98
|
+
blocksChanged = true;
|
|
99
|
+
return placeholderFor(imageNumber);
|
|
100
|
+
}
|
|
101
|
+
if (block.type === "tool_result" && Array.isArray(block.content)) {
|
|
102
|
+
const nested = transformBlocks(block.content, preserve);
|
|
103
|
+
if (nested.changed) {
|
|
104
|
+
blocksChanged = true;
|
|
105
|
+
return { ...block, content: nested.blocks };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return block;
|
|
109
|
+
});
|
|
110
|
+
return { blocks: blocksChanged ? out : blocks, changed: blocksChanged };
|
|
111
|
+
};
|
|
112
|
+
const out = messages.map((msg) => {
|
|
113
|
+
if (!Array.isArray(msg.content))
|
|
114
|
+
return msg;
|
|
115
|
+
const preserve = options.preserveMessages?.has(msg) === true;
|
|
116
|
+
const result = transformBlocks(msg.content, preserve);
|
|
117
|
+
if (!result.changed)
|
|
118
|
+
return msg;
|
|
119
|
+
changed = true;
|
|
120
|
+
return { ...msg, content: result.blocks };
|
|
121
|
+
});
|
|
122
|
+
return { messages: changed ? out : messages, replacedCount };
|
|
123
|
+
}
|
|
124
|
+
export function messageHasBase64ImagePayload(message) {
|
|
125
|
+
if (!Array.isArray(message.content))
|
|
126
|
+
return false;
|
|
127
|
+
return message.content.some(blockHasBase64ImagePayload);
|
|
128
|
+
}
|
|
129
|
+
function blockHasBase64ImagePayload(block) {
|
|
130
|
+
if (isBase64ImageBlock(block))
|
|
131
|
+
return true;
|
|
132
|
+
return (block.type === "tool_result" &&
|
|
133
|
+
Array.isArray(block.content) &&
|
|
134
|
+
block.content.some(blockHasBase64ImagePayload));
|
|
135
|
+
}
|
|
136
|
+
function imageHistoryPlaceholderNumber(block) {
|
|
137
|
+
if (block.type !== "text" || typeof block.text !== "string")
|
|
138
|
+
return undefined;
|
|
139
|
+
const escapedPrefix = IMAGE_HISTORY_PLACEHOLDER_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
140
|
+
const escapedSuffix = IMAGE_HISTORY_PLACEHOLDER_SUFFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
141
|
+
const match = block.text.match(new RegExp(`^${escapedPrefix}(\\d+)${escapedSuffix}$`));
|
|
142
|
+
if (!match?.[1])
|
|
143
|
+
return undefined;
|
|
144
|
+
const n = Number(match[1]);
|
|
145
|
+
return Number.isSafeInteger(n) && n > 0 ? n : undefined;
|
|
146
|
+
}
|
|
147
|
+
export function isBase64ImageBlock(block) {
|
|
148
|
+
if (block.type === "image" &&
|
|
149
|
+
block.source?.type === "base64" &&
|
|
150
|
+
typeof block.source.data === "string" &&
|
|
151
|
+
block.source.data.length > 0) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
const maybeOpenAI = block;
|
|
155
|
+
return (maybeOpenAI.type === "image_url" &&
|
|
156
|
+
typeof maybeOpenAI.image_url?.url === "string" &&
|
|
157
|
+
/^data:image\/[^;,]+;base64,/i.test(maybeOpenAI.image_url.url));
|
|
158
|
+
}
|
|
21
159
|
/**
|
|
22
160
|
* Reconcile user-supplied compaction ratios into a safe ordering.
|
|
23
161
|
*
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Tier 2: LLM summary (async) — generate summary of older messages via model call
|
|
6
6
|
* Tier 3: window compact (sync, emergency) — aggressive truncation fallback
|
|
7
7
|
*/
|
|
8
|
-
import type { Message } from "../types.js";
|
|
8
|
+
import type { Message, ContextUsageAnchor, PromptTokenConfidence, PromptTokenSource } from "../types.js";
|
|
9
9
|
export interface ContextManagerConfig {
|
|
10
10
|
maxTokens: number;
|
|
11
11
|
compactAtRatio: number;
|
|
@@ -41,11 +41,18 @@ export declare class ContextManager {
|
|
|
41
41
|
private config;
|
|
42
42
|
private summarizeFn;
|
|
43
43
|
private consecutiveSummaryFailures;
|
|
44
|
+
/** Prevents repeated spin-band LLM calls after a summary generated no shrink. */
|
|
45
|
+
private suppressNoOpMicroSummaryUntilCompact;
|
|
44
46
|
private lastSummary;
|
|
45
47
|
/** Last known actual token count from API usage data. */
|
|
46
48
|
private lastActualTokens;
|
|
47
49
|
/** Message count at the time lastActualTokens was recorded. */
|
|
48
50
|
private lastActualAtMessageCount;
|
|
51
|
+
/** Heuristic token estimate for the same messages as lastActualTokens. */
|
|
52
|
+
private lastActualAnchorEstimate;
|
|
53
|
+
private lastActualRecordedAt;
|
|
54
|
+
private lastActualProvider;
|
|
55
|
+
private lastActualModel;
|
|
49
56
|
/** Path to session transcript — passed to summary compaction for on-demand access. */
|
|
50
57
|
private transcriptPath;
|
|
51
58
|
/** Notified whenever any compaction tier fires, including microcompact. */
|
|
@@ -63,16 +70,24 @@ export declare class ContextManager {
|
|
|
63
70
|
* Record actual token usage from API response.
|
|
64
71
|
* Used for hybrid estimation: actual + estimate for new messages.
|
|
65
72
|
*/
|
|
66
|
-
recordActualUsage(inputTokens: number, messageCount: number):
|
|
73
|
+
recordActualUsage(inputTokens: number, messageCount: number, messages?: Message[]): ContextUsageAnchor | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Seed actual prompt-token usage from persisted session state.
|
|
76
|
+
* Returns the normalized anchor when accepted; invalid legacy/tampered data is ignored.
|
|
77
|
+
*/
|
|
78
|
+
seedActualUsage(anchor: ContextUsageAnchor | undefined): ContextUsageAnchor | undefined;
|
|
79
|
+
getActualUsageAnchor(): ContextUsageAnchor | undefined;
|
|
67
80
|
/**
|
|
68
81
|
* Best-effort token estimate: uses actual API usage as base if available,
|
|
69
82
|
* plus estimation for messages added since the last API call.
|
|
70
83
|
*/
|
|
84
|
+
private estimateTokensHybridInfo;
|
|
71
85
|
private estimateTokensHybrid;
|
|
72
86
|
/**
|
|
73
87
|
* Set the summarize function (injected by Engine).
|
|
74
88
|
*/
|
|
75
89
|
setSummarizeFn(fn: SummarizeFn): void;
|
|
90
|
+
private trySummaryCompact;
|
|
76
91
|
/**
|
|
77
92
|
* Set the transcript path so compaction can reference it. The
|
|
78
93
|
* tool-results directory lives alongside the transcript file:
|
|
@@ -97,6 +112,21 @@ export declare class ContextManager {
|
|
|
97
112
|
* Call this when you have access to the LLM (between turns).
|
|
98
113
|
*/
|
|
99
114
|
manageAsync(messages: Message[]): Promise<Message[]>;
|
|
115
|
+
/**
|
|
116
|
+
* Force maximum compaction, ignoring the ratio gates. This is what a manual
|
|
117
|
+
* `/compact` invokes: the user explicitly asked to shrink NOW, so we don't
|
|
118
|
+
* wait for the prompt to reach compactAtRatio.
|
|
119
|
+
*
|
|
120
|
+
* Order: tier-0 cleanups (persist/truncate/dedupe/mask) + microcompact, then
|
|
121
|
+
* an unconditional LLM summary of the older messages. If no summarizeFn is
|
|
122
|
+
* wired (or it fails / yields nothing), fall back to snip → window so the
|
|
123
|
+
* call still shrinks the conversation rather than no-opping.
|
|
124
|
+
*
|
|
125
|
+
* Unlike manage()/manageAsync(), there is NO `ratio >= compactAtRatio` gate:
|
|
126
|
+
* a long-but-under-threshold text-only conversation (the /compact bug) still
|
|
127
|
+
* gets summarized here.
|
|
128
|
+
*/
|
|
129
|
+
forceSummarize(messages: Message[]): Promise<Message[]>;
|
|
100
130
|
/**
|
|
101
131
|
* Persist large tool_result blocks to disk and replace them with a
|
|
102
132
|
* preview + filepath. No-op when transcript path hasn't been set
|
|
@@ -121,5 +151,7 @@ export declare class ContextManager {
|
|
|
121
151
|
ratio: number;
|
|
122
152
|
needsCompact: boolean;
|
|
123
153
|
needsEmergency: boolean;
|
|
154
|
+
promptTokensSource: PromptTokenSource;
|
|
155
|
+
promptTokensConfidence: PromptTokenConfidence;
|
|
124
156
|
};
|
|
125
157
|
}
|
package/dist/context/manager.js
CHANGED
|
@@ -37,15 +37,25 @@ const DEFAULT_CONFIG = {
|
|
|
37
37
|
function defaultKeepRecent(maxTokens) {
|
|
38
38
|
return Math.max(5, Math.floor(maxTokens / 100_000));
|
|
39
39
|
}
|
|
40
|
+
function positiveFinite(value) {
|
|
41
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
42
|
+
}
|
|
40
43
|
export class ContextManager {
|
|
41
44
|
config;
|
|
42
45
|
summarizeFn;
|
|
43
46
|
consecutiveSummaryFailures = 0;
|
|
47
|
+
/** Prevents repeated spin-band LLM calls after a summary generated no shrink. */
|
|
48
|
+
suppressNoOpMicroSummaryUntilCompact = false;
|
|
44
49
|
lastSummary;
|
|
45
50
|
/** Last known actual token count from API usage data. */
|
|
46
51
|
lastActualTokens;
|
|
47
52
|
/** Message count at the time lastActualTokens was recorded. */
|
|
48
53
|
lastActualAtMessageCount;
|
|
54
|
+
/** Heuristic token estimate for the same messages as lastActualTokens. */
|
|
55
|
+
lastActualAnchorEstimate;
|
|
56
|
+
lastActualRecordedAt;
|
|
57
|
+
lastActualProvider;
|
|
58
|
+
lastActualModel;
|
|
49
59
|
/** Path to session transcript — passed to summary compaction for on-demand access. */
|
|
50
60
|
transcriptPath;
|
|
51
61
|
/** Notified whenever any compaction tier fires, including microcompact. */
|
|
@@ -67,23 +77,84 @@ export class ContextManager {
|
|
|
67
77
|
* Record actual token usage from API response.
|
|
68
78
|
* Used for hybrid estimation: actual + estimate for new messages.
|
|
69
79
|
*/
|
|
70
|
-
recordActualUsage(inputTokens, messageCount) {
|
|
71
|
-
|
|
72
|
-
this.
|
|
80
|
+
recordActualUsage(inputTokens, messageCount, messages) {
|
|
81
|
+
const estimateAtAnchor = messages ? estimateTokens(messages) : undefined;
|
|
82
|
+
return this.seedActualUsage({
|
|
83
|
+
promptTokens: inputTokens,
|
|
84
|
+
messageCount,
|
|
85
|
+
...(estimateAtAnchor !== undefined ? { estimateAtAnchor } : {}),
|
|
86
|
+
recordedAt: Date.now(),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Seed actual prompt-token usage from persisted session state.
|
|
91
|
+
* Returns the normalized anchor when accepted; invalid legacy/tampered data is ignored.
|
|
92
|
+
*/
|
|
93
|
+
seedActualUsage(anchor) {
|
|
94
|
+
if (!anchor)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (!positiveFinite(anchor.promptTokens))
|
|
97
|
+
return undefined;
|
|
98
|
+
if (!Number.isSafeInteger(anchor.messageCount) || anchor.messageCount <= 0) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (anchor.estimateAtAnchor !== undefined && !positiveFinite(anchor.estimateAtAnchor)) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
this.lastActualTokens = anchor.promptTokens;
|
|
105
|
+
this.lastActualAtMessageCount = anchor.messageCount;
|
|
106
|
+
this.lastActualAnchorEstimate = anchor.estimateAtAnchor;
|
|
107
|
+
this.lastActualRecordedAt = positiveFinite(anchor.recordedAt) ? anchor.recordedAt : Date.now();
|
|
108
|
+
this.lastActualProvider = anchor.provider;
|
|
109
|
+
this.lastActualModel = anchor.model;
|
|
110
|
+
return this.getActualUsageAnchor();
|
|
111
|
+
}
|
|
112
|
+
getActualUsageAnchor() {
|
|
113
|
+
if (this.lastActualTokens === undefined ||
|
|
114
|
+
this.lastActualAtMessageCount === undefined ||
|
|
115
|
+
this.lastActualRecordedAt === undefined) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
promptTokens: this.lastActualTokens,
|
|
120
|
+
messageCount: this.lastActualAtMessageCount,
|
|
121
|
+
...(this.lastActualAnchorEstimate !== undefined
|
|
122
|
+
? { estimateAtAnchor: this.lastActualAnchorEstimate }
|
|
123
|
+
: {}),
|
|
124
|
+
recordedAt: this.lastActualRecordedAt,
|
|
125
|
+
...(this.lastActualProvider ? { provider: this.lastActualProvider } : {}),
|
|
126
|
+
...(this.lastActualModel ? { model: this.lastActualModel } : {}),
|
|
127
|
+
};
|
|
73
128
|
}
|
|
74
129
|
/**
|
|
75
130
|
* Best-effort token estimate: uses actual API usage as base if available,
|
|
76
131
|
* plus estimation for messages added since the last API call.
|
|
77
132
|
*/
|
|
78
|
-
|
|
133
|
+
estimateTokensHybridInfo(messages) {
|
|
134
|
+
const currentEstimate = estimateTokens(messages);
|
|
79
135
|
if (this.lastActualTokens !== undefined &&
|
|
80
|
-
this.lastActualAtMessageCount !== undefined
|
|
81
|
-
this.lastActualAtMessageCount < messages.length) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
136
|
+
this.lastActualAtMessageCount !== undefined) {
|
|
137
|
+
if (this.lastActualAtMessageCount < messages.length) {
|
|
138
|
+
const newMessages = messages.slice(this.lastActualAtMessageCount);
|
|
139
|
+
const newTokens = estimateTokens(newMessages);
|
|
140
|
+
return {
|
|
141
|
+
tokens: this.lastActualTokens + newTokens,
|
|
142
|
+
source: "anchor_delta",
|
|
143
|
+
confidence: "medium",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (this.lastActualAnchorEstimate !== undefined && this.lastActualAnchorEstimate > 0) {
|
|
147
|
+
return {
|
|
148
|
+
tokens: Math.round(this.lastActualTokens * (currentEstimate / this.lastActualAnchorEstimate)),
|
|
149
|
+
source: "anchor_rescale",
|
|
150
|
+
confidence: "medium",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
85
153
|
}
|
|
86
|
-
return
|
|
154
|
+
return { tokens: currentEstimate, source: "heuristic_estimate", confidence: "low" };
|
|
155
|
+
}
|
|
156
|
+
estimateTokensHybrid(messages) {
|
|
157
|
+
return this.estimateTokensHybridInfo(messages).tokens;
|
|
87
158
|
}
|
|
88
159
|
/**
|
|
89
160
|
* Set the summarize function (injected by Engine).
|
|
@@ -91,6 +162,64 @@ export class ContextManager {
|
|
|
91
162
|
setSummarizeFn(fn) {
|
|
92
163
|
this.summarizeFn = fn;
|
|
93
164
|
}
|
|
165
|
+
async trySummaryCompact(messages, before, logEvent) {
|
|
166
|
+
if (!this.summarizeFn || this.consecutiveSummaryFailures >= 3) {
|
|
167
|
+
return { messages, tokens: before, compacted: false, noProgress: false };
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const keepRecentN = Math.max(8, Math.floor(messages.length * 0.3));
|
|
171
|
+
const messagesToSummarize = messages.slice(1, -keepRecentN); // skip first (userContext) and recent
|
|
172
|
+
if (messagesToSummarize.length <= 3) {
|
|
173
|
+
return { messages, tokens: before, compacted: false, noProgress: false };
|
|
174
|
+
}
|
|
175
|
+
// Rolling summary: if a prior summary is already anchored in the
|
|
176
|
+
// messages (from an earlier compaction in this session or in a resumed
|
|
177
|
+
// session), feed it back so the LLM merges-updates rather than
|
|
178
|
+
// re-summarizes from scratch.
|
|
179
|
+
const priorSummary = extractAnchoredSummary(messages) ?? this.lastSummary;
|
|
180
|
+
const prompt = buildSummarizationPrompt(messagesToSummarize, priorSummary);
|
|
181
|
+
const summary = await this.summarizeFn(prompt);
|
|
182
|
+
if (!summary || summary.length <= 50) {
|
|
183
|
+
this.consecutiveSummaryFailures++;
|
|
184
|
+
logger.warn("context.summary_failed", {
|
|
185
|
+
failures: this.consecutiveSummaryFailures,
|
|
186
|
+
error: "summary was empty or too short",
|
|
187
|
+
});
|
|
188
|
+
return { messages, tokens: before, compacted: false, noProgress: true };
|
|
189
|
+
}
|
|
190
|
+
const compacted = applySummaryCompaction(messages, summary, keepRecentN, this.transcriptPath);
|
|
191
|
+
const after = this.estimateTokensHybrid(compacted);
|
|
192
|
+
if (after >= before) {
|
|
193
|
+
this.consecutiveSummaryFailures++;
|
|
194
|
+
logger.warn("context.summary_no_progress", {
|
|
195
|
+
failures: this.consecutiveSummaryFailures,
|
|
196
|
+
before,
|
|
197
|
+
after,
|
|
198
|
+
summaryLen: summary.length,
|
|
199
|
+
rolling: priorSummary !== undefined,
|
|
200
|
+
});
|
|
201
|
+
return { messages, tokens: before, compacted: false, noProgress: true };
|
|
202
|
+
}
|
|
203
|
+
this.consecutiveSummaryFailures = 0;
|
|
204
|
+
this.lastSummary = summary;
|
|
205
|
+
logger.info(logEvent, {
|
|
206
|
+
before,
|
|
207
|
+
after,
|
|
208
|
+
summaryLen: summary.length,
|
|
209
|
+
rolling: priorSummary !== undefined,
|
|
210
|
+
});
|
|
211
|
+
this.onCompact?.({ strategy: "summary", before, after });
|
|
212
|
+
return { messages: compacted, tokens: after, compacted: true, noProgress: false };
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
this.consecutiveSummaryFailures++;
|
|
216
|
+
logger.warn("context.summary_failed", {
|
|
217
|
+
failures: this.consecutiveSummaryFailures,
|
|
218
|
+
error: err.message,
|
|
219
|
+
});
|
|
220
|
+
return { messages, tokens: before, compacted: false, noProgress: false };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
94
223
|
/**
|
|
95
224
|
* Set the transcript path so compaction can reference it. The
|
|
96
225
|
* tool-results directory lives alongside the transcript file:
|
|
@@ -238,9 +367,22 @@ export class ContextManager {
|
|
|
238
367
|
result = this.truncateToolResults(result);
|
|
239
368
|
// Tier 0c: Aggregate tool result budget (per-message)
|
|
240
369
|
result = applyToolResultBudget(result);
|
|
370
|
+
// Tier 0d: same always-on waste-removal passes as manage().
|
|
371
|
+
const dedup = dedupeFileReads(result);
|
|
372
|
+
if (dedup.clearedCount > 0) {
|
|
373
|
+
result = dedup.messages;
|
|
374
|
+
logger.info("context.dedupe_file_reads", { cleared: dedup.clearedCount });
|
|
375
|
+
}
|
|
376
|
+
const masked = maskOldObservations(result);
|
|
377
|
+
if (masked.maskedCount > 0) {
|
|
378
|
+
result = masked.messages;
|
|
379
|
+
logger.info("context.mask_browser_snapshots", { masked: masked.maskedCount });
|
|
380
|
+
}
|
|
241
381
|
// Tier 1: microcompact — see manage() for the rationale on the floor.
|
|
242
382
|
const preTier1Tokens = this.estimateTokensHybrid(result);
|
|
243
|
-
|
|
383
|
+
const microFloorGate = this.config.maxTokens * this.config.microcompactFloorRatio;
|
|
384
|
+
let microNoOpAtFloor = false;
|
|
385
|
+
if (preTier1Tokens > microFloorGate) {
|
|
244
386
|
const keepRecentN = this.config.microcompactKeepRecent ?? defaultKeepRecent(this.config.maxTokens);
|
|
245
387
|
// See manage(): onClear fires synchronously before microcompact returns,
|
|
246
388
|
// so defer the token re-estimate until result is reassigned.
|
|
@@ -251,67 +393,63 @@ export class ContextManager {
|
|
|
251
393
|
clearedInfo = info;
|
|
252
394
|
},
|
|
253
395
|
});
|
|
396
|
+
const postTier1Tokens = this.estimateTokensHybrid(result);
|
|
397
|
+
microNoOpAtFloor = postTier1Tokens === preTier1Tokens;
|
|
398
|
+
if (postTier1Tokens < preTier1Tokens) {
|
|
399
|
+
this.suppressNoOpMicroSummaryUntilCompact = false;
|
|
400
|
+
}
|
|
254
401
|
if (clearedInfo) {
|
|
255
|
-
const after = this.estimateTokensHybrid(result);
|
|
256
402
|
logger.info("context.microcompact", {
|
|
257
403
|
before: preTier1Tokens,
|
|
258
|
-
after,
|
|
404
|
+
after: postTier1Tokens,
|
|
259
405
|
keepRecentN,
|
|
260
406
|
clearedRounds: clearedInfo.clearedRounds,
|
|
261
407
|
toolNames: clearedInfo.toolNames,
|
|
262
408
|
});
|
|
263
|
-
this.onCompact?.({
|
|
409
|
+
this.onCompact?.({
|
|
410
|
+
strategy: "micro",
|
|
411
|
+
before: preTier1Tokens,
|
|
412
|
+
after: postTier1Tokens,
|
|
413
|
+
});
|
|
264
414
|
}
|
|
265
415
|
}
|
|
266
|
-
|
|
416
|
+
let tokens = this.estimateTokensHybrid(result);
|
|
267
417
|
const ratio = tokens / this.config.maxTokens;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const after = estimateTokens(result);
|
|
289
|
-
logger.info("context.summary_compact", {
|
|
290
|
-
before: tokens,
|
|
291
|
-
after,
|
|
292
|
-
summaryLen: summary.length,
|
|
293
|
-
rolling: priorSummary !== undefined,
|
|
294
|
-
});
|
|
295
|
-
this.onCompact?.({ strategy: "summary", before: tokens, after });
|
|
296
|
-
return result;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
418
|
+
if (ratio < this.config.microcompactFloorRatio) {
|
|
419
|
+
this.suppressNoOpMicroSummaryUntilCompact = false;
|
|
420
|
+
}
|
|
421
|
+
const noOpMicroSpinBand = microNoOpAtFloor &&
|
|
422
|
+
ratio >= this.config.microcompactFloorRatio &&
|
|
423
|
+
ratio < this.config.compactAtRatio;
|
|
424
|
+
const shouldEscalateNoOpMicro = noOpMicroSpinBand && !this.suppressNoOpMicroSummaryUntilCompact;
|
|
425
|
+
const snipGate = this.config.maxTokens * this.config.compactAtRatio;
|
|
426
|
+
const windowGate = this.config.maxTokens * (this.config.compactAtRatio + 0.05);
|
|
427
|
+
const emergencyGate = this.config.maxTokens * this.config.summarizeAtRatio;
|
|
428
|
+
// Tier 2: LLM summary if approaching limit, or if micro was the only tier
|
|
429
|
+
// available in the 0.70-0.85 band and it freed nothing.
|
|
430
|
+
if (ratio >= this.config.compactAtRatio || shouldEscalateNoOpMicro) {
|
|
431
|
+
const summarized = await this.trySummaryCompact(result, tokens, "context.summary_compact");
|
|
432
|
+
result = summarized.messages;
|
|
433
|
+
tokens = summarized.tokens;
|
|
434
|
+
if (summarized.compacted) {
|
|
435
|
+
this.suppressNoOpMicroSummaryUntilCompact = false;
|
|
436
|
+
if (tokens <= snipGate)
|
|
437
|
+
return result;
|
|
299
438
|
}
|
|
300
|
-
|
|
301
|
-
this.
|
|
302
|
-
logger.
|
|
303
|
-
|
|
304
|
-
|
|
439
|
+
if (shouldEscalateNoOpMicro && summarized.noProgress) {
|
|
440
|
+
this.suppressNoOpMicroSummaryUntilCompact = true;
|
|
441
|
+
logger.info("context.micro_noop_summary_suppressed", {
|
|
442
|
+
tokens,
|
|
443
|
+
ratio,
|
|
444
|
+
compactAtRatio: this.config.compactAtRatio,
|
|
445
|
+
microcompactFloorRatio: this.config.microcompactFloorRatio,
|
|
305
446
|
});
|
|
306
447
|
}
|
|
307
448
|
}
|
|
308
|
-
// Reuse the `tokens` we already computed above. We
|
|
309
|
-
//
|
|
310
|
-
//
|
|
449
|
+
// Reuse the `tokens` we already computed above. We get here if no summary
|
|
450
|
+
// compacted the prompt, or if summary helped but the hybrid estimate is
|
|
451
|
+
// still above the fallback gate.
|
|
311
452
|
let live = tokens;
|
|
312
|
-
const snipGate = this.config.maxTokens * this.config.compactAtRatio;
|
|
313
|
-
const windowGate = this.config.maxTokens * (this.config.compactAtRatio + 0.05);
|
|
314
|
-
const emergencyGate = this.config.maxTokens * this.config.summarizeAtRatio;
|
|
315
453
|
// Tier 2 fallback: snip first (cheapest sync option)
|
|
316
454
|
if (live > snipGate) {
|
|
317
455
|
const before = live;
|
|
@@ -336,6 +474,61 @@ export class ContextManager {
|
|
|
336
474
|
}
|
|
337
475
|
return result;
|
|
338
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Force maximum compaction, ignoring the ratio gates. This is what a manual
|
|
479
|
+
* `/compact` invokes: the user explicitly asked to shrink NOW, so we don't
|
|
480
|
+
* wait for the prompt to reach compactAtRatio.
|
|
481
|
+
*
|
|
482
|
+
* Order: tier-0 cleanups (persist/truncate/dedupe/mask) + microcompact, then
|
|
483
|
+
* an unconditional LLM summary of the older messages. If no summarizeFn is
|
|
484
|
+
* wired (or it fails / yields nothing), fall back to snip → window so the
|
|
485
|
+
* call still shrinks the conversation rather than no-opping.
|
|
486
|
+
*
|
|
487
|
+
* Unlike manage()/manageAsync(), there is NO `ratio >= compactAtRatio` gate:
|
|
488
|
+
* a long-but-under-threshold text-only conversation (the /compact bug) still
|
|
489
|
+
* gets summarized here.
|
|
490
|
+
*/
|
|
491
|
+
async forceSummarize(messages) {
|
|
492
|
+
let result = messages;
|
|
493
|
+
// Tier 0: same waste-removal + micro as the automatic path.
|
|
494
|
+
result = this.persistLargeToolResults(result);
|
|
495
|
+
result = this.truncateToolResults(result);
|
|
496
|
+
result = applyToolResultBudget(result);
|
|
497
|
+
const dedup = dedupeFileReads(result);
|
|
498
|
+
if (dedup.clearedCount > 0)
|
|
499
|
+
result = dedup.messages;
|
|
500
|
+
const masked = maskOldObservations(result);
|
|
501
|
+
if (masked.maskedCount > 0)
|
|
502
|
+
result = masked.messages;
|
|
503
|
+
const keepRecentN = this.config.microcompactKeepRecent ?? defaultKeepRecent(this.config.maxTokens);
|
|
504
|
+
result = microcompact(result, { keepRecentN });
|
|
505
|
+
let tokens = this.estimateTokensHybrid(result);
|
|
506
|
+
// Unconditional LLM summary (no ratio gate).
|
|
507
|
+
{
|
|
508
|
+
const summarized = await this.trySummaryCompact(result, tokens, "context.force_summary_compact");
|
|
509
|
+
result = summarized.messages;
|
|
510
|
+
tokens = summarized.tokens;
|
|
511
|
+
if (summarized.compacted)
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
// Fallback: no summary available — snip, then window, so /compact still
|
|
515
|
+
// shrinks the conversation instead of no-opping.
|
|
516
|
+
{
|
|
517
|
+
const before = tokens;
|
|
518
|
+
result = snipCompact(result, 3, 8);
|
|
519
|
+
tokens = this.estimateTokensHybrid(result);
|
|
520
|
+
if (tokens < before)
|
|
521
|
+
this.onCompact?.({ strategy: "snip", before, after: tokens });
|
|
522
|
+
}
|
|
523
|
+
if (this.estimateTokensHybrid(result) >= this.estimateTokensHybrid(messages)) {
|
|
524
|
+
const before = tokens;
|
|
525
|
+
const keepN = Math.max(10, Math.floor(result.length * 0.4));
|
|
526
|
+
result = windowCompact(result, keepN);
|
|
527
|
+
tokens = this.estimateTokensHybrid(result);
|
|
528
|
+
this.onCompact?.({ strategy: "window", before, after: tokens });
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
339
532
|
/**
|
|
340
533
|
* Persist large tool_result blocks to disk and replace them with a
|
|
341
534
|
* preview + filepath. No-op when transcript path hasn't been set
|
|
@@ -395,13 +588,16 @@ export class ContextManager {
|
|
|
395
588
|
* Check if context is approaching limits.
|
|
396
589
|
*/
|
|
397
590
|
checkLimits(messages) {
|
|
398
|
-
const
|
|
591
|
+
const estimate = this.estimateTokensHybridInfo(messages);
|
|
592
|
+
const tokens = estimate.tokens;
|
|
399
593
|
const ratio = tokens / this.config.maxTokens;
|
|
400
594
|
return {
|
|
401
595
|
tokens,
|
|
402
596
|
ratio,
|
|
403
597
|
needsCompact: ratio >= this.config.compactAtRatio,
|
|
404
598
|
needsEmergency: ratio >= this.config.summarizeAtRatio,
|
|
599
|
+
promptTokensSource: estimate.source,
|
|
600
|
+
promptTokensConfidence: estimate.confidence,
|
|
405
601
|
};
|
|
406
602
|
}
|
|
407
603
|
}
|
|
@@ -69,11 +69,24 @@ function estimateBlockTokens(block) {
|
|
|
69
69
|
if (block.content && typeof block.content === "string") {
|
|
70
70
|
tokens += estimateStringTokens(block.content);
|
|
71
71
|
}
|
|
72
|
+
else if (Array.isArray(block.content)) {
|
|
73
|
+
for (const nested of block.content) {
|
|
74
|
+
tokens += estimateBlockTokens(nested);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
72
77
|
if (block.input) {
|
|
73
78
|
tokens += estimateStringTokens(JSON.stringify(block.input));
|
|
74
79
|
}
|
|
75
80
|
if (block.name)
|
|
76
81
|
tokens += estimateStringTokens(block.name);
|
|
82
|
+
if (block.source?.data) {
|
|
83
|
+
tokens += estimateStringTokens(block.source.media_type);
|
|
84
|
+
tokens += estimateStringTokens(block.source.data);
|
|
85
|
+
}
|
|
86
|
+
const maybeOpenAI = block;
|
|
87
|
+
if (typeof maybeOpenAI.image_url?.url === "string") {
|
|
88
|
+
tokens += estimateStringTokens(maybeOpenAI.image_url.url);
|
|
89
|
+
}
|
|
77
90
|
return tokens;
|
|
78
91
|
}
|
|
79
92
|
export function calculateContextUsage(messages, systemPrompt, maxTokens) {
|