sparkecoder 0.1.71 → 0.1.73

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.
Files changed (94) hide show
  1. package/dist/agent/index.d.ts +3 -3
  2. package/dist/agent/index.js +212 -51
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/cli.js +214 -53
  5. package/dist/cli.js.map +1 -1
  6. package/dist/db/index.d.ts +2 -2
  7. package/dist/{index-CYNqPa6Z.d.ts → index-dbWF1hyW.d.ts} +57 -49
  8. package/dist/index.d.ts +5 -5
  9. package/dist/index.js +214 -53
  10. package/dist/index.js.map +1 -1
  11. package/dist/{schema-C7Mm4Ykn.d.ts → schema-XcP0dedO.d.ts} +3 -3
  12. package/dist/{search-CVVfuBPZ.d.ts → search-CCffrVJE.d.ts} +4 -4
  13. package/dist/server/index.js +214 -53
  14. package/dist/server/index.js.map +1 -1
  15. package/dist/skills/default/qa.md +53 -14
  16. package/dist/tools/index.d.ts +3 -3
  17. package/dist/tools/index.js.map +1 -1
  18. package/package.json +1 -1
  19. package/src/skills/default/qa.md +53 -14
  20. package/web/.next/BUILD_ID +1 -1
  21. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  22. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  23. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  24. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  25. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  31. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  32. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  38. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  39. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  40. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  48. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  49. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  58. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
  62. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  67. package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  70. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  73. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  75. package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  78. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  79. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  80. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  81. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  82. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  83. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  84. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  85. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  86. /package/web/.next/standalone/web/.next/static/{CvXz-9ALpAXX-GsCxrOdt → 2xYE9FvjZmf0tU0NF5Jvj}/_buildManifest.js +0 -0
  87. /package/web/.next/standalone/web/.next/static/{CvXz-9ALpAXX-GsCxrOdt → 2xYE9FvjZmf0tU0NF5Jvj}/_clientMiddlewareManifest.json +0 -0
  88. /package/web/.next/standalone/web/.next/static/{CvXz-9ALpAXX-GsCxrOdt → 2xYE9FvjZmf0tU0NF5Jvj}/_ssgManifest.js +0 -0
  89. /package/web/.next/standalone/web/.next/static/static/{CvXz-9ALpAXX-GsCxrOdt → 2xYE9FvjZmf0tU0NF5Jvj}/_buildManifest.js +0 -0
  90. /package/web/.next/standalone/web/.next/static/static/{CvXz-9ALpAXX-GsCxrOdt → 2xYE9FvjZmf0tU0NF5Jvj}/_clientMiddlewareManifest.json +0 -0
  91. /package/web/.next/standalone/web/.next/static/static/{CvXz-9ALpAXX-GsCxrOdt → 2xYE9FvjZmf0tU0NF5Jvj}/_ssgManifest.js +0 -0
  92. /package/web/.next/static/{CvXz-9ALpAXX-GsCxrOdt → 2xYE9FvjZmf0tU0NF5Jvj}/_buildManifest.js +0 -0
  93. /package/web/.next/static/{CvXz-9ALpAXX-GsCxrOdt → 2xYE9FvjZmf0tU0NF5Jvj}/_clientMiddlewareManifest.json +0 -0
  94. /package/web/.next/static/{CvXz-9ALpAXX-GsCxrOdt → 2xYE9FvjZmf0tU0NF5Jvj}/_ssgManifest.js +0 -0
@@ -1,5 +1,5 @@
1
- import { C as Checkpoint, F as FileBackup, M as ModelMessage, a as Message, S as Session, L as LoadedSkill, T as TodoItem, b as ToolExecution, A as ActiveStream, I as IndexStatusRecord, c as IndexedChunk, d as SubagentExecution, e as SubagentStep, f as Terminal } from '../schema-C7Mm4Ykn.js';
2
- export { N as NewActiveStream, g as NewCheckpoint, h as NewFileBackup, i as NewIndexStatusRecord, j as NewIndexedChunk, k as NewMessage, l as NewSession, m as NewSubagentExecution, n as NewTerminal, o as NewTodoItem, p as NewToolExecution, q as SessionConfig, r as TaskConfig, U as UserContentPart, s as UserFilePart, t as UserImagePart, u as UserModelMessage, v as UserTextPart } from '../schema-C7Mm4Ykn.js';
1
+ import { C as Checkpoint, F as FileBackup, M as ModelMessage, a as Message, S as Session, L as LoadedSkill, T as TodoItem, b as ToolExecution, A as ActiveStream, I as IndexStatusRecord, c as IndexedChunk, d as SubagentExecution, e as SubagentStep, f as Terminal } from '../schema-XcP0dedO.js';
2
+ export { N as NewActiveStream, g as NewCheckpoint, h as NewFileBackup, i as NewIndexStatusRecord, j as NewIndexedChunk, k as NewMessage, l as NewSession, m as NewSubagentExecution, n as NewTerminal, o as NewTodoItem, p as NewToolExecution, q as SessionConfig, r as TaskConfig, U as UserContentPart, s as UserFilePart, t as UserImagePart, u as UserModelMessage, v as UserTextPart } from '../schema-XcP0dedO.js';
3
3
  import 'drizzle-orm/sqlite-core';
4
4
 
5
5
  /**
@@ -1,7 +1,7 @@
1
1
  import { ModelMessage, streamText } from 'ai';
2
- import { S as Session, b as ToolExecution, r as TaskConfig } from './schema-C7Mm4Ykn.js';
2
+ import { S as Session, b as ToolExecution, r as TaskConfig } from './schema-XcP0dedO.js';
3
3
  import { z } from 'zod';
4
- import { B as BashToolProgress, W as WriteFileProgress, S as SearchToolProgress } from './search-CVVfuBPZ.js';
4
+ import { B as BashToolProgress, W as WriteFileProgress, S as SearchToolProgress } from './search-CCffrVJE.js';
5
5
 
6
6
  declare const ToolApprovalConfigSchema: z.ZodObject<{
7
7
  bash: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
@@ -37,54 +37,54 @@ declare const SessionConfigSchema: z.ZodObject<{
37
37
  error: z.ZodOptional<z.ZodString>;
38
38
  iterations: z.ZodOptional<z.ZodNumber>;
39
39
  }, "strip", z.ZodTypeAny, {
40
- status: "running" | "completed" | "failed";
41
- enabled: boolean;
40
+ status: "completed" | "failed" | "running";
42
41
  outputSchema: Record<string, unknown>;
43
- webhookUrl?: string | undefined;
42
+ enabled: boolean;
43
+ error?: string | undefined;
44
44
  maxIterations?: number | undefined;
45
+ webhookUrl?: string | undefined;
45
46
  result?: unknown;
46
- error?: string | undefined;
47
47
  iterations?: number | undefined;
48
48
  }, {
49
- status: "running" | "completed" | "failed";
50
- enabled: boolean;
49
+ status: "completed" | "failed" | "running";
51
50
  outputSchema: Record<string, unknown>;
52
- webhookUrl?: string | undefined;
51
+ enabled: boolean;
52
+ error?: string | undefined;
53
53
  maxIterations?: number | undefined;
54
+ webhookUrl?: string | undefined;
54
55
  result?: unknown;
55
- error?: string | undefined;
56
56
  iterations?: number | undefined;
57
57
  }>>;
58
58
  }, "strip", z.ZodTypeAny, {
59
59
  maxContextChars: number;
60
60
  toolApprovals?: Record<string, boolean> | undefined;
61
61
  approvalWebhook?: string | undefined;
62
- skillsDirectory?: string | undefined;
63
62
  task?: {
64
- status: "running" | "completed" | "failed";
65
- enabled: boolean;
63
+ status: "completed" | "failed" | "running";
66
64
  outputSchema: Record<string, unknown>;
67
- webhookUrl?: string | undefined;
65
+ enabled: boolean;
66
+ error?: string | undefined;
68
67
  maxIterations?: number | undefined;
68
+ webhookUrl?: string | undefined;
69
69
  result?: unknown;
70
- error?: string | undefined;
71
70
  iterations?: number | undefined;
72
71
  } | undefined;
72
+ skillsDirectory?: string | undefined;
73
73
  }, {
74
74
  toolApprovals?: Record<string, boolean> | undefined;
75
75
  approvalWebhook?: string | undefined;
76
- skillsDirectory?: string | undefined;
77
- maxContextChars?: number | undefined;
78
76
  task?: {
79
- status: "running" | "completed" | "failed";
80
- enabled: boolean;
77
+ status: "completed" | "failed" | "running";
81
78
  outputSchema: Record<string, unknown>;
82
- webhookUrl?: string | undefined;
79
+ enabled: boolean;
80
+ error?: string | undefined;
83
81
  maxIterations?: number | undefined;
82
+ webhookUrl?: string | undefined;
84
83
  result?: unknown;
85
- error?: string | undefined;
86
84
  iterations?: number | undefined;
87
85
  } | undefined;
86
+ skillsDirectory?: string | undefined;
87
+ maxContextChars?: number | undefined;
88
88
  }>;
89
89
  declare const SparkcoderConfigSchema: z.ZodObject<{
90
90
  defaultModel: z.ZodDefault<z.ZodString>;
@@ -179,6 +179,7 @@ declare const SparkcoderConfigSchema: z.ZodObject<{
179
179
  exclude?: string[] | undefined;
180
180
  }>>;
181
181
  }, "strip", z.ZodTypeAny, {
182
+ defaultModel: string;
182
183
  toolApprovals: {
183
184
  bash: boolean;
184
185
  write_file: boolean;
@@ -186,7 +187,6 @@ declare const SparkcoderConfigSchema: z.ZodObject<{
186
187
  load_skill: boolean;
187
188
  todo: boolean;
188
189
  };
189
- defaultModel: string;
190
190
  skills: {
191
191
  directory: string;
192
192
  additionalDirectories: string[];
@@ -202,8 +202,8 @@ declare const SparkcoderConfigSchema: z.ZodObject<{
202
202
  publicUrl?: string | undefined;
203
203
  };
204
204
  databasePath: string;
205
- approvalWebhook?: string | undefined;
206
205
  workingDirectory?: string | undefined;
206
+ approvalWebhook?: string | undefined;
207
207
  remoteServer?: {
208
208
  url?: string | undefined;
209
209
  authKey?: string | undefined;
@@ -217,6 +217,8 @@ declare const SparkcoderConfigSchema: z.ZodObject<{
217
217
  namespace?: string | undefined;
218
218
  } | undefined;
219
219
  }, {
220
+ defaultModel?: string | undefined;
221
+ workingDirectory?: string | undefined;
220
222
  toolApprovals?: {
221
223
  bash?: boolean | undefined;
222
224
  write_file?: boolean | undefined;
@@ -225,8 +227,6 @@ declare const SparkcoderConfigSchema: z.ZodObject<{
225
227
  todo?: boolean | undefined;
226
228
  } | undefined;
227
229
  approvalWebhook?: string | undefined;
228
- workingDirectory?: string | undefined;
229
- defaultModel?: string | undefined;
230
230
  skills?: {
231
231
  directory?: string | undefined;
232
232
  additionalDirectories?: string[] | undefined;
@@ -299,40 +299,54 @@ interface ResolvedConfig extends Omit<SparkcoderConfig, 'server'> {
299
299
 
300
300
  interface ContextManagerOptions {
301
301
  sessionId: string;
302
+ modelId: string;
302
303
  maxContextChars: number;
303
304
  keepRecentMessages: number;
304
305
  autoSummarize: boolean;
305
306
  }
306
307
  /**
307
- * Manages conversation context including history and summarization
308
+ * Manages conversation context with a three-phase rolling window:
308
309
  *
309
- * Uses AI SDK's ModelMessage format directly for accurate message passing.
310
- * Messages are stored in the exact format returned by response.messages.
310
+ * Phase 1 Compact: strip todo tool calls, trim large tool outputs,
311
+ * and remove thinking blocks from older messages (no LLM cost).
312
+ * Phase 2 – Chunk-summarize: when compacted context still exceeds the
313
+ * model's rolling target, summarize the oldest ~30 K-token chunk
314
+ * via a cheap/fast model (gemini-3-flash-preview).
315
+ * Phase 3 – Roll summaries: when accumulated summaries exceed their budget
316
+ * (~15 % of rolling target), re-summarize them into one.
311
317
  */
312
318
  declare class ContextManager {
313
319
  private sessionId;
320
+ private modelId;
314
321
  private maxContextChars;
315
322
  private keepRecentMessages;
316
323
  private autoSummarize;
317
- private summary;
324
+ private summaries;
318
325
  constructor(options: ContextManagerOptions);
319
326
  /**
320
- * Get messages for the current context
321
- * Returns ModelMessage[] that can be passed directly to streamText/generateText
322
- *
323
- * Includes self-repair: if messages from the database have been corrupted
324
- * (e.g., Date objects in tool outputs from parseDates), they are automatically
325
- * sanitized to conform to the AI SDK's ModelMessage schema.
327
+ * Get messages for the current context, applying the three-phase pipeline.
326
328
  */
327
329
  getMessages(): Promise<ModelMessage[]>;
328
330
  /**
329
- * Summarize older messages to reduce context size
331
+ * Strip non-essential content from messages older than the most recent
332
+ * `recentCount`. Operates in-memory only — does not touch the DB.
330
333
  */
331
- private summarizeContext;
334
+ compactOlderMessages(messages: ModelMessage[], recentCount: number): ModelMessage[];
335
+ private compactMessage;
336
+ private trimToolResult;
332
337
  /**
333
- * Add a user message to the context
334
- * Content can be a string or an array of content parts (for messages with images/files)
338
+ * While estimated tokens exceed `rollingTarget`, peel off the oldest
339
+ * ~SUMMARY_CHUNK_TOKENS worth of messages, summarize them via the cheap
340
+ * model, and prepend the summary.
335
341
  */
342
+ private chunkSummarize;
343
+ private summarizeChunk;
344
+ /**
345
+ * If accumulated summaries exceed `budget` tokens, re-summarize them
346
+ * into a single condensed summary.
347
+ */
348
+ private rollSummaries;
349
+ private messageTokens;
336
350
  addUserMessage(content: string | Array<{
337
351
  type: string;
338
352
  text?: string;
@@ -340,22 +354,14 @@ declare class ContextManager {
340
354
  data?: string;
341
355
  mediaType?: string;
342
356
  }>): Promise<void>;
343
- /**
344
- * Add response messages from AI SDK directly
345
- * This is the preferred method - use result.response.messages from streamText/generateText
346
- */
347
357
  addResponseMessages(messages: ModelMessage[]): Promise<void>;
348
- /**
349
- * Get current context statistics
350
- */
351
358
  getStats(): Promise<{
352
359
  messageCount: number;
353
360
  contextChars: number;
361
+ estimatedTokens: number;
354
362
  hasSummary: boolean;
363
+ summaryCount: number;
355
364
  }>;
356
- /**
357
- * Clear all messages in the context
358
- */
359
365
  clear(): Promise<void>;
360
366
  }
361
367
 
@@ -540,7 +546,9 @@ declare class Agent {
540
546
  getContextStats(): Promise<{
541
547
  messageCount: number;
542
548
  contextChars: number;
549
+ estimatedTokens: number;
543
550
  hasSummary: boolean;
551
+ summaryCount: number;
544
552
  }>;
545
553
  /**
546
554
  * Clear conversation context (start fresh)
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { R as ResolvedConfig } from './index-CYNqPa6Z.js';
2
- export { A as Agent, a as AgentOptions, b as AgentRunOptions, c as AgentStreamResult, S as SparkcoderConfig, T as ToolApprovalConfig } from './index-CYNqPa6Z.js';
1
+ import { R as ResolvedConfig } from './index-dbWF1hyW.js';
2
+ export { A as Agent, a as AgentOptions, b as AgentRunOptions, c as AgentStreamResult, S as SparkcoderConfig, T as ToolApprovalConfig } from './index-dbWF1hyW.js';
3
3
  export { ServerOptions, createApp, startServer, stopServer } from './server/index.js';
4
4
  export { checkpointQueries, closeDatabase, fileBackupQueries, getDb, initDatabase, messageQueries, sessionQueries, skillQueries, todoQueries, toolExecutionQueries } from './db/index.js';
5
- import { F as FileBackup, C as Checkpoint } from './schema-C7Mm4Ykn.js';
6
- export { a as Message, M as ModelMessage, S as Session, q as SessionConfig, f as Terminal, T as TodoItem, b as ToolExecution } from './schema-C7Mm4Ykn.js';
5
+ import { F as FileBackup, C as Checkpoint } from './schema-XcP0dedO.js';
6
+ export { a as Message, M as ModelMessage, S as Session, q as SessionConfig, f as Terminal, T as TodoItem, b as ToolExecution } from './schema-XcP0dedO.js';
7
7
  export { createLoadSkillTool, createReadFileTool, createTodoTool, createTools } from './tools/index.js';
8
- export { c as createBashTool, a as createWriteFileTool } from './search-CVVfuBPZ.js';
8
+ export { c as createBashTool, a as createWriteFileTool } from './search-CCffrVJE.js';
9
9
  import 'ai';
10
10
  import 'zod';
11
11
  import 'hono/types';
package/dist/index.js CHANGED
@@ -2280,6 +2280,19 @@ import { z as z2 } from "zod";
2280
2280
  import { exec as exec2 } from "child_process";
2281
2281
  import { promisify as promisify2 } from "util";
2282
2282
 
2283
+ // src/utils/tokens.ts
2284
+ var CHARS_PER_TOKEN = 4;
2285
+ var MESSAGE_OVERHEAD_TOKENS = 4;
2286
+ function estimateTokens(text) {
2287
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
2288
+ }
2289
+ function estimateMessageTokens(messages) {
2290
+ return messages.reduce((total, msg) => {
2291
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
2292
+ return total + estimateTokens(content) + MESSAGE_OVERHEAD_TOKENS;
2293
+ }, 0);
2294
+ }
2295
+
2283
2296
  // src/utils/truncate.ts
2284
2297
  var MAX_OUTPUT_CHARS = 1e4;
2285
2298
  function truncateOutput(output, maxChars = MAX_OUTPUT_CHARS) {
@@ -6267,9 +6280,6 @@ ${conversationHistory}
6267
6280
  Summary:`;
6268
6281
  }
6269
6282
 
6270
- // src/agent/context.ts
6271
- init_config();
6272
-
6273
6283
  // src/utils/sanitize-messages.ts
6274
6284
  import { modelMessageSchema } from "ai";
6275
6285
  function convertDatesToStrings(value) {
@@ -6406,79 +6416,237 @@ function sanitizeModelMessages(messages) {
6406
6416
  return result;
6407
6417
  }
6408
6418
 
6419
+ // src/agent/model-limits.ts
6420
+ var MODEL_LIMITS = {
6421
+ "anthropic/claude-opus-4-6": { contextWindow: 2e5, rollingTarget: 15e4 },
6422
+ "anthropic/claude-sonnet-4": { contextWindow: 2e5, rollingTarget: 15e4 },
6423
+ "anthropic/claude-3.5-sonnet": { contextWindow: 2e5, rollingTarget: 15e4 },
6424
+ "anthropic/claude-3-haiku": { contextWindow: 2e5, rollingTarget: 15e4 },
6425
+ "google/gemini-3-flash-preview": { contextWindow: 1e6, rollingTarget: 15e4 },
6426
+ "google/gemini-2.5-pro": { contextWindow: 1e6, rollingTarget: 15e4 },
6427
+ "google/gemini-2.5-flash": { contextWindow: 1e6, rollingTarget: 15e4 },
6428
+ "openai/gpt-4o": { contextWindow: 128e3, rollingTarget: 78e3 },
6429
+ "openai/gpt-4.1": { contextWindow: 1e6, rollingTarget: 15e4 },
6430
+ "openai/o3": { contextWindow: 2e5, rollingTarget: 15e4 },
6431
+ "xai/grok-3": { contextWindow: 131072, rollingTarget: 8e4 }
6432
+ };
6433
+ var DEFAULT_LIMITS = { contextWindow: 2e5, rollingTarget: 15e4 };
6434
+ var PREFIX_DEFAULTS = {
6435
+ "anthropic/": { contextWindow: 2e5, rollingTarget: 15e4 },
6436
+ "google/": { contextWindow: 1e6, rollingTarget: 15e4 },
6437
+ "openai/": { contextWindow: 128e3, rollingTarget: 78e3 },
6438
+ "xai/": { contextWindow: 131072, rollingTarget: 8e4 }
6439
+ };
6440
+ function getModelLimits(modelId) {
6441
+ const normalized = modelId.trim().toLowerCase();
6442
+ const exact = MODEL_LIMITS[normalized];
6443
+ if (exact) return exact;
6444
+ for (const [prefix, limits] of Object.entries(PREFIX_DEFAULTS)) {
6445
+ if (normalized.startsWith(prefix)) return limits;
6446
+ }
6447
+ return DEFAULT_LIMITS;
6448
+ }
6449
+ var SUMMARIZATION_MODEL = "google/gemini-3-flash-preview";
6450
+ var SUMMARY_CHUNK_TOKENS = 3e4;
6451
+ var SUMMARY_BUDGET_RATIO = 0.15;
6452
+
6409
6453
  // src/agent/context.ts
6454
+ var TOOL_OUTPUT_TRIM_CHARS = 400;
6455
+ var COMPACTABLE_TOOLS = /* @__PURE__ */ new Set([
6456
+ "read_file",
6457
+ "bash",
6458
+ "explore_agent",
6459
+ "code_graph"
6460
+ ]);
6410
6461
  var ContextManager = class {
6411
6462
  sessionId;
6463
+ modelId;
6412
6464
  maxContextChars;
6413
6465
  keepRecentMessages;
6414
6466
  autoSummarize;
6415
- summary = null;
6467
+ summaries = [];
6416
6468
  constructor(options) {
6417
6469
  this.sessionId = options.sessionId;
6470
+ this.modelId = options.modelId;
6418
6471
  this.maxContextChars = options.maxContextChars;
6419
6472
  this.keepRecentMessages = options.keepRecentMessages;
6420
6473
  this.autoSummarize = options.autoSummarize;
6421
6474
  }
6422
6475
  /**
6423
- * Get messages for the current context
6424
- * Returns ModelMessage[] that can be passed directly to streamText/generateText
6425
- *
6426
- * Includes self-repair: if messages from the database have been corrupted
6427
- * (e.g., Date objects in tool outputs from parseDates), they are automatically
6428
- * sanitized to conform to the AI SDK's ModelMessage schema.
6476
+ * Get messages for the current context, applying the three-phase pipeline.
6429
6477
  */
6430
6478
  async getMessages() {
6431
- let modelMessages = await messageQueries.getModelMessages(this.sessionId);
6432
- modelMessages = sanitizeModelMessages(modelMessages);
6433
- const contextSize = calculateContextSize(modelMessages);
6434
- if (this.autoSummarize && contextSize > this.maxContextChars) {
6435
- modelMessages = await this.summarizeContext(modelMessages);
6436
- }
6437
- if (this.summary) {
6438
- modelMessages = [
6479
+ let messages = await messageQueries.getModelMessages(this.sessionId);
6480
+ messages = sanitizeModelMessages(messages);
6481
+ messages = this.compactOlderMessages(messages, this.keepRecentMessages);
6482
+ if (this.autoSummarize) {
6483
+ const { rollingTarget } = getModelLimits(this.modelId);
6484
+ const summaryBudget = Math.floor(rollingTarget * SUMMARY_BUDGET_RATIO);
6485
+ messages = await this.chunkSummarize(messages, rollingTarget);
6486
+ await this.rollSummaries(summaryBudget);
6487
+ }
6488
+ if (this.summaries.length > 0) {
6489
+ const summaryContent = this.summaries.join("\n\n---\n\n");
6490
+ messages = [
6439
6491
  {
6440
6492
  role: "system",
6441
6493
  content: `[Previous conversation summary]
6442
- ${this.summary}`
6494
+ ${summaryContent}`
6443
6495
  },
6444
- ...modelMessages
6496
+ ...messages
6445
6497
  ];
6446
6498
  }
6447
- return modelMessages;
6499
+ return messages;
6448
6500
  }
6501
+ // ---------------------------------------------------------------------------
6502
+ // Phase 1 – Compact
6503
+ // ---------------------------------------------------------------------------
6449
6504
  /**
6450
- * Summarize older messages to reduce context size
6505
+ * Strip non-essential content from messages older than the most recent
6506
+ * `recentCount`. Operates in-memory only — does not touch the DB.
6451
6507
  */
6452
- async summarizeContext(messages) {
6453
- if (messages.length <= this.keepRecentMessages) {
6454
- return messages;
6455
- }
6456
- const splitIndex = messages.length - this.keepRecentMessages;
6457
- const oldMessages = messages.slice(0, splitIndex);
6458
- const recentMessages = messages.slice(splitIndex);
6459
- const historyText = oldMessages.map((msg) => {
6508
+ compactOlderMessages(messages, recentCount) {
6509
+ if (messages.length <= recentCount) return messages;
6510
+ const boundary = messages.length - recentCount;
6511
+ const olderMessages = messages.slice(0, boundary);
6512
+ const recentMessages = messages.slice(boundary);
6513
+ const compacted = [];
6514
+ for (const msg of olderMessages) {
6515
+ const processed = this.compactMessage(msg);
6516
+ if (processed) compacted.push(processed);
6517
+ }
6518
+ return [...compacted, ...recentMessages];
6519
+ }
6520
+ compactMessage(msg) {
6521
+ if (!Array.isArray(msg.content)) return msg;
6522
+ const parts = [];
6523
+ for (const part of msg.content) {
6524
+ if (part.type === "tool-call" && part.toolName === "todo") continue;
6525
+ if (part.type === "tool-result" && part.toolName === "todo") continue;
6526
+ if (part.type === "reasoning" || part.type === "thinking") continue;
6527
+ if (part.type === "tool-result" && COMPACTABLE_TOOLS.has(part.toolName)) {
6528
+ parts.push(this.trimToolResult(part));
6529
+ continue;
6530
+ }
6531
+ parts.push(part);
6532
+ }
6533
+ if (parts.length === 0) return null;
6534
+ return { ...msg, content: parts };
6535
+ }
6536
+ trimToolResult(part) {
6537
+ const results = Array.isArray(part.result) ? part.result : [part.result];
6538
+ const trimmedResults = results.map((r) => {
6539
+ if (typeof r === "string" && r.length > TOOL_OUTPUT_TRIM_CHARS) {
6540
+ const half = Math.floor(TOOL_OUTPUT_TRIM_CHARS / 2);
6541
+ return r.slice(0, half) + `
6542
+ ...[trimmed ${r.length - TOOL_OUTPUT_TRIM_CHARS} chars]...
6543
+ ` + r.slice(-half);
6544
+ }
6545
+ if (r && typeof r === "object" && typeof r.text === "string" && r.text.length > TOOL_OUTPUT_TRIM_CHARS) {
6546
+ const half = Math.floor(TOOL_OUTPUT_TRIM_CHARS / 2);
6547
+ return {
6548
+ ...r,
6549
+ text: r.text.slice(0, half) + `
6550
+ ...[trimmed ${r.text.length - TOOL_OUTPUT_TRIM_CHARS} chars]...
6551
+ ` + r.text.slice(-half)
6552
+ };
6553
+ }
6554
+ return r;
6555
+ });
6556
+ return {
6557
+ ...part,
6558
+ result: Array.isArray(part.result) ? trimmedResults : trimmedResults[0]
6559
+ };
6560
+ }
6561
+ // ---------------------------------------------------------------------------
6562
+ // Phase 2 – Chunk-summarize
6563
+ // ---------------------------------------------------------------------------
6564
+ /**
6565
+ * While estimated tokens exceed `rollingTarget`, peel off the oldest
6566
+ * ~SUMMARY_CHUNK_TOKENS worth of messages, summarize them via the cheap
6567
+ * model, and prepend the summary.
6568
+ */
6569
+ async chunkSummarize(messages, rollingTarget) {
6570
+ let totalTokens = estimateMessageTokens(messages);
6571
+ while (totalTokens > rollingTarget && messages.length > this.keepRecentMessages) {
6572
+ let chunkTokens = 0;
6573
+ let chunkEnd = 0;
6574
+ const maxChunkable = messages.length - this.keepRecentMessages;
6575
+ for (let i = 0; i < maxChunkable; i++) {
6576
+ const msgTokens = this.messageTokens(messages[i]);
6577
+ chunkTokens += msgTokens;
6578
+ chunkEnd = i + 1;
6579
+ if (chunkTokens >= SUMMARY_CHUNK_TOKENS) break;
6580
+ }
6581
+ if (chunkEnd === 0) break;
6582
+ const chunk = messages.slice(0, chunkEnd);
6583
+ const remaining = messages.slice(chunkEnd);
6584
+ const summary = await this.summarizeChunk(chunk);
6585
+ if (summary) {
6586
+ this.summaries.push(summary);
6587
+ console.log(
6588
+ `[Context] Summarized ${chunk.length} messages (~${chunkTokens} tokens) into ${estimateTokens(summary)} tokens`
6589
+ );
6590
+ }
6591
+ messages = remaining;
6592
+ totalTokens = estimateMessageTokens(messages);
6593
+ }
6594
+ return messages;
6595
+ }
6596
+ async summarizeChunk(chunk) {
6597
+ const historyText = chunk.map((msg) => {
6460
6598
  const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
6461
6599
  return `[${msg.role}]: ${content}`;
6462
6600
  }).join("\n\n");
6463
6601
  try {
6464
- const config = getConfig();
6465
- const summaryPrompt = createSummaryPrompt(historyText);
6466
6602
  const result = await generateText2({
6467
- model: resolveModel(config.defaultModel),
6468
- prompt: summaryPrompt
6603
+ model: resolveModel(SUMMARIZATION_MODEL),
6604
+ prompt: createSummaryPrompt(historyText)
6469
6605
  });
6470
- this.summary = result.text;
6471
- console.log(`[Context] Summarized ${oldMessages.length} messages into ${this.summary.length} chars`);
6472
- return recentMessages;
6606
+ return result.text;
6473
6607
  } catch (error) {
6474
- console.error("[Context] Failed to summarize:", error);
6475
- return recentMessages;
6608
+ console.error("[Context] Chunk summarization failed:", error);
6609
+ return null;
6476
6610
  }
6477
6611
  }
6612
+ // ---------------------------------------------------------------------------
6613
+ // Phase 3 – Roll summaries
6614
+ // ---------------------------------------------------------------------------
6478
6615
  /**
6479
- * Add a user message to the context
6480
- * Content can be a string or an array of content parts (for messages with images/files)
6616
+ * If accumulated summaries exceed `budget` tokens, re-summarize them
6617
+ * into a single condensed summary.
6481
6618
  */
6619
+ async rollSummaries(budget) {
6620
+ if (this.summaries.length <= 1) return;
6621
+ const totalSummaryTokens = this.summaries.reduce(
6622
+ (t, s) => t + estimateTokens(s),
6623
+ 0
6624
+ );
6625
+ if (totalSummaryTokens <= budget) return;
6626
+ const combined = this.summaries.join("\n\n---\n\n");
6627
+ try {
6628
+ const result = await generateText2({
6629
+ model: resolveModel(SUMMARIZATION_MODEL),
6630
+ prompt: createSummaryPrompt(combined)
6631
+ });
6632
+ console.log(
6633
+ `[Context] Rolled ${this.summaries.length} summaries (${totalSummaryTokens} tokens) into ${estimateTokens(result.text)} tokens`
6634
+ );
6635
+ this.summaries = [result.text];
6636
+ } catch (error) {
6637
+ console.error("[Context] Summary rolling failed:", error);
6638
+ }
6639
+ }
6640
+ // ---------------------------------------------------------------------------
6641
+ // Helpers
6642
+ // ---------------------------------------------------------------------------
6643
+ messageTokens(msg) {
6644
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
6645
+ return estimateTokens(content) + 4;
6646
+ }
6647
+ // ---------------------------------------------------------------------------
6648
+ // Public API (unchanged)
6649
+ // ---------------------------------------------------------------------------
6482
6650
  async addUserMessage(content) {
6483
6651
  const userMessage = {
6484
6652
  role: "user",
@@ -6486,30 +6654,22 @@ ${this.summary}`
6486
6654
  };
6487
6655
  await messageQueries.create(this.sessionId, userMessage);
6488
6656
  }
6489
- /**
6490
- * Add response messages from AI SDK directly
6491
- * This is the preferred method - use result.response.messages from streamText/generateText
6492
- */
6493
6657
  async addResponseMessages(messages) {
6494
6658
  await messageQueries.addMany(this.sessionId, messages);
6495
6659
  }
6496
- /**
6497
- * Get current context statistics
6498
- */
6499
6660
  async getStats() {
6500
6661
  const messages = await messageQueries.getModelMessages(this.sessionId);
6501
6662
  return {
6502
6663
  messageCount: messages.length,
6503
6664
  contextChars: calculateContextSize(messages),
6504
- hasSummary: this.summary !== null
6665
+ estimatedTokens: estimateMessageTokens(messages),
6666
+ hasSummary: this.summaries.length > 0,
6667
+ summaryCount: this.summaries.length
6505
6668
  };
6506
6669
  }
6507
- /**
6508
- * Clear all messages in the context
6509
- */
6510
6670
  async clear() {
6511
6671
  await messageQueries.deleteBySession(this.sessionId);
6512
- this.summary = null;
6672
+ this.summaries = [];
6513
6673
  }
6514
6674
  };
6515
6675
 
@@ -6577,6 +6737,7 @@ var Agent = class _Agent {
6577
6737
  }
6578
6738
  const context = new ContextManager({
6579
6739
  sessionId: session.id,
6740
+ modelId: session.model || config.defaultModel,
6580
6741
  maxContextChars: config.context?.maxChars || 2e5,
6581
6742
  keepRecentMessages: config.context?.keepRecentMessages || 10,
6582
6743
  autoSummarize: config.context?.autoSummarize ?? true