@witqq/agent-sdk 0.7.0 → 0.8.0
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/README.md +140 -34
- package/dist/{types-CqvUAYxt.d.cts → agent-CW9XbmG_.d.ts} +137 -102
- package/dist/{types-CqvUAYxt.d.ts → agent-DxY68NZL.d.cts} +137 -102
- package/dist/auth/index.cjs +72 -1
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.cts +21 -154
- package/dist/auth/index.d.ts +21 -154
- package/dist/auth/index.js +72 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/backends/claude.cjs +480 -261
- package/dist/backends/claude.cjs.map +1 -1
- package/dist/backends/claude.d.cts +3 -1
- package/dist/backends/claude.d.ts +3 -1
- package/dist/backends/claude.js +480 -261
- package/dist/backends/claude.js.map +1 -1
- package/dist/backends/copilot.cjs +329 -97
- package/dist/backends/copilot.cjs.map +1 -1
- package/dist/backends/copilot.d.cts +12 -4
- package/dist/backends/copilot.d.ts +12 -4
- package/dist/backends/copilot.js +329 -97
- package/dist/backends/copilot.js.map +1 -1
- package/dist/backends/vercel-ai.cjs +294 -61
- package/dist/backends/vercel-ai.cjs.map +1 -1
- package/dist/backends/vercel-ai.d.cts +3 -1
- package/dist/backends/vercel-ai.d.ts +3 -1
- package/dist/backends/vercel-ai.js +294 -61
- package/dist/backends/vercel-ai.js.map +1 -1
- package/dist/backends-BSrsBYFn.d.cts +39 -0
- package/dist/backends-BSrsBYFn.d.ts +39 -0
- package/dist/chat/accumulator.cjs +1 -1
- package/dist/chat/accumulator.cjs.map +1 -1
- package/dist/chat/accumulator.d.cts +5 -2
- package/dist/chat/accumulator.d.ts +5 -2
- package/dist/chat/accumulator.js +1 -1
- package/dist/chat/accumulator.js.map +1 -1
- package/dist/chat/backends.cjs +736 -746
- package/dist/chat/backends.cjs.map +1 -1
- package/dist/chat/backends.d.cts +10 -6
- package/dist/chat/backends.d.ts +10 -6
- package/dist/chat/backends.js +736 -725
- package/dist/chat/backends.js.map +1 -1
- package/dist/chat/context.cjs +50 -0
- package/dist/chat/context.cjs.map +1 -1
- package/dist/chat/context.d.cts +27 -3
- package/dist/chat/context.d.ts +27 -3
- package/dist/chat/context.js +50 -0
- package/dist/chat/context.js.map +1 -1
- package/dist/chat/core.cjs +25 -2
- package/dist/chat/core.cjs.map +1 -1
- package/dist/chat/core.d.cts +30 -381
- package/dist/chat/core.d.ts +30 -381
- package/dist/chat/core.js +24 -3
- package/dist/chat/core.js.map +1 -1
- package/dist/chat/errors.cjs +48 -26
- package/dist/chat/errors.cjs.map +1 -1
- package/dist/chat/errors.d.cts +6 -31
- package/dist/chat/errors.d.ts +6 -31
- package/dist/chat/errors.js +48 -25
- package/dist/chat/errors.js.map +1 -1
- package/dist/chat/events.cjs.map +1 -1
- package/dist/chat/events.d.cts +6 -2
- package/dist/chat/events.d.ts +6 -2
- package/dist/chat/events.js.map +1 -1
- package/dist/chat/index.cjs +1199 -1008
- package/dist/chat/index.cjs.map +1 -1
- package/dist/chat/index.d.cts +35 -10
- package/dist/chat/index.d.ts +35 -10
- package/dist/chat/index.js +1196 -987
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react/theme.css +2517 -0
- package/dist/chat/react.cjs +2003 -1153
- package/dist/chat/react.cjs.map +1 -1
- package/dist/chat/react.d.cts +590 -121
- package/dist/chat/react.d.ts +590 -121
- package/dist/chat/react.js +1984 -1151
- package/dist/chat/react.js.map +1 -1
- package/dist/chat/runtime.cjs +401 -186
- package/dist/chat/runtime.cjs.map +1 -1
- package/dist/chat/runtime.d.cts +92 -28
- package/dist/chat/runtime.d.ts +92 -28
- package/dist/chat/runtime.js +401 -186
- package/dist/chat/runtime.js.map +1 -1
- package/dist/chat/server.cjs +2234 -209
- package/dist/chat/server.cjs.map +1 -1
- package/dist/chat/server.d.cts +451 -90
- package/dist/chat/server.d.ts +451 -90
- package/dist/chat/server.js +2221 -210
- package/dist/chat/server.js.map +1 -1
- package/dist/chat/sessions.cjs +25 -43
- package/dist/chat/sessions.cjs.map +1 -1
- package/dist/chat/sessions.d.cts +37 -118
- package/dist/chat/sessions.d.ts +37 -118
- package/dist/chat/sessions.js +25 -43
- package/dist/chat/sessions.js.map +1 -1
- package/dist/chat/sqlite.cjs +441 -0
- package/dist/chat/sqlite.cjs.map +1 -0
- package/dist/chat/sqlite.d.cts +128 -0
- package/dist/chat/sqlite.d.ts +128 -0
- package/dist/chat/sqlite.js +435 -0
- package/dist/chat/sqlite.js.map +1 -0
- package/dist/chat/state.cjs +14 -1
- package/dist/chat/state.cjs.map +1 -1
- package/dist/chat/state.d.cts +5 -2
- package/dist/chat/state.d.ts +5 -2
- package/dist/chat/state.js +14 -1
- package/dist/chat/state.js.map +1 -1
- package/dist/chat/storage.cjs +19 -10
- package/dist/chat/storage.cjs.map +1 -1
- package/dist/chat/storage.d.cts +11 -5
- package/dist/chat/storage.d.ts +11 -5
- package/dist/chat/storage.js +19 -10
- package/dist/chat/storage.js.map +1 -1
- package/dist/errors-C-so0M4t.d.cts +33 -0
- package/dist/errors-C-so0M4t.d.ts +33 -0
- package/dist/errors-CmVvczxZ.d.cts +28 -0
- package/dist/errors-CmVvczxZ.d.ts +28 -0
- package/dist/{in-process-transport-C2oPTYs6.d.ts → in-process-transport-C1JnJGVR.d.ts} +28 -23
- package/dist/{in-process-transport-DG-w5G6k.d.cts → in-process-transport-C7DSqPyX.d.cts} +28 -23
- package/dist/index.cjs +340 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +292 -123
- package/dist/index.d.ts +292 -123
- package/dist/index.js +334 -47
- package/dist/index.js.map +1 -1
- package/dist/provider-types-PTSlRPNB.d.cts +39 -0
- package/dist/provider-types-PTSlRPNB.d.ts +39 -0
- package/dist/refresh-manager-B81PpYBr.d.cts +153 -0
- package/dist/refresh-manager-Dlv_iNZi.d.ts +153 -0
- package/dist/testing.cjs +383 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +132 -0
- package/dist/testing.d.ts +132 -0
- package/dist/testing.js +377 -0
- package/dist/testing.js.map +1 -0
- package/dist/token-store-CSUBgYwn.d.ts +48 -0
- package/dist/token-store-CuC4hB9Z.d.cts +48 -0
- package/dist/{transport-DX1Nhm4N.d.cts → transport-Cdh3M0tS.d.cts} +5 -4
- package/dist/{transport-D1OaUgRk.d.ts → transport-Ciap4PWK.d.ts} +5 -4
- package/dist/{types-CGF7AEX1.d.cts → types-4vbcmPTp.d.cts} +4 -2
- package/dist/{types-Bh5AhqD-.d.ts → types-BxggH0Yh.d.ts} +4 -2
- package/dist/types-DRgd_9R7.d.cts +363 -0
- package/dist/types-ajANVzf7.d.ts +363 -0
- package/package.json +31 -6
- package/dist/errors-BDLbNu9w.d.cts +0 -13
- package/dist/errors-BDLbNu9w.d.ts +0 -13
- package/dist/types-DLZzlJxt.d.ts +0 -39
- package/dist/types-tE0CXwBl.d.cts +0 -39
package/dist/chat/context.cjs
CHANGED
|
@@ -108,6 +108,56 @@ var ContextWindowManager = class {
|
|
|
108
108
|
});
|
|
109
109
|
return { ...result, messages: updatedMessages };
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Trim messages using real token usage data from the previous API call.
|
|
113
|
+
* Uses average-based algorithm: `avgTokensPerMessage = lastPromptTokens / messageCount`.
|
|
114
|
+
* Removes oldest non-system messages until freed budget brings usage under modelContextWindow.
|
|
115
|
+
*
|
|
116
|
+
* @param messages - All messages in the session
|
|
117
|
+
* @param lastPromptTokens - Real prompt tokens from the last API response
|
|
118
|
+
* @param modelContextWindow - Model's total context window size in tokens
|
|
119
|
+
* @returns Result with fitted messages and metadata
|
|
120
|
+
*/
|
|
121
|
+
fitMessagesWithUsage(messages, lastPromptTokens, modelContextWindow) {
|
|
122
|
+
if (messages.length === 0) {
|
|
123
|
+
return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };
|
|
124
|
+
}
|
|
125
|
+
const budget = modelContextWindow - this.config.reservedTokens;
|
|
126
|
+
if (budget <= 0 || lastPromptTokens <= budget) {
|
|
127
|
+
return {
|
|
128
|
+
messages: [...messages],
|
|
129
|
+
totalTokens: lastPromptTokens,
|
|
130
|
+
removedCount: 0,
|
|
131
|
+
wasTruncated: false
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const avgTokensPerMessage = lastPromptTokens / messages.length;
|
|
135
|
+
const tokensToFree = lastPromptTokens - budget;
|
|
136
|
+
const messagesToRemove = Math.ceil(tokensToFree / avgTokensPerMessage);
|
|
137
|
+
const nonSystemIndices = [];
|
|
138
|
+
for (let i = 0; i < messages.length; i++) {
|
|
139
|
+
if (messages[i].role === "system") ; else {
|
|
140
|
+
nonSystemIndices.push(i);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const removableCount = Math.min(messagesToRemove, nonSystemIndices.length);
|
|
144
|
+
const removedIndices = new Set(nonSystemIndices.slice(0, removableCount));
|
|
145
|
+
const result = [];
|
|
146
|
+
for (let i = 0; i < messages.length; i++) {
|
|
147
|
+
if (!removedIndices.has(i)) {
|
|
148
|
+
result.push(messages[i]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const estimatedTokens = Math.round(
|
|
152
|
+
lastPromptTokens * (result.length / messages.length)
|
|
153
|
+
);
|
|
154
|
+
return {
|
|
155
|
+
messages: result,
|
|
156
|
+
totalTokens: estimatedTokens,
|
|
157
|
+
removedCount: removableCount,
|
|
158
|
+
wasTruncated: removableCount > 0
|
|
159
|
+
};
|
|
160
|
+
}
|
|
111
161
|
/**
|
|
112
162
|
* Truncate oldest: keeps system messages, removes oldest non-system messages first.
|
|
113
163
|
* Always keeps the most recent user message.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/chat/context.ts"],"names":[],"mappings":"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","file":"context.cjs","sourcesContent":["/**\n * @witqq/agent-sdk/chat/context\n *\n * Context window manager for selecting which messages fit within a token budget.\n * Stateless: takes messages in, returns trimmed messages out.\n * Three overflow strategies: truncate-oldest, sliding-window, summarize-placeholder.\n */\n\nimport type { ChatMessage, MessagePart } from \"./core.js\";\n\n// ─── Token Estimation ──────────────────────────────────────────\n\n/**\n * Options for token estimation.\n */\nexport interface TokenEstimationOptions {\n /**\n * Characters per token ratio.\n * Lower = more conservative (fewer messages fit).\n * @default 4\n */\n charsPerToken?: number;\n}\n\n/**\n * Estimate token count for a single chat message.\n * Uses character-based heuristic: `Math.ceil(charCount / charsPerToken)`.\n *\n * Counts:\n * - Text content (string or text parts)\n * - Serialized tool calls and tool results\n * - Thinking blocks\n * - Role overhead (~4 tokens)\n *\n * @param message - Chat message to estimate\n * @param options - Estimation options\n * @returns Estimated token count\n *\n * @example\n * ```typescript\n * const tokens = estimateTokens(message);\n * const conservative = estimateTokens(message, { charsPerToken: 3 });\n * ```\n */\nexport function estimateTokens(\n message: ChatMessage,\n options?: TokenEstimationOptions,\n): number {\n const ratio = options?.charsPerToken ?? 4;\n let charCount = 0;\n\n // Role overhead\n charCount += message.role.length + 4;\n\n // Parts\n for (const part of message.parts) {\n charCount += estimatePartChars(part);\n }\n\n return Math.ceil(charCount / ratio);\n}\n\nfunction estimatePartChars(part: MessagePart): number {\n switch (part.type) {\n case \"text\":\n return part.text.length;\n case \"reasoning\":\n return part.text.length;\n case \"tool_call\":\n return JSON.stringify(part.args).length + part.name.length + 20 +\n (part.result !== undefined ? JSON.stringify(part.result).length : 0);\n case \"source\":\n return (part.title?.length ?? 0) + part.url.length + 10;\n case \"file\":\n return part.name.length + part.data.length + 20;\n }\n}\n\n// ─── Overflow Strategies ───────────────────────────────────────\n\n/** Overflow strategy type */\nexport type OverflowStrategy =\n | \"truncate-oldest\"\n | \"sliding-window\"\n | \"summarize-placeholder\";\n\n// ─── Context Window Configuration ──────────────────────────────\n\n/**\n * Async summarizer function for the summarize-placeholder strategy.\n * Receives removed messages and returns a summary string.\n * When configured, replaces the static placeholder text with actual summary.\n */\nexport type ContextSummarizer = (removedMessages: readonly ChatMessage[]) => Promise<string>;\n\n/**\n * Configuration for the context window manager.\n */\nexport interface ContextWindowConfig {\n /** Maximum token budget for the context window */\n maxTokens: number;\n\n /**\n * Tokens reserved for system prompt and response generation.\n * Subtracted from maxTokens to get available budget.\n * @default 0\n */\n reservedTokens?: number;\n\n /**\n * Strategy for handling overflow when messages exceed budget.\n * @default \"truncate-oldest\"\n */\n strategy?: OverflowStrategy;\n\n /**\n * Token estimation options.\n */\n estimation?: TokenEstimationOptions;\n\n /**\n * Optional async summarizer for the summarize-placeholder strategy.\n * When provided, replaces the static placeholder with a generated summary.\n * Falls back to static placeholder if summarizer throws.\n */\n summarizer?: ContextSummarizer;\n}\n\n// ─── Context Window Result ─────────────────────────────────────\n\n/**\n * Result of context window trimming.\n */\nexport interface ContextWindowResult {\n /** Messages that fit within the budget */\n messages: ChatMessage[];\n /** Total estimated tokens for included messages */\n totalTokens: number;\n /** Number of messages removed */\n removedCount: number;\n /** Whether any messages were truncated */\n wasTruncated: boolean;\n}\n\n// ─── Context Stats ─────────────────────────────────────────────\n\n/**\n * Context usage statistics for a session.\n * Returned by `IChatRuntime.getContextStats()`.\n */\nexport interface ContextStats {\n /** Estimated total tokens in the trimmed context */\n totalTokens: number;\n /** Number of messages removed by trimming */\n removedCount: number;\n /** Whether context was truncated */\n wasTruncated: boolean;\n /** Available token budget (maxTokens − reservedTokens) */\n availableBudget: number;\n}\n\n// ─── Context Window Manager ────────────────────────────────────\n\n/**\n * Stateless context window manager.\n * Takes messages and returns the subset that fits within a token budget.\n *\n * @example\n * ```typescript\n * const manager = new ContextWindowManager({\n * maxTokens: 4096,\n * reservedTokens: 500,\n * strategy: \"sliding-window\",\n * });\n *\n * const result = manager.fitMessages(messages);\n * // result.messages — trimmed to fit budget\n * // result.totalTokens — estimated token usage\n * // result.wasTruncated — whether messages were removed\n * ```\n */\nexport class ContextWindowManager {\n private readonly config: Required<\n Pick<ContextWindowConfig, \"maxTokens\" | \"reservedTokens\" | \"strategy\">\n > &\n Pick<ContextWindowConfig, \"estimation\" | \"summarizer\">;\n\n constructor(config: ContextWindowConfig) {\n this.config = {\n maxTokens: config.maxTokens,\n reservedTokens: config.reservedTokens ?? 0,\n strategy: config.strategy ?? \"truncate-oldest\",\n estimation: config.estimation,\n summarizer: config.summarizer,\n };\n }\n\n /** Available token budget after reserving tokens */\n get availableBudget(): number {\n return Math.max(0, this.config.maxTokens - this.config.reservedTokens);\n }\n\n /**\n * Estimate tokens for a single message.\n * @param message - Message to estimate\n * @returns Estimated token count\n */\n estimateMessageTokens(message: ChatMessage): number {\n return estimateTokens(message, this.config.estimation);\n }\n\n /**\n * Fit messages within the token budget using the configured strategy.\n * @param messages - All messages to consider\n * @returns Result with fitted messages and metadata\n */\n fitMessages(messages: readonly ChatMessage[]): ContextWindowResult {\n if (messages.length === 0) {\n return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };\n }\n\n const budget = this.availableBudget;\n\n // Calculate tokens for each message\n const tokenCounts = messages.map((m) => this.estimateMessageTokens(m));\n const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);\n\n // All messages fit\n if (totalTokens <= budget) {\n return {\n messages: [...messages],\n totalTokens,\n removedCount: 0,\n wasTruncated: false,\n };\n }\n\n switch (this.config.strategy) {\n case \"truncate-oldest\":\n return this.truncateOldest(messages, tokenCounts, budget);\n case \"sliding-window\":\n return this.slidingWindow(messages, tokenCounts, budget);\n case \"summarize-placeholder\":\n return this.summarizePlaceholder(messages, tokenCounts, budget);\n }\n }\n\n /**\n * Async variant of fitMessages that supports async summarization.\n * When strategy is \"summarize-placeholder\" and a summarizer is configured,\n * calls the summarizer with removed messages and replaces the placeholder text.\n * Falls back to static placeholder if summarizer throws.\n * For other strategies, behaves identically to fitMessages().\n */\n async fitMessagesAsync(messages: readonly ChatMessage[]): Promise<ContextWindowResult> {\n const result = this.fitMessages(messages);\n\n // Only enhance if summarize-placeholder strategy, messages were removed, and summarizer is configured\n if (\n this.config.strategy !== \"summarize-placeholder\" ||\n !result.wasTruncated ||\n !this.config.summarizer\n ) {\n return result;\n }\n\n // Find removed messages (those in original but not in result)\n const keptIds = new Set(result.messages.map(m => m.id));\n const removed = messages.filter(m => !keptIds.has(m.id));\n if (removed.length === 0) return result;\n\n // Call async summarizer, fall back to static placeholder on error\n let summaryText: string;\n try {\n summaryText = await this.config.summarizer(removed);\n } catch {\n return result; // Keep static placeholder on summarizer failure\n }\n\n // Replace placeholder text with summarizer output\n const updatedMessages = result.messages.map(m => {\n if ((m.metadata as Record<string, unknown>)?.isSummary === true) {\n return {\n ...m,\n parts: [{ type: \"text\" as const, text: summaryText, status: \"complete\" as const }],\n };\n }\n return m;\n });\n\n return { ...result, messages: updatedMessages };\n }\n\n /**\n * Truncate oldest: keeps system messages, removes oldest non-system messages first.\n * Always keeps the most recent user message.\n */\n private truncateOldest(\n messages: readonly ChatMessage[],\n tokenCounts: number[],\n budget: number,\n ): ContextWindowResult {\n // Separate system messages (always kept) and non-system\n const systemIndices: number[] = [];\n const nonSystemIndices: number[] = [];\n\n for (let i = 0; i < messages.length; i++) {\n if (messages[i].role === \"system\") {\n systemIndices.push(i);\n } else {\n nonSystemIndices.push(i);\n }\n }\n\n // System messages cost\n let usedTokens = systemIndices.reduce(\n (sum, i) => sum + tokenCounts[i],\n 0,\n );\n\n // If system messages alone exceed budget, still include them\n // (caller should configure reservedTokens properly)\n\n // Try to fit non-system from newest to oldest\n const includedNonSystem: number[] = [];\n for (let i = nonSystemIndices.length - 1; i >= 0; i--) {\n const idx = nonSystemIndices[i];\n if (usedTokens + tokenCounts[idx] <= budget) {\n includedNonSystem.unshift(idx);\n usedTokens += tokenCounts[idx];\n }\n }\n\n // Build result preserving original order\n const includedSet = new Set([...systemIndices, ...includedNonSystem]);\n const result: ChatMessage[] = [];\n let resultTokens = 0;\n for (let i = 0; i < messages.length; i++) {\n if (includedSet.has(i)) {\n result.push(messages[i]);\n resultTokens += tokenCounts[i];\n }\n }\n\n return {\n messages: result,\n totalTokens: resultTokens,\n removedCount: messages.length - result.length,\n wasTruncated: true,\n };\n }\n\n /**\n * Sliding window: keeps the most recent messages that fit within budget.\n */\n private slidingWindow(\n messages: readonly ChatMessage[],\n tokenCounts: number[],\n budget: number,\n ): ContextWindowResult {\n const result: ChatMessage[] = [];\n let usedTokens = 0;\n\n // Walk from newest to oldest\n for (let i = messages.length - 1; i >= 0; i--) {\n if (usedTokens + tokenCounts[i] <= budget) {\n result.unshift(messages[i]);\n usedTokens += tokenCounts[i];\n } else {\n break;\n }\n }\n\n return {\n messages: result,\n totalTokens: usedTokens,\n removedCount: messages.length - result.length,\n wasTruncated: true,\n };\n }\n\n /**\n * Summarize placeholder: replaces truncated messages with a placeholder,\n * preserving system messages and recent context.\n */\n private summarizePlaceholder(\n messages: readonly ChatMessage[],\n tokenCounts: number[],\n budget: number,\n ): ContextWindowResult {\n // First, identify system messages and recent messages\n const systemMessages: { msg: ChatMessage; tokens: number }[] = [];\n const nonSystem: { msg: ChatMessage; tokens: number; idx: number }[] = [];\n\n for (let i = 0; i < messages.length; i++) {\n if (messages[i].role === \"system\") {\n systemMessages.push({ msg: messages[i], tokens: tokenCounts[i] });\n } else {\n nonSystem.push({ msg: messages[i], tokens: tokenCounts[i], idx: i });\n }\n }\n\n // System message cost\n let usedTokens = systemMessages.reduce((s, m) => s + m.tokens, 0);\n\n // Placeholder costs ~20 tokens\n const placeholderTokens = 20;\n usedTokens += placeholderTokens;\n\n // Fit recent non-system messages from newest\n const recentKept: typeof nonSystem = [];\n for (let i = nonSystem.length - 1; i >= 0; i--) {\n if (usedTokens + nonSystem[i].tokens <= budget) {\n recentKept.unshift(nonSystem[i]);\n usedTokens += nonSystem[i].tokens;\n } else {\n break;\n }\n }\n\n const removedCount =\n messages.length -\n systemMessages.length -\n recentKept.length;\n\n // Build result: system messages, placeholder, recent messages\n const result: ChatMessage[] = [];\n\n // System messages first\n for (const sm of systemMessages) {\n result.push(sm.msg);\n }\n\n // Placeholder if messages were removed\n if (removedCount > 0) {\n result.push({\n id: \"context-placeholder\" as ChatMessage[\"id\"],\n role: \"system\",\n parts: [{ type: \"text\", text: `[${removedCount} earlier message${removedCount === 1 ? \"\" : \"s\"} omitted for context window]`, status: \"complete\" as const }],\n metadata: { isSummary: true },\n createdAt: new Date().toISOString(),\n status: \"complete\",\n });\n }\n\n // Recent messages\n for (const m of recentKept) {\n result.push(m.msg);\n }\n\n return {\n messages: result,\n totalTokens: usedTokens,\n removedCount,\n wasTruncated: true,\n };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/chat/context.ts"],"names":[],"mappings":"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","file":"context.cjs","sourcesContent":["/**\n * @witqq/agent-sdk/chat/context\n *\n * Context window manager for selecting which messages fit within a token budget.\n * Stateless: takes messages in, returns trimmed messages out.\n * Three overflow strategies: truncate-oldest, sliding-window, summarize-placeholder.\n */\n\nimport type { ChatMessage, MessagePart } from \"./core.js\";\n\n// ─── Token Estimation ──────────────────────────────────────────\n\n/**\n * Options for token estimation.\n */\nexport interface TokenEstimationOptions {\n /**\n * Characters per token ratio.\n * Lower = more conservative (fewer messages fit).\n * @default 4\n */\n charsPerToken?: number;\n}\n\n/**\n * Estimate token count for a single chat message.\n * Uses character-based heuristic: `Math.ceil(charCount / charsPerToken)`.\n *\n * Counts:\n * - Text content (string or text parts)\n * - Serialized tool calls and tool results\n * - Thinking blocks\n * - Role overhead (~4 tokens)\n *\n * @param message - Chat message to estimate\n * @param options - Estimation options\n * @returns Estimated token count\n *\n * @example\n * ```typescript\n * const tokens = estimateTokens(message);\n * const conservative = estimateTokens(message, { charsPerToken: 3 });\n * ```\n */\nexport function estimateTokens(\n message: ChatMessage,\n options?: TokenEstimationOptions,\n): number {\n const ratio = options?.charsPerToken ?? 4;\n let charCount = 0;\n\n // Role overhead\n charCount += message.role.length + 4;\n\n // Parts\n for (const part of message.parts) {\n charCount += estimatePartChars(part);\n }\n\n return Math.ceil(charCount / ratio);\n}\n\nfunction estimatePartChars(part: MessagePart): number {\n switch (part.type) {\n case \"text\":\n return part.text.length;\n case \"reasoning\":\n return part.text.length;\n case \"tool_call\":\n return JSON.stringify(part.args).length + part.name.length + 20 +\n (part.result !== undefined ? JSON.stringify(part.result).length : 0);\n case \"source\":\n return (part.title?.length ?? 0) + part.url.length + 10;\n case \"file\":\n return part.name.length + part.data.length + 20;\n }\n}\n\n// ─── Overflow Strategies ───────────────────────────────────────\n\n/** Overflow strategy type */\nexport type OverflowStrategy =\n | \"truncate-oldest\"\n | \"sliding-window\"\n | \"summarize-placeholder\";\n\n// ─── Context Window Configuration ──────────────────────────────\n\n/**\n * Async summarizer function for the summarize-placeholder strategy.\n * Receives removed messages and returns a summary string.\n * When configured, replaces the static placeholder text with actual summary.\n */\nexport type ContextSummarizer = (removedMessages: readonly ChatMessage[]) => Promise<string>;\n\n/**\n * Configuration for the context window manager.\n */\nexport interface ContextWindowConfig {\n /** Maximum token budget for the context window */\n maxTokens: number;\n\n /**\n * Tokens reserved for system prompt and response generation.\n * Subtracted from maxTokens to get available budget.\n * @default 0\n */\n reservedTokens?: number;\n\n /**\n * Strategy for handling overflow when messages exceed budget.\n * @default \"truncate-oldest\"\n */\n strategy?: OverflowStrategy;\n\n /**\n * Token estimation options.\n */\n estimation?: TokenEstimationOptions;\n\n /**\n * Optional async summarizer for the summarize-placeholder strategy.\n * When provided, replaces the static placeholder with a generated summary.\n * Falls back to static placeholder if summarizer throws.\n */\n summarizer?: ContextSummarizer;\n}\n\n// ─── Context Window Result ─────────────────────────────────────\n\n/**\n * Result of context window trimming.\n */\nexport interface ContextWindowResult {\n /** Messages that fit within the budget */\n messages: ChatMessage[];\n /** Total estimated tokens for included messages */\n totalTokens: number;\n /** Number of messages removed */\n removedCount: number;\n /** Whether any messages were truncated */\n wasTruncated: boolean;\n}\n\n// ─── Context Stats ─────────────────────────────────────────────\n\n/**\n * Context usage statistics for a session.\n * Returned by `IChatRuntime.getContextStats()`.\n *\n * When real usage data is available (after the first API response),\n * `realPromptTokens` and `realCompletionTokens` contain actual token counts.\n * `modelContextWindow` is the model's context window from `listModels()`.\n */\nexport interface ContextStats {\n /** Estimated total tokens in the trimmed context (heuristic, kept for backward compat) */\n totalTokens: number;\n /** Number of messages removed by trimming */\n removedCount: number;\n /** Whether context was truncated */\n wasTruncated: boolean;\n /** Available token budget (maxTokens − reservedTokens) */\n availableBudget: number;\n /** Real prompt tokens from the last API response (undefined before first response) */\n realPromptTokens?: number;\n /** Real completion tokens from the last API response (undefined before first response) */\n realCompletionTokens?: number;\n /** Model's context window in tokens from listModels() (undefined if not available) */\n modelContextWindow?: number;\n}\n\n// ─── Context Window Manager ────────────────────────────────────\n\n/**\n * Stateless context window manager.\n * Takes messages and returns the subset that fits within a token budget.\n *\n * @example\n * ```typescript\n * const manager = new ContextWindowManager({\n * maxTokens: 4096,\n * reservedTokens: 500,\n * strategy: \"sliding-window\",\n * });\n *\n * const result = manager.fitMessages(messages);\n * // result.messages — trimmed to fit budget\n * // result.totalTokens — estimated token usage\n * // result.wasTruncated — whether messages were removed\n * ```\n */\nexport class ContextWindowManager {\n private readonly config: Required<\n Pick<ContextWindowConfig, \"maxTokens\" | \"reservedTokens\" | \"strategy\">\n > &\n Pick<ContextWindowConfig, \"estimation\" | \"summarizer\">;\n\n constructor(config: ContextWindowConfig) {\n this.config = {\n maxTokens: config.maxTokens,\n reservedTokens: config.reservedTokens ?? 0,\n strategy: config.strategy ?? \"truncate-oldest\",\n estimation: config.estimation,\n summarizer: config.summarizer,\n };\n }\n\n /** Available token budget after reserving tokens */\n get availableBudget(): number {\n return Math.max(0, this.config.maxTokens - this.config.reservedTokens);\n }\n\n /**\n * Estimate tokens for a single message.\n * @param message - Message to estimate\n * @returns Estimated token count\n */\n estimateMessageTokens(message: ChatMessage): number {\n return estimateTokens(message, this.config.estimation);\n }\n\n /**\n * Fit messages within the token budget using the configured strategy.\n * @param messages - All messages to consider\n * @returns Result with fitted messages and metadata\n */\n fitMessages(messages: readonly ChatMessage[]): ContextWindowResult {\n if (messages.length === 0) {\n return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };\n }\n\n const budget = this.availableBudget;\n\n // Calculate tokens for each message\n const tokenCounts = messages.map((m) => this.estimateMessageTokens(m));\n const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);\n\n // All messages fit\n if (totalTokens <= budget) {\n return {\n messages: [...messages],\n totalTokens,\n removedCount: 0,\n wasTruncated: false,\n };\n }\n\n switch (this.config.strategy) {\n case \"truncate-oldest\":\n return this.truncateOldest(messages, tokenCounts, budget);\n case \"sliding-window\":\n return this.slidingWindow(messages, tokenCounts, budget);\n case \"summarize-placeholder\":\n return this.summarizePlaceholder(messages, tokenCounts, budget);\n }\n }\n\n /**\n * Async variant of fitMessages that supports async summarization.\n * When strategy is \"summarize-placeholder\" and a summarizer is configured,\n * calls the summarizer with removed messages and replaces the placeholder text.\n * Falls back to static placeholder if summarizer throws.\n * For other strategies, behaves identically to fitMessages().\n */\n async fitMessagesAsync(messages: readonly ChatMessage[]): Promise<ContextWindowResult> {\n const result = this.fitMessages(messages);\n\n // Only enhance if summarize-placeholder strategy, messages were removed, and summarizer is configured\n if (\n this.config.strategy !== \"summarize-placeholder\" ||\n !result.wasTruncated ||\n !this.config.summarizer\n ) {\n return result;\n }\n\n // Find removed messages (those in original but not in result)\n const keptIds = new Set(result.messages.map(m => m.id));\n const removed = messages.filter(m => !keptIds.has(m.id));\n if (removed.length === 0) return result;\n\n // Call async summarizer, fall back to static placeholder on error\n let summaryText: string;\n try {\n summaryText = await this.config.summarizer(removed);\n } catch {\n return result; // Keep static placeholder on summarizer failure\n }\n\n // Replace placeholder text with summarizer output\n const updatedMessages = result.messages.map(m => {\n if ((m.metadata as Record<string, unknown>)?.isSummary === true) {\n return {\n ...m,\n parts: [{ type: \"text\" as const, text: summaryText, status: \"complete\" as const }],\n };\n }\n return m;\n });\n\n return { ...result, messages: updatedMessages };\n }\n\n /**\n * Trim messages using real token usage data from the previous API call.\n * Uses average-based algorithm: `avgTokensPerMessage = lastPromptTokens / messageCount`.\n * Removes oldest non-system messages until freed budget brings usage under modelContextWindow.\n *\n * @param messages - All messages in the session\n * @param lastPromptTokens - Real prompt tokens from the last API response\n * @param modelContextWindow - Model's total context window size in tokens\n * @returns Result with fitted messages and metadata\n */\n fitMessagesWithUsage(\n messages: readonly ChatMessage[],\n lastPromptTokens: number,\n modelContextWindow: number,\n ): ContextWindowResult {\n if (messages.length === 0) {\n return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };\n }\n\n const budget = modelContextWindow - this.config.reservedTokens;\n if (budget <= 0 || lastPromptTokens <= budget) {\n return {\n messages: [...messages],\n totalTokens: lastPromptTokens,\n removedCount: 0,\n wasTruncated: false,\n };\n }\n\n // Average tokens per message from real data\n const avgTokensPerMessage = lastPromptTokens / messages.length;\n\n // How many tokens we need to free\n const tokensToFree = lastPromptTokens - budget;\n // How many messages to remove (ceil to be safe)\n const messagesToRemove = Math.ceil(tokensToFree / avgTokensPerMessage);\n\n // Separate system and non-system messages\n const systemIndices: number[] = [];\n const nonSystemIndices: number[] = [];\n for (let i = 0; i < messages.length; i++) {\n if (messages[i].role === \"system\") {\n systemIndices.push(i);\n } else {\n nonSystemIndices.push(i);\n }\n }\n\n // Remove oldest non-system messages (from the beginning of conversation)\n const removableCount = Math.min(messagesToRemove, nonSystemIndices.length);\n const removedIndices = new Set(nonSystemIndices.slice(0, removableCount));\n\n const result: ChatMessage[] = [];\n for (let i = 0; i < messages.length; i++) {\n if (!removedIndices.has(i)) {\n result.push(messages[i]);\n }\n }\n\n // Estimate new total: proportional reduction\n const estimatedTokens = Math.round(\n lastPromptTokens * (result.length / messages.length),\n );\n\n return {\n messages: result,\n totalTokens: estimatedTokens,\n removedCount: removableCount,\n wasTruncated: removableCount > 0,\n };\n }\n\n /**\n * Truncate oldest: keeps system messages, removes oldest non-system messages first.\n * Always keeps the most recent user message.\n */\n private truncateOldest(\n messages: readonly ChatMessage[],\n tokenCounts: number[],\n budget: number,\n ): ContextWindowResult {\n // Separate system messages (always kept) and non-system\n const systemIndices: number[] = [];\n const nonSystemIndices: number[] = [];\n\n for (let i = 0; i < messages.length; i++) {\n if (messages[i].role === \"system\") {\n systemIndices.push(i);\n } else {\n nonSystemIndices.push(i);\n }\n }\n\n // System messages cost\n let usedTokens = systemIndices.reduce(\n (sum, i) => sum + tokenCounts[i],\n 0,\n );\n\n // If system messages alone exceed budget, still include them\n // (caller should configure reservedTokens properly)\n\n // Try to fit non-system from newest to oldest\n const includedNonSystem: number[] = [];\n for (let i = nonSystemIndices.length - 1; i >= 0; i--) {\n const idx = nonSystemIndices[i];\n if (usedTokens + tokenCounts[idx] <= budget) {\n includedNonSystem.unshift(idx);\n usedTokens += tokenCounts[idx];\n }\n }\n\n // Build result preserving original order\n const includedSet = new Set([...systemIndices, ...includedNonSystem]);\n const result: ChatMessage[] = [];\n let resultTokens = 0;\n for (let i = 0; i < messages.length; i++) {\n if (includedSet.has(i)) {\n result.push(messages[i]);\n resultTokens += tokenCounts[i];\n }\n }\n\n return {\n messages: result,\n totalTokens: resultTokens,\n removedCount: messages.length - result.length,\n wasTruncated: true,\n };\n }\n\n /**\n * Sliding window: keeps the most recent messages that fit within budget.\n */\n private slidingWindow(\n messages: readonly ChatMessage[],\n tokenCounts: number[],\n budget: number,\n ): ContextWindowResult {\n const result: ChatMessage[] = [];\n let usedTokens = 0;\n\n // Walk from newest to oldest\n for (let i = messages.length - 1; i >= 0; i--) {\n if (usedTokens + tokenCounts[i] <= budget) {\n result.unshift(messages[i]);\n usedTokens += tokenCounts[i];\n } else {\n break;\n }\n }\n\n return {\n messages: result,\n totalTokens: usedTokens,\n removedCount: messages.length - result.length,\n wasTruncated: true,\n };\n }\n\n /**\n * Summarize placeholder: replaces truncated messages with a placeholder,\n * preserving system messages and recent context.\n */\n private summarizePlaceholder(\n messages: readonly ChatMessage[],\n tokenCounts: number[],\n budget: number,\n ): ContextWindowResult {\n // First, identify system messages and recent messages\n const systemMessages: { msg: ChatMessage; tokens: number }[] = [];\n const nonSystem: { msg: ChatMessage; tokens: number; idx: number }[] = [];\n\n for (let i = 0; i < messages.length; i++) {\n if (messages[i].role === \"system\") {\n systemMessages.push({ msg: messages[i], tokens: tokenCounts[i] });\n } else {\n nonSystem.push({ msg: messages[i], tokens: tokenCounts[i], idx: i });\n }\n }\n\n // System message cost\n let usedTokens = systemMessages.reduce((s, m) => s + m.tokens, 0);\n\n // Placeholder costs ~20 tokens\n const placeholderTokens = 20;\n usedTokens += placeholderTokens;\n\n // Fit recent non-system messages from newest\n const recentKept: typeof nonSystem = [];\n for (let i = nonSystem.length - 1; i >= 0; i--) {\n if (usedTokens + nonSystem[i].tokens <= budget) {\n recentKept.unshift(nonSystem[i]);\n usedTokens += nonSystem[i].tokens;\n } else {\n break;\n }\n }\n\n const removedCount =\n messages.length -\n systemMessages.length -\n recentKept.length;\n\n // Build result: system messages, placeholder, recent messages\n const result: ChatMessage[] = [];\n\n // System messages first\n for (const sm of systemMessages) {\n result.push(sm.msg);\n }\n\n // Placeholder if messages were removed\n if (removedCount > 0) {\n result.push({\n id: \"context-placeholder\" as ChatMessage[\"id\"],\n role: \"system\",\n parts: [{ type: \"text\", text: `[${removedCount} earlier message${removedCount === 1 ? \"\" : \"s\"} omitted for context window]`, status: \"complete\" as const }],\n metadata: { isSummary: true },\n createdAt: new Date().toISOString(),\n status: \"complete\",\n });\n }\n\n // Recent messages\n for (const m of recentKept) {\n result.push(m.msg);\n }\n\n return {\n messages: result,\n totalTokens: usedTokens,\n removedCount,\n wasTruncated: true,\n };\n }\n}\n"]}
|
package/dist/chat/context.d.cts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { ChatMessage } from '
|
|
2
|
-
import '../
|
|
1
|
+
import { c as ChatMessage } from '../types-DRgd_9R7.cjs';
|
|
2
|
+
import '../agent-DxY68NZL.cjs';
|
|
3
3
|
import 'zod';
|
|
4
|
+
import '../errors-C-so0M4t.cjs';
|
|
5
|
+
import '../types-4vbcmPTp.cjs';
|
|
6
|
+
import '../errors-CmVvczxZ.cjs';
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* @witqq/agent-sdk/chat/context
|
|
@@ -94,9 +97,13 @@ interface ContextWindowResult {
|
|
|
94
97
|
/**
|
|
95
98
|
* Context usage statistics for a session.
|
|
96
99
|
* Returned by `IChatRuntime.getContextStats()`.
|
|
100
|
+
*
|
|
101
|
+
* When real usage data is available (after the first API response),
|
|
102
|
+
* `realPromptTokens` and `realCompletionTokens` contain actual token counts.
|
|
103
|
+
* `modelContextWindow` is the model's context window from `listModels()`.
|
|
97
104
|
*/
|
|
98
105
|
interface ContextStats {
|
|
99
|
-
/** Estimated total tokens in the trimmed context */
|
|
106
|
+
/** Estimated total tokens in the trimmed context (heuristic, kept for backward compat) */
|
|
100
107
|
totalTokens: number;
|
|
101
108
|
/** Number of messages removed by trimming */
|
|
102
109
|
removedCount: number;
|
|
@@ -104,6 +111,12 @@ interface ContextStats {
|
|
|
104
111
|
wasTruncated: boolean;
|
|
105
112
|
/** Available token budget (maxTokens − reservedTokens) */
|
|
106
113
|
availableBudget: number;
|
|
114
|
+
/** Real prompt tokens from the last API response (undefined before first response) */
|
|
115
|
+
realPromptTokens?: number;
|
|
116
|
+
/** Real completion tokens from the last API response (undefined before first response) */
|
|
117
|
+
realCompletionTokens?: number;
|
|
118
|
+
/** Model's context window in tokens from listModels() (undefined if not available) */
|
|
119
|
+
modelContextWindow?: number;
|
|
107
120
|
}
|
|
108
121
|
/**
|
|
109
122
|
* Stateless context window manager.
|
|
@@ -148,6 +161,17 @@ declare class ContextWindowManager {
|
|
|
148
161
|
* For other strategies, behaves identically to fitMessages().
|
|
149
162
|
*/
|
|
150
163
|
fitMessagesAsync(messages: readonly ChatMessage[]): Promise<ContextWindowResult>;
|
|
164
|
+
/**
|
|
165
|
+
* Trim messages using real token usage data from the previous API call.
|
|
166
|
+
* Uses average-based algorithm: `avgTokensPerMessage = lastPromptTokens / messageCount`.
|
|
167
|
+
* Removes oldest non-system messages until freed budget brings usage under modelContextWindow.
|
|
168
|
+
*
|
|
169
|
+
* @param messages - All messages in the session
|
|
170
|
+
* @param lastPromptTokens - Real prompt tokens from the last API response
|
|
171
|
+
* @param modelContextWindow - Model's total context window size in tokens
|
|
172
|
+
* @returns Result with fitted messages and metadata
|
|
173
|
+
*/
|
|
174
|
+
fitMessagesWithUsage(messages: readonly ChatMessage[], lastPromptTokens: number, modelContextWindow: number): ContextWindowResult;
|
|
151
175
|
/**
|
|
152
176
|
* Truncate oldest: keeps system messages, removes oldest non-system messages first.
|
|
153
177
|
* Always keeps the most recent user message.
|
package/dist/chat/context.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { ChatMessage } from '
|
|
2
|
-
import '../
|
|
1
|
+
import { c as ChatMessage } from '../types-ajANVzf7.js';
|
|
2
|
+
import '../agent-CW9XbmG_.js';
|
|
3
3
|
import 'zod';
|
|
4
|
+
import '../errors-C-so0M4t.js';
|
|
5
|
+
import '../types-BxggH0Yh.js';
|
|
6
|
+
import '../errors-CmVvczxZ.js';
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* @witqq/agent-sdk/chat/context
|
|
@@ -94,9 +97,13 @@ interface ContextWindowResult {
|
|
|
94
97
|
/**
|
|
95
98
|
* Context usage statistics for a session.
|
|
96
99
|
* Returned by `IChatRuntime.getContextStats()`.
|
|
100
|
+
*
|
|
101
|
+
* When real usage data is available (after the first API response),
|
|
102
|
+
* `realPromptTokens` and `realCompletionTokens` contain actual token counts.
|
|
103
|
+
* `modelContextWindow` is the model's context window from `listModels()`.
|
|
97
104
|
*/
|
|
98
105
|
interface ContextStats {
|
|
99
|
-
/** Estimated total tokens in the trimmed context */
|
|
106
|
+
/** Estimated total tokens in the trimmed context (heuristic, kept for backward compat) */
|
|
100
107
|
totalTokens: number;
|
|
101
108
|
/** Number of messages removed by trimming */
|
|
102
109
|
removedCount: number;
|
|
@@ -104,6 +111,12 @@ interface ContextStats {
|
|
|
104
111
|
wasTruncated: boolean;
|
|
105
112
|
/** Available token budget (maxTokens − reservedTokens) */
|
|
106
113
|
availableBudget: number;
|
|
114
|
+
/** Real prompt tokens from the last API response (undefined before first response) */
|
|
115
|
+
realPromptTokens?: number;
|
|
116
|
+
/** Real completion tokens from the last API response (undefined before first response) */
|
|
117
|
+
realCompletionTokens?: number;
|
|
118
|
+
/** Model's context window in tokens from listModels() (undefined if not available) */
|
|
119
|
+
modelContextWindow?: number;
|
|
107
120
|
}
|
|
108
121
|
/**
|
|
109
122
|
* Stateless context window manager.
|
|
@@ -148,6 +161,17 @@ declare class ContextWindowManager {
|
|
|
148
161
|
* For other strategies, behaves identically to fitMessages().
|
|
149
162
|
*/
|
|
150
163
|
fitMessagesAsync(messages: readonly ChatMessage[]): Promise<ContextWindowResult>;
|
|
164
|
+
/**
|
|
165
|
+
* Trim messages using real token usage data from the previous API call.
|
|
166
|
+
* Uses average-based algorithm: `avgTokensPerMessage = lastPromptTokens / messageCount`.
|
|
167
|
+
* Removes oldest non-system messages until freed budget brings usage under modelContextWindow.
|
|
168
|
+
*
|
|
169
|
+
* @param messages - All messages in the session
|
|
170
|
+
* @param lastPromptTokens - Real prompt tokens from the last API response
|
|
171
|
+
* @param modelContextWindow - Model's total context window size in tokens
|
|
172
|
+
* @returns Result with fitted messages and metadata
|
|
173
|
+
*/
|
|
174
|
+
fitMessagesWithUsage(messages: readonly ChatMessage[], lastPromptTokens: number, modelContextWindow: number): ContextWindowResult;
|
|
151
175
|
/**
|
|
152
176
|
* Truncate oldest: keeps system messages, removes oldest non-system messages first.
|
|
153
177
|
* Always keeps the most recent user message.
|
package/dist/chat/context.js
CHANGED
|
@@ -106,6 +106,56 @@ var ContextWindowManager = class {
|
|
|
106
106
|
});
|
|
107
107
|
return { ...result, messages: updatedMessages };
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Trim messages using real token usage data from the previous API call.
|
|
111
|
+
* Uses average-based algorithm: `avgTokensPerMessage = lastPromptTokens / messageCount`.
|
|
112
|
+
* Removes oldest non-system messages until freed budget brings usage under modelContextWindow.
|
|
113
|
+
*
|
|
114
|
+
* @param messages - All messages in the session
|
|
115
|
+
* @param lastPromptTokens - Real prompt tokens from the last API response
|
|
116
|
+
* @param modelContextWindow - Model's total context window size in tokens
|
|
117
|
+
* @returns Result with fitted messages and metadata
|
|
118
|
+
*/
|
|
119
|
+
fitMessagesWithUsage(messages, lastPromptTokens, modelContextWindow) {
|
|
120
|
+
if (messages.length === 0) {
|
|
121
|
+
return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };
|
|
122
|
+
}
|
|
123
|
+
const budget = modelContextWindow - this.config.reservedTokens;
|
|
124
|
+
if (budget <= 0 || lastPromptTokens <= budget) {
|
|
125
|
+
return {
|
|
126
|
+
messages: [...messages],
|
|
127
|
+
totalTokens: lastPromptTokens,
|
|
128
|
+
removedCount: 0,
|
|
129
|
+
wasTruncated: false
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const avgTokensPerMessage = lastPromptTokens / messages.length;
|
|
133
|
+
const tokensToFree = lastPromptTokens - budget;
|
|
134
|
+
const messagesToRemove = Math.ceil(tokensToFree / avgTokensPerMessage);
|
|
135
|
+
const nonSystemIndices = [];
|
|
136
|
+
for (let i = 0; i < messages.length; i++) {
|
|
137
|
+
if (messages[i].role === "system") ; else {
|
|
138
|
+
nonSystemIndices.push(i);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const removableCount = Math.min(messagesToRemove, nonSystemIndices.length);
|
|
142
|
+
const removedIndices = new Set(nonSystemIndices.slice(0, removableCount));
|
|
143
|
+
const result = [];
|
|
144
|
+
for (let i = 0; i < messages.length; i++) {
|
|
145
|
+
if (!removedIndices.has(i)) {
|
|
146
|
+
result.push(messages[i]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const estimatedTokens = Math.round(
|
|
150
|
+
lastPromptTokens * (result.length / messages.length)
|
|
151
|
+
);
|
|
152
|
+
return {
|
|
153
|
+
messages: result,
|
|
154
|
+
totalTokens: estimatedTokens,
|
|
155
|
+
removedCount: removableCount,
|
|
156
|
+
wasTruncated: removableCount > 0
|
|
157
|
+
};
|
|
158
|
+
}
|
|
109
159
|
/**
|
|
110
160
|
* Truncate oldest: keeps system messages, removes oldest non-system messages first.
|
|
111
161
|
* Always keeps the most recent user message.
|
package/dist/chat/context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/chat/context.ts"],"names":[],"mappings":"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","file":"context.js","sourcesContent":["/**\n * @witqq/agent-sdk/chat/context\n *\n * Context window manager for selecting which messages fit within a token budget.\n * Stateless: takes messages in, returns trimmed messages out.\n * Three overflow strategies: truncate-oldest, sliding-window, summarize-placeholder.\n */\n\nimport type { ChatMessage, MessagePart } from \"./core.js\";\n\n// ─── Token Estimation ──────────────────────────────────────────\n\n/**\n * Options for token estimation.\n */\nexport interface TokenEstimationOptions {\n /**\n * Characters per token ratio.\n * Lower = more conservative (fewer messages fit).\n * @default 4\n */\n charsPerToken?: number;\n}\n\n/**\n * Estimate token count for a single chat message.\n * Uses character-based heuristic: `Math.ceil(charCount / charsPerToken)`.\n *\n * Counts:\n * - Text content (string or text parts)\n * - Serialized tool calls and tool results\n * - Thinking blocks\n * - Role overhead (~4 tokens)\n *\n * @param message - Chat message to estimate\n * @param options - Estimation options\n * @returns Estimated token count\n *\n * @example\n * ```typescript\n * const tokens = estimateTokens(message);\n * const conservative = estimateTokens(message, { charsPerToken: 3 });\n * ```\n */\nexport function estimateTokens(\n message: ChatMessage,\n options?: TokenEstimationOptions,\n): number {\n const ratio = options?.charsPerToken ?? 4;\n let charCount = 0;\n\n // Role overhead\n charCount += message.role.length + 4;\n\n // Parts\n for (const part of message.parts) {\n charCount += estimatePartChars(part);\n }\n\n return Math.ceil(charCount / ratio);\n}\n\nfunction estimatePartChars(part: MessagePart): number {\n switch (part.type) {\n case \"text\":\n return part.text.length;\n case \"reasoning\":\n return part.text.length;\n case \"tool_call\":\n return JSON.stringify(part.args).length + part.name.length + 20 +\n (part.result !== undefined ? JSON.stringify(part.result).length : 0);\n case \"source\":\n return (part.title?.length ?? 0) + part.url.length + 10;\n case \"file\":\n return part.name.length + part.data.length + 20;\n }\n}\n\n// ─── Overflow Strategies ───────────────────────────────────────\n\n/** Overflow strategy type */\nexport type OverflowStrategy =\n | \"truncate-oldest\"\n | \"sliding-window\"\n | \"summarize-placeholder\";\n\n// ─── Context Window Configuration ──────────────────────────────\n\n/**\n * Async summarizer function for the summarize-placeholder strategy.\n * Receives removed messages and returns a summary string.\n * When configured, replaces the static placeholder text with actual summary.\n */\nexport type ContextSummarizer = (removedMessages: readonly ChatMessage[]) => Promise<string>;\n\n/**\n * Configuration for the context window manager.\n */\nexport interface ContextWindowConfig {\n /** Maximum token budget for the context window */\n maxTokens: number;\n\n /**\n * Tokens reserved for system prompt and response generation.\n * Subtracted from maxTokens to get available budget.\n * @default 0\n */\n reservedTokens?: number;\n\n /**\n * Strategy for handling overflow when messages exceed budget.\n * @default \"truncate-oldest\"\n */\n strategy?: OverflowStrategy;\n\n /**\n * Token estimation options.\n */\n estimation?: TokenEstimationOptions;\n\n /**\n * Optional async summarizer for the summarize-placeholder strategy.\n * When provided, replaces the static placeholder with a generated summary.\n * Falls back to static placeholder if summarizer throws.\n */\n summarizer?: ContextSummarizer;\n}\n\n// ─── Context Window Result ─────────────────────────────────────\n\n/**\n * Result of context window trimming.\n */\nexport interface ContextWindowResult {\n /** Messages that fit within the budget */\n messages: ChatMessage[];\n /** Total estimated tokens for included messages */\n totalTokens: number;\n /** Number of messages removed */\n removedCount: number;\n /** Whether any messages were truncated */\n wasTruncated: boolean;\n}\n\n// ─── Context Stats ─────────────────────────────────────────────\n\n/**\n * Context usage statistics for a session.\n * Returned by `IChatRuntime.getContextStats()`.\n */\nexport interface ContextStats {\n /** Estimated total tokens in the trimmed context */\n totalTokens: number;\n /** Number of messages removed by trimming */\n removedCount: number;\n /** Whether context was truncated */\n wasTruncated: boolean;\n /** Available token budget (maxTokens − reservedTokens) */\n availableBudget: number;\n}\n\n// ─── Context Window Manager ────────────────────────────────────\n\n/**\n * Stateless context window manager.\n * Takes messages and returns the subset that fits within a token budget.\n *\n * @example\n * ```typescript\n * const manager = new ContextWindowManager({\n * maxTokens: 4096,\n * reservedTokens: 500,\n * strategy: \"sliding-window\",\n * });\n *\n * const result = manager.fitMessages(messages);\n * // result.messages — trimmed to fit budget\n * // result.totalTokens — estimated token usage\n * // result.wasTruncated — whether messages were removed\n * ```\n */\nexport class ContextWindowManager {\n private readonly config: Required<\n Pick<ContextWindowConfig, \"maxTokens\" | \"reservedTokens\" | \"strategy\">\n > &\n Pick<ContextWindowConfig, \"estimation\" | \"summarizer\">;\n\n constructor(config: ContextWindowConfig) {\n this.config = {\n maxTokens: config.maxTokens,\n reservedTokens: config.reservedTokens ?? 0,\n strategy: config.strategy ?? \"truncate-oldest\",\n estimation: config.estimation,\n summarizer: config.summarizer,\n };\n }\n\n /** Available token budget after reserving tokens */\n get availableBudget(): number {\n return Math.max(0, this.config.maxTokens - this.config.reservedTokens);\n }\n\n /**\n * Estimate tokens for a single message.\n * @param message - Message to estimate\n * @returns Estimated token count\n */\n estimateMessageTokens(message: ChatMessage): number {\n return estimateTokens(message, this.config.estimation);\n }\n\n /**\n * Fit messages within the token budget using the configured strategy.\n * @param messages - All messages to consider\n * @returns Result with fitted messages and metadata\n */\n fitMessages(messages: readonly ChatMessage[]): ContextWindowResult {\n if (messages.length === 0) {\n return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };\n }\n\n const budget = this.availableBudget;\n\n // Calculate tokens for each message\n const tokenCounts = messages.map((m) => this.estimateMessageTokens(m));\n const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);\n\n // All messages fit\n if (totalTokens <= budget) {\n return {\n messages: [...messages],\n totalTokens,\n removedCount: 0,\n wasTruncated: false,\n };\n }\n\n switch (this.config.strategy) {\n case \"truncate-oldest\":\n return this.truncateOldest(messages, tokenCounts, budget);\n case \"sliding-window\":\n return this.slidingWindow(messages, tokenCounts, budget);\n case \"summarize-placeholder\":\n return this.summarizePlaceholder(messages, tokenCounts, budget);\n }\n }\n\n /**\n * Async variant of fitMessages that supports async summarization.\n * When strategy is \"summarize-placeholder\" and a summarizer is configured,\n * calls the summarizer with removed messages and replaces the placeholder text.\n * Falls back to static placeholder if summarizer throws.\n * For other strategies, behaves identically to fitMessages().\n */\n async fitMessagesAsync(messages: readonly ChatMessage[]): Promise<ContextWindowResult> {\n const result = this.fitMessages(messages);\n\n // Only enhance if summarize-placeholder strategy, messages were removed, and summarizer is configured\n if (\n this.config.strategy !== \"summarize-placeholder\" ||\n !result.wasTruncated ||\n !this.config.summarizer\n ) {\n return result;\n }\n\n // Find removed messages (those in original but not in result)\n const keptIds = new Set(result.messages.map(m => m.id));\n const removed = messages.filter(m => !keptIds.has(m.id));\n if (removed.length === 0) return result;\n\n // Call async summarizer, fall back to static placeholder on error\n let summaryText: string;\n try {\n summaryText = await this.config.summarizer(removed);\n } catch {\n return result; // Keep static placeholder on summarizer failure\n }\n\n // Replace placeholder text with summarizer output\n const updatedMessages = result.messages.map(m => {\n if ((m.metadata as Record<string, unknown>)?.isSummary === true) {\n return {\n ...m,\n parts: [{ type: \"text\" as const, text: summaryText, status: \"complete\" as const }],\n };\n }\n return m;\n });\n\n return { ...result, messages: updatedMessages };\n }\n\n /**\n * Truncate oldest: keeps system messages, removes oldest non-system messages first.\n * Always keeps the most recent user message.\n */\n private truncateOldest(\n messages: readonly ChatMessage[],\n tokenCounts: number[],\n budget: number,\n ): ContextWindowResult {\n // Separate system messages (always kept) and non-system\n const systemIndices: number[] = [];\n const nonSystemIndices: number[] = [];\n\n for (let i = 0; i < messages.length; i++) {\n if (messages[i].role === \"system\") {\n systemIndices.push(i);\n } else {\n nonSystemIndices.push(i);\n }\n }\n\n // System messages cost\n let usedTokens = systemIndices.reduce(\n (sum, i) => sum + tokenCounts[i],\n 0,\n );\n\n // If system messages alone exceed budget, still include them\n // (caller should configure reservedTokens properly)\n\n // Try to fit non-system from newest to oldest\n const includedNonSystem: number[] = [];\n for (let i = nonSystemIndices.length - 1; i >= 0; i--) {\n const idx = nonSystemIndices[i];\n if (usedTokens + tokenCounts[idx] <= budget) {\n includedNonSystem.unshift(idx);\n usedTokens += tokenCounts[idx];\n }\n }\n\n // Build result preserving original order\n const includedSet = new Set([...systemIndices, ...includedNonSystem]);\n const result: ChatMessage[] = [];\n let resultTokens = 0;\n for (let i = 0; i < messages.length; i++) {\n if (includedSet.has(i)) {\n result.push(messages[i]);\n resultTokens += tokenCounts[i];\n }\n }\n\n return {\n messages: result,\n totalTokens: resultTokens,\n removedCount: messages.length - result.length,\n wasTruncated: true,\n };\n }\n\n /**\n * Sliding window: keeps the most recent messages that fit within budget.\n */\n private slidingWindow(\n messages: readonly ChatMessage[],\n tokenCounts: number[],\n budget: number,\n ): ContextWindowResult {\n const result: ChatMessage[] = [];\n let usedTokens = 0;\n\n // Walk from newest to oldest\n for (let i = messages.length - 1; i >= 0; i--) {\n if (usedTokens + tokenCounts[i] <= budget) {\n result.unshift(messages[i]);\n usedTokens += tokenCounts[i];\n } else {\n break;\n }\n }\n\n return {\n messages: result,\n totalTokens: usedTokens,\n removedCount: messages.length - result.length,\n wasTruncated: true,\n };\n }\n\n /**\n * Summarize placeholder: replaces truncated messages with a placeholder,\n * preserving system messages and recent context.\n */\n private summarizePlaceholder(\n messages: readonly ChatMessage[],\n tokenCounts: number[],\n budget: number,\n ): ContextWindowResult {\n // First, identify system messages and recent messages\n const systemMessages: { msg: ChatMessage; tokens: number }[] = [];\n const nonSystem: { msg: ChatMessage; tokens: number; idx: number }[] = [];\n\n for (let i = 0; i < messages.length; i++) {\n if (messages[i].role === \"system\") {\n systemMessages.push({ msg: messages[i], tokens: tokenCounts[i] });\n } else {\n nonSystem.push({ msg: messages[i], tokens: tokenCounts[i], idx: i });\n }\n }\n\n // System message cost\n let usedTokens = systemMessages.reduce((s, m) => s + m.tokens, 0);\n\n // Placeholder costs ~20 tokens\n const placeholderTokens = 20;\n usedTokens += placeholderTokens;\n\n // Fit recent non-system messages from newest\n const recentKept: typeof nonSystem = [];\n for (let i = nonSystem.length - 1; i >= 0; i--) {\n if (usedTokens + nonSystem[i].tokens <= budget) {\n recentKept.unshift(nonSystem[i]);\n usedTokens += nonSystem[i].tokens;\n } else {\n break;\n }\n }\n\n const removedCount =\n messages.length -\n systemMessages.length -\n recentKept.length;\n\n // Build result: system messages, placeholder, recent messages\n const result: ChatMessage[] = [];\n\n // System messages first\n for (const sm of systemMessages) {\n result.push(sm.msg);\n }\n\n // Placeholder if messages were removed\n if (removedCount > 0) {\n result.push({\n id: \"context-placeholder\" as ChatMessage[\"id\"],\n role: \"system\",\n parts: [{ type: \"text\", text: `[${removedCount} earlier message${removedCount === 1 ? \"\" : \"s\"} omitted for context window]`, status: \"complete\" as const }],\n metadata: { isSummary: true },\n createdAt: new Date().toISOString(),\n status: \"complete\",\n });\n }\n\n // Recent messages\n for (const m of recentKept) {\n result.push(m.msg);\n }\n\n return {\n messages: result,\n totalTokens: usedTokens,\n removedCount,\n wasTruncated: true,\n };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/chat/context.ts"],"names":[],"mappings":"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","file":"context.js","sourcesContent":["/**\n * @witqq/agent-sdk/chat/context\n *\n * Context window manager for selecting which messages fit within a token budget.\n * Stateless: takes messages in, returns trimmed messages out.\n * Three overflow strategies: truncate-oldest, sliding-window, summarize-placeholder.\n */\n\nimport type { ChatMessage, MessagePart } from \"./core.js\";\n\n// ─── Token Estimation ──────────────────────────────────────────\n\n/**\n * Options for token estimation.\n */\nexport interface TokenEstimationOptions {\n /**\n * Characters per token ratio.\n * Lower = more conservative (fewer messages fit).\n * @default 4\n */\n charsPerToken?: number;\n}\n\n/**\n * Estimate token count for a single chat message.\n * Uses character-based heuristic: `Math.ceil(charCount / charsPerToken)`.\n *\n * Counts:\n * - Text content (string or text parts)\n * - Serialized tool calls and tool results\n * - Thinking blocks\n * - Role overhead (~4 tokens)\n *\n * @param message - Chat message to estimate\n * @param options - Estimation options\n * @returns Estimated token count\n *\n * @example\n * ```typescript\n * const tokens = estimateTokens(message);\n * const conservative = estimateTokens(message, { charsPerToken: 3 });\n * ```\n */\nexport function estimateTokens(\n message: ChatMessage,\n options?: TokenEstimationOptions,\n): number {\n const ratio = options?.charsPerToken ?? 4;\n let charCount = 0;\n\n // Role overhead\n charCount += message.role.length + 4;\n\n // Parts\n for (const part of message.parts) {\n charCount += estimatePartChars(part);\n }\n\n return Math.ceil(charCount / ratio);\n}\n\nfunction estimatePartChars(part: MessagePart): number {\n switch (part.type) {\n case \"text\":\n return part.text.length;\n case \"reasoning\":\n return part.text.length;\n case \"tool_call\":\n return JSON.stringify(part.args).length + part.name.length + 20 +\n (part.result !== undefined ? JSON.stringify(part.result).length : 0);\n case \"source\":\n return (part.title?.length ?? 0) + part.url.length + 10;\n case \"file\":\n return part.name.length + part.data.length + 20;\n }\n}\n\n// ─── Overflow Strategies ───────────────────────────────────────\n\n/** Overflow strategy type */\nexport type OverflowStrategy =\n | \"truncate-oldest\"\n | \"sliding-window\"\n | \"summarize-placeholder\";\n\n// ─── Context Window Configuration ──────────────────────────────\n\n/**\n * Async summarizer function for the summarize-placeholder strategy.\n * Receives removed messages and returns a summary string.\n * When configured, replaces the static placeholder text with actual summary.\n */\nexport type ContextSummarizer = (removedMessages: readonly ChatMessage[]) => Promise<string>;\n\n/**\n * Configuration for the context window manager.\n */\nexport interface ContextWindowConfig {\n /** Maximum token budget for the context window */\n maxTokens: number;\n\n /**\n * Tokens reserved for system prompt and response generation.\n * Subtracted from maxTokens to get available budget.\n * @default 0\n */\n reservedTokens?: number;\n\n /**\n * Strategy for handling overflow when messages exceed budget.\n * @default \"truncate-oldest\"\n */\n strategy?: OverflowStrategy;\n\n /**\n * Token estimation options.\n */\n estimation?: TokenEstimationOptions;\n\n /**\n * Optional async summarizer for the summarize-placeholder strategy.\n * When provided, replaces the static placeholder with a generated summary.\n * Falls back to static placeholder if summarizer throws.\n */\n summarizer?: ContextSummarizer;\n}\n\n// ─── Context Window Result ─────────────────────────────────────\n\n/**\n * Result of context window trimming.\n */\nexport interface ContextWindowResult {\n /** Messages that fit within the budget */\n messages: ChatMessage[];\n /** Total estimated tokens for included messages */\n totalTokens: number;\n /** Number of messages removed */\n removedCount: number;\n /** Whether any messages were truncated */\n wasTruncated: boolean;\n}\n\n// ─── Context Stats ─────────────────────────────────────────────\n\n/**\n * Context usage statistics for a session.\n * Returned by `IChatRuntime.getContextStats()`.\n *\n * When real usage data is available (after the first API response),\n * `realPromptTokens` and `realCompletionTokens` contain actual token counts.\n * `modelContextWindow` is the model's context window from `listModels()`.\n */\nexport interface ContextStats {\n /** Estimated total tokens in the trimmed context (heuristic, kept for backward compat) */\n totalTokens: number;\n /** Number of messages removed by trimming */\n removedCount: number;\n /** Whether context was truncated */\n wasTruncated: boolean;\n /** Available token budget (maxTokens − reservedTokens) */\n availableBudget: number;\n /** Real prompt tokens from the last API response (undefined before first response) */\n realPromptTokens?: number;\n /** Real completion tokens from the last API response (undefined before first response) */\n realCompletionTokens?: number;\n /** Model's context window in tokens from listModels() (undefined if not available) */\n modelContextWindow?: number;\n}\n\n// ─── Context Window Manager ────────────────────────────────────\n\n/**\n * Stateless context window manager.\n * Takes messages and returns the subset that fits within a token budget.\n *\n * @example\n * ```typescript\n * const manager = new ContextWindowManager({\n * maxTokens: 4096,\n * reservedTokens: 500,\n * strategy: \"sliding-window\",\n * });\n *\n * const result = manager.fitMessages(messages);\n * // result.messages — trimmed to fit budget\n * // result.totalTokens — estimated token usage\n * // result.wasTruncated — whether messages were removed\n * ```\n */\nexport class ContextWindowManager {\n private readonly config: Required<\n Pick<ContextWindowConfig, \"maxTokens\" | \"reservedTokens\" | \"strategy\">\n > &\n Pick<ContextWindowConfig, \"estimation\" | \"summarizer\">;\n\n constructor(config: ContextWindowConfig) {\n this.config = {\n maxTokens: config.maxTokens,\n reservedTokens: config.reservedTokens ?? 0,\n strategy: config.strategy ?? \"truncate-oldest\",\n estimation: config.estimation,\n summarizer: config.summarizer,\n };\n }\n\n /** Available token budget after reserving tokens */\n get availableBudget(): number {\n return Math.max(0, this.config.maxTokens - this.config.reservedTokens);\n }\n\n /**\n * Estimate tokens for a single message.\n * @param message - Message to estimate\n * @returns Estimated token count\n */\n estimateMessageTokens(message: ChatMessage): number {\n return estimateTokens(message, this.config.estimation);\n }\n\n /**\n * Fit messages within the token budget using the configured strategy.\n * @param messages - All messages to consider\n * @returns Result with fitted messages and metadata\n */\n fitMessages(messages: readonly ChatMessage[]): ContextWindowResult {\n if (messages.length === 0) {\n return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };\n }\n\n const budget = this.availableBudget;\n\n // Calculate tokens for each message\n const tokenCounts = messages.map((m) => this.estimateMessageTokens(m));\n const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);\n\n // All messages fit\n if (totalTokens <= budget) {\n return {\n messages: [...messages],\n totalTokens,\n removedCount: 0,\n wasTruncated: false,\n };\n }\n\n switch (this.config.strategy) {\n case \"truncate-oldest\":\n return this.truncateOldest(messages, tokenCounts, budget);\n case \"sliding-window\":\n return this.slidingWindow(messages, tokenCounts, budget);\n case \"summarize-placeholder\":\n return this.summarizePlaceholder(messages, tokenCounts, budget);\n }\n }\n\n /**\n * Async variant of fitMessages that supports async summarization.\n * When strategy is \"summarize-placeholder\" and a summarizer is configured,\n * calls the summarizer with removed messages and replaces the placeholder text.\n * Falls back to static placeholder if summarizer throws.\n * For other strategies, behaves identically to fitMessages().\n */\n async fitMessagesAsync(messages: readonly ChatMessage[]): Promise<ContextWindowResult> {\n const result = this.fitMessages(messages);\n\n // Only enhance if summarize-placeholder strategy, messages were removed, and summarizer is configured\n if (\n this.config.strategy !== \"summarize-placeholder\" ||\n !result.wasTruncated ||\n !this.config.summarizer\n ) {\n return result;\n }\n\n // Find removed messages (those in original but not in result)\n const keptIds = new Set(result.messages.map(m => m.id));\n const removed = messages.filter(m => !keptIds.has(m.id));\n if (removed.length === 0) return result;\n\n // Call async summarizer, fall back to static placeholder on error\n let summaryText: string;\n try {\n summaryText = await this.config.summarizer(removed);\n } catch {\n return result; // Keep static placeholder on summarizer failure\n }\n\n // Replace placeholder text with summarizer output\n const updatedMessages = result.messages.map(m => {\n if ((m.metadata as Record<string, unknown>)?.isSummary === true) {\n return {\n ...m,\n parts: [{ type: \"text\" as const, text: summaryText, status: \"complete\" as const }],\n };\n }\n return m;\n });\n\n return { ...result, messages: updatedMessages };\n }\n\n /**\n * Trim messages using real token usage data from the previous API call.\n * Uses average-based algorithm: `avgTokensPerMessage = lastPromptTokens / messageCount`.\n * Removes oldest non-system messages until freed budget brings usage under modelContextWindow.\n *\n * @param messages - All messages in the session\n * @param lastPromptTokens - Real prompt tokens from the last API response\n * @param modelContextWindow - Model's total context window size in tokens\n * @returns Result with fitted messages and metadata\n */\n fitMessagesWithUsage(\n messages: readonly ChatMessage[],\n lastPromptTokens: number,\n modelContextWindow: number,\n ): ContextWindowResult {\n if (messages.length === 0) {\n return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };\n }\n\n const budget = modelContextWindow - this.config.reservedTokens;\n if (budget <= 0 || lastPromptTokens <= budget) {\n return {\n messages: [...messages],\n totalTokens: lastPromptTokens,\n removedCount: 0,\n wasTruncated: false,\n };\n }\n\n // Average tokens per message from real data\n const avgTokensPerMessage = lastPromptTokens / messages.length;\n\n // How many tokens we need to free\n const tokensToFree = lastPromptTokens - budget;\n // How many messages to remove (ceil to be safe)\n const messagesToRemove = Math.ceil(tokensToFree / avgTokensPerMessage);\n\n // Separate system and non-system messages\n const systemIndices: number[] = [];\n const nonSystemIndices: number[] = [];\n for (let i = 0; i < messages.length; i++) {\n if (messages[i].role === \"system\") {\n systemIndices.push(i);\n } else {\n nonSystemIndices.push(i);\n }\n }\n\n // Remove oldest non-system messages (from the beginning of conversation)\n const removableCount = Math.min(messagesToRemove, nonSystemIndices.length);\n const removedIndices = new Set(nonSystemIndices.slice(0, removableCount));\n\n const result: ChatMessage[] = [];\n for (let i = 0; i < messages.length; i++) {\n if (!removedIndices.has(i)) {\n result.push(messages[i]);\n }\n }\n\n // Estimate new total: proportional reduction\n const estimatedTokens = Math.round(\n lastPromptTokens * (result.length / messages.length),\n );\n\n return {\n messages: result,\n totalTokens: estimatedTokens,\n removedCount: removableCount,\n wasTruncated: removableCount > 0,\n };\n }\n\n /**\n * Truncate oldest: keeps system messages, removes oldest non-system messages first.\n * Always keeps the most recent user message.\n */\n private truncateOldest(\n messages: readonly ChatMessage[],\n tokenCounts: number[],\n budget: number,\n ): ContextWindowResult {\n // Separate system messages (always kept) and non-system\n const systemIndices: number[] = [];\n const nonSystemIndices: number[] = [];\n\n for (let i = 0; i < messages.length; i++) {\n if (messages[i].role === \"system\") {\n systemIndices.push(i);\n } else {\n nonSystemIndices.push(i);\n }\n }\n\n // System messages cost\n let usedTokens = systemIndices.reduce(\n (sum, i) => sum + tokenCounts[i],\n 0,\n );\n\n // If system messages alone exceed budget, still include them\n // (caller should configure reservedTokens properly)\n\n // Try to fit non-system from newest to oldest\n const includedNonSystem: number[] = [];\n for (let i = nonSystemIndices.length - 1; i >= 0; i--) {\n const idx = nonSystemIndices[i];\n if (usedTokens + tokenCounts[idx] <= budget) {\n includedNonSystem.unshift(idx);\n usedTokens += tokenCounts[idx];\n }\n }\n\n // Build result preserving original order\n const includedSet = new Set([...systemIndices, ...includedNonSystem]);\n const result: ChatMessage[] = [];\n let resultTokens = 0;\n for (let i = 0; i < messages.length; i++) {\n if (includedSet.has(i)) {\n result.push(messages[i]);\n resultTokens += tokenCounts[i];\n }\n }\n\n return {\n messages: result,\n totalTokens: resultTokens,\n removedCount: messages.length - result.length,\n wasTruncated: true,\n };\n }\n\n /**\n * Sliding window: keeps the most recent messages that fit within budget.\n */\n private slidingWindow(\n messages: readonly ChatMessage[],\n tokenCounts: number[],\n budget: number,\n ): ContextWindowResult {\n const result: ChatMessage[] = [];\n let usedTokens = 0;\n\n // Walk from newest to oldest\n for (let i = messages.length - 1; i >= 0; i--) {\n if (usedTokens + tokenCounts[i] <= budget) {\n result.unshift(messages[i]);\n usedTokens += tokenCounts[i];\n } else {\n break;\n }\n }\n\n return {\n messages: result,\n totalTokens: usedTokens,\n removedCount: messages.length - result.length,\n wasTruncated: true,\n };\n }\n\n /**\n * Summarize placeholder: replaces truncated messages with a placeholder,\n * preserving system messages and recent context.\n */\n private summarizePlaceholder(\n messages: readonly ChatMessage[],\n tokenCounts: number[],\n budget: number,\n ): ContextWindowResult {\n // First, identify system messages and recent messages\n const systemMessages: { msg: ChatMessage; tokens: number }[] = [];\n const nonSystem: { msg: ChatMessage; tokens: number; idx: number }[] = [];\n\n for (let i = 0; i < messages.length; i++) {\n if (messages[i].role === \"system\") {\n systemMessages.push({ msg: messages[i], tokens: tokenCounts[i] });\n } else {\n nonSystem.push({ msg: messages[i], tokens: tokenCounts[i], idx: i });\n }\n }\n\n // System message cost\n let usedTokens = systemMessages.reduce((s, m) => s + m.tokens, 0);\n\n // Placeholder costs ~20 tokens\n const placeholderTokens = 20;\n usedTokens += placeholderTokens;\n\n // Fit recent non-system messages from newest\n const recentKept: typeof nonSystem = [];\n for (let i = nonSystem.length - 1; i >= 0; i--) {\n if (usedTokens + nonSystem[i].tokens <= budget) {\n recentKept.unshift(nonSystem[i]);\n usedTokens += nonSystem[i].tokens;\n } else {\n break;\n }\n }\n\n const removedCount =\n messages.length -\n systemMessages.length -\n recentKept.length;\n\n // Build result: system messages, placeholder, recent messages\n const result: ChatMessage[] = [];\n\n // System messages first\n for (const sm of systemMessages) {\n result.push(sm.msg);\n }\n\n // Placeholder if messages were removed\n if (removedCount > 0) {\n result.push({\n id: \"context-placeholder\" as ChatMessage[\"id\"],\n role: \"system\",\n parts: [{ type: \"text\", text: `[${removedCount} earlier message${removedCount === 1 ? \"\" : \"s\"} omitted for context window]`, status: \"complete\" as const }],\n metadata: { isSummary: true },\n createdAt: new Date().toISOString(),\n status: \"complete\",\n });\n }\n\n // Recent messages\n for (const m of recentKept) {\n result.push(m.msg);\n }\n\n return {\n messages: result,\n totalTokens: usedTokens,\n removedCount,\n wasTruncated: true,\n };\n }\n}\n"]}
|
package/dist/chat/core.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// src/chat/
|
|
3
|
+
// src/chat/types.ts
|
|
4
4
|
function createChatId() {
|
|
5
5
|
return crypto.randomUUID();
|
|
6
6
|
}
|
|
@@ -11,6 +11,20 @@ function toChatId(value) {
|
|
|
11
11
|
}
|
|
12
12
|
return value;
|
|
13
13
|
}
|
|
14
|
+
function createTextMessage(text, role = "user") {
|
|
15
|
+
return {
|
|
16
|
+
id: createChatId(),
|
|
17
|
+
role,
|
|
18
|
+
parts: [{ type: "text", text, status: "complete" }],
|
|
19
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20
|
+
status: "complete"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function isObservableSession(session) {
|
|
24
|
+
return "subscribe" in session && typeof session.subscribe === "function" && "getSnapshot" in session && typeof session.getSnapshot === "function";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/chat/chat-utils.ts
|
|
14
28
|
function getMessageText(message) {
|
|
15
29
|
return message.parts.filter((p) => p.type === "text").map((p) => p.text).join("");
|
|
16
30
|
}
|
|
@@ -20,6 +34,8 @@ function getMessageToolCalls(message) {
|
|
|
20
34
|
function getMessageReasoning(message) {
|
|
21
35
|
return message.parts.filter((p) => p.type === "reasoning").map((p) => p.text).join("");
|
|
22
36
|
}
|
|
37
|
+
|
|
38
|
+
// src/chat/guards.ts
|
|
23
39
|
function isChatMessage(value) {
|
|
24
40
|
if (typeof value !== "object" || value === null) return false;
|
|
25
41
|
const obj = value;
|
|
@@ -85,6 +101,8 @@ function isChatEvent(value) {
|
|
|
85
101
|
];
|
|
86
102
|
return validTypes.includes(obj.type);
|
|
87
103
|
}
|
|
104
|
+
|
|
105
|
+
// src/chat/bridge.ts
|
|
88
106
|
function agentEventToChatEvent(event, messageId) {
|
|
89
107
|
switch (event.type) {
|
|
90
108
|
case "text_delta":
|
|
@@ -137,6 +155,7 @@ function agentEventToChatEvent(event, messageId) {
|
|
|
137
155
|
type: "error",
|
|
138
156
|
error: event.error,
|
|
139
157
|
recoverable: event.recoverable,
|
|
158
|
+
code: event.code,
|
|
140
159
|
messageId
|
|
141
160
|
};
|
|
142
161
|
case "heartbeat":
|
|
@@ -183,11 +202,13 @@ function chatEventToAgentEvent(event) {
|
|
|
183
202
|
result: event.result
|
|
184
203
|
};
|
|
185
204
|
case "error":
|
|
186
|
-
return { type: "error", error: event.error, recoverable: event.recoverable };
|
|
205
|
+
return { type: "error", error: event.error, recoverable: event.recoverable, code: event.code };
|
|
187
206
|
default:
|
|
188
207
|
return null;
|
|
189
208
|
}
|
|
190
209
|
}
|
|
210
|
+
|
|
211
|
+
// src/chat/conversion.ts
|
|
191
212
|
function toAgentMessage(message) {
|
|
192
213
|
const textContent = getMessageText(message);
|
|
193
214
|
const toolCallParts = getMessageToolCalls(message);
|
|
@@ -262,6 +283,7 @@ exports.adaptAgentEvents = adaptAgentEvents;
|
|
|
262
283
|
exports.agentEventToChatEvent = agentEventToChatEvent;
|
|
263
284
|
exports.chatEventToAgentEvent = chatEventToAgentEvent;
|
|
264
285
|
exports.createChatId = createChatId;
|
|
286
|
+
exports.createTextMessage = createTextMessage;
|
|
265
287
|
exports.extractToolResults = extractToolResults;
|
|
266
288
|
exports.fromAgentMessage = fromAgentMessage;
|
|
267
289
|
exports.getMessageReasoning = getMessageReasoning;
|
|
@@ -272,6 +294,7 @@ exports.isChatMessage = isChatMessage;
|
|
|
272
294
|
exports.isChatSession = isChatSession;
|
|
273
295
|
exports.isFilePart = isFilePart;
|
|
274
296
|
exports.isMessagePart = isMessagePart;
|
|
297
|
+
exports.isObservableSession = isObservableSession;
|
|
275
298
|
exports.isReasoningPart = isReasoningPart;
|
|
276
299
|
exports.isSourcePart = isSourcePart;
|
|
277
300
|
exports.isTextPart = isTextPart;
|