open-agent-sdk 0.1.0-alpha.0

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 (196) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -0
  3. package/README.zh.md +285 -0
  4. package/dist/agent/agent-definition.d.ts +107 -0
  5. package/dist/agent/agent-definition.d.ts.map +1 -0
  6. package/dist/agent/agent-definition.js +90 -0
  7. package/dist/agent/agent-definition.js.map +1 -0
  8. package/dist/agent/react-loop.d.ts +117 -0
  9. package/dist/agent/react-loop.d.ts.map +1 -0
  10. package/dist/agent/react-loop.js +674 -0
  11. package/dist/agent/react-loop.js.map +1 -0
  12. package/dist/agent/subagent-runner.d.ts +67 -0
  13. package/dist/agent/subagent-runner.d.ts.map +1 -0
  14. package/dist/agent/subagent-runner.js +168 -0
  15. package/dist/agent/subagent-runner.js.map +1 -0
  16. package/dist/hooks/index.d.ts +8 -0
  17. package/dist/hooks/index.d.ts.map +1 -0
  18. package/dist/hooks/index.js +9 -0
  19. package/dist/hooks/index.js.map +1 -0
  20. package/dist/hooks/inputs.d.ts +56 -0
  21. package/dist/hooks/inputs.d.ts.map +1 -0
  22. package/dist/hooks/inputs.js +150 -0
  23. package/dist/hooks/inputs.js.map +1 -0
  24. package/dist/hooks/manager.d.ts +63 -0
  25. package/dist/hooks/manager.d.ts.map +1 -0
  26. package/dist/hooks/manager.js +137 -0
  27. package/dist/hooks/manager.js.map +1 -0
  28. package/dist/hooks/types.d.ts +191 -0
  29. package/dist/hooks/types.d.ts.map +1 -0
  30. package/dist/hooks/types.js +6 -0
  31. package/dist/hooks/types.js.map +1 -0
  32. package/dist/index.d.ts +109 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +218 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/mcp/errors.d.ts +26 -0
  37. package/dist/mcp/errors.d.ts.map +1 -0
  38. package/dist/mcp/errors.js +43 -0
  39. package/dist/mcp/errors.js.map +1 -0
  40. package/dist/mcp/index.d.ts +11 -0
  41. package/dist/mcp/index.d.ts.map +1 -0
  42. package/dist/mcp/index.js +13 -0
  43. package/dist/mcp/index.js.map +1 -0
  44. package/dist/mcp/manager.d.ts +50 -0
  45. package/dist/mcp/manager.d.ts.map +1 -0
  46. package/dist/mcp/manager.js +170 -0
  47. package/dist/mcp/manager.js.map +1 -0
  48. package/dist/mcp/server-registry.d.ts +48 -0
  49. package/dist/mcp/server-registry.d.ts.map +1 -0
  50. package/dist/mcp/server-registry.js +121 -0
  51. package/dist/mcp/server-registry.js.map +1 -0
  52. package/dist/mcp/tool-adapter.d.ts +42 -0
  53. package/dist/mcp/tool-adapter.d.ts.map +1 -0
  54. package/dist/mcp/tool-adapter.js +89 -0
  55. package/dist/mcp/tool-adapter.js.map +1 -0
  56. package/dist/mcp/types.d.ts +74 -0
  57. package/dist/mcp/types.d.ts.map +1 -0
  58. package/dist/mcp/types.js +21 -0
  59. package/dist/mcp/types.js.map +1 -0
  60. package/dist/permissions/index.d.ts +3 -0
  61. package/dist/permissions/index.d.ts.map +1 -0
  62. package/dist/permissions/index.js +4 -0
  63. package/dist/permissions/index.js.map +1 -0
  64. package/dist/permissions/manager.d.ts +40 -0
  65. package/dist/permissions/manager.d.ts.map +1 -0
  66. package/dist/permissions/manager.js +115 -0
  67. package/dist/permissions/manager.js.map +1 -0
  68. package/dist/permissions/types.d.ts +124 -0
  69. package/dist/permissions/types.d.ts.map +1 -0
  70. package/dist/permissions/types.js +25 -0
  71. package/dist/permissions/types.js.map +1 -0
  72. package/dist/providers/anthropic.d.ts +18 -0
  73. package/dist/providers/anthropic.d.ts.map +1 -0
  74. package/dist/providers/anthropic.js +126 -0
  75. package/dist/providers/anthropic.js.map +1 -0
  76. package/dist/providers/base.d.ts +85 -0
  77. package/dist/providers/base.d.ts.map +1 -0
  78. package/dist/providers/base.js +36 -0
  79. package/dist/providers/base.js.map +1 -0
  80. package/dist/providers/google.d.ts +12 -0
  81. package/dist/providers/google.d.ts.map +1 -0
  82. package/dist/providers/google.js +123 -0
  83. package/dist/providers/google.js.map +1 -0
  84. package/dist/providers/openai.d.ts +12 -0
  85. package/dist/providers/openai.d.ts.map +1 -0
  86. package/dist/providers/openai.js +110 -0
  87. package/dist/providers/openai.js.map +1 -0
  88. package/dist/session/factory.d.ts +156 -0
  89. package/dist/session/factory.d.ts.map +1 -0
  90. package/dist/session/factory.js +311 -0
  91. package/dist/session/factory.js.map +1 -0
  92. package/dist/session/index.d.ts +8 -0
  93. package/dist/session/index.d.ts.map +1 -0
  94. package/dist/session/index.js +7 -0
  95. package/dist/session/index.js.map +1 -0
  96. package/dist/session/session.d.ts +144 -0
  97. package/dist/session/session.d.ts.map +1 -0
  98. package/dist/session/session.js +319 -0
  99. package/dist/session/session.js.map +1 -0
  100. package/dist/session/storage.d.ts +105 -0
  101. package/dist/session/storage.d.ts.map +1 -0
  102. package/dist/session/storage.js +148 -0
  103. package/dist/session/storage.js.map +1 -0
  104. package/dist/tools/ask-user-question.d.ts +31 -0
  105. package/dist/tools/ask-user-question.d.ts.map +1 -0
  106. package/dist/tools/ask-user-question.js +66 -0
  107. package/dist/tools/ask-user-question.js.map +1 -0
  108. package/dist/tools/bash-output.d.ts +22 -0
  109. package/dist/tools/bash-output.d.ts.map +1 -0
  110. package/dist/tools/bash-output.js +43 -0
  111. package/dist/tools/bash-output.js.map +1 -0
  112. package/dist/tools/bash.d.ts +36 -0
  113. package/dist/tools/bash.d.ts.map +1 -0
  114. package/dist/tools/bash.js +161 -0
  115. package/dist/tools/bash.js.map +1 -0
  116. package/dist/tools/edit.d.ts +24 -0
  117. package/dist/tools/edit.d.ts.map +1 -0
  118. package/dist/tools/edit.js +83 -0
  119. package/dist/tools/edit.js.map +1 -0
  120. package/dist/tools/glob.d.ts +22 -0
  121. package/dist/tools/glob.d.ts.map +1 -0
  122. package/dist/tools/glob.js +248 -0
  123. package/dist/tools/glob.js.map +1 -0
  124. package/dist/tools/grep.d.ts +39 -0
  125. package/dist/tools/grep.d.ts.map +1 -0
  126. package/dist/tools/grep.js +312 -0
  127. package/dist/tools/grep.js.map +1 -0
  128. package/dist/tools/kill-bash.d.ts +19 -0
  129. package/dist/tools/kill-bash.d.ts.map +1 -0
  130. package/dist/tools/kill-bash.js +64 -0
  131. package/dist/tools/kill-bash.js.map +1 -0
  132. package/dist/tools/read.d.ts +26 -0
  133. package/dist/tools/read.d.ts.map +1 -0
  134. package/dist/tools/read.js +87 -0
  135. package/dist/tools/read.js.map +1 -0
  136. package/dist/tools/registry.d.ts +32 -0
  137. package/dist/tools/registry.d.ts.map +1 -0
  138. package/dist/tools/registry.js +91 -0
  139. package/dist/tools/registry.js.map +1 -0
  140. package/dist/tools/task-create.d.ts +22 -0
  141. package/dist/tools/task-create.d.ts.map +1 -0
  142. package/dist/tools/task-create.js +42 -0
  143. package/dist/tools/task-create.js.map +1 -0
  144. package/dist/tools/task-get.d.ts +19 -0
  145. package/dist/tools/task-get.d.ts.map +1 -0
  146. package/dist/tools/task-get.js +38 -0
  147. package/dist/tools/task-get.js.map +1 -0
  148. package/dist/tools/task-list.d.ts +18 -0
  149. package/dist/tools/task-list.d.ts.map +1 -0
  150. package/dist/tools/task-list.js +27 -0
  151. package/dist/tools/task-list.js.map +1 -0
  152. package/dist/tools/task-storage.d.ts +6 -0
  153. package/dist/tools/task-storage.d.ts.map +1 -0
  154. package/dist/tools/task-storage.js +83 -0
  155. package/dist/tools/task-storage.js.map +1 -0
  156. package/dist/tools/task-update.d.ts +28 -0
  157. package/dist/tools/task-update.d.ts.map +1 -0
  158. package/dist/tools/task-update.js +118 -0
  159. package/dist/tools/task-update.js.map +1 -0
  160. package/dist/tools/task.d.ts +80 -0
  161. package/dist/tools/task.d.ts.map +1 -0
  162. package/dist/tools/task.js +99 -0
  163. package/dist/tools/task.js.map +1 -0
  164. package/dist/tools/web-fetch.d.ts +21 -0
  165. package/dist/tools/web-fetch.d.ts.map +1 -0
  166. package/dist/tools/web-fetch.js +124 -0
  167. package/dist/tools/web-fetch.js.map +1 -0
  168. package/dist/tools/web-search.d.ts +20 -0
  169. package/dist/tools/web-search.d.ts.map +1 -0
  170. package/dist/tools/web-search.js +127 -0
  171. package/dist/tools/web-search.js.map +1 -0
  172. package/dist/tools/write.d.ts +22 -0
  173. package/dist/tools/write.d.ts.map +1 -0
  174. package/dist/tools/write.js +46 -0
  175. package/dist/tools/write.js.map +1 -0
  176. package/dist/types/messages.d.ts +138 -0
  177. package/dist/types/messages.d.ts.map +1 -0
  178. package/dist/types/messages.js +88 -0
  179. package/dist/types/messages.js.map +1 -0
  180. package/dist/types/task.d.ts +29 -0
  181. package/dist/types/task.d.ts.map +1 -0
  182. package/dist/types/task.js +5 -0
  183. package/dist/types/task.js.map +1 -0
  184. package/dist/types/tools.d.ts +56 -0
  185. package/dist/types/tools.d.ts.map +1 -0
  186. package/dist/types/tools.js +25 -0
  187. package/dist/types/tools.js.map +1 -0
  188. package/dist/utils/logger.d.ts +19 -0
  189. package/dist/utils/logger.d.ts.map +1 -0
  190. package/dist/utils/logger.js +46 -0
  191. package/dist/utils/logger.js.map +1 -0
  192. package/dist/utils/uuid.d.ts +3 -0
  193. package/dist/utils/uuid.d.ts.map +1 -0
  194. package/dist/utils/uuid.js +5 -0
  195. package/dist/utils/uuid.js.map +1 -0
  196. package/package.json +38 -0
@@ -0,0 +1,674 @@
1
+ /**
2
+ * ReAct (Reasoning + Acting) loop implementation
3
+ * Core agent logic for tool use and reasoning
4
+ */
5
+ import { logger } from '../utils/logger';
6
+ import { generateUUID } from '../utils/uuid';
7
+ import { createUserMessage, createSystemMessage, createAssistantMessage, createToolResultMessage, createCompactBoundaryMessage, } from '../types/messages';
8
+ import { HookManager } from '../hooks/manager';
9
+ import { createPreToolUseInput, createPostToolUseInput, createSessionStartInput, createSessionEndInput, createPermissionRequestInput, createPostToolUseFailureInput, createUserPromptSubmitInput, createStopInput, createPreCompactInput, } from '../hooks/inputs';
10
+ import { PermissionManager } from '../permissions/manager';
11
+ export class ReActLoop {
12
+ provider;
13
+ toolRegistry;
14
+ config;
15
+ sessionId;
16
+ hookManager;
17
+ permissionManager;
18
+ constructor(provider, toolRegistry, config, sessionId) {
19
+ this.provider = provider;
20
+ this.toolRegistry = toolRegistry;
21
+ this.config = {
22
+ maxTurns: config.maxTurns,
23
+ systemPrompt: config.systemPrompt,
24
+ allowedTools: config.allowedTools,
25
+ cwd: config.cwd ?? process.cwd(),
26
+ env: config.env ?? {},
27
+ abortController: config.abortController,
28
+ permissionMode: config.permissionMode,
29
+ allowDangerouslySkipPermissions: config.allowDangerouslySkipPermissions,
30
+ canUseTool: config.canUseTool,
31
+ mcpServers: config.mcpServers,
32
+ hooks: config.hooks,
33
+ };
34
+ this.sessionId = sessionId ?? generateUUID();
35
+ // Initialize HookManager
36
+ if (config.hooks instanceof HookManager) {
37
+ this.hookManager = config.hooks;
38
+ }
39
+ else if (config.hooks) {
40
+ this.hookManager = new HookManager(config.hooks);
41
+ }
42
+ else {
43
+ this.hookManager = new HookManager();
44
+ }
45
+ // Initialize PermissionManager
46
+ this.permissionManager = new PermissionManager({
47
+ mode: config.permissionMode ?? 'default',
48
+ allowDangerouslySkipPermissions: config.allowDangerouslySkipPermissions ?? false,
49
+ canUseTool: config.canUseTool,
50
+ });
51
+ }
52
+ /**
53
+ * Get the permission manager instance
54
+ * Used for testing and inspection
55
+ */
56
+ getPermissionManager() {
57
+ return this.permissionManager;
58
+ }
59
+ async run(userPrompt) {
60
+ const messages = [];
61
+ // Add system message metadata if system prompt is configured
62
+ // The actual system prompt content is passed via ChatOptions to the provider
63
+ if (this.config.systemPrompt) {
64
+ messages.push(createSystemMessage(this.provider.getModel(), this.provider.constructor.name.toLowerCase().replace('provider', ''), this.config.allowedTools ?? this.toolRegistry.getAll().map((t) => t.name), this.config.cwd ?? process.cwd(), this.sessionId, generateUUID(), {
65
+ permissionMode: this.config.permissionMode,
66
+ }));
67
+ }
68
+ // Add user message
69
+ messages.push(createUserMessage(userPrompt, this.sessionId, generateUUID()));
70
+ // Trigger UserPromptSubmit hook
71
+ const userPromptSubmitInput = createUserPromptSubmitInput(this.sessionId, this.config.cwd ?? process.cwd(), userPrompt);
72
+ await this.hookManager.emit('UserPromptSubmit', userPromptSubmitInput, undefined);
73
+ let turnCount = 0;
74
+ let totalInputTokens = 0;
75
+ let totalOutputTokens = 0;
76
+ // Get allowed tools
77
+ const availableTools = this.config.allowedTools
78
+ ? this.toolRegistry.getAllowedTools(this.config.allowedTools)
79
+ : this.toolRegistry.getAll();
80
+ const toolDefinitions = availableTools.map((tool) => ({
81
+ type: 'function',
82
+ function: {
83
+ name: tool.name,
84
+ description: tool.description,
85
+ parameters: tool.parameters,
86
+ },
87
+ }));
88
+ const toolContext = {
89
+ cwd: this.config.cwd,
90
+ env: this.config.env,
91
+ abortController: this.config.abortController,
92
+ provider: this.provider,
93
+ };
94
+ while (turnCount < this.config.maxTurns) {
95
+ // Check for abort
96
+ if (this.config.abortController?.signal.aborted) {
97
+ return {
98
+ result: 'Operation aborted',
99
+ messages,
100
+ turnCount,
101
+ usage: {
102
+ input_tokens: totalInputTokens,
103
+ output_tokens: totalOutputTokens,
104
+ },
105
+ isError: true,
106
+ };
107
+ }
108
+ turnCount++;
109
+ // Check for abort again after incrementing turn count
110
+ // This prevents race conditions where abort was triggered between the start of the loop and turnCount++
111
+ if (this.config.abortController?.signal.aborted) {
112
+ return {
113
+ result: 'Operation aborted',
114
+ messages,
115
+ turnCount,
116
+ usage: {
117
+ input_tokens: totalInputTokens,
118
+ output_tokens: totalOutputTokens,
119
+ },
120
+ isError: true,
121
+ };
122
+ }
123
+ // Call LLM
124
+ let assistantMessage;
125
+ try {
126
+ assistantMessage = await this.callLLM(messages, toolDefinitions, (tokens) => {
127
+ totalInputTokens += tokens.input;
128
+ totalOutputTokens += tokens.output;
129
+ });
130
+ }
131
+ catch (error) {
132
+ // Handle abort error from provider
133
+ if (error instanceof Error && error.message === 'Operation aborted') {
134
+ return {
135
+ result: 'Operation aborted',
136
+ messages,
137
+ turnCount,
138
+ usage: {
139
+ input_tokens: totalInputTokens,
140
+ output_tokens: totalOutputTokens,
141
+ },
142
+ isError: true,
143
+ };
144
+ }
145
+ throw error;
146
+ }
147
+ messages.push(assistantMessage);
148
+ // Check for auto-compaction after each LLM call
149
+ if (this.config.autoCompactThreshold !== undefined &&
150
+ totalInputTokens > this.config.autoCompactThreshold) {
151
+ logger.debug('[ReActLoop] Auto-compaction triggered:', {
152
+ threshold: this.config.autoCompactThreshold,
153
+ currentTokens: totalInputTokens,
154
+ });
155
+ const compactResult = await this.compact(messages, 'auto', totalInputTokens);
156
+ if (compactResult.summaryGenerated) {
157
+ // Replace messages with compacted version
158
+ messages.length = 0;
159
+ messages.push(...compactResult.messages);
160
+ logger.debug('[ReActLoop] Auto-compaction completed:', {
161
+ preservedRounds: compactResult.preservedRounds,
162
+ newMessageCount: messages.length,
163
+ });
164
+ }
165
+ }
166
+ // Check if assistant wants to use tools
167
+ const assistantToolCalls = assistantMessage.message.tool_calls;
168
+ if (assistantToolCalls && assistantToolCalls.length > 0) {
169
+ // Execute tools and add results
170
+ for (const toolCall of assistantToolCalls) {
171
+ const result = await this.executeTool(toolCall, availableTools, toolContext);
172
+ messages.push(createToolResultMessage(toolCall.id, toolCall.function.name, result.content, result.isError, this.sessionId, generateUUID()));
173
+ }
174
+ }
175
+ else {
176
+ // No tool calls - agent produced final answer
177
+ // Trigger Stop hook — allows hooks to request continuation via { continue: true }
178
+ const shouldContinue = await this.emitStopHook();
179
+ if (shouldContinue) {
180
+ // Hook requested continuation — keep looping
181
+ continue;
182
+ }
183
+ const textContent = assistantMessage.message.content.find((c) => c.type === 'text');
184
+ return {
185
+ result: textContent?.text ?? '',
186
+ messages,
187
+ turnCount,
188
+ usage: {
189
+ input_tokens: totalInputTokens,
190
+ output_tokens: totalOutputTokens,
191
+ },
192
+ };
193
+ }
194
+ }
195
+ // Max turns reached
196
+ return {
197
+ result: 'Maximum turns reached without completion',
198
+ messages,
199
+ turnCount,
200
+ usage: {
201
+ input_tokens: totalInputTokens,
202
+ output_tokens: totalOutputTokens,
203
+ },
204
+ isError: true,
205
+ };
206
+ }
207
+ /**
208
+ * Run the ReAct loop with streaming output
209
+ * Yields events for assistant messages, tool results, usage stats, and completion
210
+ * @param userPrompt - The current user message content
211
+ * @param history - Previous conversation messages (optional)
212
+ */
213
+ async *runStream(userPrompt, history = []) {
214
+ // Trigger SessionStart hook
215
+ const sessionStartInput = createSessionStartInput(this.sessionId, this.config.cwd ?? process.cwd(), history.length > 0 ? 'resume' : 'startup');
216
+ await this.hookManager.emit('SessionStart', sessionStartInput, undefined);
217
+ // Trigger UserPromptSubmit hook
218
+ const userPromptSubmitInput = createUserPromptSubmitInput(this.sessionId, this.config.cwd ?? process.cwd(), userPrompt);
219
+ await this.hookManager.emit('UserPromptSubmit', userPromptSubmitInput, undefined);
220
+ // Check if history already has a system message (metadata)
221
+ const hasSystemInHistory = history.some((msg) => msg.type === 'system');
222
+ const messages = [
223
+ // Add system message metadata if system prompt is configured and not already in history
224
+ // The actual system prompt content is passed via ChatOptions to the provider
225
+ ...(this.config.systemPrompt && !hasSystemInHistory
226
+ ? [
227
+ createSystemMessage(this.provider.getModel(), this.provider.constructor.name.toLowerCase().replace('provider', ''), this.config.allowedTools ?? this.toolRegistry.getAll().map((t) => t.name), this.config.cwd ?? process.cwd(), this.sessionId, generateUUID(), {
228
+ permissionMode: this.config.permissionMode,
229
+ }),
230
+ ]
231
+ : []),
232
+ // Add history messages
233
+ ...history,
234
+ // Add current user message
235
+ createUserMessage(userPrompt, this.sessionId, generateUUID()),
236
+ ];
237
+ logger.debug('[ReActLoop] Total messages:', messages.length);
238
+ logger.debug('[ReActLoop] Messages:', JSON.stringify(messages, null, 2));
239
+ let turnCount = 0;
240
+ let totalInputTokens = 0;
241
+ let totalOutputTokens = 0;
242
+ // Get allowed tools
243
+ const availableTools = this.config.allowedTools
244
+ ? this.toolRegistry.getAllowedTools(this.config.allowedTools)
245
+ : this.toolRegistry.getAll();
246
+ const toolDefinitions = availableTools.map((tool) => ({
247
+ type: 'function',
248
+ function: {
249
+ name: tool.name,
250
+ description: tool.description,
251
+ parameters: tool.parameters,
252
+ },
253
+ }));
254
+ const toolContext = {
255
+ cwd: this.config.cwd,
256
+ env: this.config.env,
257
+ abortController: this.config.abortController,
258
+ provider: this.provider,
259
+ };
260
+ while (turnCount < this.config.maxTurns) {
261
+ // Check for abort
262
+ if (this.config.abortController?.signal.aborted) {
263
+ yield {
264
+ type: 'done',
265
+ result: 'Operation aborted',
266
+ };
267
+ // Trigger SessionEnd hook on abort
268
+ const sessionEndInput = createSessionEndInput(this.sessionId, this.config.cwd ?? process.cwd(), 'abort');
269
+ await this.hookManager.emit('SessionEnd', sessionEndInput, undefined);
270
+ return;
271
+ }
272
+ turnCount++;
273
+ // Call LLM
274
+ let assistantMessage;
275
+ try {
276
+ assistantMessage = await this.callLLM(messages, toolDefinitions, (tokens) => {
277
+ totalInputTokens += tokens.input;
278
+ totalOutputTokens += tokens.output;
279
+ });
280
+ }
281
+ catch (error) {
282
+ // Handle abort error from provider
283
+ if (error instanceof Error && error.message === 'Operation aborted') {
284
+ yield { type: 'done', result: 'Operation aborted' };
285
+ // Trigger SessionEnd hook on abort
286
+ const sessionEndInput = createSessionEndInput(this.sessionId, this.config.cwd ?? process.cwd(), 'abort');
287
+ await this.hookManager.emit('SessionEnd', sessionEndInput, undefined);
288
+ return;
289
+ }
290
+ throw error;
291
+ }
292
+ messages.push(assistantMessage);
293
+ yield { type: 'assistant', message: assistantMessage };
294
+ // Check for auto-compaction after each LLM call
295
+ if (this.config.autoCompactThreshold !== undefined &&
296
+ totalInputTokens > this.config.autoCompactThreshold) {
297
+ logger.debug('[ReActLoop] Auto-compaction triggered in stream:', {
298
+ threshold: this.config.autoCompactThreshold,
299
+ currentTokens: totalInputTokens,
300
+ });
301
+ const compactResult = await this.compact(messages, 'auto', totalInputTokens);
302
+ if (compactResult.summaryGenerated) {
303
+ // Replace messages with compacted version
304
+ messages.length = 0;
305
+ messages.push(...compactResult.messages);
306
+ logger.debug('[ReActLoop] Auto-compaction completed in stream:', {
307
+ preservedRounds: compactResult.preservedRounds,
308
+ newMessageCount: messages.length,
309
+ });
310
+ }
311
+ }
312
+ // Check if assistant wants to use tools
313
+ const assistantToolCalls = assistantMessage.message.tool_calls;
314
+ if (assistantToolCalls && assistantToolCalls.length > 0) {
315
+ // Execute tools and add results
316
+ for (const toolCall of assistantToolCalls) {
317
+ const result = await this.executeTool(toolCall, availableTools, toolContext);
318
+ const toolResultMessage = createToolResultMessage(toolCall.id, toolCall.function.name, result.content, result.isError, this.sessionId, generateUUID());
319
+ messages.push(toolResultMessage);
320
+ yield { type: 'tool_result', message: toolResultMessage };
321
+ }
322
+ }
323
+ else {
324
+ // No tool calls - agent produced final answer
325
+ // Trigger Stop hook — allows hooks to request continuation via { continue: true }
326
+ const shouldContinue = await this.emitStopHook();
327
+ if (shouldContinue) {
328
+ // Hook requested continuation — keep looping
329
+ continue;
330
+ }
331
+ const textContent = assistantMessage.message.content.find((c) => c.type === 'text');
332
+ const result = textContent?.text ?? '';
333
+ yield { type: 'usage', usage: { input_tokens: totalInputTokens, output_tokens: totalOutputTokens } };
334
+ yield { type: 'done', result };
335
+ // Trigger SessionEnd hook on successful completion
336
+ const sessionEndInput = createSessionEndInput(this.sessionId, this.config.cwd ?? process.cwd(), 'completed');
337
+ await this.hookManager.emit('SessionEnd', sessionEndInput, undefined);
338
+ return;
339
+ }
340
+ }
341
+ // Max turns reached
342
+ yield { type: 'usage', usage: { input_tokens: totalInputTokens, output_tokens: totalOutputTokens } };
343
+ yield { type: 'done', result: 'Maximum turns reached without completion' };
344
+ // Trigger SessionEnd hook
345
+ const sessionEndInput = createSessionEndInput(this.sessionId, this.config.cwd ?? process.cwd(), 'max_turns_reached');
346
+ await this.hookManager.emit('SessionEnd', sessionEndInput, undefined);
347
+ }
348
+ /**
349
+ * Get the hook manager instance
350
+ * Used for testing and inspection
351
+ */
352
+ getHookManager() {
353
+ return this.hookManager;
354
+ }
355
+ /**
356
+ * Compact conversation history to reduce token usage.
357
+ * Generates a summary of older messages and preserves recent rounds.
358
+ *
359
+ * @param messages - Current conversation messages
360
+ * @param trigger - What triggered the compaction ('manual' or 'auto')
361
+ * @param preTokens - Token count before compaction
362
+ * @returns Compacted messages and metadata
363
+ */
364
+ async compact(messages, trigger, preTokens) {
365
+ const preserveRecentRounds = this.config.preserveRecentRounds ?? 2;
366
+ // Separate system messages from conversation messages
367
+ const systemInitMsg = messages.find((m) => m.type === 'system' && 'subtype' in m && m.subtype === 'init');
368
+ // Get conversation messages (non-system)
369
+ const conversationMessages = messages.filter((m) => m.type !== 'system' || ('subtype' in m && m.subtype !== 'init'));
370
+ // Group messages into rounds (user -> assistant -> optional tool results)
371
+ const rounds = [];
372
+ let currentRound = [];
373
+ for (const msg of conversationMessages) {
374
+ if (msg.type === 'user') {
375
+ // Start a new round
376
+ if (currentRound.length > 0) {
377
+ rounds.push(currentRound);
378
+ }
379
+ currentRound = [msg];
380
+ }
381
+ else {
382
+ // Add to current round (assistant or tool_result)
383
+ currentRound.push(msg);
384
+ }
385
+ }
386
+ // Don't forget the last round
387
+ if (currentRound.length > 0) {
388
+ rounds.push(currentRound);
389
+ }
390
+ // Determine which rounds to preserve and which to summarize
391
+ const totalRounds = rounds.length;
392
+ const roundsToPreserve = Math.min(preserveRecentRounds, totalRounds);
393
+ const roundsToSummarize = totalRounds - roundsToPreserve;
394
+ if (roundsToSummarize <= 0) {
395
+ // Nothing to compact
396
+ return {
397
+ messages,
398
+ preTokens,
399
+ trigger,
400
+ preservedRounds: totalRounds,
401
+ summaryGenerated: false,
402
+ };
403
+ }
404
+ // Trigger PreCompact hook
405
+ const preCompactInput = createPreCompactInput(this.sessionId, this.config.cwd ?? process.cwd(), trigger, null // custom_instructions - can be modified by hooks in future
406
+ );
407
+ const preCompactResults = await this.hookManager.emit('PreCompact', preCompactInput, undefined);
408
+ // Check if any hook blocked the compaction
409
+ for (const result of preCompactResults) {
410
+ if (result && typeof result === 'object' && 'stopReason' in result) {
411
+ const syncResult = result;
412
+ if (syncResult.stopReason) {
413
+ logger.debug('[ReActLoop] PreCompact hook blocked compaction:', syncResult.stopReason);
414
+ return {
415
+ messages,
416
+ preTokens,
417
+ trigger,
418
+ preservedRounds: totalRounds,
419
+ summaryGenerated: false,
420
+ };
421
+ }
422
+ }
423
+ }
424
+ // Messages to summarize (older rounds)
425
+ const messagesToSummarize = rounds.slice(0, roundsToSummarize).flat();
426
+ // Generate summary
427
+ const summary = await this.generateSummary(messagesToSummarize);
428
+ // Create compact boundary message
429
+ const boundaryMessage = createCompactBoundaryMessage(this.sessionId, generateUUID(), trigger, preTokens);
430
+ // Create summary message as an assistant message
431
+ const summaryMessage = createAssistantMessage([{ type: 'text', text: `Summary of previous conversation:\n${summary}` }], this.sessionId, generateUUID());
432
+ // Messages to preserve (recent rounds)
433
+ const preservedMessages = rounds.slice(roundsToSummarize).flat();
434
+ // Build compacted message list
435
+ const compactedMessages = [
436
+ ...(systemInitMsg ? [systemInitMsg] : []),
437
+ boundaryMessage,
438
+ summaryMessage,
439
+ ...preservedMessages,
440
+ ];
441
+ return {
442
+ messages: compactedMessages,
443
+ preTokens,
444
+ trigger,
445
+ preservedRounds: roundsToPreserve,
446
+ summaryGenerated: true,
447
+ };
448
+ }
449
+ /**
450
+ * Generate a summary of conversation messages using the LLM.
451
+ */
452
+ async generateSummary(messages) {
453
+ const summaryPrompt = `Please summarize the following conversation history, keeping key information:
454
+ 1. The user's original request/goal
455
+ 2. Major steps completed so far
456
+ 3. Important file modifications or code changes
457
+ 4. Current pending tasks or unfinished work
458
+ 5. Any significant errors and their solutions
459
+
460
+ Conversation history:
461
+ ${JSON.stringify(messages, null, 2)}
462
+
463
+ Generate a concise but comprehensive summary.`;
464
+ try {
465
+ // Use the provider to generate summary
466
+ const chatOptions = {
467
+ systemInstruction: 'You are a helpful assistant that summarizes conversations concisely.',
468
+ };
469
+ // Create a minimal message list for summary generation
470
+ const summaryMessages = [
471
+ createUserMessage(summaryPrompt, this.sessionId, generateUUID()),
472
+ ];
473
+ const stream = this.provider.chat(summaryMessages, [], this.config.abortController?.signal, chatOptions);
474
+ let summary = '';
475
+ for await (const chunk of stream) {
476
+ if (chunk.type === 'content' && chunk.delta) {
477
+ summary += chunk.delta;
478
+ }
479
+ }
480
+ return summary.trim() || 'No summary available.';
481
+ }
482
+ catch (error) {
483
+ logger.warn('[ReActLoop] Failed to generate summary:', error);
484
+ return 'Summary generation failed. Continuing with preserved context.';
485
+ }
486
+ }
487
+ /**
488
+ * Emit the Stop hook and check if any handler requests continuation.
489
+ * Returns true if the loop should continue (hook returned { continue: true }).
490
+ */
491
+ async emitStopHook() {
492
+ const stopInput = createStopInput(this.sessionId, this.config.cwd ?? process.cwd(), true // stop_hook_active
493
+ );
494
+ const results = await this.hookManager.emit('Stop', stopInput, undefined);
495
+ // Check if any hook result requests continuation
496
+ for (const result of results) {
497
+ if (result && typeof result === 'object' && 'continue' in result) {
498
+ const syncResult = result;
499
+ if (syncResult.continue === true) {
500
+ logger.debug('[ReActLoop] Stop hook requested continuation');
501
+ return true;
502
+ }
503
+ }
504
+ }
505
+ return false;
506
+ }
507
+ async callLLM(messages, tools, onUsage) {
508
+ // Pass system prompt via ChatOptions, not in messages
509
+ const chatOptions = {
510
+ systemInstruction: this.config.systemPrompt,
511
+ };
512
+ const stream = this.provider.chat(messages, tools, this.config.abortController?.signal, chatOptions);
513
+ let content = '';
514
+ const toolCalls = new Map();
515
+ let inputTokens = 0;
516
+ let outputTokens = 0;
517
+ for await (const chunk of stream) {
518
+ switch (chunk.type) {
519
+ case 'content':
520
+ if (chunk.delta) {
521
+ content += chunk.delta;
522
+ }
523
+ break;
524
+ case 'tool_call':
525
+ if (chunk.tool_call) {
526
+ const existing = toolCalls.get(chunk.tool_call.id);
527
+ if (existing) {
528
+ existing.function.arguments += chunk.tool_call.arguments;
529
+ }
530
+ else {
531
+ toolCalls.set(chunk.tool_call.id, {
532
+ id: chunk.tool_call.id,
533
+ type: 'function',
534
+ function: {
535
+ name: chunk.tool_call.name,
536
+ arguments: chunk.tool_call.arguments,
537
+ },
538
+ });
539
+ }
540
+ }
541
+ break;
542
+ case 'usage':
543
+ if (chunk.usage) {
544
+ inputTokens = chunk.usage.input_tokens;
545
+ outputTokens = chunk.usage.output_tokens;
546
+ }
547
+ break;
548
+ case 'error':
549
+ if (chunk.error) {
550
+ throw new Error(chunk.error);
551
+ }
552
+ break;
553
+ }
554
+ }
555
+ onUsage({ input: inputTokens, output: outputTokens });
556
+ const contentBlocks = content
557
+ ? [{ type: 'text', text: content }]
558
+ : [];
559
+ return createAssistantMessage(contentBlocks, this.sessionId, generateUUID(), null, toolCalls.size > 0 ? Array.from(toolCalls.values()) : undefined);
560
+ }
561
+ async executeTool(toolCall, availableTools, context) {
562
+ const tool = availableTools.find((t) => t.name === toolCall.function.name);
563
+ if (!tool) {
564
+ return {
565
+ content: `Error: Tool "${toolCall.function.name}" not found`,
566
+ isError: true,
567
+ };
568
+ }
569
+ let args;
570
+ try {
571
+ args = JSON.parse(toolCall.function.arguments);
572
+ }
573
+ catch (error) {
574
+ return {
575
+ content: `Error: Invalid JSON arguments - ${error instanceof Error ? error.message : String(error)}`,
576
+ isError: true,
577
+ };
578
+ }
579
+ const cwd = this.config.cwd ?? process.cwd();
580
+ // Special handling for AskUserQuestion tool
581
+ if (toolCall.function.name === 'AskUserQuestion') {
582
+ // AskUserQuestion requires canUseTool callback
583
+ if (!this.config.canUseTool) {
584
+ return {
585
+ content: 'Error: AskUserQuestion requires a canUseTool callback to be configured. The tool cannot function without user interaction capability.',
586
+ isError: true,
587
+ };
588
+ }
589
+ // If canUseTool exists, it will be handled by the normal permission flow
590
+ // The canUseTool callback will fill in the answers via updatedInput
591
+ }
592
+ // Trigger PreToolUse hook
593
+ const preToolInput = createPreToolUseInput(this.sessionId, cwd, toolCall.function.name, args);
594
+ const preToolResults = await this.hookManager.emitForTool('PreToolUse', preToolInput, toolCall.function.name, toolCall.id);
595
+ // Check if any PreToolUse hook denied the tool
596
+ const hookDenial = preToolResults.find((r) => r !== null && r !== undefined && typeof r === 'object' && 'hookSpecificOutput' in r &&
597
+ r.hookSpecificOutput !== undefined &&
598
+ r.hookSpecificOutput?.hookEventName === 'PreToolUse' &&
599
+ r.hookSpecificOutput?.permissionDecision === 'deny');
600
+ if (hookDenial) {
601
+ const errorMsg = hookDenial.hookSpecificOutput?.permissionDecisionReason || 'Tool denied by PreToolUse hook';
602
+ // Trigger PermissionRequest hook
603
+ const permissionRequestInput = createPermissionRequestInput(this.sessionId, cwd, toolCall.function.name, args);
604
+ await this.hookManager.emit('PermissionRequest', permissionRequestInput, toolCall.id);
605
+ return {
606
+ content: `Error: ${errorMsg}`,
607
+ isError: true,
608
+ };
609
+ }
610
+ // Apply any input modifications from PreToolUse hooks
611
+ let modifiedInput = args;
612
+ const inputModification = preToolResults.find((r) => r !== null && r !== undefined && typeof r === 'object' && 'hookSpecificOutput' in r &&
613
+ r.hookSpecificOutput !== undefined &&
614
+ r.hookSpecificOutput?.hookEventName === 'PreToolUse' &&
615
+ r.hookSpecificOutput?.updatedInput !== undefined);
616
+ if (inputModification?.hookSpecificOutput?.updatedInput) {
617
+ modifiedInput = inputModification.hookSpecificOutput.updatedInput;
618
+ }
619
+ // Check permissions using PermissionManager
620
+ // For AskUserQuestion, add 60-second timeout
621
+ let permissionResult;
622
+ if (toolCall.function.name === 'AskUserQuestion') {
623
+ const timeoutMs = 60_000;
624
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('AskUserQuestion timed out after 60 seconds')), timeoutMs));
625
+ try {
626
+ permissionResult = await Promise.race([
627
+ this.permissionManager.checkPermission(toolCall.function.name, modifiedInput, { signal: this.config.abortController?.signal ?? new AbortController().signal }),
628
+ timeoutPromise,
629
+ ]);
630
+ }
631
+ catch (error) {
632
+ return {
633
+ content: `Error: ${error instanceof Error ? error.message : 'AskUserQuestion timed out'}`,
634
+ isError: true,
635
+ };
636
+ }
637
+ }
638
+ else {
639
+ permissionResult = await this.permissionManager.checkPermission(toolCall.function.name, modifiedInput, { signal: this.config.abortController?.signal ?? new AbortController().signal });
640
+ }
641
+ if (!permissionResult.approved) {
642
+ // Trigger PermissionRequest hook on denial
643
+ const permissionRequestInput = createPermissionRequestInput(this.sessionId, cwd, toolCall.function.name, modifiedInput);
644
+ await this.hookManager.emit('PermissionRequest', permissionRequestInput, toolCall.id);
645
+ return {
646
+ content: `Error: ${permissionResult.error || 'Permission denied'}`,
647
+ isError: true,
648
+ };
649
+ }
650
+ // Use modified input from permission check (if any)
651
+ const finalInput = permissionResult.updatedInput ?? modifiedInput;
652
+ try {
653
+ const result = await tool.handler(finalInput, context);
654
+ // Trigger PostToolUse hook
655
+ const postToolInput = createPostToolUseInput(this.sessionId, cwd, toolCall.function.name, finalInput, result);
656
+ await this.hookManager.emitForTool('PostToolUse', postToolInput, toolCall.function.name, toolCall.id);
657
+ return {
658
+ content: JSON.stringify(result),
659
+ isError: false,
660
+ };
661
+ }
662
+ catch (error) {
663
+ const errorMessage = error instanceof Error ? error.message : String(error);
664
+ // Trigger PostToolUseFailure hook
665
+ const postToolFailureInput = createPostToolUseFailureInput(this.sessionId, cwd, toolCall.function.name, finalInput, errorMessage);
666
+ await this.hookManager.emit('PostToolUseFailure', postToolFailureInput, toolCall.id);
667
+ return {
668
+ content: `Error: ${errorMessage}`,
669
+ isError: true,
670
+ };
671
+ }
672
+ }
673
+ }
674
+ //# sourceMappingURL=react-loop.js.map