sparkecoder 0.1.72 → 0.1.74

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 (91) hide show
  1. package/dist/agent/index.d.ts +1 -1
  2. package/dist/agent/index.js +292 -57
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/cli.js +294 -59
  5. package/dist/cli.js.map +1 -1
  6. package/dist/{index-Dm6wGcYv.d.ts → index-DT1l57s0.d.ts} +38 -24
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +294 -59
  9. package/dist/index.js.map +1 -1
  10. package/dist/server/index.js +294 -59
  11. package/dist/server/index.js.map +1 -1
  12. package/dist/skills/default/qa.md +15 -11
  13. package/dist/tools/index.js +16 -6
  14. package/dist/tools/index.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/skills/default/qa.md +15 -11
  17. package/web/.next/BUILD_ID +1 -1
  18. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  19. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  20. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  21. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  22. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  23. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  31. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  32. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  37. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
  38. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
  39. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  46. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
  48. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  55. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  62. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  64. package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  70. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  72. package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
  73. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  78. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  79. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  80. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  81. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  82. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  83. /package/web/.next/standalone/web/.next/static/{static/xXtjibFdZ768DUoqjM6zS → _Xl7dVMD-6ghn4EberTUE}/_buildManifest.js +0 -0
  84. /package/web/.next/standalone/web/.next/static/{static/xXtjibFdZ768DUoqjM6zS → _Xl7dVMD-6ghn4EberTUE}/_clientMiddlewareManifest.json +0 -0
  85. /package/web/.next/standalone/web/.next/static/{static/xXtjibFdZ768DUoqjM6zS → _Xl7dVMD-6ghn4EberTUE}/_ssgManifest.js +0 -0
  86. /package/web/.next/standalone/web/.next/static/{xXtjibFdZ768DUoqjM6zS → static/_Xl7dVMD-6ghn4EberTUE}/_buildManifest.js +0 -0
  87. /package/web/.next/standalone/web/.next/static/{xXtjibFdZ768DUoqjM6zS → static/_Xl7dVMD-6ghn4EberTUE}/_clientMiddlewareManifest.json +0 -0
  88. /package/web/.next/standalone/web/.next/static/{xXtjibFdZ768DUoqjM6zS → static/_Xl7dVMD-6ghn4EberTUE}/_ssgManifest.js +0 -0
  89. /package/web/.next/static/{xXtjibFdZ768DUoqjM6zS → _Xl7dVMD-6ghn4EberTUE}/_buildManifest.js +0 -0
  90. /package/web/.next/static/{xXtjibFdZ768DUoqjM6zS → _Xl7dVMD-6ghn4EberTUE}/_clientMiddlewareManifest.json +0 -0
  91. /package/web/.next/static/{xXtjibFdZ768DUoqjM6zS → _Xl7dVMD-6ghn4EberTUE}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  import 'ai';
2
2
  import '../schema-XcP0dedO.js';
3
- export { A as Agent, a as AgentOptions, b as AgentRunOptions, c as AgentStreamResult, C as ContextManager, M as MessageAttachment, d as buildSystemPrompt, e as buildTaskPromptAddendum } from '../index-Dm6wGcYv.js';
3
+ export { A as Agent, a as AgentOptions, b as AgentRunOptions, c as AgentStreamResult, C as ContextManager, M as MessageAttachment, d as buildSystemPrompt, e as buildTaskPromptAddendum } from '../index-DT1l57s0.js';
4
4
  import '../search-CCffrVJE.js';
5
5
  import 'drizzle-orm/sqlite-core';
6
6
  import 'zod';
@@ -1794,9 +1794,8 @@ function createRemoteModel(modelId, config) {
1794
1794
  });
1795
1795
  if (!res.ok) {
1796
1796
  const err = await res.json().catch(() => ({}));
1797
- throw new Error(
1798
- `Remote inference failed (${res.status}): ${err.error || res.statusText}`
1799
- );
1797
+ const detail = formatRemoteError(res.status, modelId, err);
1798
+ throw new Error(detail);
1800
1799
  }
1801
1800
  const result = await res.json();
1802
1801
  return deserializeValue(result);
@@ -1813,9 +1812,8 @@ function createRemoteModel(modelId, config) {
1813
1812
  });
1814
1813
  if (!res.ok) {
1815
1814
  const err = await res.json().catch(() => ({}));
1816
- throw new Error(
1817
- `Remote inference failed (${res.status}): ${err.error || res.statusText}`
1818
- );
1815
+ const detail = formatRemoteError(res.status, modelId, err);
1816
+ throw new Error(detail);
1819
1817
  }
1820
1818
  const reader = res.body.getReader();
1821
1819
  const decoder = new TextDecoder();
@@ -1873,6 +1871,18 @@ function createRemoteModel(modelId, config) {
1873
1871
  }
1874
1872
  };
1875
1873
  }
1874
+ function formatRemoteError(status, modelId, body) {
1875
+ const parts = [`Remote inference failed (${status}) for ${modelId}`];
1876
+ if (body.error) parts.push(body.error);
1877
+ if (body.details) {
1878
+ const d = body.details;
1879
+ if (d.type) parts.push(`type=${d.type}`);
1880
+ if (d.statusCode && d.statusCode !== status) parts.push(`upstream=${d.statusCode}`);
1881
+ if (d.cause) parts.push(`cause: ${d.cause}`);
1882
+ if (d.orderWarnings?.length) parts.push(`prompt issues: ${d.orderWarnings.join("; ")}`);
1883
+ }
1884
+ return parts.join(" \u2014 ");
1885
+ }
1876
1886
 
1877
1887
  // src/agent/model.ts
1878
1888
  init_config();
@@ -1912,6 +1922,19 @@ import { z as z2 } from "zod";
1912
1922
  import { exec as exec2 } from "child_process";
1913
1923
  import { promisify as promisify2 } from "util";
1914
1924
 
1925
+ // src/utils/tokens.ts
1926
+ var CHARS_PER_TOKEN = 4;
1927
+ var MESSAGE_OVERHEAD_TOKENS = 4;
1928
+ function estimateTokens(text) {
1929
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
1930
+ }
1931
+ function estimateMessageTokens(messages) {
1932
+ return messages.reduce((total, msg) => {
1933
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
1934
+ return total + estimateTokens(content) + MESSAGE_OVERHEAD_TOKENS;
1935
+ }, 0);
1936
+ }
1937
+
1915
1938
  // src/utils/truncate.ts
1916
1939
  var MAX_OUTPUT_CHARS = 1e4;
1917
1940
  function truncateOutput(output, maxChars = MAX_OUTPUT_CHARS) {
@@ -5698,9 +5721,6 @@ ${conversationHistory}
5698
5721
  Summary:`;
5699
5722
  }
5700
5723
 
5701
- // src/agent/context.ts
5702
- init_config();
5703
-
5704
5724
  // src/utils/sanitize-messages.ts
5705
5725
  import { modelMessageSchema } from "ai";
5706
5726
  function convertDatesToStrings(value) {
@@ -5837,79 +5857,256 @@ function sanitizeModelMessages(messages) {
5837
5857
  return result;
5838
5858
  }
5839
5859
 
5860
+ // src/agent/model-limits.ts
5861
+ var MODEL_LIMITS = {
5862
+ "anthropic/claude-opus-4-6": { contextWindow: 2e5, rollingTarget: 15e4 },
5863
+ "anthropic/claude-sonnet-4": { contextWindow: 2e5, rollingTarget: 15e4 },
5864
+ "anthropic/claude-3.5-sonnet": { contextWindow: 2e5, rollingTarget: 15e4 },
5865
+ "anthropic/claude-3-haiku": { contextWindow: 2e5, rollingTarget: 15e4 },
5866
+ "google/gemini-3-flash-preview": { contextWindow: 1e6, rollingTarget: 15e4 },
5867
+ "google/gemini-2.5-pro": { contextWindow: 1e6, rollingTarget: 15e4 },
5868
+ "google/gemini-2.5-flash": { contextWindow: 1e6, rollingTarget: 15e4 },
5869
+ "openai/gpt-4o": { contextWindow: 128e3, rollingTarget: 78e3 },
5870
+ "openai/gpt-4.1": { contextWindow: 1e6, rollingTarget: 15e4 },
5871
+ "openai/o3": { contextWindow: 2e5, rollingTarget: 15e4 },
5872
+ "xai/grok-3": { contextWindow: 131072, rollingTarget: 8e4 }
5873
+ };
5874
+ var DEFAULT_LIMITS = { contextWindow: 2e5, rollingTarget: 15e4 };
5875
+ var PREFIX_DEFAULTS = {
5876
+ "anthropic/": { contextWindow: 2e5, rollingTarget: 15e4 },
5877
+ "google/": { contextWindow: 1e6, rollingTarget: 15e4 },
5878
+ "openai/": { contextWindow: 128e3, rollingTarget: 78e3 },
5879
+ "xai/": { contextWindow: 131072, rollingTarget: 8e4 }
5880
+ };
5881
+ function getModelLimits(modelId) {
5882
+ const normalized = modelId.trim().toLowerCase();
5883
+ const exact = MODEL_LIMITS[normalized];
5884
+ if (exact) return exact;
5885
+ for (const [prefix, limits] of Object.entries(PREFIX_DEFAULTS)) {
5886
+ if (normalized.startsWith(prefix)) return limits;
5887
+ }
5888
+ return DEFAULT_LIMITS;
5889
+ }
5890
+ var SUMMARIZATION_MODEL = "google/gemini-3-flash-preview";
5891
+ var SUMMARY_CHUNK_TOKENS = 3e4;
5892
+ var SUMMARY_BUDGET_RATIO = 0.15;
5893
+
5840
5894
  // src/agent/context.ts
5895
+ var TOOL_OUTPUT_TRIM_CHARS = 400;
5896
+ var COMPACTABLE_TOOLS = /* @__PURE__ */ new Set([
5897
+ "read_file",
5898
+ "bash",
5899
+ "explore_agent",
5900
+ "code_graph"
5901
+ ]);
5841
5902
  var ContextManager = class {
5842
5903
  sessionId;
5904
+ modelId;
5843
5905
  maxContextChars;
5844
5906
  keepRecentMessages;
5845
5907
  autoSummarize;
5846
- summary = null;
5908
+ summaries = [];
5847
5909
  constructor(options) {
5848
5910
  this.sessionId = options.sessionId;
5911
+ this.modelId = options.modelId;
5849
5912
  this.maxContextChars = options.maxContextChars;
5850
5913
  this.keepRecentMessages = options.keepRecentMessages;
5851
5914
  this.autoSummarize = options.autoSummarize;
5852
5915
  }
5853
5916
  /**
5854
- * Get messages for the current context
5855
- * Returns ModelMessage[] that can be passed directly to streamText/generateText
5856
- *
5857
- * Includes self-repair: if messages from the database have been corrupted
5858
- * (e.g., Date objects in tool outputs from parseDates), they are automatically
5859
- * sanitized to conform to the AI SDK's ModelMessage schema.
5917
+ * Get messages for the current context, applying the three-phase pipeline.
5860
5918
  */
5861
5919
  async getMessages() {
5862
- let modelMessages = await messageQueries.getModelMessages(this.sessionId);
5863
- modelMessages = sanitizeModelMessages(modelMessages);
5864
- const contextSize = calculateContextSize(modelMessages);
5865
- if (this.autoSummarize && contextSize > this.maxContextChars) {
5866
- modelMessages = await this.summarizeContext(modelMessages);
5920
+ let messages = await messageQueries.getModelMessages(this.sessionId);
5921
+ messages = sanitizeModelMessages(messages);
5922
+ messages = this.compactOlderMessages(messages, this.keepRecentMessages);
5923
+ if (this.autoSummarize) {
5924
+ const { rollingTarget } = getModelLimits(this.modelId);
5925
+ const summaryBudget = Math.floor(rollingTarget * SUMMARY_BUDGET_RATIO);
5926
+ messages = await this.chunkSummarize(messages, rollingTarget);
5927
+ await this.rollSummaries(summaryBudget);
5867
5928
  }
5868
- if (this.summary) {
5869
- modelMessages = [
5929
+ if (this.summaries.length > 0) {
5930
+ const summaryContent = this.summaries.join("\n\n---\n\n");
5931
+ messages = [
5870
5932
  {
5871
5933
  role: "system",
5872
5934
  content: `[Previous conversation summary]
5873
- ${this.summary}`
5935
+ ${summaryContent}`
5874
5936
  },
5875
- ...modelMessages
5937
+ ...messages
5876
5938
  ];
5877
5939
  }
5878
- return modelMessages;
5940
+ messages = repairToolPairing(messages);
5941
+ return messages;
5942
+ }
5943
+ // ---------------------------------------------------------------------------
5944
+ // Phase 1 – Compact
5945
+ // ---------------------------------------------------------------------------
5946
+ /**
5947
+ * Strip non-essential content from messages older than the most recent
5948
+ * `recentCount`. Operates in-memory only — does not touch the DB.
5949
+ *
5950
+ * Tracks removed tool-call IDs so matching tool-results are also removed,
5951
+ * preventing orphaned tool_result blocks that providers reject.
5952
+ */
5953
+ compactOlderMessages(messages, recentCount) {
5954
+ if (messages.length <= recentCount) return messages;
5955
+ const boundary = messages.length - recentCount;
5956
+ const olderMessages = messages.slice(0, boundary);
5957
+ const recentMessages = messages.slice(boundary);
5958
+ const removedToolCallIds = /* @__PURE__ */ new Set();
5959
+ const compacted = [];
5960
+ for (const msg of olderMessages) {
5961
+ const processed = this.compactMessage(msg, removedToolCallIds);
5962
+ if (processed) compacted.push(processed);
5963
+ }
5964
+ if (removedToolCallIds.size > 0) {
5965
+ const cleaned = [];
5966
+ for (const msg of compacted) {
5967
+ const result = stripOrphanedToolResults(msg, removedToolCallIds);
5968
+ if (result) cleaned.push(result);
5969
+ }
5970
+ return [...cleaned, ...recentMessages];
5971
+ }
5972
+ return [...compacted, ...recentMessages];
5973
+ }
5974
+ compactMessage(msg, removedToolCallIds) {
5975
+ if (!Array.isArray(msg.content)) return msg;
5976
+ const parts = [];
5977
+ for (const part of msg.content) {
5978
+ if (part.type === "tool-call" && part.toolName === "todo") {
5979
+ if (part.toolCallId) removedToolCallIds.add(part.toolCallId);
5980
+ continue;
5981
+ }
5982
+ if (part.type === "tool-result" && part.toolName === "todo") {
5983
+ if (part.toolCallId) removedToolCallIds.add(part.toolCallId);
5984
+ continue;
5985
+ }
5986
+ if (part.type === "reasoning" || part.type === "thinking") continue;
5987
+ if (part.type === "tool-result" && COMPACTABLE_TOOLS.has(part.toolName)) {
5988
+ parts.push(this.trimToolResult(part));
5989
+ continue;
5990
+ }
5991
+ parts.push(part);
5992
+ }
5993
+ if (parts.length === 0) return null;
5994
+ return { ...msg, content: parts };
5995
+ }
5996
+ trimToolResult(part) {
5997
+ const results = Array.isArray(part.result) ? part.result : [part.result];
5998
+ const trimmedResults = results.map((r) => {
5999
+ if (typeof r === "string" && r.length > TOOL_OUTPUT_TRIM_CHARS) {
6000
+ const half = Math.floor(TOOL_OUTPUT_TRIM_CHARS / 2);
6001
+ return r.slice(0, half) + `
6002
+ ...[trimmed ${r.length - TOOL_OUTPUT_TRIM_CHARS} chars]...
6003
+ ` + r.slice(-half);
6004
+ }
6005
+ if (r && typeof r === "object" && typeof r.text === "string" && r.text.length > TOOL_OUTPUT_TRIM_CHARS) {
6006
+ const half = Math.floor(TOOL_OUTPUT_TRIM_CHARS / 2);
6007
+ return {
6008
+ ...r,
6009
+ text: r.text.slice(0, half) + `
6010
+ ...[trimmed ${r.text.length - TOOL_OUTPUT_TRIM_CHARS} chars]...
6011
+ ` + r.text.slice(-half)
6012
+ };
6013
+ }
6014
+ return r;
6015
+ });
6016
+ return {
6017
+ ...part,
6018
+ result: Array.isArray(part.result) ? trimmedResults : trimmedResults[0]
6019
+ };
5879
6020
  }
6021
+ // ---------------------------------------------------------------------------
6022
+ // Phase 2 – Chunk-summarize
6023
+ // ---------------------------------------------------------------------------
5880
6024
  /**
5881
- * Summarize older messages to reduce context size
6025
+ * While estimated tokens exceed `rollingTarget`, peel off the oldest
6026
+ * ~SUMMARY_CHUNK_TOKENS worth of messages, summarize them via the cheap
6027
+ * model, and prepend the summary.
5882
6028
  */
5883
- async summarizeContext(messages) {
5884
- if (messages.length <= this.keepRecentMessages) {
5885
- return messages;
6029
+ async chunkSummarize(messages, rollingTarget) {
6030
+ let totalTokens = estimateMessageTokens(messages);
6031
+ while (totalTokens > rollingTarget && messages.length > this.keepRecentMessages) {
6032
+ let chunkTokens = 0;
6033
+ let chunkEnd = 0;
6034
+ const maxChunkable = messages.length - this.keepRecentMessages;
6035
+ for (let i = 0; i < maxChunkable; i++) {
6036
+ const msgTokens = this.messageTokens(messages[i]);
6037
+ chunkTokens += msgTokens;
6038
+ chunkEnd = i + 1;
6039
+ if (chunkTokens >= SUMMARY_CHUNK_TOKENS) break;
6040
+ }
6041
+ if (chunkEnd === 0) break;
6042
+ const chunk = messages.slice(0, chunkEnd);
6043
+ const remaining = messages.slice(chunkEnd);
6044
+ const summary = await this.summarizeChunk(chunk);
6045
+ if (summary) {
6046
+ this.summaries.push(summary);
6047
+ console.log(
6048
+ `[Context] Summarized ${chunk.length} messages (~${chunkTokens} tokens) into ${estimateTokens(summary)} tokens`
6049
+ );
6050
+ }
6051
+ messages = remaining;
6052
+ totalTokens = estimateMessageTokens(messages);
5886
6053
  }
5887
- const splitIndex = messages.length - this.keepRecentMessages;
5888
- const oldMessages = messages.slice(0, splitIndex);
5889
- const recentMessages = messages.slice(splitIndex);
5890
- const historyText = oldMessages.map((msg) => {
6054
+ return messages;
6055
+ }
6056
+ async summarizeChunk(chunk) {
6057
+ const historyText = chunk.map((msg) => {
5891
6058
  const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
5892
6059
  return `[${msg.role}]: ${content}`;
5893
6060
  }).join("\n\n");
5894
6061
  try {
5895
- const config = getConfig();
5896
- const summaryPrompt = createSummaryPrompt(historyText);
5897
6062
  const result = await generateText2({
5898
- model: resolveModel(config.defaultModel),
5899
- prompt: summaryPrompt
6063
+ model: resolveModel(SUMMARIZATION_MODEL),
6064
+ prompt: createSummaryPrompt(historyText)
5900
6065
  });
5901
- this.summary = result.text;
5902
- console.log(`[Context] Summarized ${oldMessages.length} messages into ${this.summary.length} chars`);
5903
- return recentMessages;
6066
+ return result.text;
5904
6067
  } catch (error) {
5905
- console.error("[Context] Failed to summarize:", error);
5906
- return recentMessages;
6068
+ console.error("[Context] Chunk summarization failed:", error);
6069
+ return null;
5907
6070
  }
5908
6071
  }
6072
+ // ---------------------------------------------------------------------------
6073
+ // Phase 3 – Roll summaries
6074
+ // ---------------------------------------------------------------------------
5909
6075
  /**
5910
- * Add a user message to the context
5911
- * Content can be a string or an array of content parts (for messages with images/files)
6076
+ * If accumulated summaries exceed `budget` tokens, re-summarize them
6077
+ * into a single condensed summary.
5912
6078
  */
6079
+ async rollSummaries(budget) {
6080
+ if (this.summaries.length <= 1) return;
6081
+ const totalSummaryTokens = this.summaries.reduce(
6082
+ (t, s) => t + estimateTokens(s),
6083
+ 0
6084
+ );
6085
+ if (totalSummaryTokens <= budget) return;
6086
+ const combined = this.summaries.join("\n\n---\n\n");
6087
+ try {
6088
+ const result = await generateText2({
6089
+ model: resolveModel(SUMMARIZATION_MODEL),
6090
+ prompt: createSummaryPrompt(combined)
6091
+ });
6092
+ console.log(
6093
+ `[Context] Rolled ${this.summaries.length} summaries (${totalSummaryTokens} tokens) into ${estimateTokens(result.text)} tokens`
6094
+ );
6095
+ this.summaries = [result.text];
6096
+ } catch (error) {
6097
+ console.error("[Context] Summary rolling failed:", error);
6098
+ }
6099
+ }
6100
+ // ---------------------------------------------------------------------------
6101
+ // Helpers
6102
+ // ---------------------------------------------------------------------------
6103
+ messageTokens(msg) {
6104
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
6105
+ return estimateTokens(content) + 4;
6106
+ }
6107
+ // ---------------------------------------------------------------------------
6108
+ // Public API (unchanged)
6109
+ // ---------------------------------------------------------------------------
5913
6110
  async addUserMessage(content) {
5914
6111
  const userMessage = {
5915
6112
  role: "user",
@@ -5917,32 +6114,69 @@ ${this.summary}`
5917
6114
  };
5918
6115
  await messageQueries.create(this.sessionId, userMessage);
5919
6116
  }
5920
- /**
5921
- * Add response messages from AI SDK directly
5922
- * This is the preferred method - use result.response.messages from streamText/generateText
5923
- */
5924
6117
  async addResponseMessages(messages) {
5925
6118
  await messageQueries.addMany(this.sessionId, messages);
5926
6119
  }
5927
- /**
5928
- * Get current context statistics
5929
- */
5930
6120
  async getStats() {
5931
6121
  const messages = await messageQueries.getModelMessages(this.sessionId);
5932
6122
  return {
5933
6123
  messageCount: messages.length,
5934
6124
  contextChars: calculateContextSize(messages),
5935
- hasSummary: this.summary !== null
6125
+ estimatedTokens: estimateMessageTokens(messages),
6126
+ hasSummary: this.summaries.length > 0,
6127
+ summaryCount: this.summaries.length
5936
6128
  };
5937
6129
  }
5938
- /**
5939
- * Clear all messages in the context
5940
- */
5941
6130
  async clear() {
5942
6131
  await messageQueries.deleteBySession(this.sessionId);
5943
- this.summary = null;
6132
+ this.summaries = [];
5944
6133
  }
5945
6134
  };
6135
+ function stripOrphanedToolResults(msg, removedIds) {
6136
+ if (!Array.isArray(msg.content)) return msg;
6137
+ const parts = msg.content.filter((part) => {
6138
+ if (part.type === "tool-result" && removedIds.has(part.toolCallId)) return false;
6139
+ if (part.type === "tool-call" && removedIds.has(part.toolCallId)) return false;
6140
+ return true;
6141
+ });
6142
+ if (parts.length === 0) return null;
6143
+ return { ...msg, content: parts };
6144
+ }
6145
+ function repairToolPairing(messages) {
6146
+ const toolCallIds = /* @__PURE__ */ new Set();
6147
+ const toolResultIds = /* @__PURE__ */ new Set();
6148
+ for (const msg of messages) {
6149
+ if (!Array.isArray(msg.content)) continue;
6150
+ for (const part of msg.content) {
6151
+ if (part.type === "tool-call" && part.toolCallId) toolCallIds.add(part.toolCallId);
6152
+ if (part.type === "tool-result" && part.toolCallId) toolResultIds.add(part.toolCallId);
6153
+ }
6154
+ }
6155
+ const orphanedCalls = new Set([...toolCallIds].filter((id) => !toolResultIds.has(id)));
6156
+ const orphanedResults = new Set([...toolResultIds].filter((id) => !toolCallIds.has(id)));
6157
+ if (orphanedCalls.size === 0 && orphanedResults.size === 0) return messages;
6158
+ if (orphanedCalls.size > 0) {
6159
+ console.warn(`[tool-repair] Removing ${orphanedCalls.size} orphaned tool-call(s) with no matching result`);
6160
+ }
6161
+ if (orphanedResults.size > 0) {
6162
+ console.warn(`[tool-repair] Removing ${orphanedResults.size} orphaned tool-result(s) with no matching call`);
6163
+ }
6164
+ const repaired = [];
6165
+ for (const msg of messages) {
6166
+ if (!Array.isArray(msg.content)) {
6167
+ repaired.push(msg);
6168
+ continue;
6169
+ }
6170
+ const parts = msg.content.filter((part) => {
6171
+ if (part.type === "tool-call" && orphanedCalls.has(part.toolCallId)) return false;
6172
+ if (part.type === "tool-result" && orphanedResults.has(part.toolCallId)) return false;
6173
+ return true;
6174
+ });
6175
+ if (parts.length === 0) continue;
6176
+ repaired.push({ ...msg, content: parts });
6177
+ }
6178
+ return repaired;
6179
+ }
5946
6180
 
5947
6181
  // src/utils/webhook.ts
5948
6182
  async function sendWebhook(url, event) {
@@ -6026,6 +6260,7 @@ var Agent = class _Agent {
6026
6260
  }
6027
6261
  const context = new ContextManager({
6028
6262
  sessionId: session.id,
6263
+ modelId: session.model || config.defaultModel,
6029
6264
  maxContextChars: config.context?.maxChars || 2e5,
6030
6265
  keepRecentMessages: config.context?.keepRecentMessages || 10,
6031
6266
  autoSummarize: config.context?.autoSummarize ?? true