neoctl 0.2.30 → 0.2.32

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 (151) hide show
  1. package/README.md +37 -3
  2. package/dist/agents/agent-activity.d.ts +3 -0
  3. package/dist/agents/agent-activity.js +33 -6
  4. package/dist/agents/agent-activity.js.map +1 -1
  5. package/dist/agents/agent-definition.d.ts +2 -2
  6. package/dist/agents/agent-definition.js +6 -6
  7. package/dist/agents/agent-definition.js.map +1 -1
  8. package/dist/agents/agent-report-tool.d.ts +1 -1
  9. package/dist/agents/agent-report-tool.js +4 -4
  10. package/dist/agents/agent-report-tool.js.map +1 -1
  11. package/dist/agents/agent-tool.d.ts +1 -1
  12. package/dist/agents/agent-tool.js +97 -18
  13. package/dist/agents/agent-tool.js.map +1 -1
  14. package/dist/agents/local-agent-task.d.ts +12 -0
  15. package/dist/agents/local-agent-task.js +38 -1
  16. package/dist/agents/local-agent-task.js.map +1 -1
  17. package/dist/agents/smoke-agents.js +37 -26
  18. package/dist/agents/smoke-agents.js.map +1 -1
  19. package/dist/context/compaction.d.ts +20 -4
  20. package/dist/context/compaction.js +378 -187
  21. package/dist/context/compaction.js.map +1 -1
  22. package/dist/context/context-manager.d.ts +7 -1
  23. package/dist/context/context-manager.js +15 -11
  24. package/dist/context/context-manager.js.map +1 -1
  25. package/dist/context/prompts.js +8 -6
  26. package/dist/context/prompts.js.map +1 -1
  27. package/dist/context/smoke-context.js +273 -24
  28. package/dist/context/smoke-context.js.map +1 -1
  29. package/dist/core/assistant-output-filter.d.ts +1 -0
  30. package/dist/core/assistant-output-filter.js +54 -2
  31. package/dist/core/assistant-output-filter.js.map +1 -1
  32. package/dist/core/context-metrics.d.ts +1 -0
  33. package/dist/core/context-metrics.js +18 -12
  34. package/dist/core/context-metrics.js.map +1 -1
  35. package/dist/core/image-registry.d.ts +16 -3
  36. package/dist/core/image-registry.js +65 -30
  37. package/dist/core/image-registry.js.map +1 -1
  38. package/dist/core/image-storage.d.ts +15 -1
  39. package/dist/core/image-storage.js +140 -17
  40. package/dist/core/image-storage.js.map +1 -1
  41. package/dist/core/message-pipeline.js +18 -4
  42. package/dist/core/message-pipeline.js.map +1 -1
  43. package/dist/core/query-engine.d.ts +12 -2
  44. package/dist/core/query-engine.js +59 -8
  45. package/dist/core/query-engine.js.map +1 -1
  46. package/dist/core/query.d.ts +13 -2
  47. package/dist/core/query.js +144 -27
  48. package/dist/core/query.js.map +1 -1
  49. package/dist/core/run-agent.d.ts +13 -2
  50. package/dist/core/run-agent.js +16 -13
  51. package/dist/core/run-agent.js.map +1 -1
  52. package/dist/core/smoke-core-loop.js +121 -3
  53. package/dist/core/smoke-core-loop.js.map +1 -1
  54. package/dist/core/smoke-image-integrity.d.ts +1 -0
  55. package/dist/core/smoke-image-integrity.js +67 -0
  56. package/dist/core/smoke-image-integrity.js.map +1 -0
  57. package/dist/core/state.d.ts +1 -1
  58. package/dist/core/state.js.map +1 -1
  59. package/dist/index.d.ts +2 -1
  60. package/dist/index.js +2 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/model/model-metadata.json +4 -4
  63. package/dist/plugins/plugin-system.d.ts +61 -0
  64. package/dist/plugins/plugin-system.js +189 -0
  65. package/dist/plugins/plugin-system.js.map +1 -0
  66. package/dist/plugins/smoke-plugin-system.d.ts +1 -0
  67. package/dist/plugins/smoke-plugin-system.js +63 -0
  68. package/dist/plugins/smoke-plugin-system.js.map +1 -0
  69. package/dist/repl/index.js +22 -17
  70. package/dist/repl/index.js.map +1 -1
  71. package/dist/secrets/secret-redaction.d.ts +5 -0
  72. package/dist/secrets/secret-redaction.js +43 -0
  73. package/dist/secrets/secret-redaction.js.map +1 -1
  74. package/dist/secrets/secret-types.d.ts +4 -0
  75. package/dist/secrets/secret-types.js.map +1 -1
  76. package/dist/secrets/smoke-secrets.js +7 -3
  77. package/dist/secrets/smoke-secrets.js.map +1 -1
  78. package/dist/session/session-export.d.ts +2 -0
  79. package/dist/session/session-export.js +10 -0
  80. package/dist/session/session-export.js.map +1 -1
  81. package/dist/session/session-store.d.ts +41 -0
  82. package/dist/session/session-store.js +292 -59
  83. package/dist/session/session-store.js.map +1 -1
  84. package/dist/session/smoke-session.js +127 -7
  85. package/dist/session/smoke-session.js.map +1 -1
  86. package/dist/skills/smoke-skills.js +5 -5
  87. package/dist/skills/smoke-skills.js.map +1 -1
  88. package/dist/tasks/subagent-tools.d.ts +31 -0
  89. package/dist/tasks/{task-tools.js → subagent-tools.js} +26 -32
  90. package/dist/tasks/subagent-tools.js.map +1 -0
  91. package/dist/tools/builtins/edit-tool.js +24 -6
  92. package/dist/tools/builtins/edit-tool.js.map +1 -1
  93. package/dist/tools/builtins/exec-process-manager.d.ts +13 -0
  94. package/dist/tools/builtins/exec-process-manager.js +57 -7
  95. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  96. package/dist/tools/builtins/exec-tool.js +34 -10
  97. package/dist/tools/builtins/exec-tool.js.map +1 -1
  98. package/dist/tools/builtins/filesystem-tools.js +2 -4
  99. package/dist/tools/builtins/filesystem-tools.js.map +1 -1
  100. package/dist/tools/builtins/grep-tool.js +2 -3
  101. package/dist/tools/builtins/grep-tool.js.map +1 -1
  102. package/dist/tools/builtins/image-generation-tool.js +36 -46
  103. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  104. package/dist/tools/builtins/image-loader-tool.js +12 -7
  105. package/dist/tools/builtins/image-loader-tool.js.map +1 -1
  106. package/dist/tools/builtins/image-note-tool.js +8 -4
  107. package/dist/tools/builtins/image-note-tool.js.map +1 -1
  108. package/dist/tools/builtins/plan-tool.js +1 -1
  109. package/dist/tools/builtins/plan-tool.js.map +1 -1
  110. package/dist/tools/builtins/search-tool.js +3 -3
  111. package/dist/tools/builtins/search-tool.js.map +1 -1
  112. package/dist/tools/registry.d.ts +8 -1
  113. package/dist/tools/registry.js +37 -13
  114. package/dist/tools/registry.js.map +1 -1
  115. package/dist/tools/run-tool-use.js +3 -2
  116. package/dist/tools/run-tool-use.js.map +1 -1
  117. package/dist/tools/smoke-exec-process.js +12 -0
  118. package/dist/tools/smoke-exec-process.js.map +1 -1
  119. package/dist/tools/smoke-tool-system.js +68 -31
  120. package/dist/tools/smoke-tool-system.js.map +1 -1
  121. package/dist/tools/tool-catalog.d.ts +36 -0
  122. package/dist/tools/tool-catalog.js +64 -0
  123. package/dist/tools/tool-catalog.js.map +1 -0
  124. package/dist/tools/tool-orchestration.d.ts +20 -3
  125. package/dist/tools/tool-orchestration.js +29 -4
  126. package/dist/tools/tool-orchestration.js.map +1 -1
  127. package/dist/tools/tool.d.ts +13 -0
  128. package/dist/tools/tool.js.map +1 -1
  129. package/dist/types/events.d.ts +28 -1
  130. package/dist/types/messages.d.ts +13 -5
  131. package/dist/types/messages.js +4 -0
  132. package/dist/types/messages.js.map +1 -1
  133. package/dist/ui/display-message.d.ts +10 -2
  134. package/dist/ui/display-message.js +18 -9
  135. package/dist/ui/display-message.js.map +1 -1
  136. package/dist/web/index.d.ts +198 -9
  137. package/dist/web/index.js +1208 -146
  138. package/dist/web/index.js.map +1 -1
  139. package/dist/web/runtime-context-protocol.d.ts +19 -1
  140. package/dist/web/runtime-context-protocol.js +19 -3
  141. package/dist/web/runtime-context-protocol.js.map +1 -1
  142. package/dist/web/smoke-runtime-context-protocol.js +13 -2
  143. package/dist/web/smoke-runtime-context-protocol.js.map +1 -1
  144. package/dist/web/smoke-web-history.d.ts +1 -0
  145. package/dist/web/smoke-web-history.js +179 -0
  146. package/dist/web/smoke-web-history.js.map +1 -0
  147. package/dist/web/smoke-web-terminal-tasks.js +35 -3
  148. package/dist/web/smoke-web-terminal-tasks.js.map +1 -1
  149. package/package.json +4 -1
  150. package/dist/tasks/task-tools.d.ts +0 -31
  151. package/dist/tasks/task-tools.js.map +0 -1
@@ -1,17 +1,18 @@
1
- import { createTextMessage } from "../types/messages.js";
1
+ import { createTextMessage, withoutThinkingBlocks } from "../types/messages.js";
2
2
  import { estimateTextTokens } from "../core/context-metrics.js";
3
3
  import { buildImageRegistry, extractRegistryFromBoundary, mergeImageRegistries, formatImageRegistryForContext } from "../core/image-registry.js";
4
4
  import { resolveImageBlockDataLengthSync } from "../core/image-storage.js";
5
5
  export const CLEARED_TOOL_RESULT_CONTENT = "[Old tool result content cleared]";
6
+ const COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000;
6
7
  export class DeterministicCompactor {
7
8
  async compact(messages, options = {}) {
8
9
  const micro = microCompactIfNeeded(messages, options);
9
- const auto = autoCompactIfNeeded(micro.messages, options);
10
- const snipped = snipCompactIfNeeded(auto.messages, options);
11
- if (snipped.changed)
12
- return mergeResults([micro, auto, snipped], snipped.reason);
10
+ const auto = autoCompactIfNeeded(micro.messages, options, messages);
13
11
  if (auto.changed)
14
- return mergeResults([micro, auto], auto.reason);
12
+ return auto;
13
+ const snipped = snipCompactIfNeeded(micro.messages, options);
14
+ if (snipped.changed)
15
+ return mergeResults([micro, snipped], snipped.reason);
15
16
  if (micro.changed)
16
17
  return micro;
17
18
  return { messages: [...messages], changed: false, reason: "none" };
@@ -24,87 +25,53 @@ export class DeterministicCompactor {
24
25
  }
25
26
  async reactiveCompact(messages, error, options = {}) {
26
27
  const micro = microCompactIfNeeded(messages, options);
27
- const reactive = reactiveCompactWithSummary(micro.messages, `Reactive compact after model context error: ${error.message}`, options);
28
- return micro.changed ? mergeResults([micro, reactive], reactive.reason) : reactive;
28
+ const reactive = reactiveCompactWithSummary(messages, micro.messages, `Reactive compact after model context error: ${error.message}`, options);
29
+ return reactive;
29
30
  }
30
31
  }
31
32
  export class ModelDrivenCompactor {
32
33
  modelGateway;
33
- fallback = new DeterministicCompactor();
34
34
  constructor(modelGateway) {
35
35
  this.modelGateway = modelGateway;
36
36
  }
37
37
  async compact(messages, options = {}) {
38
- const micro = microCompactIfNeeded(messages, options);
39
- const auto = await this.modelAutoCompactIfNeeded(micro.messages, options);
40
- const snipped = snipCompactIfNeeded(auto.messages, options);
41
- if (snipped.changed)
42
- return mergeResults([micro, auto, snipped], snipped.reason);
43
- if (auto.changed)
44
- return mergeResults([micro, auto], auto.reason);
45
- if (micro.changed)
46
- return micro;
47
- return { messages: [...messages], changed: false, reason: "none" };
48
- }
49
- async manualCompact(messages, options = {}) {
50
- const splitIndex = computeKeepRecentSplit(messages, options);
51
- const recent = messages.slice(splitIndex);
52
- const older = messages.slice(0, splitIndex);
53
- if (older.length === 0)
38
+ if (!shouldCompactForBudget(messages, options, options.autoCompactMaxChars)) {
54
39
  return { messages: [...messages], changed: false, reason: "none" };
55
- try {
56
- const summary = await this.summarizeWithModel(older, MANUAL_COMPACT_INSTRUCTIONS, options);
57
- return buildCompactionResult(messages, recent, summary, "manualcompact", true);
58
- }
59
- catch {
60
- return this.fallback.manualCompact?.(messages, options) ?? manualCompactWithSummary(messages, options);
61
40
  }
41
+ return this.createCheckpoint(messages, "autocompact", options);
42
+ }
43
+ async manualCompact(messages, options = {}) {
44
+ return this.createCheckpoint(messages, "manualcompact", options);
62
45
  }
63
46
  async pureCompact(messages, options = {}) {
64
47
  return pureCompactWithSanitizedSummary(messages, options);
65
48
  }
66
- async reactiveCompact(messages, error, options = {}) {
67
- const micro = microCompactIfNeeded(messages, options);
68
- const splitIndex = computeKeepRecentSplit(micro.messages, options);
69
- const recent = micro.messages.slice(splitIndex);
70
- const older = micro.messages.slice(0, splitIndex);
71
- try {
72
- const summary = await this.summarizeWithModel(older, buildReactiveCompactInstruction(error), options);
73
- const reactive = buildCompactionResult(micro.messages, recent, summary, "reactive_compact", true);
74
- return micro.changed ? mergeResults([micro, reactive], reactive.reason) : reactive;
75
- }
76
- catch {
77
- const fallback = await this.fallback.reactiveCompact(micro.messages, error, options);
78
- return micro.changed ? mergeResults([micro, fallback], fallback.reason) : fallback;
79
- }
49
+ async reactiveCompact(messages, _error, options = {}) {
50
+ return this.createCheckpoint(messages, "reactive_compact", options);
80
51
  }
81
- async modelAutoCompactIfNeeded(messages, options) {
82
- if (!shouldCompactForBudget(messages, options, options.autoCompactMaxChars ?? 50000)) {
83
- return { messages: [...messages], changed: false, reason: "none" };
84
- }
85
- const splitIndex = computeKeepRecentSplit(messages, options);
86
- const recent = messages.slice(splitIndex);
87
- const older = messages.slice(0, splitIndex);
88
- if (older.length === 0)
89
- return { messages: [...messages], changed: false, reason: "none" };
52
+ async createCheckpoint(messages, reason, options) {
53
+ if (messages.length === 0)
54
+ return { messages: [], changed: false, reason: "none" };
90
55
  try {
91
- const summary = await this.summarizeWithModel(older, AUTO_COMPACT_INSTRUCTIONS, options);
92
- return buildCompactionResult(messages, recent, summary, "autocompact", true);
56
+ const summary = await this.summarizeWithModel(messages, CHECKPOINT_COMPACT_INSTRUCTIONS, options);
57
+ return buildCompactionResult(messages, summary, reason, true, options);
93
58
  }
94
59
  catch {
95
- return autoCompactIfNeeded(messages, options);
60
+ return checkpointCompactWithSummary(messages, options, reason);
96
61
  }
97
62
  }
98
63
  async summarizeWithModel(messages, instructions, options) {
99
- const transcript = serializeTranscriptForSummary(messages, options.summaryMaxChars ?? 12000);
100
- if (!transcript.trim())
64
+ const compactMessages = prepareMessagesForModelSummary(messages, instructions);
65
+ if (compactMessages.length === 1)
101
66
  return "No earlier conversation content required preservation.";
102
- let text = "";
67
+ let streamedText = "";
68
+ let finalAssistantText = "";
69
+ let completed = false;
103
70
  for await (const event of this.modelGateway.stream({
104
71
  model: options.compactModel,
105
- instructions,
106
- messages: [createTextMessage("user", transcript)],
72
+ messages: compactMessages,
107
73
  tools: [],
74
+ toolChoice: "none",
108
75
  stream: true,
109
76
  maxOutputTokens: options.compactMaxOutputTokens ?? 2500,
110
77
  queryOrigin: "compact",
@@ -113,18 +80,22 @@ export class ModelDrivenCompactor {
113
80
  },
114
81
  })) {
115
82
  if (event.type === "assistant_delta")
116
- text += event.text;
83
+ streamedText += event.text;
117
84
  if (event.type === "assistant_message")
118
- text += extractText(event.message);
85
+ finalAssistantText = extractText(event.message).trim() || finalAssistantText;
86
+ if (event.type === "response_completed")
87
+ completed = true;
88
+ if (event.type === "response_incomplete")
89
+ throw new Error(`Model compaction response was incomplete: ${event.reason ?? "unknown reason"}`);
119
90
  if (event.type === "error")
120
91
  throw event.error;
121
92
  }
122
- const summary = text.trim();
93
+ if (!completed)
94
+ throw new Error("Model compaction stream ended before completion");
95
+ const summary = (finalAssistantText || streamedText).trim();
123
96
  if (!summary)
124
97
  throw new Error("Model compaction produced an empty summary");
125
- return summary.length > (options.summaryMaxChars ?? 6000)
126
- ? `${summary.slice(0, options.summaryMaxChars ?? 6000)}\n- ...model summary truncated...`
127
- : summary;
98
+ return summary;
128
99
  }
129
100
  }
130
101
  export class NoopCompactor {
@@ -160,6 +131,40 @@ export class ManualOnlyCompactor {
160
131
  return { messages: [...messages], changed: false, reason: "none" };
161
132
  }
162
133
  }
134
+ export function withCompactionReport(result, sourceMessages) {
135
+ if (!result.changed || result.report)
136
+ return result;
137
+ const reason = result.reason && result.reason !== "none" ? result.reason : "autocompact";
138
+ const boundary = findLastCompactBoundary(result.messages);
139
+ const boundaryText = boundary ? extractText(boundary) : "";
140
+ const continuationState = extractCompactState(boundaryText) || result.summary || "";
141
+ const imageRegistry = boundary?.metadata?.imageRegistry;
142
+ const report = {
143
+ reason,
144
+ summary: result.summary || continuationState,
145
+ continuationState,
146
+ sourceMessages: Math.max(0, Math.round(sourceMessages)),
147
+ preservedUserMessages: result.messages.filter((message) => message.metadata?.compactPreservedUser === true).length,
148
+ newWindowMessages: result.messages.length,
149
+ charsFreed: Math.max(0, result.charsFreed ?? result.tokensFreed ?? 0),
150
+ modelDriven: boundary?.metadata?.modelDriven === true,
151
+ imageCount: Array.isArray(imageRegistry?.images) ? imageRegistry.images.length : 0,
152
+ };
153
+ return {
154
+ ...result,
155
+ messages: result.messages.map((message) => message === boundary
156
+ ? { ...message, metadata: { ...message.metadata, compactionReport: report } }
157
+ : message),
158
+ report,
159
+ };
160
+ }
161
+ function findLastCompactBoundary(messages) {
162
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
163
+ if (messages[index].metadata?.compactBoundary === true)
164
+ return messages[index];
165
+ }
166
+ return undefined;
167
+ }
163
168
  export function estimateMessagesChars(messages) {
164
169
  return messages.reduce((total, message) => total + serializeMessage(message).length, 0);
165
170
  }
@@ -234,20 +239,17 @@ export function microCompactIfNeeded(messages, options = {}) {
234
239
  tokensFreed: freed,
235
240
  };
236
241
  }
237
- export function autoCompactIfNeeded(messages, options = {}) {
242
+ export function autoCompactIfNeeded(messages, options = {}, sourceMessages = messages) {
238
243
  const maxChars = options.autoCompactMaxChars ?? 50000;
239
244
  if (!shouldCompactForBudget(messages, options, maxChars))
240
245
  return { messages: [...messages], changed: false, reason: "none" };
241
- const splitIndex = computeKeepRecentSplit(messages, options);
242
- const recent = messages.slice(splitIndex);
243
- const older = messages.slice(0, splitIndex);
244
- if (older.length === 0)
245
- return { messages: [...messages], changed: false, reason: "none" };
246
- const summary = buildHistorySummary(older, options.summaryMaxChars ?? 5000);
247
- return buildCompactionResult(messages, recent, summary, "autocompact", false);
246
+ if (sourceMessages.length === 0)
247
+ return { messages: [], changed: false, reason: "none" };
248
+ const summary = buildCheckpointFallbackSummary(sourceMessages, options.summaryMaxChars ?? 5000);
249
+ return buildCompactionResult(sourceMessages, summary || "No conversation content was available to summarize.", "autocompact", false, options, messages);
248
250
  }
249
251
  function computeKeepRecentSplit(messages, options, fallbackCount) {
250
- const tokenBudget = options.keepRecentTokenBudget;
252
+ const tokenBudget = options.keepRecentTokenBudget ?? defaultRecentTokenBudget(options.contextWindowTokens);
251
253
  const keepCount = fallbackCount ?? options.keepRecentMessages ?? 8;
252
254
  if (!tokenBudget)
253
255
  return Math.max(0, messages.length - keepCount);
@@ -265,23 +267,20 @@ function shouldCompactForBudget(messages, options, fallbackMaxChars, triggerRati
265
267
  const triggerRatio = triggerRatioOverride ?? options.autoCompactTriggerRatio ?? 0.92;
266
268
  return options.estimatedInputTokens / options.contextWindowTokens >= triggerRatio;
267
269
  }
268
- return estimateMessagesChars(messages) > fallbackMaxChars;
270
+ return fallbackMaxChars !== undefined && estimateMessagesChars(messages) > fallbackMaxChars;
269
271
  }
270
272
  function manualCompactWithSummary(messages, options) {
271
- const splitIndex = computeKeepRecentSplit(messages, options);
272
- const recent = messages.slice(splitIndex);
273
- const older = messages.slice(0, splitIndex);
274
- if (older.length === 0)
275
- return { messages: [...messages], changed: false, reason: "none" };
276
- const summary = buildHistorySummary(older, options.summaryMaxChars ?? 6000);
277
- return buildCompactionResult(messages, recent, summary || "No older messages were available to summarize.", "manualcompact", false);
273
+ return checkpointCompactWithSummary(messages, options, "manualcompact");
274
+ }
275
+ function checkpointCompactWithSummary(messages, options, reason) {
276
+ if (messages.length === 0)
277
+ return { messages: [], changed: false, reason: "none" };
278
+ const summary = buildCheckpointFallbackSummary(messages, options.summaryMaxChars ?? 6000);
279
+ return buildCompactionResult(messages, summary || "No conversation content was available to summarize.", reason, false, options);
278
280
  }
279
- function reactiveCompactWithSummary(messages, heading, options) {
280
- const splitIndex = computeKeepRecentSplit(messages, options);
281
- const recent = messages.slice(splitIndex);
282
- const older = messages.slice(0, splitIndex);
283
- const summary = buildHistorySummary(older, options.summaryMaxChars ?? 6000);
284
- return buildCompactionResult(messages, recent, `${heading}\n\n${summary || "No older messages were available to summarize."}`, "reactive_compact", false);
281
+ function reactiveCompactWithSummary(sourceMessages, messagesForSelection, heading, options) {
282
+ const summary = buildCheckpointFallbackSummary(sourceMessages, options.summaryMaxChars ?? 6000);
283
+ return buildCompactionResult(sourceMessages, `${heading}\n\n${summary || "No conversation content was available to summarize."}`, "reactive_compact", false, options, messagesForSelection);
285
284
  }
286
285
  function pureCompactWithSanitizedSummary(messages, options) {
287
286
  if (messages.length === 0)
@@ -299,68 +298,111 @@ function pureCompactWithSanitizedSummary(messages, options) {
299
298
  tokensFreed: freed,
300
299
  };
301
300
  }
302
- function extractPersistentFacts(messages) {
303
- const facts = [];
304
- for (const message of messages) {
305
- if (message.metadata?.compactBoundary) {
306
- const text = extractText(message);
307
- const existingFacts = text.match(/(?<=Persistent facts:\n)([\s\S]*?)(?=\n(?:Working context:|<\/compact_state>))/);
308
- if (existingFacts) {
309
- for (const line of existingFacts[1].split("\n")) {
310
- const trimmed = line.trim();
311
- if (trimmed && trimmed !== "- ...summary truncated...")
312
- facts.push(trimmed);
313
- }
314
- }
315
- continue;
316
- }
317
- for (const block of message.blocks) {
318
- if (block.type === "text") {
319
- const text = block.text;
320
- if (message.role === "user") {
321
- if (/\b(must|require|constraint|never|always|important|rule)\b/i.test(text)) {
322
- facts.push(`- User constraint: ${text.replace(/\s+/g, " ").trim().slice(0, 200)}`);
323
- }
324
- }
325
- if (/\b(error|fail|exception|bug)\b/i.test(text) && message.role !== "progress") {
326
- const errorLine = text.split("\n").find((l) => /error|fail|exception|bug/i.test(l));
327
- if (errorLine)
328
- facts.push(`- Error encountered: ${errorLine.trim().slice(0, 200)}`);
329
- }
330
- if (/\b(decide|chosen|approach|architecture|design|agreed)\b/i.test(text) && message.role === "assistant") {
331
- facts.push(`- Decision: ${text.replace(/\s+/g, " ").trim().slice(0, 200)}`);
332
- }
333
- }
334
- if (block.type === "tool_result" && !block.ok) {
335
- facts.push(`- Failed tool: ${block.name} (${serializeToolOutput(block.output).slice(0, 150)})`);
336
- }
337
- }
338
- }
339
- return dedupeLines(facts).slice(0, 30);
340
- }
341
- function buildCompactionResult(originalMessages, recentMessages, summary, reason, modelDriven) {
301
+ function buildCompactionResult(originalMessages, summary, reason, modelDriven, options, messagesForSelection = originalMessages) {
342
302
  const label = reason === "autocompact"
343
303
  ? "Auto compacted earlier conversation."
344
304
  : reason === "manualcompact"
345
305
  ? "Manually compacted earlier conversation."
346
306
  : "Reactive compacted earlier conversation.";
347
- const persistentFacts = extractPersistentFacts(originalMessages);
348
- const layeredSummary = persistentFacts.length > 0
349
- ? `Persistent facts:\n${persistentFacts.join("\n")}\n\nWorking context:\n${summary}`
350
- : summary;
351
307
  const imageRegistry = buildMergedImageRegistry(originalMessages);
352
- const boundary = createCompactionBoundaryMessage(`${label}\n\n${layeredSummary}`, reason, modelDriven, imageRegistry);
353
- const compacted = [boundary, ...recentMessages];
308
+ const preservedUsers = selectRecentUserMessages(messagesForSelection, options.keepRecentTokenBudget ?? COMPACT_USER_MESSAGE_MAX_TOKENS);
309
+ const boundary = createCompactionBoundaryMessage(`${label}\n\n${summary}`, reason, modelDriven, imageRegistry);
310
+ const compacted = [...preservedUsers, boundary];
354
311
  const freed = Math.max(0, estimateMessagesChars(originalMessages) - estimateMessagesChars(compacted));
355
312
  return {
356
313
  messages: compacted,
357
- summary: layeredSummary,
314
+ summary,
358
315
  changed: true,
359
316
  reason,
360
317
  charsFreed: freed,
361
318
  tokensFreed: freed,
362
319
  };
363
320
  }
321
+ function selectRecentUserMessages(messages, tokenBudget) {
322
+ if (tokenBudget <= 0)
323
+ return [];
324
+ const users = messages.filter(isRealUserMessage);
325
+ const selected = [];
326
+ let remaining = tokenBudget;
327
+ for (let index = users.length - 1; index >= 0 && remaining > 0; index -= 1) {
328
+ const message = users[index];
329
+ const tokens = estimateMessageTokens(message);
330
+ if (tokens <= remaining) {
331
+ selected.push(markPreservedUserMessage(message));
332
+ remaining -= tokens;
333
+ continue;
334
+ }
335
+ const truncated = truncateUserMessageToTokenBudget(message, remaining);
336
+ if (truncated)
337
+ selected.push(markPreservedUserMessage(truncated));
338
+ break;
339
+ }
340
+ return selected.reverse();
341
+ }
342
+ function isRealUserMessage(message) {
343
+ return message.role === "user" && message.isMeta !== true && message.metadata?.compactBoundary !== true;
344
+ }
345
+ function estimateMessageTokens(message) {
346
+ return estimateTextTokens(serializeMessage(message));
347
+ }
348
+ function markPreservedUserMessage(message) {
349
+ return {
350
+ ...message,
351
+ blocks: message.blocks.map((block) => ({ ...block })),
352
+ metadata: { ...message.metadata, compactPreservedUser: true },
353
+ };
354
+ }
355
+ function truncateUserMessageToTokenBudget(message, maxTokens) {
356
+ if (maxTokens <= 0)
357
+ return undefined;
358
+ const marker = "[... user message truncated for compaction token budget ...]";
359
+ const textLength = message.blocks.reduce((total, block) => total + (block.type === "text" ? block.text.length : 0), 0);
360
+ if (textLength === 0)
361
+ return undefined;
362
+ let low = 0;
363
+ let high = textLength;
364
+ let best;
365
+ while (low <= high) {
366
+ const mid = Math.floor((low + high) / 2);
367
+ const blocks = truncateUserBlocks(message.blocks, mid, marker);
368
+ if (estimateTextTokens(blocks.map(serializeBlock).join("\n")) <= maxTokens) {
369
+ best = blocks;
370
+ low = mid + 1;
371
+ }
372
+ else {
373
+ high = mid - 1;
374
+ }
375
+ }
376
+ return best ? { ...message, blocks: best } : undefined;
377
+ }
378
+ function truncateUserBlocks(blocks, textChars, marker) {
379
+ const result = [];
380
+ let remaining = textChars;
381
+ let markerInserted = false;
382
+ for (const block of blocks) {
383
+ if (block.type !== "text") {
384
+ result.push({ ...block });
385
+ continue;
386
+ }
387
+ if (remaining <= 0)
388
+ continue;
389
+ const kept = block.text.slice(0, remaining);
390
+ remaining -= kept.length;
391
+ if (kept)
392
+ result.push({ type: "text", text: kept });
393
+ }
394
+ for (let index = result.length - 1; index >= 0; index -= 1) {
395
+ const block = result[index];
396
+ if (block.type !== "text")
397
+ continue;
398
+ result[index] = { type: "text", text: `${block.text.trimEnd()}\n${marker}` };
399
+ markerInserted = true;
400
+ break;
401
+ }
402
+ if (!markerInserted)
403
+ result.unshift({ type: "text", text: marker });
404
+ return result;
405
+ }
364
406
  function buildMergedImageRegistry(messages) {
365
407
  const previousRegistry = extractRegistryFromBoundary(messages);
366
408
  const currentRegistry = buildImageRegistry(messages);
@@ -405,6 +447,10 @@ function renderInternalContinuationState(summary, reason) {
405
447
  "</compact_state>",
406
448
  ].join("\n");
407
449
  }
450
+ function extractCompactState(text) {
451
+ const match = /<compact_state>\s*([\s\S]*?)\s*<\/compact_state>/i.exec(text);
452
+ return match?.[1]?.trim() ?? "";
453
+ }
408
454
  function normalizeSummaryForInternalState(summary) {
409
455
  return summary
410
456
  .split(/\r?\n/)
@@ -412,6 +458,141 @@ function normalizeSummaryForInternalState(summary) {
412
458
  .join("\n")
413
459
  .trim();
414
460
  }
461
+ function buildCheckpointFallbackSummary(messages, maxChars) {
462
+ const previousState = findLatestCheckpointState(messages);
463
+ const runtimeContext = messages
464
+ .filter((message) => message.isMeta === true && (message.metadata?.runtimeContext === true || message.metadata?.userContext === true || message.metadata?.systemContext === true))
465
+ .map(extractText)
466
+ .filter(Boolean)
467
+ .at(-1);
468
+ const userContext = messages
469
+ .filter(isRealUserMessage)
470
+ .map((message) => summarizeMessageText(message, 700))
471
+ .filter(Boolean)
472
+ .slice(-12);
473
+ const assistantProgress = messages
474
+ .filter((message) => message.role === "assistant" && message.isApiErrorMessage !== true)
475
+ .map((message) => summarizeMessageText(message, 700))
476
+ .filter(Boolean)
477
+ .slice(-10);
478
+ const references = collectContinuationReferences(messages).slice(0, 24);
479
+ const toolActivity = summarizeFallbackToolActivity(messages);
480
+ const sections = [
481
+ formatFallbackSection("User goals, constraints, and requests", userContext),
482
+ formatFallbackSection("Assistant progress and decisions", assistantProgress),
483
+ previousState ? `Previous checkpoint state:\n${truncateSummaryText(previousState, 2400)}` : "",
484
+ runtimeContext ? `Runtime context:\n${truncateSummaryText(runtimeContext, 900)}` : "",
485
+ references.length > 0 ? `Critical references:\n${references.map((reference) => `- ${reference}`).join("\n")}` : "",
486
+ toolActivity ? `Tool activity relevant to continuation:\n${toolActivity}` : "",
487
+ ].filter(Boolean);
488
+ const nextStep = "Next step: continue from the latest unresolved user request using the checkpoint state above; verify the current workspace before making further changes.";
489
+ return fitSummarySectionsWithFooter(sections, nextStep, maxChars);
490
+ }
491
+ function findLatestCheckpointState(messages) {
492
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
493
+ const message = messages[index];
494
+ if (message.metadata?.compactBoundary !== true)
495
+ continue;
496
+ return extractCompactState(extractText(message)) || extractText(message);
497
+ }
498
+ return "";
499
+ }
500
+ function summarizeMessageText(message, maxChars) {
501
+ const parts = message.blocks.flatMap((block) => {
502
+ if (block.type === "text")
503
+ return [block.text];
504
+ if (block.type === "image")
505
+ return [`[image: ${block.label ?? "unlabeled"}, ${block.mimeType}]`];
506
+ return [];
507
+ });
508
+ return truncateSummaryText(parts.join("\n").replace(/\s+/g, " ").trim(), maxChars);
509
+ }
510
+ function collectContinuationReferences(messages) {
511
+ const references = [];
512
+ const patterns = [
513
+ /[A-Za-z]:[\\/](?:[^\s"'`<>|]+[\\/]?)+/g,
514
+ /(?:^|\s)(\/(?:[^\s"'`<>]+\/?)+)/g,
515
+ /https?:\/\/[^\s"'`<>]+/g,
516
+ /\b[0-9a-f]{7,40}\b/gi,
517
+ /\b(?:task|session|request|response|artifact|turn|thread)[-_ ]?id\s*[:=]\s*[^\s,;]+/gi,
518
+ ];
519
+ for (const message of messages) {
520
+ for (const block of message.blocks) {
521
+ const text = block.type === "text"
522
+ ? block.text
523
+ : block.type === "tool_use"
524
+ ? serializeToolOutput(block.input)
525
+ : block.type === "tool_result"
526
+ ? serializeToolOutput(block.output)
527
+ : "";
528
+ for (const pattern of patterns) {
529
+ pattern.lastIndex = 0;
530
+ for (const match of text.matchAll(pattern)) {
531
+ const value = (match[1] ?? match[0]).trim().replace(/[),.;:]+$/, "");
532
+ if (value)
533
+ references.push(value);
534
+ }
535
+ }
536
+ }
537
+ }
538
+ return dedupeLines(references).slice(-24);
539
+ }
540
+ function summarizeFallbackToolActivity(messages) {
541
+ const successful = [];
542
+ const failedByName = new Map();
543
+ for (const message of messages) {
544
+ for (const block of message.blocks) {
545
+ if (block.type !== "tool_result")
546
+ continue;
547
+ if (block.ok) {
548
+ const output = serializeToolOutput(block.output).replace(/\s+/g, " ").trim();
549
+ if (output && output !== CLEARED_TOOL_RESULT_CONTENT) {
550
+ successful.push(`- ${block.name}: ${truncateSummaryText(output, 320)}`);
551
+ }
552
+ }
553
+ else {
554
+ failedByName.set(block.name, (failedByName.get(block.name) ?? 0) + 1);
555
+ }
556
+ }
557
+ }
558
+ const lines = successful.slice(-6);
559
+ if (failedByName.size > 0) {
560
+ const counts = [...failedByName.entries()].map(([name, count]) => `${name} ×${count}`).join(", ");
561
+ lines.push(`- Some tool attempts failed (${counts}); individual transient errors were omitted. Re-check only if still relevant.`);
562
+ }
563
+ return lines.join("\n");
564
+ }
565
+ function formatFallbackSection(title, items) {
566
+ if (items.length === 0)
567
+ return "";
568
+ return `${title}:\n${items.map((item) => `- ${item}`).join("\n")}`;
569
+ }
570
+ function truncateSummaryText(text, maxChars) {
571
+ if (text.length <= maxChars)
572
+ return text;
573
+ if (maxChars <= 1)
574
+ return "…".slice(0, maxChars);
575
+ return `${text.slice(0, maxChars - 1).trimEnd()}…`;
576
+ }
577
+ function fitSummarySectionsWithFooter(sections, footer, maxChars) {
578
+ if (maxChars <= 0)
579
+ return "";
580
+ if (maxChars <= footer.length)
581
+ return truncateSummaryText(footer, maxChars);
582
+ const footerWithSeparator = `\n\n${footer}`;
583
+ const bodyBudget = maxChars - footerWithSeparator.length;
584
+ let body = "";
585
+ for (const section of sections) {
586
+ const separator = body ? "\n\n" : "";
587
+ const remaining = bodyBudget - body.length - separator.length;
588
+ if (remaining <= 0)
589
+ break;
590
+ body += separator + truncateSummaryText(section, remaining);
591
+ if (section.length > remaining)
592
+ break;
593
+ }
594
+ return body ? `${body}${footerWithSeparator}` : footer;
595
+ }
415
596
  function scoreMessageImportance(message) {
416
597
  let score = 1;
417
598
  if (message.metadata?.compactBoundary)
@@ -505,7 +686,7 @@ function buildSmartMessageSummary(message) {
505
686
  else if (block.type === "image") {
506
687
  const label = block.label ? `"${block.label}"` : "unlabeled";
507
688
  const stored = block.storage?.path ? ", stored payload available via image registry" : "";
508
- parts.push(`[image omitted from text compaction: ${label}, ${block.mimeType}${stored}; use load_image by id/label for visual inspection]`);
689
+ parts.push(`[image omitted from text compaction: ${label}, ${block.mimeType}${stored}; use image_inspect by id/label for visual inspection]`);
509
690
  }
510
691
  else if (block.type === "tool_result") {
511
692
  parts.push(extractStructuredToolResult(block));
@@ -618,38 +799,58 @@ function summarizePathForPureState(value) {
618
799
  const tail = parts.slice(Math.max(0, parts.length - 3)).join("/");
619
800
  return tail ? `[path:${tail}]` : "[path]";
620
801
  }
621
- function serializeTranscriptForSummary(messages, maxChars) {
622
- const lines = messages.flatMap((message) => {
623
- const content = serializeMessageForSummary(message).trim();
624
- if (!content)
625
- return [];
626
- const metadata = [
627
- message.providerMessageId ? `provider=${message.providerMessageId}` : undefined,
628
- message.requestId ? `request=${message.requestId}` : undefined,
629
- message.isMeta ? "meta" : undefined,
630
- ].filter(Boolean).join(" ");
631
- return [`<message role="${message.role}"${metadata ? ` ${metadata}` : ""}>\n${content}\n</message>`];
802
+ function prepareMessagesForModelSummary(messages, instructions) {
803
+ const history = withoutThinkingBlocks(messages)
804
+ .filter((message) => message.role !== "progress" && message.role !== "tombstone")
805
+ .map(normalizeMessageForModelSummary);
806
+ const compactInstruction = {
807
+ ...createTextMessage("user", instructions),
808
+ isMeta: true,
809
+ metadata: { compactInstruction: true },
810
+ };
811
+ return [...history, compactInstruction];
812
+ }
813
+ function normalizeMessageForModelSummary(message) {
814
+ let changed = false;
815
+ const blocks = message.blocks.map((block) => {
816
+ if (block.type === "image") {
817
+ changed = true;
818
+ const storage = block.storage?.path ? `; stored payload=${block.storage.path}` : "";
819
+ return {
820
+ type: "text",
821
+ text: `[Historical image: ${block.label ?? "unlabeled"}, ${block.mimeType}${storage}. Pixels are omitted from compaction input; preserve the image reference if it may still matter.]`,
822
+ };
823
+ }
824
+ if (block.type === "tool_result") {
825
+ const output = serializeToolOutput(block.output);
826
+ const normalizedOutput = summarizeToolResultForModelSummary(output);
827
+ if (normalizedOutput !== output) {
828
+ changed = true;
829
+ return { ...block, output: normalizedOutput };
830
+ }
831
+ }
832
+ return { ...block };
632
833
  });
633
- const joined = lines.join("\n\n");
634
- return joined.length > maxChars ? joined.slice(Math.max(0, joined.length - maxChars)) : joined;
834
+ return changed
835
+ ? { ...message, blocks, metadata: { ...message.metadata, compactInputNormalized: true } }
836
+ : { ...message, blocks };
837
+ }
838
+ function summarizeToolResultForModelSummary(output) {
839
+ const maxChars = 12_000;
840
+ if (output.length <= maxChars)
841
+ return output;
842
+ const edgeChars = Math.floor((maxChars - 160) / 2);
843
+ return [
844
+ `[Tool result normalized for compaction: ${output.length} chars total]`,
845
+ "Head:",
846
+ output.slice(0, edgeChars),
847
+ "Tail:",
848
+ output.slice(-edgeChars),
849
+ ].join("\n");
635
850
  }
636
851
  function serializeMessage(message) {
637
852
  return message.blocks.map(serializeBlock).join("\n");
638
853
  }
639
- function serializeMessageForSummary(message) {
640
- if (message.metadata?.compactBoundary === true)
641
- return extractText(message) || "[compact boundary]";
642
- return message.blocks
643
- .map((block) => {
644
- if (block.type !== "tool_result")
645
- return serializeBlock(block);
646
- const output = serializeToolOutput(block.output);
647
- if (output === CLEARED_TOOL_RESULT_CONTENT)
648
- return `tool_result ${block.name}: ${CLEARED_TOOL_RESULT_CONTENT}`;
649
- return `tool_result ${block.name}: ${summarizeToolOutput(output)}`;
650
- })
651
- .join("\n");
652
- }
653
854
  function serializeBlock(block) {
654
855
  if (block.type === "text")
655
856
  return block.text;
@@ -670,11 +871,6 @@ function serializeBlock(block) {
670
871
  function serializeToolOutput(output) {
671
872
  return typeof output === "string" ? output : JSON.stringify(output);
672
873
  }
673
- function summarizeToolOutput(output) {
674
- if (output.length <= 800)
675
- return output;
676
- return `[large tool result: ${output.length} chars, head: ${output.slice(0, 400)}]`;
677
- }
678
874
  function collectToolResultIds(messages) {
679
875
  const ids = [];
680
876
  for (const message of messages) {
@@ -691,21 +887,16 @@ function extractText(message) {
691
887
  .map((block) => block.text)
692
888
  .join("\n");
693
889
  }
694
- function buildReactiveCompactInstruction(error) {
695
- return `${AUTO_COMPACT_INSTRUCTIONS}\n\nThe previous model request failed because the prompt was too long. Preserve enough task state to continue after this error. Error: ${error.message}`;
696
- }
697
- const MANUAL_COMPACT_INSTRUCTIONS = [
698
- "Summarize the earlier agent conversation for continuation after a user-requested context compaction.",
699
- "Preserve: user goals and constraints, decisions made, files or commands mentioned, completed work, pending work, task ids, important tool results, and any errors or blockers.",
700
- "Drop: repetitive logs, transient progress chatter, and irrelevant wording.",
701
- "Return concise plain text labels, not Markdown headings. Use labels like Goal:, Constraints:, Work completed:, Important facts:, Pending work:, Open risks:.",
702
- "Do not include final-answer prose; this summary is an internal continuation state only.",
703
- ].join("\n");
704
- const AUTO_COMPACT_INSTRUCTIONS = [
705
- "Summarize the earlier agent conversation for continuation after context compaction.",
706
- "Preserve: user goals and constraints, decisions made, files or commands mentioned, completed work, pending work, task ids, important tool results, and any errors or blockers.",
707
- "Drop: repetitive logs, transient progress chatter, and irrelevant wording.",
708
- "Return concise plain text labels, not Markdown headings. Use labels like Goal:, Constraints:, Work completed:, Important facts:, Pending work:, Open risks:.",
709
- "Do not include final-answer prose; this summary is an internal continuation state only.",
890
+ function defaultRecentTokenBudget(contextWindowTokens) {
891
+ if (!contextWindowTokens)
892
+ return undefined;
893
+ return Math.max(4_000, Math.min(20_000, Math.floor(contextWindowTokens * 0.15)));
894
+ }
895
+ const CHECKPOINT_COMPACT_INSTRUCTIONS = [
896
+ "You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task without access to the earlier transcript.",
897
+ "Include the current goal, progress, and key decisions; important context, constraints, and user preferences; completed work and validation status; what remains to be done with clear next steps; and any critical files, paths, identifiers, commands, data, examples, or references needed to continue.",
898
+ "Use task continuity and current relevance to decide what to preserve. Report tool activity through durable findings and current consequences, not as a chronological call log. Include errors only when they remain relevant as blockers, risks, unresolved failures, or important lessons.",
899
+ "Treat any earlier compaction summary as prior handoff state. Merge it with later developments, preserve still-valid constraints, and replace stale or superseded status with the newest known state instead of repeating both.",
900
+ "Be concise, structured, and specific enough for the next LLM to continue immediately. Use the conversation's primary language when practical. Do not invent facts, do not continue the task, and do not write a user-facing final answer.",
710
901
  ].join("\n");
711
902
  //# sourceMappingURL=compaction.js.map