@bubblebrain-ai/bubble 0.0.6 → 0.0.8

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 (85) hide show
  1. package/dist/agent/execution-governor.d.ts +5 -13
  2. package/dist/agent/execution-governor.js +33 -142
  3. package/dist/agent.d.ts +6 -0
  4. package/dist/agent.js +36 -3
  5. package/dist/context/budget.d.ts +1 -0
  6. package/dist/context/budget.js +1 -1
  7. package/dist/context/usage.d.ts +34 -0
  8. package/dist/context/usage.js +213 -0
  9. package/dist/diff-stats.d.ts +5 -0
  10. package/dist/diff-stats.js +21 -0
  11. package/dist/main.js +83 -44
  12. package/dist/mcp/transports.d.ts +1 -0
  13. package/dist/mcp/transports.js +8 -0
  14. package/dist/model-catalog.js +1 -1
  15. package/dist/orchestrator/default-hooks.js +9 -33
  16. package/dist/prompt/compose.js +2 -1
  17. package/dist/prompt/provider-prompts/kimi.js +3 -1
  18. package/dist/prompt/reminders.d.ts +2 -1
  19. package/dist/prompt/reminders.js +4 -3
  20. package/dist/provider-registry.js +3 -3
  21. package/dist/provider-transform.d.ts +3 -1
  22. package/dist/provider-transform.js +15 -0
  23. package/dist/provider.d.ts +4 -1
  24. package/dist/provider.js +89 -4
  25. package/dist/reasoning-debug.d.ts +7 -0
  26. package/dist/reasoning-debug.js +30 -0
  27. package/dist/session-log.js +13 -2
  28. package/dist/session-types.d.ts +1 -1
  29. package/dist/slash-commands/commands.js +36 -19
  30. package/dist/tools/edit.js +5 -0
  31. package/dist/tools/file-state.d.ts +19 -0
  32. package/dist/tools/file-state.js +15 -0
  33. package/dist/tools/read.d.ts +1 -1
  34. package/dist/tools/read.js +92 -11
  35. package/dist/tui/escape-confirmation.d.ts +15 -0
  36. package/dist/tui/escape-confirmation.js +30 -0
  37. package/dist/tui/run.js +93 -23
  38. package/dist/tui-ink/app.d.ts +43 -0
  39. package/dist/tui-ink/app.js +1016 -0
  40. package/dist/tui-ink/approval/approval-dialog.d.ts +13 -0
  41. package/dist/tui-ink/approval/approval-dialog.js +129 -0
  42. package/dist/tui-ink/approval/diff-view.d.ts +7 -0
  43. package/dist/tui-ink/approval/diff-view.js +43 -0
  44. package/dist/tui-ink/approval/select.d.ts +35 -0
  45. package/dist/tui-ink/approval/select.js +87 -0
  46. package/dist/tui-ink/code-highlight.d.ts +6 -0
  47. package/dist/tui-ink/code-highlight.js +94 -0
  48. package/dist/tui-ink/display-history.d.ts +38 -0
  49. package/dist/tui-ink/display-history.js +130 -0
  50. package/dist/tui-ink/edit-diff.d.ts +11 -0
  51. package/dist/tui-ink/edit-diff.js +52 -0
  52. package/dist/tui-ink/file-mentions.d.ts +29 -0
  53. package/dist/tui-ink/file-mentions.js +174 -0
  54. package/dist/tui-ink/footer.d.ts +19 -0
  55. package/dist/tui-ink/footer.js +44 -0
  56. package/dist/tui-ink/image-paste.d.ts +54 -0
  57. package/dist/tui-ink/image-paste.js +288 -0
  58. package/dist/tui-ink/input-box.d.ts +41 -0
  59. package/dist/tui-ink/input-box.js +637 -0
  60. package/dist/tui-ink/markdown.d.ts +38 -0
  61. package/dist/tui-ink/markdown.js +384 -0
  62. package/dist/tui-ink/message-list.d.ts +33 -0
  63. package/dist/tui-ink/message-list.js +571 -0
  64. package/dist/tui-ink/model-picker.d.ts +43 -0
  65. package/dist/tui-ink/model-picker.js +326 -0
  66. package/dist/tui-ink/plan-confirm.d.ts +7 -0
  67. package/dist/tui-ink/plan-confirm.js +104 -0
  68. package/dist/tui-ink/question-dialog.d.ts +8 -0
  69. package/dist/tui-ink/question-dialog.js +98 -0
  70. package/dist/tui-ink/recent-activity.d.ts +8 -0
  71. package/dist/tui-ink/recent-activity.js +71 -0
  72. package/dist/tui-ink/run.d.ts +33 -0
  73. package/dist/tui-ink/run.js +25 -0
  74. package/dist/tui-ink/theme.d.ts +37 -0
  75. package/dist/tui-ink/theme.js +42 -0
  76. package/dist/tui-ink/todos.d.ts +7 -0
  77. package/dist/tui-ink/todos.js +44 -0
  78. package/dist/tui-ink/trace-groups.d.ts +25 -0
  79. package/dist/tui-ink/trace-groups.js +310 -0
  80. package/dist/tui-ink/use-terminal-size.d.ts +4 -0
  81. package/dist/tui-ink/use-terminal-size.js +21 -0
  82. package/dist/tui-ink/welcome.d.ts +18 -0
  83. package/dist/tui-ink/welcome.js +119 -0
  84. package/dist/types.d.ts +4 -0
  85. package/package.json +6 -1
@@ -3,21 +3,16 @@ import type { TaskType } from "./task-classifier.js";
3
3
  export interface GovernorDecision {
4
4
  blockedResult?: ToolResult;
5
5
  }
6
- type WorkPhase = "explore" | "modify" | "verify";
7
6
  export declare class ExecutionGovernor {
8
- private taskType;
9
7
  private budget;
10
8
  private history;
11
9
  private totalSteps;
12
10
  private searchSteps;
13
11
  private readSteps;
14
- private explorationStepsWithoutWrite;
15
- private searchFrozen;
16
- private explorationFrozen;
17
- private phase;
18
- private codeChanged;
12
+ private mutationVersion;
19
13
  private reminderQueue;
20
14
  private warnedFamilies;
15
+ private warnedSignatures;
21
16
  private softTotalWarned;
22
17
  private softSearchWarned;
23
18
  private softReadWarned;
@@ -29,16 +24,13 @@ export declare class ExecutionGovernor {
29
24
  readSteps: number;
30
25
  searchFrozen: boolean;
31
26
  explorationFrozen: boolean;
32
- phase: WorkPhase;
27
+ phase: "observe";
33
28
  };
34
29
  filterToolDefinitions(toolDefinitions: ToolRegistryEntry[]): ToolRegistryEntry[];
35
30
  beforeToolCall(toolCall: ParsedToolCall): GovernorDecision;
36
31
  afterToolResult(toolCall: ParsedToolCall, result: ToolResult): void;
37
32
  private trailingNoProgressCount;
38
- private freezeSearch;
39
- private enterModifyPhase;
40
- private isModificationTask;
41
- private historyCount;
33
+ private hasCurrentMutationObservation;
34
+ private warnOnce;
42
35
  private maybeWarnOnSoftBudgets;
43
36
  }
44
- export {};
@@ -1,118 +1,84 @@
1
1
  import { analyzeToolIntent } from "./tool-intent.js";
2
- import { buildExplorationFreezeReminder, buildInvestigationReminder, buildLoopWarningReminder, buildSearchFreezeReminder } from "../prompt/reminders.js";
2
+ import { buildInvestigationReminder, buildLoopWarningReminder } from "../prompt/reminders.js";
3
3
  const BUDGETS = {
4
4
  security_investigation: {
5
5
  softTotalSteps: 14,
6
6
  softSearchSteps: 6,
7
7
  softReadSteps: 8,
8
- maxExplorationStepsWithoutWrite: 12,
9
- maxNoProgressReadExactRepeats: 2,
10
- maxNoProgressExactRepeats: 2,
11
- maxNoProgressFamilyRepeats: 3,
8
+ warningExactRepeats: 2,
12
9
  warningFamilyRepeats: 2,
13
10
  },
14
11
  code_search: {
15
12
  softTotalSteps: 16,
16
13
  softSearchSteps: 8,
17
14
  softReadSteps: 10,
18
- maxExplorationStepsWithoutWrite: 14,
19
- maxNoProgressReadExactRepeats: 2,
20
- maxNoProgressExactRepeats: 3,
21
- maxNoProgressFamilyRepeats: 4,
15
+ warningExactRepeats: 2,
22
16
  warningFamilyRepeats: 3,
23
17
  },
24
18
  debugging: {
25
19
  softTotalSteps: 18,
26
20
  softSearchSteps: 8,
27
21
  softReadSteps: 7,
28
- maxExplorationStepsWithoutWrite: 10,
29
- maxNoProgressReadExactRepeats: 1,
30
- maxNoProgressExactRepeats: 3,
31
- maxNoProgressFamilyRepeats: 4,
22
+ warningExactRepeats: 1,
32
23
  warningFamilyRepeats: 3,
33
24
  },
34
25
  implementation: {
35
26
  softTotalSteps: 18,
36
27
  softSearchSteps: 8,
37
28
  softReadSteps: 6,
38
- maxExplorationStepsWithoutWrite: 8,
39
- maxNoProgressReadExactRepeats: 1,
40
- maxNoProgressExactRepeats: 3,
41
- maxNoProgressFamilyRepeats: 4,
29
+ warningExactRepeats: 1,
42
30
  warningFamilyRepeats: 3,
43
31
  },
44
32
  code_review: {
45
33
  softTotalSteps: 14,
46
34
  softSearchSteps: 6,
47
35
  softReadSteps: 8,
48
- maxExplorationStepsWithoutWrite: 12,
49
- maxNoProgressReadExactRepeats: 2,
50
- maxNoProgressExactRepeats: 3,
51
- maxNoProgressFamilyRepeats: 4,
36
+ warningExactRepeats: 2,
52
37
  warningFamilyRepeats: 3,
53
38
  },
54
39
  code_explanation: {
55
40
  softTotalSteps: 12,
56
41
  softSearchSteps: 6,
57
42
  softReadSteps: 8,
58
- maxExplorationStepsWithoutWrite: 12,
59
- maxNoProgressReadExactRepeats: 2,
60
- maxNoProgressExactRepeats: 3,
61
- maxNoProgressFamilyRepeats: 4,
43
+ warningExactRepeats: 2,
62
44
  warningFamilyRepeats: 3,
63
45
  },
64
46
  repo_orientation: {
65
47
  softTotalSteps: 12,
66
48
  softSearchSteps: 6,
67
49
  softReadSteps: 8,
68
- maxExplorationStepsWithoutWrite: 12,
69
- maxNoProgressReadExactRepeats: 2,
70
- maxNoProgressExactRepeats: 3,
71
- maxNoProgressFamilyRepeats: 4,
50
+ warningExactRepeats: 2,
72
51
  warningFamilyRepeats: 3,
73
52
  },
74
53
  product_discussion: {
75
54
  softTotalSteps: 10,
76
55
  softSearchSteps: 4,
77
56
  softReadSteps: 4,
78
- maxExplorationStepsWithoutWrite: 8,
79
- maxNoProgressReadExactRepeats: 2,
80
- maxNoProgressExactRepeats: 2,
81
- maxNoProgressFamilyRepeats: 3,
57
+ warningExactRepeats: 2,
82
58
  warningFamilyRepeats: 2,
83
59
  },
84
60
  general: {
85
61
  softTotalSteps: 18,
86
62
  softSearchSteps: 8,
87
63
  softReadSteps: 10,
88
- maxExplorationStepsWithoutWrite: 14,
89
- maxNoProgressReadExactRepeats: 2,
90
- maxNoProgressExactRepeats: 3,
91
- maxNoProgressFamilyRepeats: 4,
64
+ warningExactRepeats: 2,
92
65
  warningFamilyRepeats: 3,
93
66
  },
94
67
  };
95
- const SEARCH_TOOLS_DISABLED = new Set(["grep", "web_search", "web_fetch"]);
96
- const EXPLORATION_TOOLS_DISABLED = new Set(["read", "glob", "grep", "web_search", "web_fetch", "spawn_agent", "wait_agent", "send_input", "tool_search"]);
97
68
  export class ExecutionGovernor {
98
- taskType;
99
69
  budget;
100
70
  history = [];
101
71
  totalSteps = 0;
102
72
  searchSteps = 0;
103
73
  readSteps = 0;
104
- explorationStepsWithoutWrite = 0;
105
- searchFrozen = false;
106
- explorationFrozen = false;
107
- phase = "explore";
108
- codeChanged = false;
74
+ mutationVersion = 0;
109
75
  reminderQueue = [];
110
76
  warnedFamilies = new Set();
77
+ warnedSignatures = new Set();
111
78
  softTotalWarned = false;
112
79
  softSearchWarned = false;
113
80
  softReadWarned = false;
114
81
  constructor(taskType) {
115
- this.taskType = taskType;
116
82
  this.budget = BUDGETS[taskType];
117
83
  if (taskType === "security_investigation") {
118
84
  this.reminderQueue.push(buildInvestigationReminder());
@@ -128,62 +94,33 @@ export class ExecutionGovernor {
128
94
  totalSteps: this.totalSteps,
129
95
  searchSteps: this.searchSteps,
130
96
  readSteps: this.readSteps,
131
- searchFrozen: this.searchFrozen,
132
- explorationFrozen: this.explorationFrozen,
133
- phase: this.phase,
97
+ searchFrozen: false,
98
+ explorationFrozen: false,
99
+ phase: "observe",
134
100
  };
135
101
  }
136
102
  filterToolDefinitions(toolDefinitions) {
137
- let filtered = toolDefinitions;
138
- if (this.explorationFrozen) {
139
- filtered = filtered.filter((tool) => !EXPLORATION_TOOLS_DISABLED.has(tool.name));
140
- }
141
- else if (this.searchFrozen) {
142
- filtered = filtered.filter((tool) => !SEARCH_TOOLS_DISABLED.has(tool.name));
143
- }
144
- return filtered;
103
+ return toolDefinitions;
145
104
  }
146
105
  beforeToolCall(toolCall) {
147
106
  const intent = analyzeToolIntent(toolCall);
148
- if (this.explorationFrozen && isExplorationIntent(intent)) {
149
- return {
150
- blockedResult: blockedResult("Exploration blocked: this implementation task already has enough context. Use edit/write, verify an existing change, or explain the blocker.", "blocked", "Exploration frozen because tool calls stopped producing task progress.", metadataKindForFamily(intent.family)),
151
- };
152
- }
153
- if (this.isModificationTask() && !this.codeChanged && intent.family === "read") {
107
+ if (intent.family === "read") {
154
108
  const signature = intent.read?.signature;
155
- if (signature && this.historyCount((entry) => entry.signature === signature) >= this.budget.maxNoProgressReadExactRepeats) {
156
- this.enterModifyPhase(`Repeated the same file range without making progress: ${signature}`);
157
- return {
158
- blockedResult: blockedResult("Read blocked: this file range was already read. You have enough context to make the requested change; use edit/write now or explain the blocker.", "blocked", "Repeated identical read before modification.", "read"),
159
- };
109
+ if (signature && this.hasCurrentMutationObservation((entry) => entry.signature === signature)) {
110
+ this.warnOnce(`read:${signature}`, "This exact file range was already read since the last successful edit/write. If the content is still available and nothing changed, use the prior result; otherwise it is okay to re-read to recover context or verify a change.");
160
111
  }
161
112
  }
162
113
  if (intent.family === "search") {
163
- if (this.searchFrozen) {
164
- return {
165
- blockedResult: blockedResult("Search blocked: repeated low-yield searching is now frozen for this task.", "blocked", "Search frozen due to repeated low-yield searching.", "search"),
166
- };
167
- }
168
114
  const signature = intent.search?.signature;
169
115
  const familyKey = intent.search?.familyKey;
170
- if (signature && this.trailingNoProgressCount((entry) => entry.signature === signature) >= this.budget.maxNoProgressExactRepeats) {
171
- this.freezeSearch(`Repeated the same search signature without new evidence: ${signature}`);
172
- return {
173
- blockedResult: blockedResult("Search blocked: repeated the same search multiple times without new evidence.", "blocked", "Repeated identical search without progress.", "search"),
174
- };
116
+ if (signature && this.trailingNoProgressCount((entry) => entry.signature === signature) >= this.budget.warningExactRepeats) {
117
+ this.warnOnce(`search:${signature}`, "This search is very similar to one you already ran and it did not produce new evidence. Change the query/path, follow a concrete lead, or summarize the strongest findings.");
175
118
  }
176
119
  if (familyKey) {
177
120
  const familyNoProgress = this.trailingNoProgressCount((entry) => entry.familyKey === familyKey);
178
- if (familyNoProgress >= this.budget.maxNoProgressFamilyRepeats) {
179
- this.freezeSearch(`Repeated the same search family without new evidence: ${familyKey}`);
180
- return {
181
- blockedResult: blockedResult("Search blocked: repeated the same search family without new evidence.", "blocked", "Repeated similar searches without progress.", "search"),
182
- };
183
- }
184
121
  if (familyNoProgress >= this.budget.warningFamilyRepeats && !this.warnedFamilies.has(familyKey)) {
185
122
  this.warnedFamilies.add(familyKey);
186
- this.reminderQueue.push(buildLoopWarningReminder("Repeated searches are yielding little new evidence. Change your hypothesis, narrow the path, or summarize current findings instead of repeating variants."));
123
+ this.reminderQueue.push(buildLoopWarningReminder("Repeated searches in the same family are yielding little new evidence. Change your hypothesis, narrow the path, follow a specific file lead, or summarize current findings instead of repeating variants."));
187
124
  }
188
125
  }
189
126
  }
@@ -194,9 +131,6 @@ export class ExecutionGovernor {
194
131
  if (intent.family === "read") {
195
132
  this.readSteps += 1;
196
133
  }
197
- if (isExplorationIntent(intent) && !this.codeChanged) {
198
- this.explorationStepsWithoutWrite += 1;
199
- }
200
134
  this.maybeWarnOnSoftBudgets(intent.family === "search", intent.family === "read");
201
135
  return {};
202
136
  }
@@ -211,23 +145,19 @@ export class ExecutionGovernor {
211
145
  signature: intent.search?.signature ?? intent.read?.signature,
212
146
  familyKey: intent.search?.familyKey ?? intent.read?.familyKey,
213
147
  progress,
148
+ mutationVersion: this.mutationVersion,
214
149
  });
215
150
  if (isSuccessfulWriteIntent(intent, result)) {
216
- this.codeChanged = true;
217
- this.phase = "verify";
218
- return;
219
- }
220
- if (this.isModificationTask()
221
- && !this.codeChanged
222
- && isExplorationIntent(intent)
223
- && this.explorationStepsWithoutWrite >= this.budget.maxExplorationStepsWithoutWrite) {
224
- this.enterModifyPhase(`Used ${this.explorationStepsWithoutWrite} exploration tools without editing files.`);
151
+ this.mutationVersion += 1;
225
152
  }
226
153
  }
227
154
  trailingNoProgressCount(predicate) {
228
155
  let count = 0;
229
156
  for (let index = this.history.length - 1; index >= 0; index--) {
230
157
  const entry = this.history[index];
158
+ if (entry.mutationVersion !== this.mutationVersion) {
159
+ break;
160
+ }
231
161
  if (!predicate(entry)) {
232
162
  break;
233
163
  }
@@ -238,27 +168,15 @@ export class ExecutionGovernor {
238
168
  }
239
169
  return count;
240
170
  }
241
- freezeSearch(reason) {
242
- if (this.searchFrozen) {
243
- return;
244
- }
245
- this.searchFrozen = true;
246
- this.reminderQueue.push(buildSearchFreezeReminder(reason));
171
+ hasCurrentMutationObservation(predicate) {
172
+ return this.history.some((entry) => entry.mutationVersion === this.mutationVersion && predicate(entry));
247
173
  }
248
- enterModifyPhase(reason) {
249
- if (this.explorationFrozen) {
174
+ warnOnce(key, reason) {
175
+ if (this.warnedSignatures.has(key)) {
250
176
  return;
251
177
  }
252
- this.phase = "modify";
253
- this.explorationFrozen = true;
254
- this.searchFrozen = true;
255
- this.reminderQueue.push(buildExplorationFreezeReminder(reason));
256
- }
257
- isModificationTask() {
258
- return this.taskType === "implementation" || this.taskType === "debugging";
259
- }
260
- historyCount(predicate) {
261
- return this.history.reduce((count, entry) => count + (predicate(entry) ? 1 : 0), 0);
178
+ this.warnedSignatures.add(key);
179
+ this.reminderQueue.push(buildLoopWarningReminder(reason));
262
180
  }
263
181
  maybeWarnOnSoftBudgets(isSearchStep, isReadStep) {
264
182
  if (!this.softTotalWarned && this.totalSteps >= this.budget.softTotalSteps) {
@@ -275,28 +193,12 @@ export class ExecutionGovernor {
275
193
  }
276
194
  }
277
195
  }
278
- function isExplorationIntent(intent) {
279
- return intent.family === "search" || intent.family === "read" || intent.family === "web";
280
- }
281
196
  function isSuccessfulWriteIntent(intent, result) {
282
197
  if (result.isError || result.status === "blocked" || result.status === "command_error") {
283
198
  return false;
284
199
  }
285
200
  return intent.family === "write" || intent.family === "edit" || result.metadata?.kind === "write" || result.metadata?.kind === "edit";
286
201
  }
287
- function metadataKindForFamily(family) {
288
- switch (family) {
289
- case "search":
290
- case "read":
291
- case "write":
292
- case "edit":
293
- case "shell":
294
- case "web":
295
- return family;
296
- default:
297
- return "security";
298
- }
299
- }
300
202
  function inferProgress(intent, result) {
301
203
  if (result.status === "blocked" || result.status === "timeout" || result.status === "command_error") {
302
204
  return false;
@@ -314,14 +216,3 @@ function inferProgress(intent, result) {
314
216
  }
315
217
  return !result.isError;
316
218
  }
317
- function blockedResult(content, status, reason, kind = "security") {
318
- return {
319
- content,
320
- isError: true,
321
- status,
322
- metadata: {
323
- kind,
324
- reason,
325
- },
326
- };
327
- }
package/dist/agent.d.ts CHANGED
@@ -2,12 +2,14 @@
2
2
  * Agent - The core decision loop.
3
3
  * It maintains message state, calls the LLM, executes tools, and auto-continues.
4
4
  */
5
+ import { type ContextUsageSnapshot } from "./context/usage.js";
5
6
  import type { AgentEvent, ContentPart, PermissionMode, Message, Provider, ThinkingLevel, Todo, ToolResult, ToolRegistryEntry, ToolUpdate } from "./types.js";
6
7
  import { type TurnHooks } from "./orchestrator/hooks.js";
7
8
  import { BudgetLedger } from "./agent/budget-ledger.js";
8
9
  import { type AgentProfile, type SubagentRunResult } from "./agent/profiles.js";
9
10
  import { type SubagentThreadSnapshot } from "./agent/subagent-control.js";
10
11
  import type { SkillSummary } from "./skills/types.js";
12
+ import type { FileStateTracker } from "./tools/file-state.js";
11
13
  export declare class AgentAbortError extends Error {
12
14
  constructor(message?: string);
13
15
  }
@@ -39,6 +41,7 @@ export interface AgentOptions {
39
41
  };
40
42
  skills?: SkillSummary[];
41
43
  memoryPrompt?: string;
44
+ fileStateTracker?: FileStateTracker;
42
45
  }
43
46
  export declare class Agent {
44
47
  messages: Message[];
@@ -65,6 +68,7 @@ export declare class Agent {
65
68
  private budgetSource;
66
69
  private skillSummaries;
67
70
  private memoryPrompt?;
71
+ private fileStateTracker?;
68
72
  private subagentThreads;
69
73
  private pendingSubagentUpdates;
70
74
  private lastInputTokens;
@@ -74,8 +78,10 @@ export declare class Agent {
74
78
  unlockDeferredTools(names: string[]): void;
75
79
  /** All deferred tools in this session (for tool_search to inspect). */
76
80
  listDeferredTools(): ToolRegistryEntry[];
81
+ getContextUsageSnapshot(): ContextUsageSnapshot;
77
82
  /** Whether a given tool is deferred and not yet unlocked. */
78
83
  isDeferredAndLocked(name: string): boolean;
84
+ private getActiveToolEntries;
79
85
  injectSystemReminder(content: string): void;
80
86
  injectModeReminder(): void;
81
87
  get model(): string;
package/dist/agent.js CHANGED
@@ -6,6 +6,7 @@ import { compactMessages } from "./context/compact.js";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { compactMessagesWithLLM } from "./context/compact-llm.js";
8
8
  import { getContextBudget } from "./context/budget.js";
9
+ import { buildContextUsageSnapshot } from "./context/usage.js";
9
10
  import { isContextOverflowError } from "./context/overflow.js";
10
11
  import { projectMessages } from "./context/projector.js";
11
12
  import { aggressivePruneMessages } from "./context/prune.js";
@@ -18,6 +19,7 @@ import { assignAgentNickname, builtinAgentProfiles, mergeUsage, selectToolsForAg
18
19
  import { snapshotSubagentThread, subagentResultFromThread } from "./agent/subagent-control.js";
19
20
  import { buildSystemPrompt } from "./system-prompt.js";
20
21
  import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "./provider-artifacts.js";
22
+ import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
21
23
  const MAX_CONSECUTIVE_OVERFLOW_RECOVERIES = 3;
22
24
  const RESIDENT_HISTORY_KEEP_RECENT_TURNS = 3;
23
25
  const RESIDENT_HISTORY_MESSAGE_LIMIT = 160;
@@ -56,6 +58,7 @@ export class Agent {
56
58
  budgetSource;
57
59
  skillSummaries;
58
60
  memoryPrompt;
61
+ fileStateTracker;
59
62
  subagentThreads = new Map();
60
63
  pendingSubagentUpdates = [];
61
64
  lastInputTokens = null;
@@ -80,6 +83,7 @@ export class Agent {
80
83
  this.budgetSource = options.budgetSource ?? { runId: this.sessionID ?? "agent" };
81
84
  this.skillSummaries = options.skills ?? [];
82
85
  this.memoryPrompt = options.memoryPrompt;
86
+ this.fileStateTracker = options.fileStateTracker;
83
87
  if (options.systemPrompt) {
84
88
  this.messages.push({ role: "system", content: options.systemPrompt });
85
89
  }
@@ -111,11 +115,26 @@ export class Agent {
111
115
  listDeferredTools() {
112
116
  return [...this.tools.values()].filter((t) => t.deferred);
113
117
  }
118
+ getContextUsageSnapshot() {
119
+ return buildContextUsageSnapshot({
120
+ providerId: this.providerId,
121
+ modelId: this.apiModel,
122
+ messages: this.messages,
123
+ toolEntries: this.getActiveToolEntries(),
124
+ deferredToolEntries: this.listDeferredTools(),
125
+ skills: this.skillSummaries,
126
+ });
127
+ }
114
128
  /** Whether a given tool is deferred and not yet unlocked. */
115
129
  isDeferredAndLocked(name) {
116
130
  const tool = this.tools.get(name);
117
131
  return !!tool?.deferred && !this.unlockedDeferred.has(name);
118
132
  }
133
+ getActiveToolEntries() {
134
+ return [...this.tools.values()]
135
+ .filter((tool) => !tool.deferred || this.unlockedDeferred.has(tool.name))
136
+ .filter((tool) => this._mode === "plan" || tool.name !== "exit_plan_mode");
137
+ }
119
138
  injectSystemReminder(content) {
120
139
  this.appendMessage({ role: "meta", kind: "system-reminder", content });
121
140
  }
@@ -320,6 +339,15 @@ export class Agent {
320
339
  yield { type: "text_delta", content: chunk.content };
321
340
  break;
322
341
  case "reasoning_delta":
342
+ debugReasoningStream({
343
+ stage: "agent_receive",
344
+ providerId: this._providerId,
345
+ modelId: this.apiModel,
346
+ turnStep: step,
347
+ beforeLength: assistantMsg.reasoning?.length ?? 0,
348
+ delta: summarizeDebugText(chunk.content),
349
+ afterLength: (assistantMsg.reasoning?.length ?? 0) + chunk.content.length,
350
+ });
323
351
  assistantMsg.reasoning = (assistantMsg.reasoning || "") + chunk.content;
324
352
  yield { type: "reasoning_delta", content: chunk.content };
325
353
  break;
@@ -533,7 +561,7 @@ export class Agent {
533
561
  },
534
562
  });
535
563
  flushGovernorReminders();
536
- yield { type: "turn_end", usage: turnUsage };
564
+ yield { type: "turn_end", usage: turnUsage, willContinue: true };
537
565
  // Auto-continue: if we have tool results, the LLM needs to respond to them.
538
566
  // Emitting the turn boundary keeps UI renderers aligned with the persisted
539
567
  // assistant/tool message sequence instead of merging the next answer into
@@ -549,8 +577,9 @@ export class Agent {
549
577
  flushReminders: flushGovernorReminders,
550
578
  });
551
579
  flushGovernorReminders();
552
- yield { type: "turn_end", usage: turnUsage };
553
- if (hookState.forceContinuationReason) {
580
+ const willContinue = !!hookState.forceContinuationReason;
581
+ yield { type: "turn_end", usage: turnUsage, willContinue };
582
+ if (willContinue) {
554
583
  delete hookState.forceContinuationReason;
555
584
  continue;
556
585
  }
@@ -569,6 +598,7 @@ export class Agent {
569
598
  if (afterTokens < beforeTokens) {
570
599
  this.lastInputTokens = null;
571
600
  this.lastAnchorMessageCount = null;
601
+ this.fileStateTracker?.invalidateReadHistory();
572
602
  return before - this.messages.length;
573
603
  }
574
604
  }
@@ -583,6 +613,7 @@ export class Agent {
583
613
  this.messages = llmResult.messages;
584
614
  this.lastInputTokens = null;
585
615
  this.lastAnchorMessageCount = null;
616
+ this.fileStateTracker?.invalidateReadHistory();
586
617
  return before - this.messages.length;
587
618
  }
588
619
  const fallback = compactMessages(this.messages, { keepRecentTurns });
@@ -590,6 +621,7 @@ export class Agent {
590
621
  this.messages = fallback.messages;
591
622
  this.lastInputTokens = null;
592
623
  this.lastAnchorMessageCount = null;
624
+ this.fileStateTracker?.invalidateReadHistory();
593
625
  return before - this.messages.length;
594
626
  }
595
627
  return 0;
@@ -1056,6 +1088,7 @@ export class Agent {
1056
1088
  this.messages = candidate;
1057
1089
  this.lastInputTokens = null;
1058
1090
  this.lastAnchorMessageCount = null;
1091
+ this.fileStateTracker?.invalidateReadHistory();
1059
1092
  }
1060
1093
  }
1061
1094
  appendMessage(message) {
@@ -19,3 +19,4 @@ export interface ContextBudgetOptions {
19
19
  export declare function estimateMessageTokens(message: Message): number;
20
20
  export declare function estimateContextTokens(messages: Message[]): number;
21
21
  export declare function getContextBudget(providerId: string, modelId: string, messages: Message[], options?: ContextBudgetOptions): ContextBudget;
22
+ export declare function estimateTextTokens(text: string): number;
@@ -65,7 +65,7 @@ function shouldTriggerCompact(estimatedTokens, contextWindow) {
65
65
  : contextWindow * 0.75;
66
66
  return estimatedTokens >= threshold;
67
67
  }
68
- function estimateTextTokens(text) {
68
+ export function estimateTextTokens(text) {
69
69
  if (!text) {
70
70
  return 0;
71
71
  }
@@ -0,0 +1,34 @@
1
+ import type { SkillSummary } from "../skills/types.js";
2
+ import type { Message, ToolRegistryEntry } from "../types.js";
3
+ export interface ContextUsageBucket {
4
+ label: string;
5
+ tokens: number;
6
+ detail?: string;
7
+ }
8
+ export interface ContextUsageSnapshot {
9
+ providerId: string;
10
+ modelId: string;
11
+ contextWindow?: number;
12
+ usedTokens: number;
13
+ freeTokens?: number;
14
+ buckets: {
15
+ systemPrompt: ContextUsageBucket;
16
+ tools: ContextUsageBucket;
17
+ skills: ContextUsageBucket;
18
+ deferredTools: ContextUsageBucket;
19
+ other: ContextUsageBucket;
20
+ };
21
+ toolCount: number;
22
+ deferredToolCount: number;
23
+ skillCount: number;
24
+ messageCount: number;
25
+ }
26
+ export declare function buildContextUsageSnapshot(input: {
27
+ providerId: string;
28
+ modelId: string;
29
+ messages: Message[];
30
+ toolEntries: ToolRegistryEntry[];
31
+ deferredToolEntries?: ToolRegistryEntry[];
32
+ skills: SkillSummary[];
33
+ }): ContextUsageSnapshot;
34
+ export declare function formatContextUsage(snapshot: ContextUsageSnapshot): string;