@bubblebrain-ai/bubble 0.0.1

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 (248) hide show
  1. package/README.md +70 -0
  2. package/dist/agent/evidence-tracker.d.ts +15 -0
  3. package/dist/agent/evidence-tracker.js +93 -0
  4. package/dist/agent/execution-governor.d.ts +30 -0
  5. package/dist/agent/execution-governor.js +169 -0
  6. package/dist/agent/subtask-policy.d.ts +14 -0
  7. package/dist/agent/subtask-policy.js +60 -0
  8. package/dist/agent/task-classifier.d.ts +3 -0
  9. package/dist/agent/task-classifier.js +36 -0
  10. package/dist/agent/tool-arbiter.d.ts +7 -0
  11. package/dist/agent/tool-arbiter.js +33 -0
  12. package/dist/agent/tool-intent.d.ts +20 -0
  13. package/dist/agent/tool-intent.js +176 -0
  14. package/dist/agent.d.ts +95 -0
  15. package/dist/agent.js +672 -0
  16. package/dist/approval/controller.d.ts +48 -0
  17. package/dist/approval/controller.js +78 -0
  18. package/dist/approval/danger.d.ts +13 -0
  19. package/dist/approval/danger.js +55 -0
  20. package/dist/approval/diff-hunks.d.ts +12 -0
  21. package/dist/approval/diff-hunks.js +32 -0
  22. package/dist/approval/session-cache.d.ts +35 -0
  23. package/dist/approval/session-cache.js +68 -0
  24. package/dist/approval/tool-helper.d.ts +14 -0
  25. package/dist/approval/tool-helper.js +32 -0
  26. package/dist/approval/types.d.ts +56 -0
  27. package/dist/approval/types.js +8 -0
  28. package/dist/bubble-home.d.ts +8 -0
  29. package/dist/bubble-home.js +19 -0
  30. package/dist/cli.d.ts +19 -0
  31. package/dist/cli.js +82 -0
  32. package/dist/config.d.ts +41 -0
  33. package/dist/config.js +144 -0
  34. package/dist/context/budget.d.ts +21 -0
  35. package/dist/context/budget.js +72 -0
  36. package/dist/context/compact-llm.d.ts +16 -0
  37. package/dist/context/compact-llm.js +132 -0
  38. package/dist/context/compact.d.ts +15 -0
  39. package/dist/context/compact.js +251 -0
  40. package/dist/context/overflow.d.ts +9 -0
  41. package/dist/context/overflow.js +46 -0
  42. package/dist/context/projector.d.ts +26 -0
  43. package/dist/context/projector.js +150 -0
  44. package/dist/context/prune.d.ts +9 -0
  45. package/dist/context/prune.js +111 -0
  46. package/dist/lsp/config.d.ts +18 -0
  47. package/dist/lsp/config.js +58 -0
  48. package/dist/lsp/diagnostics.d.ts +24 -0
  49. package/dist/lsp/diagnostics.js +103 -0
  50. package/dist/lsp/index.d.ts +3 -0
  51. package/dist/lsp/index.js +3 -0
  52. package/dist/lsp/service.d.ts +85 -0
  53. package/dist/lsp/service.js +695 -0
  54. package/dist/main.d.ts +5 -0
  55. package/dist/main.js +352 -0
  56. package/dist/mcp/client.d.ts +68 -0
  57. package/dist/mcp/client.js +163 -0
  58. package/dist/mcp/config.d.ts +26 -0
  59. package/dist/mcp/config.js +127 -0
  60. package/dist/mcp/manager.d.ts +55 -0
  61. package/dist/mcp/manager.js +296 -0
  62. package/dist/mcp/name.d.ts +26 -0
  63. package/dist/mcp/name.js +40 -0
  64. package/dist/mcp/transports.d.ts +53 -0
  65. package/dist/mcp/transports.js +248 -0
  66. package/dist/mcp/types.d.ts +111 -0
  67. package/dist/mcp/types.js +14 -0
  68. package/dist/memory/db.d.ts +62 -0
  69. package/dist/memory/db.js +313 -0
  70. package/dist/memory/index.d.ts +9 -0
  71. package/dist/memory/index.js +9 -0
  72. package/dist/memory/paths.d.ts +18 -0
  73. package/dist/memory/paths.js +38 -0
  74. package/dist/memory/phase1.d.ts +23 -0
  75. package/dist/memory/phase1.js +172 -0
  76. package/dist/memory/phase2.d.ts +19 -0
  77. package/dist/memory/phase2.js +100 -0
  78. package/dist/memory/prompts.d.ts +19 -0
  79. package/dist/memory/prompts.js +99 -0
  80. package/dist/memory/reset.d.ts +1 -0
  81. package/dist/memory/reset.js +13 -0
  82. package/dist/memory/start.d.ts +24 -0
  83. package/dist/memory/start.js +50 -0
  84. package/dist/memory/storage.d.ts +10 -0
  85. package/dist/memory/storage.js +82 -0
  86. package/dist/memory/store.d.ts +43 -0
  87. package/dist/memory/store.js +193 -0
  88. package/dist/memory/usage.d.ts +1 -0
  89. package/dist/memory/usage.js +38 -0
  90. package/dist/model-catalog.d.ts +20 -0
  91. package/dist/model-catalog.js +99 -0
  92. package/dist/model-config.d.ts +32 -0
  93. package/dist/model-config.js +59 -0
  94. package/dist/model-pricing.d.ts +23 -0
  95. package/dist/model-pricing.js +46 -0
  96. package/dist/oauth/index.d.ts +3 -0
  97. package/dist/oauth/index.js +2 -0
  98. package/dist/oauth/openai-codex.d.ts +9 -0
  99. package/dist/oauth/openai-codex.js +173 -0
  100. package/dist/oauth/storage.d.ts +18 -0
  101. package/dist/oauth/storage.js +60 -0
  102. package/dist/oauth/types.d.ts +15 -0
  103. package/dist/oauth/types.js +1 -0
  104. package/dist/orchestrator/default-hooks.d.ts +2 -0
  105. package/dist/orchestrator/default-hooks.js +96 -0
  106. package/dist/orchestrator/hooks.d.ts +78 -0
  107. package/dist/orchestrator/hooks.js +52 -0
  108. package/dist/orchestrator/workflow.d.ts +10 -0
  109. package/dist/orchestrator/workflow.js +22 -0
  110. package/dist/permission/mode.d.ts +23 -0
  111. package/dist/permission/mode.js +20 -0
  112. package/dist/permissions/rule.d.ts +39 -0
  113. package/dist/permissions/rule.js +234 -0
  114. package/dist/permissions/settings.d.ts +71 -0
  115. package/dist/permissions/settings.js +202 -0
  116. package/dist/permissions/types.d.ts +61 -0
  117. package/dist/permissions/types.js +14 -0
  118. package/dist/prompt/compose.d.ts +12 -0
  119. package/dist/prompt/compose.js +67 -0
  120. package/dist/prompt/environment.d.ts +12 -0
  121. package/dist/prompt/environment.js +38 -0
  122. package/dist/prompt/provider-prompts/anthropic.d.ts +1 -0
  123. package/dist/prompt/provider-prompts/anthropic.js +5 -0
  124. package/dist/prompt/provider-prompts/codex.d.ts +1 -0
  125. package/dist/prompt/provider-prompts/codex.js +5 -0
  126. package/dist/prompt/provider-prompts/default.d.ts +1 -0
  127. package/dist/prompt/provider-prompts/default.js +6 -0
  128. package/dist/prompt/provider-prompts/gemini.d.ts +1 -0
  129. package/dist/prompt/provider-prompts/gemini.js +5 -0
  130. package/dist/prompt/provider-prompts/gpt.d.ts +1 -0
  131. package/dist/prompt/provider-prompts/gpt.js +5 -0
  132. package/dist/prompt/reminders.d.ts +30 -0
  133. package/dist/prompt/reminders.js +164 -0
  134. package/dist/prompt/runtime.d.ts +12 -0
  135. package/dist/prompt/runtime.js +31 -0
  136. package/dist/prompt/skills.d.ts +2 -0
  137. package/dist/prompt/skills.js +4 -0
  138. package/dist/provider-openai-codex.d.ts +14 -0
  139. package/dist/provider-openai-codex.js +409 -0
  140. package/dist/provider-registry.d.ts +56 -0
  141. package/dist/provider-registry.js +244 -0
  142. package/dist/provider-transform.d.ts +10 -0
  143. package/dist/provider-transform.js +69 -0
  144. package/dist/provider.d.ts +31 -0
  145. package/dist/provider.js +269 -0
  146. package/dist/question/controller.d.ts +22 -0
  147. package/dist/question/controller.js +97 -0
  148. package/dist/question/index.d.ts +2 -0
  149. package/dist/question/index.js +2 -0
  150. package/dist/question/types.d.ts +42 -0
  151. package/dist/question/types.js +6 -0
  152. package/dist/session-log.d.ts +16 -0
  153. package/dist/session-log.js +267 -0
  154. package/dist/session-types.d.ts +55 -0
  155. package/dist/session-types.js +1 -0
  156. package/dist/session.d.ts +32 -0
  157. package/dist/session.js +135 -0
  158. package/dist/skills/discovery.d.ts +12 -0
  159. package/dist/skills/discovery.js +148 -0
  160. package/dist/skills/format.d.ts +2 -0
  161. package/dist/skills/format.js +47 -0
  162. package/dist/skills/frontmatter.d.ts +5 -0
  163. package/dist/skills/frontmatter.js +60 -0
  164. package/dist/skills/invocation.d.ts +8 -0
  165. package/dist/skills/invocation.js +51 -0
  166. package/dist/skills/registry.d.ts +17 -0
  167. package/dist/skills/registry.js +42 -0
  168. package/dist/skills/types.d.ts +32 -0
  169. package/dist/skills/types.js +1 -0
  170. package/dist/slash-commands/commands.d.ts +7 -0
  171. package/dist/slash-commands/commands.js +779 -0
  172. package/dist/slash-commands/index.d.ts +4 -0
  173. package/dist/slash-commands/index.js +8 -0
  174. package/dist/slash-commands/registry.d.ts +31 -0
  175. package/dist/slash-commands/registry.js +70 -0
  176. package/dist/slash-commands/types.d.ts +44 -0
  177. package/dist/slash-commands/types.js +1 -0
  178. package/dist/slash-commands/unified.d.ts +38 -0
  179. package/dist/slash-commands/unified.js +38 -0
  180. package/dist/system-prompt.d.ts +34 -0
  181. package/dist/system-prompt.js +7 -0
  182. package/dist/tools/bash.d.ts +6 -0
  183. package/dist/tools/bash.js +135 -0
  184. package/dist/tools/edit.d.ts +16 -0
  185. package/dist/tools/edit.js +95 -0
  186. package/dist/tools/exa-mcp.d.ts +3 -0
  187. package/dist/tools/exa-mcp.js +74 -0
  188. package/dist/tools/exit-plan-mode.d.ts +17 -0
  189. package/dist/tools/exit-plan-mode.js +68 -0
  190. package/dist/tools/glob.d.ts +5 -0
  191. package/dist/tools/glob.js +129 -0
  192. package/dist/tools/grep.d.ts +5 -0
  193. package/dist/tools/grep.js +111 -0
  194. package/dist/tools/index.d.ts +36 -0
  195. package/dist/tools/index.js +59 -0
  196. package/dist/tools/lsp.d.ts +4 -0
  197. package/dist/tools/lsp.js +92 -0
  198. package/dist/tools/memory.d.ts +3 -0
  199. package/dist/tools/memory.js +90 -0
  200. package/dist/tools/question.d.ts +3 -0
  201. package/dist/tools/question.js +174 -0
  202. package/dist/tools/read.d.ts +7 -0
  203. package/dist/tools/read.js +83 -0
  204. package/dist/tools/sensitive-paths.d.ts +3 -0
  205. package/dist/tools/sensitive-paths.js +24 -0
  206. package/dist/tools/skill.d.ts +5 -0
  207. package/dist/tools/skill.js +51 -0
  208. package/dist/tools/task.d.ts +2 -0
  209. package/dist/tools/task.js +57 -0
  210. package/dist/tools/todo.d.ts +12 -0
  211. package/dist/tools/todo.js +151 -0
  212. package/dist/tools/tool-search.d.ts +23 -0
  213. package/dist/tools/tool-search.js +124 -0
  214. package/dist/tools/web-fetch.d.ts +6 -0
  215. package/dist/tools/web-fetch.js +75 -0
  216. package/dist/tools/web-search.d.ts +5 -0
  217. package/dist/tools/web-search.js +49 -0
  218. package/dist/tools/write.d.ts +11 -0
  219. package/dist/tools/write.js +77 -0
  220. package/dist/tui/display-history.d.ts +35 -0
  221. package/dist/tui/display-history.js +243 -0
  222. package/dist/tui/file-mentions.d.ts +29 -0
  223. package/dist/tui/file-mentions.js +174 -0
  224. package/dist/tui/image-paste.d.ts +54 -0
  225. package/dist/tui/image-paste.js +288 -0
  226. package/dist/tui/markdown-theme-rules.d.ts +23 -0
  227. package/dist/tui/markdown-theme-rules.js +164 -0
  228. package/dist/tui/markdown-theme.d.ts +5 -0
  229. package/dist/tui/markdown-theme.js +27 -0
  230. package/dist/tui/opencode-spinner.d.ts +21 -0
  231. package/dist/tui/opencode-spinner.js +216 -0
  232. package/dist/tui/prompt-keybindings.d.ts +41 -0
  233. package/dist/tui/prompt-keybindings.js +28 -0
  234. package/dist/tui/recent-activity.d.ts +8 -0
  235. package/dist/tui/recent-activity.js +71 -0
  236. package/dist/tui/run.d.ts +39 -0
  237. package/dist/tui/run.js +5696 -0
  238. package/dist/tui/sidebar-mcp.d.ts +31 -0
  239. package/dist/tui/sidebar-mcp.js +62 -0
  240. package/dist/tui/sidebar-state.d.ts +12 -0
  241. package/dist/tui/sidebar-state.js +69 -0
  242. package/dist/types.d.ts +219 -0
  243. package/dist/types.js +4 -0
  244. package/dist/variant/thinking-level.d.ts +5 -0
  245. package/dist/variant/thinking-level.js +25 -0
  246. package/dist/variant/variant-resolver.d.ts +4 -0
  247. package/dist/variant/variant-resolver.js +12 -0
  248. package/package.json +47 -0
package/dist/agent.js ADDED
@@ -0,0 +1,672 @@
1
+ /**
2
+ * Agent - The core decision loop.
3
+ * It maintains message state, calls the LLM, executes tools, and auto-continues.
4
+ */
5
+ import { compactMessages } from "./context/compact.js";
6
+ import { compactMessagesWithLLM } from "./context/compact-llm.js";
7
+ import { getContextBudget } from "./context/budget.js";
8
+ import { isContextOverflowError } from "./context/overflow.js";
9
+ import { projectMessages } from "./context/projector.js";
10
+ import { aggressivePruneMessages } from "./context/prune.js";
11
+ import { buildDeferredToolsReminder, buildToolFreezeReminder, reminderForMode } from "./prompt/reminders.js";
12
+ import { HookBus } from "./orchestrator/hooks.js";
13
+ import { createDefaultHooks } from "./orchestrator/default-hooks.js";
14
+ import { filterToolsForSubtask, getSubtaskPolicy } from "./agent/subtask-policy.js";
15
+ const MAX_CONSECUTIVE_OVERFLOW_RECOVERIES = 3;
16
+ const RESIDENT_HISTORY_KEEP_RECENT_TURNS = 3;
17
+ const RESIDENT_HISTORY_MESSAGE_LIMIT = 160;
18
+ const RESIDENT_HISTORY_CHAR_SOFT_LIMIT = 256 * 1024;
19
+ const RESIDENT_HISTORY_CHAR_HARD_LIMIT = 512 * 1024;
20
+ const RESIDENT_HISTORY_HEAP_SOFT_LIMIT = 512 * 1024 * 1024;
21
+ const RESIDENT_HISTORY_HEAP_HARD_LIMIT = 768 * 1024 * 1024;
22
+ export class Agent {
23
+ messages = [];
24
+ provider;
25
+ sessionID;
26
+ _providerId;
27
+ _model;
28
+ tools = new Map();
29
+ unlockedDeferred = new Set();
30
+ temperature;
31
+ thinkingLevel;
32
+ _mode;
33
+ _modeVersion = 0;
34
+ onModeUpdate;
35
+ _todos;
36
+ _todosVersion = 0;
37
+ onTodosUpdate;
38
+ onMessageAppend;
39
+ onToolResult;
40
+ hookDefinitions;
41
+ maxTurns;
42
+ taskBudget;
43
+ lastInputTokens = null;
44
+ lastAnchorMessageCount = null;
45
+ constructor(options) {
46
+ this.provider = options.provider;
47
+ this.sessionID = options.sessionID;
48
+ this._providerId = options.providerId ?? "";
49
+ this._model = options.model;
50
+ this.temperature = options.temperature ?? 0.2;
51
+ this.thinkingLevel = options.thinkingLevel ?? "off";
52
+ this._mode = options.mode ?? "default";
53
+ this._todos = options.todos ? [...options.todos] : [];
54
+ this.onMessageAppend = options.onMessageAppend;
55
+ this.onToolResult = options.onToolResult;
56
+ this.onTodosUpdate = options.onTodosUpdate;
57
+ this.onModeUpdate = options.onModeUpdate;
58
+ this.hookDefinitions = options.hooks ?? [];
59
+ this.maxTurns = options.maxTurns ?? options.steps;
60
+ this.taskBudget = options.taskBudget;
61
+ if (options.systemPrompt) {
62
+ this.messages.push({ role: "system", content: options.systemPrompt });
63
+ }
64
+ for (const tool of options.tools) {
65
+ this.tools.set(tool.name, tool);
66
+ }
67
+ // If the agent boots in a non-default mode, inject the corresponding reminder so the
68
+ // model sees the active rules on its very first turn. Default mode needs no reminder.
69
+ if (this._mode !== "default") {
70
+ this.injectSystemReminder(reminderForMode(this._mode));
71
+ }
72
+ // Advertise any deferred tools so the model knows they exist and how to
73
+ // reach them. Keeps the per-turn tool list small; schemas load on demand.
74
+ const deferredNames = [...this.tools.values()]
75
+ .filter((t) => t.deferred)
76
+ .map((t) => t.name);
77
+ if (deferredNames.length > 0) {
78
+ this.injectSystemReminder(buildDeferredToolsReminder(deferredNames));
79
+ }
80
+ }
81
+ /** Unlock a list of deferred tools so they're included in subsequent turns. */
82
+ unlockDeferredTools(names) {
83
+ for (const n of names) {
84
+ if (this.tools.has(n))
85
+ this.unlockedDeferred.add(n);
86
+ }
87
+ }
88
+ /** All deferred tools in this session (for tool_search to inspect). */
89
+ listDeferredTools() {
90
+ return [...this.tools.values()].filter((t) => t.deferred);
91
+ }
92
+ /** Whether a given tool is deferred and not yet unlocked. */
93
+ isDeferredAndLocked(name) {
94
+ const tool = this.tools.get(name);
95
+ return !!tool?.deferred && !this.unlockedDeferred.has(name);
96
+ }
97
+ injectSystemReminder(content) {
98
+ this.appendMessage({ role: "user", content, isMeta: true });
99
+ }
100
+ get model() {
101
+ return this._model;
102
+ }
103
+ set model(value) {
104
+ this._model = value;
105
+ }
106
+ get providerId() {
107
+ return this._providerId;
108
+ }
109
+ set providerId(value) {
110
+ this._providerId = value;
111
+ }
112
+ get apiModel() {
113
+ if (this._model.includes(":")) {
114
+ return this._model.split(":").slice(1).join(":");
115
+ }
116
+ return this._model;
117
+ }
118
+ setProvider(provider) {
119
+ this.provider = provider;
120
+ }
121
+ complete(messages, options) {
122
+ return this.provider.complete(messages, {
123
+ model: options?.model ?? this.apiModel,
124
+ temperature: options?.temperature ?? this.temperature,
125
+ thinkingLevel: options?.thinkingLevel ?? this.thinkingLevel,
126
+ });
127
+ }
128
+ get thinking() {
129
+ return this.thinkingLevel;
130
+ }
131
+ set thinking(value) {
132
+ this.thinkingLevel = value;
133
+ }
134
+ get reasoning() {
135
+ return this.thinkingLevel;
136
+ }
137
+ set reasoning(value) {
138
+ this.thinkingLevel = value;
139
+ }
140
+ get mode() {
141
+ return this._mode;
142
+ }
143
+ set mode(value) {
144
+ this.setMode(value);
145
+ }
146
+ setMode(value) {
147
+ if (this._mode === value)
148
+ return;
149
+ this._mode = value;
150
+ this._modeVersion += 1;
151
+ this.injectSystemReminder(reminderForMode(value));
152
+ this.onModeUpdate?.(value);
153
+ }
154
+ /** Internal: snapshot counter that bumps on every mode change. Used by run loop. */
155
+ get modeVersion() {
156
+ return this._modeVersion;
157
+ }
158
+ getTodos() {
159
+ return this._todos.map((todo) => ({ ...todo }));
160
+ }
161
+ setTodos(next) {
162
+ this._todos = next.map((todo) => ({ ...todo }));
163
+ this._todosVersion += 1;
164
+ this.onTodosUpdate?.(this.getTodos());
165
+ }
166
+ /** Internal: snapshot counter that bumps on every setTodos. Used by run loop to detect mutations. */
167
+ get todosVersion() {
168
+ return this._todosVersion;
169
+ }
170
+ setSystemPrompt(prompt) {
171
+ const systemMessage = { role: "system", content: prompt };
172
+ if (this.messages[0]?.role === "system") {
173
+ this.messages[0] = systemMessage;
174
+ return;
175
+ }
176
+ this.messages.unshift(systemMessage);
177
+ }
178
+ async *run(userInput, cwd) {
179
+ const hookBus = new HookBus();
180
+ for (const hooks of createDefaultHooks()) {
181
+ hookBus.register(hooks);
182
+ }
183
+ for (const hooks of this.hookDefinitions) {
184
+ hookBus.register(hooks);
185
+ }
186
+ const hookState = {};
187
+ const reminderQueue = [];
188
+ const queueReminder = (reminder) => {
189
+ reminderQueue.push(reminder);
190
+ };
191
+ const flushGovernorReminders = () => {
192
+ for (const reminder of reminderQueue.splice(0, reminderQueue.length)) {
193
+ this.injectSystemReminder(reminder);
194
+ }
195
+ };
196
+ if (this._todos.length > 0 && this._todos.every((t) => t.status === "completed")) {
197
+ this.setTodos([]);
198
+ yield { type: "todos_updated", todos: [] };
199
+ }
200
+ this.appendMessage({ role: "user", content: userInput });
201
+ await hookBus.runBeforeTurn({
202
+ agent: this,
203
+ cwd,
204
+ input: userInput,
205
+ state: hookState,
206
+ queueReminder,
207
+ flushReminders: flushGovernorReminders,
208
+ });
209
+ flushGovernorReminders();
210
+ let consecutiveOverflowRecoveries = 0;
211
+ let step = 0;
212
+ while (true) {
213
+ flushGovernorReminders();
214
+ yield { type: "turn_start" };
215
+ step += 1;
216
+ hookState.turnCount = step;
217
+ if (this.taskBudget) {
218
+ hookState.taskBudget = {
219
+ total: this.taskBudget.total,
220
+ spent: hookState.taskBudget?.spent ?? 0,
221
+ };
222
+ }
223
+ let forceTextOnlyReason = hookState.forceTextOnlyReason;
224
+ if (!forceTextOnlyReason && this.maxTurns !== undefined && step >= this.maxTurns) {
225
+ forceTextOnlyReason = "The configured maximum turns for this agent have been reached.";
226
+ hookState.forceTextOnlyReason = forceTextOnlyReason;
227
+ }
228
+ if (forceTextOnlyReason) {
229
+ this.injectSystemReminder(buildToolFreezeReminder(forceTextOnlyReason));
230
+ }
231
+ const assistantMsg = {
232
+ role: "assistant",
233
+ content: "",
234
+ reasoning: "",
235
+ toolCalls: [],
236
+ };
237
+ let currentToolCall = null;
238
+ let turnUsage;
239
+ let assistantAppended = false;
240
+ let toolEntries = Array.from(this.tools.values())
241
+ .filter((t) => !t.deferred || this.unlockedDeferred.has(t.name));
242
+ const beforeModelCallCtx = {
243
+ agent: this,
244
+ cwd,
245
+ input: userInput,
246
+ state: hookState,
247
+ queueReminder,
248
+ flushReminders: flushGovernorReminders,
249
+ toolEntries,
250
+ disableTools: (reason) => {
251
+ hookState.forceTextOnlyReason = reason;
252
+ },
253
+ };
254
+ await hookBus.runBeforeModelCall(beforeModelCallCtx);
255
+ toolEntries = beforeModelCallCtx.toolEntries;
256
+ flushGovernorReminders();
257
+ const toolDefinitions = ((hookState.forceTextOnlyReason ? [] : toolEntries))
258
+ .map((t) => ({
259
+ name: t.name,
260
+ description: t.description,
261
+ parameters: t.parameters,
262
+ }));
263
+ try {
264
+ const projectedMessages = projectMessages(this.messages, {
265
+ mode: "budgeted",
266
+ providerId: this.providerId,
267
+ modelId: this.apiModel,
268
+ usageAnchorTokens: this.lastInputTokens ?? undefined,
269
+ anchorMessageCount: this.lastAnchorMessageCount ?? undefined,
270
+ });
271
+ const stream = this.provider.streamChat(projectedMessages, {
272
+ model: this.apiModel,
273
+ tools: toolDefinitions,
274
+ temperature: this.temperature,
275
+ thinkingLevel: this.thinkingLevel,
276
+ });
277
+ for await (const chunk of stream) {
278
+ switch (chunk.type) {
279
+ case "text":
280
+ assistantMsg.content += chunk.content;
281
+ yield { type: "text_delta", content: chunk.content };
282
+ break;
283
+ case "reasoning_delta":
284
+ assistantMsg.reasoning = (assistantMsg.reasoning || "") + chunk.content;
285
+ yield { type: "reasoning_delta", content: chunk.content };
286
+ break;
287
+ case "tool_call":
288
+ if (chunk.isStart) {
289
+ currentToolCall = { id: chunk.id, name: chunk.name, args: "" };
290
+ }
291
+ if (currentToolCall) {
292
+ currentToolCall.args += chunk.arguments;
293
+ }
294
+ if (chunk.isEnd && currentToolCall) {
295
+ assistantMsg.toolCalls.push({
296
+ id: currentToolCall.id,
297
+ name: currentToolCall.name,
298
+ arguments: currentToolCall.args,
299
+ });
300
+ currentToolCall = null;
301
+ }
302
+ break;
303
+ case "usage":
304
+ turnUsage = chunk.usage;
305
+ this.lastInputTokens = chunk.usage.promptTokens;
306
+ this.lastAnchorMessageCount = this.messages.length;
307
+ if (hookState.taskBudget) {
308
+ hookState.taskBudget.spent += chunk.usage.promptTokens + chunk.usage.completionTokens;
309
+ if (hookState.taskBudget.spent >= hookState.taskBudget.total) {
310
+ hookState.forceTextOnlyReason = "The configured task budget for this agent has been exhausted.";
311
+ }
312
+ }
313
+ break;
314
+ }
315
+ }
316
+ this.appendMessage(assistantMsg);
317
+ assistantAppended = true;
318
+ }
319
+ catch (error) {
320
+ if (assistantAppended) {
321
+ throw error;
322
+ }
323
+ if (!isContextOverflowError(error)) {
324
+ throw error;
325
+ }
326
+ if (consecutiveOverflowRecoveries >= MAX_CONSECUTIVE_OVERFLOW_RECOVERIES) {
327
+ throw error;
328
+ }
329
+ const droppedMessages = await this.recoverFromOverflow(consecutiveOverflowRecoveries);
330
+ consecutiveOverflowRecoveries += 1;
331
+ yield { type: "context_recovered", droppedMessages, reason: "overflow" };
332
+ continue;
333
+ }
334
+ consecutiveOverflowRecoveries = 0;
335
+ // Execute tools if any
336
+ if (assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0) {
337
+ const parsedCalls = [];
338
+ for (let index = 0; index < assistantMsg.toolCalls.length; index++) {
339
+ const tc = assistantMsg.toolCalls[index];
340
+ try {
341
+ parsedCalls.push({ ...tc, parsedArgs: JSON.parse(tc.arguments) });
342
+ }
343
+ catch {
344
+ parsedCalls.push({ ...tc, parsedArgs: {} });
345
+ }
346
+ }
347
+ const executedResults = [];
348
+ for (let index = 0; index < parsedCalls.length; index++) {
349
+ let tc = parsedCalls[index];
350
+ let blockedResult;
351
+ await hookBus.runBeforeToolCall({
352
+ agent: this,
353
+ cwd,
354
+ input: userInput,
355
+ state: hookState,
356
+ queueReminder,
357
+ flushReminders: flushGovernorReminders,
358
+ toolCall: tc,
359
+ blockedResult,
360
+ replaceToolCall: (toolCall) => {
361
+ tc = toolCall;
362
+ },
363
+ blockToolCall: (result) => {
364
+ blockedResult = result;
365
+ },
366
+ });
367
+ assistantMsg.toolCalls[index] = {
368
+ id: tc.id,
369
+ name: tc.name,
370
+ arguments: tc.arguments,
371
+ };
372
+ flushGovernorReminders();
373
+ yield { type: "tool_start", id: tc.id, name: tc.name, args: tc.parsedArgs };
374
+ const todosVersionBefore = this._todosVersion;
375
+ const modeVersionBefore = this._modeVersion;
376
+ let result = blockedResult ?? await this.executeTool(tc, cwd);
377
+ await hookBus.runAfterToolCall({
378
+ agent: this,
379
+ cwd,
380
+ input: userInput,
381
+ state: hookState,
382
+ queueReminder,
383
+ flushReminders: flushGovernorReminders,
384
+ toolCall: tc,
385
+ result,
386
+ replaceResult: (next) => {
387
+ result = next;
388
+ },
389
+ });
390
+ this.appendMessage({
391
+ role: "tool",
392
+ toolCallId: tc.id,
393
+ content: result.content,
394
+ metadata: result.metadata,
395
+ isError: result.isError,
396
+ });
397
+ this.compactResidentHistory();
398
+ flushGovernorReminders();
399
+ this.onToolResult?.(tc.name, result);
400
+ executedResults.push(result);
401
+ yield { type: "tool_end", id: tc.id, name: tc.name, result };
402
+ if (this._todosVersion !== todosVersionBefore) {
403
+ yield { type: "todos_updated", todos: this.getTodos() };
404
+ }
405
+ if (this._modeVersion !== modeVersionBefore) {
406
+ yield { type: "mode_changed", mode: this._mode };
407
+ }
408
+ }
409
+ await hookBus.runBeforeContinuation({
410
+ agent: this,
411
+ cwd,
412
+ input: userInput,
413
+ state: hookState,
414
+ queueReminder,
415
+ flushReminders: flushGovernorReminders,
416
+ toolCalls: parsedCalls,
417
+ toolResults: executedResults,
418
+ requestTextOnlyTurn: (reason) => {
419
+ hookState.forceTextOnlyReason = reason;
420
+ },
421
+ });
422
+ flushGovernorReminders();
423
+ yield { type: "turn_end", usage: turnUsage };
424
+ // Auto-continue: if we have tool results, the LLM needs to respond to them.
425
+ // Emitting the turn boundary keeps UI renderers aligned with the persisted
426
+ // assistant/tool message sequence instead of merging the next answer into
427
+ // the tool-call turn.
428
+ continue;
429
+ }
430
+ await hookBus.runAfterTurn({
431
+ agent: this,
432
+ cwd,
433
+ input: userInput,
434
+ state: hookState,
435
+ queueReminder,
436
+ flushReminders: flushGovernorReminders,
437
+ });
438
+ flushGovernorReminders();
439
+ yield { type: "turn_end", usage: turnUsage };
440
+ break;
441
+ }
442
+ yield { type: "agent_end" };
443
+ }
444
+ async recoverFromOverflow(attempt) {
445
+ const before = this.messages.length;
446
+ const beforeTokens = this.messages.reduce((sum, m) => sum + JSON.stringify(m).length, 0);
447
+ if (attempt === 0) {
448
+ this.messages = aggressivePruneMessages(this.messages);
449
+ const afterTokens = this.messages.reduce((sum, m) => sum + JSON.stringify(m).length, 0);
450
+ if (afterTokens < beforeTokens) {
451
+ this.lastInputTokens = null;
452
+ this.lastAnchorMessageCount = null;
453
+ return before - this.messages.length;
454
+ }
455
+ }
456
+ const keepRecentTurns = attempt >= 2 ? 1 : 2;
457
+ const llmResult = await compactMessagesWithLLM(this.messages, {
458
+ provider: this.provider,
459
+ model: this.apiModel,
460
+ thinkingLevel: this.thinkingLevel,
461
+ keepRecentTurns,
462
+ });
463
+ if (llmResult.compacted && llmResult.messages) {
464
+ this.messages = llmResult.messages;
465
+ this.lastInputTokens = null;
466
+ this.lastAnchorMessageCount = null;
467
+ return before - this.messages.length;
468
+ }
469
+ const fallback = compactMessages(this.messages, { keepRecentTurns });
470
+ if (fallback.compacted && fallback.messages) {
471
+ this.messages = fallback.messages;
472
+ this.lastInputTokens = null;
473
+ this.lastAnchorMessageCount = null;
474
+ return before - this.messages.length;
475
+ }
476
+ return 0;
477
+ }
478
+ compactResidentHistory() {
479
+ this.maybeCompactResidentHistory();
480
+ }
481
+ async runSubtask(input, cwd, options) {
482
+ const subtaskType = options?.subtaskType;
483
+ const policy = getSubtaskPolicy(subtaskType);
484
+ const tools = filterToolsForSubtask([...this.tools.values()].filter((tool) => tool.name !== "task"), subtaskType);
485
+ const subAgent = new Agent({
486
+ provider: this.provider,
487
+ providerId: this.providerId,
488
+ model: this.model,
489
+ tools,
490
+ temperature: this.temperature,
491
+ thinkingLevel: this.thinkingLevel,
492
+ mode: "plan",
493
+ maxTurns: policy.maxTurns,
494
+ taskBudget: policy.taskBudget,
495
+ systemPrompt: this.messages.find((message) => message.role === "system")?.content,
496
+ hooks: this.hookDefinitions,
497
+ });
498
+ subAgent.injectSystemReminder(`<system-reminder>\n${policy.reminder}\n</system-reminder>`);
499
+ let summary = "";
500
+ const toolNotes = [];
501
+ for await (const event of subAgent.run(input, cwd)) {
502
+ if (event.type === "text_delta") {
503
+ summary += event.content;
504
+ }
505
+ if (event.type === "tool_end") {
506
+ const detail = event.result.metadata?.reason
507
+ || event.result.content.split("\n").find((line) => line.trim())?.trim()
508
+ || "completed";
509
+ toolNotes.push(`${event.name}: ${detail}`);
510
+ }
511
+ }
512
+ const lines = [];
513
+ const trimmedSummary = summary.trim();
514
+ lines.push(`Subtask type: ${policy.type}`);
515
+ if (options?.description) {
516
+ lines.push(`Subtask description: ${options.description}`);
517
+ }
518
+ if (trimmedSummary) {
519
+ lines.push("", "Subtask summary:", trimmedSummary);
520
+ }
521
+ if (toolNotes.length > 0) {
522
+ lines.push("", "Subtask tools:");
523
+ for (const note of toolNotes.slice(0, 8)) {
524
+ lines.push(`- ${note}`);
525
+ }
526
+ }
527
+ if (lines.length === 0) {
528
+ lines.push("Subtask summary:", "No conclusive findings were produced.");
529
+ }
530
+ return {
531
+ content: lines.join("\n"),
532
+ status: policy.resultStatus,
533
+ metadata: {
534
+ kind: "security",
535
+ reason: `Subtask (${policy.type}) investigation completed.`,
536
+ },
537
+ };
538
+ }
539
+ maybeCompactResidentHistory() {
540
+ if (this.messages.length === 0) {
541
+ return;
542
+ }
543
+ const before = this.messages;
544
+ const beforeChars = estimateResidentChars(before);
545
+ const beforeToolChars = estimateToolPayloadChars(before);
546
+ let candidate = projectMessages(before, { mode: "pruned" });
547
+ const budget = this.providerId && this.apiModel
548
+ ? getContextBudget(this.providerId, this.apiModel, candidate)
549
+ : undefined;
550
+ const heapUsed = getCurrentHeapUsed();
551
+ const residentChars = estimateResidentChars(candidate);
552
+ const keepRecentTurns = countUserTurns(candidate) > 10
553
+ ? 2
554
+ : RESIDENT_HISTORY_KEEP_RECENT_TURNS;
555
+ const shouldAggressivelyPrune = residentChars >= RESIDENT_HISTORY_CHAR_HARD_LIMIT
556
+ || heapUsed >= RESIDENT_HISTORY_HEAP_HARD_LIMIT;
557
+ const shouldCompact = !!budget?.shouldCompact
558
+ || candidate.length >= RESIDENT_HISTORY_MESSAGE_LIMIT
559
+ || residentChars >= RESIDENT_HISTORY_CHAR_SOFT_LIMIT
560
+ || heapUsed >= RESIDENT_HISTORY_HEAP_SOFT_LIMIT;
561
+ if (shouldAggressivelyPrune) {
562
+ candidate = aggressivePruneMessages(candidate);
563
+ }
564
+ if (shouldCompact) {
565
+ const compacted = compactMessages(candidate, { keepRecentTurns });
566
+ if (compacted.compacted && compacted.messages) {
567
+ candidate = compacted.messages;
568
+ }
569
+ }
570
+ const afterChars = estimateResidentChars(candidate);
571
+ const afterToolChars = estimateToolPayloadChars(candidate);
572
+ if (afterChars < beforeChars
573
+ || afterToolChars < beforeToolChars
574
+ || candidate.length < before.length) {
575
+ this.messages = candidate;
576
+ this.lastInputTokens = null;
577
+ this.lastAnchorMessageCount = null;
578
+ }
579
+ }
580
+ appendMessage(message) {
581
+ this.messages.push(message);
582
+ this.onMessageAppend?.(message);
583
+ }
584
+ async executeTool(toolCall, cwd) {
585
+ const tool = this.tools.get(toolCall.name);
586
+ if (!tool) {
587
+ return {
588
+ content: `Error: Unknown tool "${toolCall.name}"`,
589
+ isError: true,
590
+ };
591
+ }
592
+ if (this._mode === "plan" && !tool.readOnly) {
593
+ return {
594
+ content: `Error: Tool "${toolCall.name}" is not allowed in plan mode. ` +
595
+ `In plan mode you may only use read-only tools (read, glob, grep, lsp, web_search, web_fetch, task, skill, todo_write, tool_search, question, exit_plan_mode). ` +
596
+ `To modify files or run commands, present your proposal and call exit_plan_mode so the user can review and approve it.`,
597
+ isError: true,
598
+ };
599
+ }
600
+ if (tool.deferred && !this.unlockedDeferred.has(tool.name)) {
601
+ return {
602
+ content: `Error: Tool "${toolCall.name}" is a deferred tool; its schema is not yet loaded. ` +
603
+ `Call tool_search first with query "select:${toolCall.name}" to load its schema, then retry.`,
604
+ isError: true,
605
+ };
606
+ }
607
+ try {
608
+ return await tool.execute(toolCall.parsedArgs, {
609
+ cwd,
610
+ sessionID: this.sessionID,
611
+ toolCall: { id: toolCall.id, name: toolCall.name },
612
+ agent: this,
613
+ });
614
+ }
615
+ catch (err) {
616
+ return {
617
+ content: `Error executing ${toolCall.name}: ${err.message || String(err)}`,
618
+ isError: true,
619
+ };
620
+ }
621
+ }
622
+ }
623
+ function estimateResidentChars(messages) {
624
+ let total = 0;
625
+ for (const message of messages) {
626
+ switch (message.role) {
627
+ case "system":
628
+ total += message.content.length;
629
+ break;
630
+ case "tool":
631
+ total += message.content.length + message.toolCallId.length;
632
+ break;
633
+ case "assistant":
634
+ total += message.content.length + (message.reasoning?.length ?? 0);
635
+ total += message.toolCalls?.reduce((sum, toolCall) => sum + toolCall.id.length + toolCall.name.length + toolCall.arguments.length, 0) ?? 0;
636
+ break;
637
+ case "user":
638
+ if (typeof message.content === "string") {
639
+ total += message.content.length;
640
+ }
641
+ else {
642
+ total += message.content.reduce((sum, part) => {
643
+ if (part.type === "text") {
644
+ return sum + part.text.length;
645
+ }
646
+ return sum + part.image_url.url.length;
647
+ }, 0);
648
+ }
649
+ break;
650
+ }
651
+ }
652
+ return total;
653
+ }
654
+ function estimateToolPayloadChars(messages) {
655
+ return messages.reduce((sum, message) => {
656
+ if (message.role !== "tool") {
657
+ return sum;
658
+ }
659
+ return sum + message.content.length;
660
+ }, 0);
661
+ }
662
+ function countUserTurns(messages) {
663
+ return messages.reduce((count, message) => count + (message.role === "user" && !message.isMeta ? 1 : 0), 0);
664
+ }
665
+ function getCurrentHeapUsed() {
666
+ try {
667
+ return process.memoryUsage().heapUsed;
668
+ }
669
+ catch {
670
+ return 0;
671
+ }
672
+ }