open-research 1.1.2 → 1.2.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.
@@ -0,0 +1,272 @@
1
+ import {
2
+ selectModelForTask
3
+ } from "./chunk-GVEVKDGV.js";
4
+
5
+ // src/lib/agent/context-manager.ts
6
+ var BYTES_PER_TOKEN = 4;
7
+ function estimateTokens(text) {
8
+ const bytes = Buffer.byteLength(text, "utf8");
9
+ return Math.ceil(bytes / BYTES_PER_TOKEN);
10
+ }
11
+ function estimateMessageTokens(msg) {
12
+ let tokens = 4;
13
+ if (typeof msg.content === "string") {
14
+ tokens += estimateTokens(msg.content);
15
+ } else if (Array.isArray(msg.content)) {
16
+ for (const part of msg.content) {
17
+ if ("text" in part) tokens += estimateTokens(part.text);
18
+ }
19
+ }
20
+ if (msg.tool_calls) {
21
+ for (const tc of msg.tool_calls) {
22
+ tokens += estimateTokens(tc.function.name) + estimateTokens(tc.function.arguments);
23
+ }
24
+ }
25
+ return tokens;
26
+ }
27
+ function estimateConversationTokens(messages) {
28
+ let total = 0;
29
+ for (const msg of messages) {
30
+ total += estimateMessageTokens(msg);
31
+ }
32
+ return total;
33
+ }
34
+ var MODEL_CONTEXT_WINDOWS = {
35
+ "gpt-5.4": 272e3,
36
+ "gpt-5.3-codex": 272e3,
37
+ "gpt-5.2-codex": 272e3,
38
+ "gpt-5.2": 272e3,
39
+ "gpt-5.1-codex": 272e3,
40
+ "gpt-5.1": 272e3,
41
+ "gpt-5": 272e3,
42
+ "gpt-4o": 128e3,
43
+ "gpt-5.4-mini": 128e3,
44
+ "o3": 2e5,
45
+ "o4-mini": 2e5,
46
+ "gemini-3.1-pro-preview": 1048576,
47
+ "gemini-3-flash-preview": 1048576
48
+ };
49
+ var DEFAULT_CONTEXT_WINDOW = 128e3;
50
+ var AUTO_COMPACT_TOKEN_LIMIT = 25e4;
51
+ function getContextWindow(model) {
52
+ return MODEL_CONTEXT_WINDOWS[model] ?? DEFAULT_CONTEXT_WINDOW;
53
+ }
54
+ function getCompactThreshold(model) {
55
+ const window = getContextWindow(model);
56
+ return window > AUTO_COMPACT_TOKEN_LIMIT ? AUTO_COMPACT_TOKEN_LIMIT : Math.floor(window * 0.8);
57
+ }
58
+ function emptyBreakdown() {
59
+ return { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 }, total: 0 };
60
+ }
61
+ function createSessionUsage() {
62
+ return {
63
+ cumulative: emptyBreakdown(),
64
+ lastTurn: emptyBreakdown(),
65
+ estimatedCurrentTokens: 0,
66
+ compactionCount: 0,
67
+ inputTokens: 0,
68
+ outputTokens: 0,
69
+ totalTokens: 0,
70
+ lastTurnTokens: 0
71
+ };
72
+ }
73
+ function updateUsageFromApi(usage, apiUsage) {
74
+ const cached = apiUsage.cachedTokens ?? 0;
75
+ const reasoning = apiUsage.reasoningTokens ?? 0;
76
+ const adjustedInput = Math.max(0, apiUsage.promptTokens - cached);
77
+ const adjustedOutput = Math.max(0, apiUsage.completionTokens - reasoning);
78
+ usage.cumulative.input += adjustedInput;
79
+ usage.cumulative.output += adjustedOutput;
80
+ usage.cumulative.reasoning += reasoning;
81
+ usage.cumulative.cache.read += cached;
82
+ usage.cumulative.total += apiUsage.totalTokens;
83
+ usage.lastTurn = {
84
+ input: adjustedInput,
85
+ output: adjustedOutput,
86
+ reasoning,
87
+ cache: { read: cached, write: 0 },
88
+ total: apiUsage.totalTokens
89
+ };
90
+ usage.inputTokens = usage.cumulative.input;
91
+ usage.outputTokens = usage.cumulative.output;
92
+ usage.totalTokens = usage.cumulative.total;
93
+ usage.lastTurnTokens = apiUsage.totalTokens;
94
+ }
95
+ var PRUNE_PROTECT_TOKENS = 4e4;
96
+ var PRUNE_MIN_SAVINGS = 2e4;
97
+ var PRUNE_SKIP_RECENT_USER_TURNS = 2;
98
+ function pruneToolOutputs(messages) {
99
+ const userIndices = [];
100
+ for (let i = messages.length - 1; i >= 0; i--) {
101
+ if (messages[i].role === "user") userIndices.push(i);
102
+ }
103
+ const protectBoundary = userIndices.length >= PRUNE_SKIP_RECENT_USER_TURNS ? userIndices[PRUNE_SKIP_RECENT_USER_TURNS - 1] : 0;
104
+ const toolIndices = [];
105
+ for (let i = 0; i < messages.length; i++) {
106
+ if (messages[i].role === "tool" && i < protectBoundary) {
107
+ toolIndices.push(i);
108
+ }
109
+ }
110
+ if (toolIndices.length === 0) return { messages, savedTokens: 0 };
111
+ let protectedTokens = 0;
112
+ let cutoffIdx = toolIndices.length;
113
+ for (let i = toolIndices.length - 1; i >= 0; i--) {
114
+ const msg = messages[toolIndices[i]];
115
+ const tokens = estimateMessageTokens(msg);
116
+ if (protectedTokens + tokens > PRUNE_PROTECT_TOKENS) {
117
+ cutoffIdx = i;
118
+ break;
119
+ }
120
+ protectedTokens += tokens;
121
+ }
122
+ let savedTokens = 0;
123
+ const result = [...messages];
124
+ for (let i = 0; i < cutoffIdx; i++) {
125
+ const idx = toolIndices[i];
126
+ const msg = result[idx];
127
+ const oldTokens = estimateMessageTokens(msg);
128
+ const stub = "[output pruned \u2014 use read_file to re-read if needed]";
129
+ savedTokens += oldTokens - estimateTokens(stub);
130
+ result[idx] = { ...msg, content: stub };
131
+ }
132
+ if (savedTokens < PRUNE_MIN_SAVINGS) {
133
+ return { messages, savedTokens: 0 };
134
+ }
135
+ return { messages: result, savedTokens };
136
+ }
137
+ var COMPACTION_SYSTEM_PROMPT = `You are a conversation summarizer for a research agent. Your job is to create a handoff summary that another agent instance can use to seamlessly continue the work.
138
+
139
+ Do not respond to any questions in the conversation. Only output the summary.
140
+ Respond in the same language the user used.`;
141
+ var COMPACTION_USER_TEMPLATE = `Provide a detailed summary of our conversation above for handoff to another agent that will continue the work.
142
+
143
+ Stick to this template:
144
+
145
+ ## Goal
146
+ [What is the user trying to accomplish? Be specific.]
147
+
148
+ ## Instructions
149
+ - [Important instructions or preferences the user gave]
150
+ - [Research methodology constraints or requirements]
151
+ - [If there is a research charter or plan, summarize its key points]
152
+
153
+ ## Discoveries
154
+ - [Key findings from paper searches, data analysis, or experiments]
155
+ - [Important facts, numbers, or evidence discovered]
156
+ - [Any surprising or contradicting results]
157
+
158
+ ## Accomplished
159
+ - [What work has been completed]
160
+ - [What is currently in progress]
161
+ - [What remains to be done]
162
+
163
+ ## Relevant Files
164
+ [List workspace files that were read, created, or modified. Include what each contains.]
165
+ - path/to/file.md \u2014 description of contents
166
+ - experiments/script.py \u2014 what it does and its results
167
+
168
+ ## Active Context
169
+ - [Current research question or hypothesis being investigated]
170
+ - [Which skills are active]
171
+ - [Any pending user decisions or questions]
172
+
173
+ ## Next Steps
174
+ 1. [Most immediate next action]
175
+ 2. [Following action]
176
+ 3. [And so on]
177
+
178
+ {CUSTOM_INSTRUCTIONS}`;
179
+ async function compactConversation(messages, provider, model, customInstructions, signal) {
180
+ const systemMsg = messages.find((m) => m.role === "system");
181
+ const conversationMsgs = messages.filter((m) => m.role !== "system");
182
+ const conversationText = conversationMsgs.map((m) => {
183
+ const role = m.role === "assistant" ? "Agent" : m.role === "user" ? "User" : "Tool";
184
+ let content;
185
+ if (typeof m.content === "string") {
186
+ content = m.content.length > 3e3 ? m.content.slice(0, 3e3) + "\n[... truncated]" : m.content;
187
+ } else if (m.content) {
188
+ content = JSON.stringify(m.content).slice(0, 1e3);
189
+ } else if (m.tool_calls?.length) {
190
+ content = m.tool_calls.map((tc) => `[tool: ${tc.function.name}]`).join(", ");
191
+ } else {
192
+ content = "[empty]";
193
+ }
194
+ return `[${role}]: ${content}`;
195
+ }).join("\n\n");
196
+ const customBlock = customInstructions ? `
197
+
198
+ Additional instructions: ${customInstructions}` : "";
199
+ const userPrompt = COMPACTION_USER_TEMPLATE.replace("{CUSTOM_INSTRUCTIONS}", customBlock);
200
+ const compactionModel = selectModelForTask(provider.kind, model, "compaction");
201
+ const summaryResponse = await provider.callLLM({
202
+ messages: [
203
+ { role: "system", content: COMPACTION_SYSTEM_PROMPT },
204
+ {
205
+ role: "user",
206
+ content: `Here is the conversation to summarize:
207
+
208
+ ${conversationText.slice(0, 12e4)}
209
+
210
+ ---
211
+
212
+ ${userPrompt}`
213
+ }
214
+ ],
215
+ model: compactionModel,
216
+ maxTokens: 4096
217
+ });
218
+ const compacted = [];
219
+ if (systemMsg) compacted.push(systemMsg);
220
+ compacted.push({
221
+ role: "user",
222
+ content: "What have we accomplished so far in this research session?"
223
+ });
224
+ compacted.push({
225
+ role: "assistant",
226
+ content: summaryResponse.content
227
+ });
228
+ return compacted;
229
+ }
230
+ async function maybeCompact(messages, model, provider, usage, signal) {
231
+ const estimated = estimateConversationTokens(messages);
232
+ usage.estimatedCurrentTokens = estimated;
233
+ const threshold = getCompactThreshold(model);
234
+ if (estimated < threshold) {
235
+ return { messages, didCompact: false };
236
+ }
237
+ const { messages: pruned, savedTokens } = pruneToolOutputs(messages);
238
+ const afterPrune = estimated - savedTokens;
239
+ if (afterPrune < threshold) {
240
+ usage.estimatedCurrentTokens = afterPrune;
241
+ usage.compactionCount++;
242
+ return { messages: pruned, didCompact: true };
243
+ }
244
+ const compacted = await compactConversation(pruned, provider, model, void 0, signal);
245
+ usage.estimatedCurrentTokens = estimateConversationTokens(compacted);
246
+ usage.compactionCount++;
247
+ return { messages: compacted, didCompact: true };
248
+ }
249
+ async function manualCompact(messages, model, provider, usage, customInstructions, signal) {
250
+ if (messages.length <= 2) {
251
+ return { messages, didCompact: false };
252
+ }
253
+ const { messages: pruned } = pruneToolOutputs(messages);
254
+ const compacted = await compactConversation(pruned, provider, model, customInstructions, signal);
255
+ usage.estimatedCurrentTokens = estimateConversationTokens(compacted);
256
+ usage.compactionCount++;
257
+ return { messages: compacted, didCompact: true };
258
+ }
259
+
260
+ export {
261
+ estimateTokens,
262
+ estimateMessageTokens,
263
+ estimateConversationTokens,
264
+ getContextWindow,
265
+ getCompactThreshold,
266
+ createSessionUsage,
267
+ updateUsageFromApi,
268
+ pruneToolOutputs,
269
+ compactConversation,
270
+ maybeCompact,
271
+ manualCompact
272
+ };