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
@@ -85,7 +85,7 @@ declare const sessions: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
85
85
  tableName: "sessions";
86
86
  dataType: "string";
87
87
  columnType: "SQLiteText";
88
- data: "completed" | "error" | "active" | "waiting";
88
+ data: "error" | "completed" | "active" | "waiting";
89
89
  driverParam: string;
90
90
  notNull: true;
91
91
  hasDefault: true;
@@ -391,7 +391,7 @@ declare const toolExecutions: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
391
391
  tableName: "tool_executions";
392
392
  dataType: "string";
393
393
  columnType: "SQLiteText";
394
- data: "completed" | "error" | "pending" | "approved" | "rejected";
394
+ data: "error" | "completed" | "pending" | "approved" | "rejected";
395
395
  driverParam: string;
396
396
  notNull: true;
397
397
  hasDefault: true;
@@ -814,7 +814,7 @@ declare const terminals: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
814
814
  tableName: "terminals";
815
815
  dataType: "string";
816
816
  columnType: "SQLiteText";
817
- data: "running" | "error" | "stopped";
817
+ data: "error" | "running" | "stopped";
818
818
  driverParam: string;
819
819
  notNull: true;
820
820
  hasDefault: true;
@@ -15,12 +15,12 @@ interface BashToolOptions {
15
15
  }
16
16
  declare function createBashTool(options: BashToolOptions): ai.Tool<{
17
17
  background: boolean;
18
- id?: string | undefined;
19
18
  input?: string | undefined;
19
+ id?: string | undefined;
20
20
  command?: string | undefined;
21
21
  kill?: boolean | undefined;
22
22
  tail?: number | undefined;
23
- key?: "Enter" | "Escape" | "Up" | "Down" | "Left" | "Right" | "Tab" | "C-c" | "C-d" | "y" | "n" | undefined;
23
+ key?: "y" | "Enter" | "Escape" | "Up" | "Down" | "Left" | "Right" | "Tab" | "C-c" | "C-d" | "n" | undefined;
24
24
  }, {
25
25
  success: boolean;
26
26
  id: string;
@@ -66,7 +66,7 @@ declare function createBashTool(options: BashToolOptions): ai.Tool<{
66
66
  id: string;
67
67
  output: string;
68
68
  exitCode: number;
69
- status: "running" | "completed" | "error" | "stopped";
69
+ status: "error" | "completed" | "running" | "stopped";
70
70
  message?: undefined;
71
71
  error?: undefined;
72
72
  } | {
@@ -218,8 +218,8 @@ interface SearchToolOptions {
218
218
  * Progress is streamed back to the UI so users can see exploration happening.
219
219
  */
220
220
  declare function createSearchTool(options: SearchToolOptions): ai.Tool<{
221
- query: string;
222
221
  context: string;
222
+ query: string;
223
223
  }, {
224
224
  success: boolean;
225
225
  error: string;
@@ -2302,6 +2302,19 @@ import { z as z2 } from "zod";
2302
2302
  import { exec as exec2 } from "child_process";
2303
2303
  import { promisify as promisify2 } from "util";
2304
2304
 
2305
+ // src/utils/tokens.ts
2306
+ var CHARS_PER_TOKEN = 4;
2307
+ var MESSAGE_OVERHEAD_TOKENS = 4;
2308
+ function estimateTokens(text) {
2309
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
2310
+ }
2311
+ function estimateMessageTokens(messages) {
2312
+ return messages.reduce((total, msg) => {
2313
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
2314
+ return total + estimateTokens(content) + MESSAGE_OVERHEAD_TOKENS;
2315
+ }, 0);
2316
+ }
2317
+
2305
2318
  // src/utils/truncate.ts
2306
2319
  var MAX_OUTPUT_CHARS = 1e4;
2307
2320
  function truncateOutput(output, maxChars = MAX_OUTPUT_CHARS) {
@@ -6272,9 +6285,6 @@ ${conversationHistory}
6272
6285
  Summary:`;
6273
6286
  }
6274
6287
 
6275
- // src/agent/context.ts
6276
- init_config();
6277
-
6278
6288
  // src/utils/sanitize-messages.ts
6279
6289
  import { modelMessageSchema } from "ai";
6280
6290
  function convertDatesToStrings(value) {
@@ -6411,79 +6421,237 @@ function sanitizeModelMessages(messages) {
6411
6421
  return result;
6412
6422
  }
6413
6423
 
6424
+ // src/agent/model-limits.ts
6425
+ var MODEL_LIMITS = {
6426
+ "anthropic/claude-opus-4-6": { contextWindow: 2e5, rollingTarget: 15e4 },
6427
+ "anthropic/claude-sonnet-4": { contextWindow: 2e5, rollingTarget: 15e4 },
6428
+ "anthropic/claude-3.5-sonnet": { contextWindow: 2e5, rollingTarget: 15e4 },
6429
+ "anthropic/claude-3-haiku": { contextWindow: 2e5, rollingTarget: 15e4 },
6430
+ "google/gemini-3-flash-preview": { contextWindow: 1e6, rollingTarget: 15e4 },
6431
+ "google/gemini-2.5-pro": { contextWindow: 1e6, rollingTarget: 15e4 },
6432
+ "google/gemini-2.5-flash": { contextWindow: 1e6, rollingTarget: 15e4 },
6433
+ "openai/gpt-4o": { contextWindow: 128e3, rollingTarget: 78e3 },
6434
+ "openai/gpt-4.1": { contextWindow: 1e6, rollingTarget: 15e4 },
6435
+ "openai/o3": { contextWindow: 2e5, rollingTarget: 15e4 },
6436
+ "xai/grok-3": { contextWindow: 131072, rollingTarget: 8e4 }
6437
+ };
6438
+ var DEFAULT_LIMITS = { contextWindow: 2e5, rollingTarget: 15e4 };
6439
+ var PREFIX_DEFAULTS = {
6440
+ "anthropic/": { contextWindow: 2e5, rollingTarget: 15e4 },
6441
+ "google/": { contextWindow: 1e6, rollingTarget: 15e4 },
6442
+ "openai/": { contextWindow: 128e3, rollingTarget: 78e3 },
6443
+ "xai/": { contextWindow: 131072, rollingTarget: 8e4 }
6444
+ };
6445
+ function getModelLimits(modelId) {
6446
+ const normalized = modelId.trim().toLowerCase();
6447
+ const exact = MODEL_LIMITS[normalized];
6448
+ if (exact) return exact;
6449
+ for (const [prefix, limits] of Object.entries(PREFIX_DEFAULTS)) {
6450
+ if (normalized.startsWith(prefix)) return limits;
6451
+ }
6452
+ return DEFAULT_LIMITS;
6453
+ }
6454
+ var SUMMARIZATION_MODEL = "google/gemini-3-flash-preview";
6455
+ var SUMMARY_CHUNK_TOKENS = 3e4;
6456
+ var SUMMARY_BUDGET_RATIO = 0.15;
6457
+
6414
6458
  // src/agent/context.ts
6459
+ var TOOL_OUTPUT_TRIM_CHARS = 400;
6460
+ var COMPACTABLE_TOOLS = /* @__PURE__ */ new Set([
6461
+ "read_file",
6462
+ "bash",
6463
+ "explore_agent",
6464
+ "code_graph"
6465
+ ]);
6415
6466
  var ContextManager = class {
6416
6467
  sessionId;
6468
+ modelId;
6417
6469
  maxContextChars;
6418
6470
  keepRecentMessages;
6419
6471
  autoSummarize;
6420
- summary = null;
6472
+ summaries = [];
6421
6473
  constructor(options) {
6422
6474
  this.sessionId = options.sessionId;
6475
+ this.modelId = options.modelId;
6423
6476
  this.maxContextChars = options.maxContextChars;
6424
6477
  this.keepRecentMessages = options.keepRecentMessages;
6425
6478
  this.autoSummarize = options.autoSummarize;
6426
6479
  }
6427
6480
  /**
6428
- * Get messages for the current context
6429
- * Returns ModelMessage[] that can be passed directly to streamText/generateText
6430
- *
6431
- * Includes self-repair: if messages from the database have been corrupted
6432
- * (e.g., Date objects in tool outputs from parseDates), they are automatically
6433
- * sanitized to conform to the AI SDK's ModelMessage schema.
6481
+ * Get messages for the current context, applying the three-phase pipeline.
6434
6482
  */
6435
6483
  async getMessages() {
6436
- let modelMessages = await messageQueries.getModelMessages(this.sessionId);
6437
- modelMessages = sanitizeModelMessages(modelMessages);
6438
- const contextSize = calculateContextSize(modelMessages);
6439
- if (this.autoSummarize && contextSize > this.maxContextChars) {
6440
- modelMessages = await this.summarizeContext(modelMessages);
6441
- }
6442
- if (this.summary) {
6443
- modelMessages = [
6484
+ let messages = await messageQueries.getModelMessages(this.sessionId);
6485
+ messages = sanitizeModelMessages(messages);
6486
+ messages = this.compactOlderMessages(messages, this.keepRecentMessages);
6487
+ if (this.autoSummarize) {
6488
+ const { rollingTarget } = getModelLimits(this.modelId);
6489
+ const summaryBudget = Math.floor(rollingTarget * SUMMARY_BUDGET_RATIO);
6490
+ messages = await this.chunkSummarize(messages, rollingTarget);
6491
+ await this.rollSummaries(summaryBudget);
6492
+ }
6493
+ if (this.summaries.length > 0) {
6494
+ const summaryContent = this.summaries.join("\n\n---\n\n");
6495
+ messages = [
6444
6496
  {
6445
6497
  role: "system",
6446
6498
  content: `[Previous conversation summary]
6447
- ${this.summary}`
6499
+ ${summaryContent}`
6448
6500
  },
6449
- ...modelMessages
6501
+ ...messages
6450
6502
  ];
6451
6503
  }
6452
- return modelMessages;
6504
+ return messages;
6453
6505
  }
6506
+ // ---------------------------------------------------------------------------
6507
+ // Phase 1 – Compact
6508
+ // ---------------------------------------------------------------------------
6454
6509
  /**
6455
- * Summarize older messages to reduce context size
6510
+ * Strip non-essential content from messages older than the most recent
6511
+ * `recentCount`. Operates in-memory only — does not touch the DB.
6456
6512
  */
6457
- async summarizeContext(messages) {
6458
- if (messages.length <= this.keepRecentMessages) {
6459
- return messages;
6460
- }
6461
- const splitIndex = messages.length - this.keepRecentMessages;
6462
- const oldMessages = messages.slice(0, splitIndex);
6463
- const recentMessages = messages.slice(splitIndex);
6464
- const historyText = oldMessages.map((msg) => {
6513
+ compactOlderMessages(messages, recentCount) {
6514
+ if (messages.length <= recentCount) return messages;
6515
+ const boundary = messages.length - recentCount;
6516
+ const olderMessages = messages.slice(0, boundary);
6517
+ const recentMessages = messages.slice(boundary);
6518
+ const compacted = [];
6519
+ for (const msg of olderMessages) {
6520
+ const processed = this.compactMessage(msg);
6521
+ if (processed) compacted.push(processed);
6522
+ }
6523
+ return [...compacted, ...recentMessages];
6524
+ }
6525
+ compactMessage(msg) {
6526
+ if (!Array.isArray(msg.content)) return msg;
6527
+ const parts = [];
6528
+ for (const part of msg.content) {
6529
+ if (part.type === "tool-call" && part.toolName === "todo") continue;
6530
+ if (part.type === "tool-result" && part.toolName === "todo") continue;
6531
+ if (part.type === "reasoning" || part.type === "thinking") continue;
6532
+ if (part.type === "tool-result" && COMPACTABLE_TOOLS.has(part.toolName)) {
6533
+ parts.push(this.trimToolResult(part));
6534
+ continue;
6535
+ }
6536
+ parts.push(part);
6537
+ }
6538
+ if (parts.length === 0) return null;
6539
+ return { ...msg, content: parts };
6540
+ }
6541
+ trimToolResult(part) {
6542
+ const results = Array.isArray(part.result) ? part.result : [part.result];
6543
+ const trimmedResults = results.map((r) => {
6544
+ if (typeof r === "string" && r.length > TOOL_OUTPUT_TRIM_CHARS) {
6545
+ const half = Math.floor(TOOL_OUTPUT_TRIM_CHARS / 2);
6546
+ return r.slice(0, half) + `
6547
+ ...[trimmed ${r.length - TOOL_OUTPUT_TRIM_CHARS} chars]...
6548
+ ` + r.slice(-half);
6549
+ }
6550
+ if (r && typeof r === "object" && typeof r.text === "string" && r.text.length > TOOL_OUTPUT_TRIM_CHARS) {
6551
+ const half = Math.floor(TOOL_OUTPUT_TRIM_CHARS / 2);
6552
+ return {
6553
+ ...r,
6554
+ text: r.text.slice(0, half) + `
6555
+ ...[trimmed ${r.text.length - TOOL_OUTPUT_TRIM_CHARS} chars]...
6556
+ ` + r.text.slice(-half)
6557
+ };
6558
+ }
6559
+ return r;
6560
+ });
6561
+ return {
6562
+ ...part,
6563
+ result: Array.isArray(part.result) ? trimmedResults : trimmedResults[0]
6564
+ };
6565
+ }
6566
+ // ---------------------------------------------------------------------------
6567
+ // Phase 2 – Chunk-summarize
6568
+ // ---------------------------------------------------------------------------
6569
+ /**
6570
+ * While estimated tokens exceed `rollingTarget`, peel off the oldest
6571
+ * ~SUMMARY_CHUNK_TOKENS worth of messages, summarize them via the cheap
6572
+ * model, and prepend the summary.
6573
+ */
6574
+ async chunkSummarize(messages, rollingTarget) {
6575
+ let totalTokens = estimateMessageTokens(messages);
6576
+ while (totalTokens > rollingTarget && messages.length > this.keepRecentMessages) {
6577
+ let chunkTokens = 0;
6578
+ let chunkEnd = 0;
6579
+ const maxChunkable = messages.length - this.keepRecentMessages;
6580
+ for (let i = 0; i < maxChunkable; i++) {
6581
+ const msgTokens = this.messageTokens(messages[i]);
6582
+ chunkTokens += msgTokens;
6583
+ chunkEnd = i + 1;
6584
+ if (chunkTokens >= SUMMARY_CHUNK_TOKENS) break;
6585
+ }
6586
+ if (chunkEnd === 0) break;
6587
+ const chunk = messages.slice(0, chunkEnd);
6588
+ const remaining = messages.slice(chunkEnd);
6589
+ const summary = await this.summarizeChunk(chunk);
6590
+ if (summary) {
6591
+ this.summaries.push(summary);
6592
+ console.log(
6593
+ `[Context] Summarized ${chunk.length} messages (~${chunkTokens} tokens) into ${estimateTokens(summary)} tokens`
6594
+ );
6595
+ }
6596
+ messages = remaining;
6597
+ totalTokens = estimateMessageTokens(messages);
6598
+ }
6599
+ return messages;
6600
+ }
6601
+ async summarizeChunk(chunk) {
6602
+ const historyText = chunk.map((msg) => {
6465
6603
  const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
6466
6604
  return `[${msg.role}]: ${content}`;
6467
6605
  }).join("\n\n");
6468
6606
  try {
6469
- const config = getConfig();
6470
- const summaryPrompt = createSummaryPrompt(historyText);
6471
6607
  const result = await generateText2({
6472
- model: resolveModel(config.defaultModel),
6473
- prompt: summaryPrompt
6608
+ model: resolveModel(SUMMARIZATION_MODEL),
6609
+ prompt: createSummaryPrompt(historyText)
6474
6610
  });
6475
- this.summary = result.text;
6476
- console.log(`[Context] Summarized ${oldMessages.length} messages into ${this.summary.length} chars`);
6477
- return recentMessages;
6611
+ return result.text;
6478
6612
  } catch (error) {
6479
- console.error("[Context] Failed to summarize:", error);
6480
- return recentMessages;
6613
+ console.error("[Context] Chunk summarization failed:", error);
6614
+ return null;
6481
6615
  }
6482
6616
  }
6617
+ // ---------------------------------------------------------------------------
6618
+ // Phase 3 – Roll summaries
6619
+ // ---------------------------------------------------------------------------
6483
6620
  /**
6484
- * Add a user message to the context
6485
- * Content can be a string or an array of content parts (for messages with images/files)
6621
+ * If accumulated summaries exceed `budget` tokens, re-summarize them
6622
+ * into a single condensed summary.
6486
6623
  */
6624
+ async rollSummaries(budget) {
6625
+ if (this.summaries.length <= 1) return;
6626
+ const totalSummaryTokens = this.summaries.reduce(
6627
+ (t, s) => t + estimateTokens(s),
6628
+ 0
6629
+ );
6630
+ if (totalSummaryTokens <= budget) return;
6631
+ const combined = this.summaries.join("\n\n---\n\n");
6632
+ try {
6633
+ const result = await generateText2({
6634
+ model: resolveModel(SUMMARIZATION_MODEL),
6635
+ prompt: createSummaryPrompt(combined)
6636
+ });
6637
+ console.log(
6638
+ `[Context] Rolled ${this.summaries.length} summaries (${totalSummaryTokens} tokens) into ${estimateTokens(result.text)} tokens`
6639
+ );
6640
+ this.summaries = [result.text];
6641
+ } catch (error) {
6642
+ console.error("[Context] Summary rolling failed:", error);
6643
+ }
6644
+ }
6645
+ // ---------------------------------------------------------------------------
6646
+ // Helpers
6647
+ // ---------------------------------------------------------------------------
6648
+ messageTokens(msg) {
6649
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
6650
+ return estimateTokens(content) + 4;
6651
+ }
6652
+ // ---------------------------------------------------------------------------
6653
+ // Public API (unchanged)
6654
+ // ---------------------------------------------------------------------------
6487
6655
  async addUserMessage(content) {
6488
6656
  const userMessage = {
6489
6657
  role: "user",
@@ -6491,30 +6659,22 @@ ${this.summary}`
6491
6659
  };
6492
6660
  await messageQueries.create(this.sessionId, userMessage);
6493
6661
  }
6494
- /**
6495
- * Add response messages from AI SDK directly
6496
- * This is the preferred method - use result.response.messages from streamText/generateText
6497
- */
6498
6662
  async addResponseMessages(messages) {
6499
6663
  await messageQueries.addMany(this.sessionId, messages);
6500
6664
  }
6501
- /**
6502
- * Get current context statistics
6503
- */
6504
6665
  async getStats() {
6505
6666
  const messages = await messageQueries.getModelMessages(this.sessionId);
6506
6667
  return {
6507
6668
  messageCount: messages.length,
6508
6669
  contextChars: calculateContextSize(messages),
6509
- hasSummary: this.summary !== null
6670
+ estimatedTokens: estimateMessageTokens(messages),
6671
+ hasSummary: this.summaries.length > 0,
6672
+ summaryCount: this.summaries.length
6510
6673
  };
6511
6674
  }
6512
- /**
6513
- * Clear all messages in the context
6514
- */
6515
6675
  async clear() {
6516
6676
  await messageQueries.deleteBySession(this.sessionId);
6517
- this.summary = null;
6677
+ this.summaries = [];
6518
6678
  }
6519
6679
  };
6520
6680
 
@@ -6582,6 +6742,7 @@ var Agent = class _Agent {
6582
6742
  }
6583
6743
  const context = new ContextManager({
6584
6744
  sessionId: session.id,
6745
+ modelId: session.model || config.defaultModel,
6585
6746
  maxContextChars: config.context?.maxChars || 2e5,
6586
6747
  keepRecentMessages: config.context?.keepRecentMessages || 10,
6587
6748
  autoSummarize: config.context?.autoSummarize ?? true