nova-terminal-assistant 0.1.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.

Potentially problematic release.


This version of nova-terminal-assistant might be problematic. Click here for more details.

Files changed (192) hide show
  1. package/README.md +358 -0
  2. package/bin/nova +38 -0
  3. package/bin/nova.js +12 -0
  4. package/package.json +67 -0
  5. package/src/cli/commands/SmartCompletion.ts +458 -0
  6. package/src/cli/index.ts +5 -0
  7. package/src/cli/startup/IFlowRepl.ts +212 -0
  8. package/src/cli/startup/InkBasedRepl.ts +1056 -0
  9. package/src/cli/startup/InteractiveRepl.ts +2833 -0
  10. package/src/cli/startup/NovaApp.ts +1861 -0
  11. package/src/cli/startup/index.ts +4 -0
  12. package/src/cli/startup/parseArgs.ts +293 -0
  13. package/src/cli/test-modules.ts +27 -0
  14. package/src/cli/ui/IFlowDropdown.ts +425 -0
  15. package/src/cli/ui/ModernReplUI.ts +276 -0
  16. package/src/cli/ui/SimpleSelector2.ts +215 -0
  17. package/src/cli/ui/components/ConfirmDialog.ts +176 -0
  18. package/src/cli/ui/components/ErrorPanel.ts +364 -0
  19. package/src/cli/ui/components/InkAppRunner.tsx +67 -0
  20. package/src/cli/ui/components/InkComponents.tsx +613 -0
  21. package/src/cli/ui/components/NovaInkApp.tsx +312 -0
  22. package/src/cli/ui/components/ProgressBar.ts +177 -0
  23. package/src/cli/ui/components/ProgressIndicator.ts +298 -0
  24. package/src/cli/ui/components/QuickActions.ts +396 -0
  25. package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
  26. package/src/cli/ui/components/StatusBar.ts +194 -0
  27. package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
  28. package/src/cli/ui/components/index.ts +27 -0
  29. package/src/cli/ui/ink-prototype.tsx +347 -0
  30. package/src/cli/utils/CliUI.ts +336 -0
  31. package/src/cli/utils/CompletionHelper.ts +388 -0
  32. package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
  33. package/src/cli/utils/EnhancedCompleter.ts +513 -0
  34. package/src/cli/utils/ErrorEnhancer.ts +429 -0
  35. package/src/cli/utils/OutputFormatter.ts +193 -0
  36. package/src/cli/utils/index.ts +9 -0
  37. package/src/core/agents/AgentOrchestrator.ts +515 -0
  38. package/src/core/agents/index.ts +17 -0
  39. package/src/core/audit/AuditLogger.ts +509 -0
  40. package/src/core/audit/index.ts +11 -0
  41. package/src/core/auth/AuthManager.d.ts.map +1 -0
  42. package/src/core/auth/AuthManager.ts +138 -0
  43. package/src/core/auth/index.d.ts.map +1 -0
  44. package/src/core/auth/index.ts +2 -0
  45. package/src/core/config/ConfigManager.d.ts.map +1 -0
  46. package/src/core/config/ConfigManager.test.ts +183 -0
  47. package/src/core/config/ConfigManager.ts +1219 -0
  48. package/src/core/config/index.d.ts.map +1 -0
  49. package/src/core/config/index.ts +1 -0
  50. package/src/core/context/ContextBuilder.d.ts.map +1 -0
  51. package/src/core/context/ContextBuilder.ts +171 -0
  52. package/src/core/context/ContextCompressor.d.ts.map +1 -0
  53. package/src/core/context/ContextCompressor.ts +642 -0
  54. package/src/core/context/LayeredMemoryManager.ts +657 -0
  55. package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
  56. package/src/core/context/MemoryDiscovery.ts +175 -0
  57. package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
  58. package/src/core/context/defaultSystemPrompt.ts +35 -0
  59. package/src/core/context/index.d.ts.map +1 -0
  60. package/src/core/context/index.ts +22 -0
  61. package/src/core/extensions/SkillGenerator.ts +421 -0
  62. package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
  63. package/src/core/extensions/SkillInstaller.ts +257 -0
  64. package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
  65. package/src/core/extensions/SkillRegistry.ts +361 -0
  66. package/src/core/extensions/SkillValidator.ts +525 -0
  67. package/src/core/extensions/index.ts +15 -0
  68. package/src/core/index.d.ts.map +1 -0
  69. package/src/core/index.ts +42 -0
  70. package/src/core/mcp/McpManager.d.ts.map +1 -0
  71. package/src/core/mcp/McpManager.ts +632 -0
  72. package/src/core/mcp/index.d.ts.map +1 -0
  73. package/src/core/mcp/index.ts +2 -0
  74. package/src/core/model/ModelClient.d.ts.map +1 -0
  75. package/src/core/model/ModelClient.ts +217 -0
  76. package/src/core/model/ModelConnectionTester.ts +363 -0
  77. package/src/core/model/ModelValidator.ts +348 -0
  78. package/src/core/model/index.d.ts.map +1 -0
  79. package/src/core/model/index.ts +6 -0
  80. package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
  81. package/src/core/model/providers/AnthropicProvider.ts +279 -0
  82. package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
  83. package/src/core/model/providers/CodingPlanProvider.ts +210 -0
  84. package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
  85. package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
  86. package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
  87. package/src/core/model/providers/OllamaManager.ts +201 -0
  88. package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
  89. package/src/core/model/providers/OllamaProvider.ts +73 -0
  90. package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  91. package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
  92. package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
  93. package/src/core/model/providers/OpenAIProvider.ts +29 -0
  94. package/src/core/model/providers/index.d.ts.map +1 -0
  95. package/src/core/model/providers/index.ts +12 -0
  96. package/src/core/model/types.d.ts.map +1 -0
  97. package/src/core/model/types.ts +77 -0
  98. package/src/core/security/ApprovalManager.d.ts.map +1 -0
  99. package/src/core/security/ApprovalManager.ts +174 -0
  100. package/src/core/security/FileFilter.d.ts.map +1 -0
  101. package/src/core/security/FileFilter.ts +141 -0
  102. package/src/core/security/HookExecutor.d.ts.map +1 -0
  103. package/src/core/security/HookExecutor.ts +178 -0
  104. package/src/core/security/SandboxExecutor.ts +447 -0
  105. package/src/core/security/index.d.ts.map +1 -0
  106. package/src/core/security/index.ts +8 -0
  107. package/src/core/session/AgentLoop.d.ts.map +1 -0
  108. package/src/core/session/AgentLoop.ts +501 -0
  109. package/src/core/session/SessionManager.d.ts.map +1 -0
  110. package/src/core/session/SessionManager.test.ts +183 -0
  111. package/src/core/session/SessionManager.ts +460 -0
  112. package/src/core/session/index.d.ts.map +1 -0
  113. package/src/core/session/index.ts +3 -0
  114. package/src/core/telemetry/Telemetry.d.ts.map +1 -0
  115. package/src/core/telemetry/Telemetry.ts +90 -0
  116. package/src/core/telemetry/TelemetryService.ts +531 -0
  117. package/src/core/telemetry/index.d.ts.map +1 -0
  118. package/src/core/telemetry/index.ts +12 -0
  119. package/src/core/testing/AutoFixer.ts +385 -0
  120. package/src/core/testing/ErrorAnalyzer.ts +499 -0
  121. package/src/core/testing/TestRunner.ts +265 -0
  122. package/src/core/testing/agent-cli-tests.ts +538 -0
  123. package/src/core/testing/index.ts +11 -0
  124. package/src/core/tools/ToolRegistry.d.ts.map +1 -0
  125. package/src/core/tools/ToolRegistry.test.ts +206 -0
  126. package/src/core/tools/ToolRegistry.ts +260 -0
  127. package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
  128. package/src/core/tools/impl/EditFileTool.ts +97 -0
  129. package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
  130. package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
  131. package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
  132. package/src/core/tools/impl/MemoryTool.ts +102 -0
  133. package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
  134. package/src/core/tools/impl/ReadFileTool.ts +58 -0
  135. package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
  136. package/src/core/tools/impl/SearchContentTool.ts +94 -0
  137. package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
  138. package/src/core/tools/impl/SearchFileTool.ts +61 -0
  139. package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
  140. package/src/core/tools/impl/ShellTool.ts +118 -0
  141. package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
  142. package/src/core/tools/impl/TaskTool.ts +207 -0
  143. package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
  144. package/src/core/tools/impl/TodoTool.ts +122 -0
  145. package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
  146. package/src/core/tools/impl/WebFetchTool.ts +103 -0
  147. package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
  148. package/src/core/tools/impl/WebSearchTool.ts +89 -0
  149. package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
  150. package/src/core/tools/impl/WriteFileTool.ts +49 -0
  151. package/src/core/tools/impl/index.d.ts.map +1 -0
  152. package/src/core/tools/impl/index.ts +16 -0
  153. package/src/core/tools/index.d.ts.map +1 -0
  154. package/src/core/tools/index.ts +7 -0
  155. package/src/core/tools/schemas/execution.d.ts.map +1 -0
  156. package/src/core/tools/schemas/execution.ts +42 -0
  157. package/src/core/tools/schemas/file.d.ts.map +1 -0
  158. package/src/core/tools/schemas/file.ts +119 -0
  159. package/src/core/tools/schemas/index.d.ts.map +1 -0
  160. package/src/core/tools/schemas/index.ts +11 -0
  161. package/src/core/tools/schemas/memory.d.ts.map +1 -0
  162. package/src/core/tools/schemas/memory.ts +52 -0
  163. package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
  164. package/src/core/tools/schemas/orchestration.ts +44 -0
  165. package/src/core/tools/schemas/search.d.ts.map +1 -0
  166. package/src/core/tools/schemas/search.ts +112 -0
  167. package/src/core/tools/schemas/todo.d.ts.map +1 -0
  168. package/src/core/tools/schemas/todo.ts +32 -0
  169. package/src/core/tools/schemas/web.d.ts.map +1 -0
  170. package/src/core/tools/schemas/web.ts +86 -0
  171. package/src/core/types/config.d.ts.map +1 -0
  172. package/src/core/types/config.ts +200 -0
  173. package/src/core/types/errors.d.ts.map +1 -0
  174. package/src/core/types/errors.ts +204 -0
  175. package/src/core/types/index.d.ts.map +1 -0
  176. package/src/core/types/index.ts +8 -0
  177. package/src/core/types/session.d.ts.map +1 -0
  178. package/src/core/types/session.ts +216 -0
  179. package/src/core/types/tools.d.ts.map +1 -0
  180. package/src/core/types/tools.ts +157 -0
  181. package/src/core/utils/CheckpointManager.d.ts.map +1 -0
  182. package/src/core/utils/CheckpointManager.ts +327 -0
  183. package/src/core/utils/Logger.d.ts.map +1 -0
  184. package/src/core/utils/Logger.ts +98 -0
  185. package/src/core/utils/RetryManager.ts +471 -0
  186. package/src/core/utils/TokenCounter.d.ts.map +1 -0
  187. package/src/core/utils/TokenCounter.ts +414 -0
  188. package/src/core/utils/VectorMemoryStore.ts +440 -0
  189. package/src/core/utils/helpers.d.ts.map +1 -0
  190. package/src/core/utils/helpers.ts +89 -0
  191. package/src/core/utils/index.d.ts.map +1 -0
  192. package/src/core/utils/index.ts +19 -0
@@ -0,0 +1,501 @@
1
+ // ============================================================================
2
+ // AgentLoop - Core agent execution loop with tool use
3
+ // ============================================================================
4
+
5
+ import type {
6
+ SessionId,
7
+ Message,
8
+ ContentBlock,
9
+ ToolUseContent,
10
+ ToolResultContent,
11
+ ToolCallId,
12
+ SessionConfig,
13
+ ApprovalMode,
14
+ ApprovalRequest,
15
+ ApprovalResponse,
16
+ } from '../types/session.js';
17
+ import type { ToolDefinition, ToolHandlerInput, ToolHandlerOutput } from '../types/tools.js';
18
+ import type { ModelClient } from '../model/ModelClient.js';
19
+ import type { SessionManager } from './SessionManager.js';
20
+ import type { ContextCompressor } from '../context/ContextCompressor.js';
21
+ import { ToolRegistry } from '../tools/ToolRegistry.js';
22
+ import { createMessageId, createToolCallId } from '../types/session.js';
23
+ import { ToolError, CancelledError, SessionError, TimeoutError } from '../types/errors.js';
24
+
25
+ /** Maximum tool result size in characters (to prevent token waste) */
26
+ const MAX_TOOL_RESULT_CHARS = 10_000;
27
+
28
+ function truncateToolResult(content: string): string {
29
+ if (content.length <= MAX_TOOL_RESULT_CHARS) return content;
30
+ return content.slice(0, MAX_TOOL_RESULT_CHARS) +
31
+ `\n... [truncated: ${content.length} total chars, showing first ${MAX_TOOL_RESULT_CHARS}]`;
32
+ }
33
+
34
+ export interface AgentLoopOptions {
35
+ modelClient: ModelClient;
36
+ sessionManager: SessionManager;
37
+ toolRegistry: ToolRegistry;
38
+ /** System prompt to send with every request */
39
+ systemPrompt?: string;
40
+ /** Context compressor for intelligent context management */
41
+ contextCompressor?: ContextCompressor;
42
+ /** Maximum context window tokens (for compression decisions) */
43
+ maxContextTokens?: number;
44
+ /** Control thinking mode: enabled|disabled|auto */
45
+ thinking?: string;
46
+ /** Called when tool approval is needed */
47
+ onApprovalRequired?: (request: ApprovalRequest) => Promise<ApprovalResponse>;
48
+ /** Called for each streaming text delta */
49
+ onTextDelta?: (text: string) => void;
50
+ /** Called when a tool starts executing */
51
+ onToolStart?: (toolName: string, toolCallId: string) => void;
52
+ /** Called when a tool finishes */
53
+ onToolComplete?: (toolName: string, toolCallId: string, result: ToolHandlerOutput) => void;
54
+ /** Called when the loop iteration starts */
55
+ onTurnStart?: (turn: number) => void;
56
+ /** Called when the loop iteration ends */
57
+ onTurnEnd?: (turn: number) => void;
58
+ /** Called when thinking/reasoning content starts streaming */
59
+ onThinkingStart?: () => void;
60
+ /** Called for each thinking content delta */
61
+ onThinkingDelta?: (delta: string) => void;
62
+ /** Called when thinking content ends */
63
+ onThinkingEnd?: () => void;
64
+ /** Called when context compression happens */
65
+ onContextCompress?: (originalTokens: number, resultingTokens: number, action: string) => void;
66
+ }
67
+
68
+ export interface AgentLoopResult {
69
+ messages: Message[];
70
+ turnsCompleted: number;
71
+ totalInputTokens: number;
72
+ totalOutputTokens: number;
73
+ stopReason: string;
74
+ }
75
+
76
+ export class AgentLoop {
77
+ private modelClient: ModelClient;
78
+ private sessionManager: SessionManager;
79
+ private toolRegistry: ToolRegistry;
80
+ private options: Omit<AgentLoopOptions, 'modelClient' | 'sessionManager' | 'toolRegistry'>;
81
+ private systemPrompt?: string;
82
+ private abortController: AbortController | null = null;
83
+ private isRunning = false;
84
+ private contextCompressor?: ContextCompressor;
85
+ private maxContextTokens: number;
86
+ private thinking?: string;
87
+
88
+ constructor(options: AgentLoopOptions) {
89
+ this.modelClient = options.modelClient;
90
+ this.sessionManager = options.sessionManager;
91
+ this.toolRegistry = options.toolRegistry;
92
+ this.systemPrompt = options.systemPrompt;
93
+ this.contextCompressor = options.contextCompressor;
94
+ this.maxContextTokens = options.maxContextTokens || 128000;
95
+ this.thinking = options.thinking;
96
+ this.options = {
97
+ onApprovalRequired: options.onApprovalRequired,
98
+ onTextDelta: options.onTextDelta,
99
+ onToolStart: options.onToolStart,
100
+ onToolComplete: options.onToolComplete,
101
+ onTurnStart: options.onTurnStart,
102
+ onTurnEnd: options.onTurnEnd,
103
+ onThinkingStart: options.onThinkingStart,
104
+ onThinkingDelta: options.onThinkingDelta,
105
+ onThinkingEnd: options.onThinkingEnd,
106
+ onContextCompress: options.onContextCompress,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Check if context compression is needed and apply it.
112
+ * Returns the (possibly compressed) message list.
113
+ */
114
+ private maybeCompressContext(messages: Message[]): Message[] {
115
+ if (!this.contextCompressor) return messages;
116
+
117
+ const result = this.contextCompressor.compress(messages, {
118
+ maxTokens: this.maxContextTokens,
119
+ });
120
+
121
+ if (result.action !== 'keep' && result.originalTokens !== result.resultingTokens) {
122
+ this.options.onContextCompress?.(
123
+ result.originalTokens,
124
+ result.resultingTokens,
125
+ result.action
126
+ );
127
+ }
128
+
129
+ return result.messages;
130
+ }
131
+
132
+ /** Run the agent loop until completion or max turns */
133
+ async run(sessionId: SessionId, userMessage?: string): Promise<AgentLoopResult> {
134
+ if (this.isRunning) {
135
+ throw new SessionError('Agent loop is already running', sessionId);
136
+ }
137
+
138
+ this.isRunning = true;
139
+ this.abortController = new AbortController();
140
+
141
+ const sessionManager = this.sessionManager;
142
+ const config = sessionManager.getConfig(sessionId);
143
+
144
+ // Set state to running
145
+ sessionManager.setState(sessionId, 'running');
146
+
147
+ try {
148
+ // Add user message if provided
149
+ if (userMessage) {
150
+ sessionManager.addMessage(sessionId, 'user', [
151
+ { type: 'text', text: userMessage },
152
+ ]);
153
+ }
154
+
155
+ let turnsCompleted = 0;
156
+ let totalInputTokens = 0;
157
+ let totalOutputTokens = 0;
158
+ let finalStopReason = 'end_turn';
159
+
160
+ // Agent loop
161
+ while (turnsCompleted < (config.maxTurns || 100)) {
162
+ if (this.abortController.signal.aborted) {
163
+ throw new CancelledError('Agent loop cancelled');
164
+ }
165
+
166
+ turnsCompleted++;
167
+ this.options.onTurnStart?.(turnsCompleted);
168
+
169
+ // Get current messages (with optional context compression)
170
+ let messages = sessionManager.getMessages(sessionId);
171
+ messages = this.maybeCompressContext(messages);
172
+ const tools = this.toolRegistry.getAllDefinitions();
173
+
174
+ // Call the model
175
+ const response = await this.modelClient.complete(messages, tools, sessionId, {
176
+ systemPrompt: this.systemPrompt,
177
+ thinking: this.thinking,
178
+ });
179
+
180
+ // Update token usage
181
+ totalInputTokens += response.usage.inputTokens;
182
+ totalOutputTokens += response.usage.outputTokens;
183
+ sessionManager.updateTokenUsage(sessionId, response.usage.inputTokens, response.usage.outputTokens);
184
+
185
+ // Add assistant message
186
+ sessionManager.addMessage(sessionId, 'assistant', response.content);
187
+
188
+ // Check if we need to execute tools
189
+ const toolUseBlocks = response.content.filter(
190
+ (c): c is ToolUseContent => c.type === 'tool_use'
191
+ );
192
+
193
+ if (toolUseBlocks.length === 0 || response.stopReason === 'end_turn') {
194
+ finalStopReason = response.stopReason;
195
+ break;
196
+ }
197
+
198
+ // Execute each tool
199
+ for (const toolUse of toolUseBlocks) {
200
+ if (this.abortController.signal.aborted) {
201
+ throw new CancelledError('Agent loop cancelled');
202
+ }
203
+
204
+ const result = await this.executeTool(
205
+ sessionId,
206
+ toolUse.id,
207
+ toolUse.name,
208
+ toolUse.input,
209
+ config
210
+ );
211
+
212
+ // Add tool result message
213
+ const truncatedContent = truncateToolResult(
214
+ typeof result.content === 'string' ? result.content : JSON.stringify(result.content)
215
+ );
216
+ sessionManager.addMessage(sessionId, 'tool', [
217
+ {
218
+ type: 'tool_result',
219
+ tool_use_id: toolUse.id,
220
+ content: truncatedContent,
221
+ is_error: result.isError,
222
+ } as ToolResultContent,
223
+ ]);
224
+ }
225
+
226
+ finalStopReason = response.stopReason;
227
+ this.options.onTurnEnd?.(turnsCompleted);
228
+
229
+ // Increment turn
230
+ const newTurn = sessionManager.incrementTurn(sessionId);
231
+ if (sessionManager.getState(sessionId) === 'completed') {
232
+ break;
233
+ }
234
+ }
235
+
236
+ sessionManager.setState(sessionId, 'idle');
237
+ return {
238
+ messages: sessionManager.getMessages(sessionId),
239
+ turnsCompleted,
240
+ totalInputTokens,
241
+ totalOutputTokens,
242
+ stopReason: finalStopReason,
243
+ };
244
+ } catch (err) {
245
+ sessionManager.setState(sessionId, 'error');
246
+ throw err;
247
+ } finally {
248
+ this.isRunning = false;
249
+ this.abortController = null;
250
+ }
251
+ }
252
+
253
+ /** Run the agent loop with streaming */
254
+ async runStream(sessionId: SessionId, userMessage?: string): Promise<AgentLoopResult> {
255
+ if (this.isRunning) {
256
+ throw new SessionError('Agent loop is already running', sessionId);
257
+ }
258
+
259
+ this.isRunning = true;
260
+ this.abortController = new AbortController();
261
+
262
+ const sessionManager = this.sessionManager;
263
+ const config = sessionManager.getConfig(sessionId);
264
+
265
+ sessionManager.setState(sessionId, 'running');
266
+
267
+ try {
268
+ if (userMessage) {
269
+ sessionManager.addMessage(sessionId, 'user', [
270
+ { type: 'text', text: userMessage },
271
+ ]);
272
+ }
273
+
274
+ let turnsCompleted = 0;
275
+ let totalInputTokens = 0;
276
+ let totalOutputTokens = 0;
277
+ let finalStopReason = 'end_turn';
278
+
279
+ while (turnsCompleted < (config.maxTurns || 100)) {
280
+ if (this.abortController.signal.aborted) {
281
+ throw new CancelledError('Agent loop cancelled');
282
+ }
283
+
284
+ turnsCompleted++;
285
+ this.options.onTurnStart?.(turnsCompleted);
286
+
287
+ // Get current messages (with optional context compression)
288
+ let messages = sessionManager.getMessages(sessionId);
289
+ messages = this.maybeCompressContext(messages);
290
+ const tools = this.toolRegistry.getAllDefinitions();
291
+
292
+ // Streaming call
293
+ let currentText = '';
294
+ let currentThinking = '';
295
+ let thinkingActive = false;
296
+ const toolCalls = new Map<string, { name: string; inputJson: string }>();
297
+
298
+ for await (const event of this.modelClient.stream(messages, tools, sessionId, {
299
+ systemPrompt: this.systemPrompt,
300
+ thinking: this.thinking,
301
+ })) {
302
+ if (event.type === 'text_delta') {
303
+ // If we were thinking and now get text, close thinking block
304
+ if (thinkingActive) {
305
+ this.options.onThinkingEnd?.();
306
+ thinkingActive = false;
307
+ }
308
+ currentText += event.delta;
309
+ this.options.onTextDelta?.(event.delta);
310
+ } else if (event.type === 'thinking_delta') {
311
+ // If thinking just started, emit start event
312
+ if (!thinkingActive) {
313
+ this.options.onThinkingStart?.();
314
+ thinkingActive = true;
315
+ }
316
+ currentThinking += event.delta;
317
+ this.options.onThinkingDelta?.(event.delta);
318
+ } else if (event.type === 'tool_call_start') {
319
+ // Close thinking block if active
320
+ if (thinkingActive) {
321
+ this.options.onThinkingEnd?.();
322
+ thinkingActive = false;
323
+ }
324
+ toolCalls.set(event.toolCallId, { name: event.toolName, inputJson: '' });
325
+ this.options.onToolStart?.(event.toolName, event.toolCallId);
326
+ } else if (event.type === 'tool_call_delta') {
327
+ const tc = toolCalls.get(event.toolCallId);
328
+ if (tc) tc.inputJson += event.delta;
329
+ } else if (event.type === 'message_complete') {
330
+ // Close thinking block if still active
331
+ if (thinkingActive) {
332
+ this.options.onThinkingEnd?.();
333
+ thinkingActive = false;
334
+ }
335
+ totalInputTokens += event.usage.inputTokens;
336
+ totalOutputTokens += event.usage.outputTokens;
337
+ sessionManager.updateTokenUsage(sessionId, event.usage.inputTokens, event.usage.outputTokens);
338
+ finalStopReason = event.stopReason;
339
+ } else if (event.type === 'error') {
340
+ throw event.error;
341
+ }
342
+ }
343
+
344
+ // Build assistant content blocks
345
+ const content: ContentBlock[] = [];
346
+ if (currentThinking) {
347
+ content.push({ type: 'thinking', thinking: currentThinking });
348
+ }
349
+ if (currentText) {
350
+ content.push({ type: 'text', text: currentText });
351
+ }
352
+
353
+ for (const [id, tc] of toolCalls) {
354
+ let input: Record<string, unknown>;
355
+ try {
356
+ input = JSON.parse(tc.inputJson);
357
+ } catch {
358
+ input = { raw: tc.inputJson };
359
+ }
360
+ content.push({
361
+ type: 'tool_use',
362
+ id: id as ToolCallId,
363
+ name: tc.name,
364
+ input,
365
+ });
366
+ }
367
+
368
+ sessionManager.addMessage(sessionId, 'assistant', content);
369
+
370
+ const toolUseBlocks = content.filter(
371
+ (c): c is ToolUseContent => c.type === 'tool_use'
372
+ );
373
+
374
+ if (toolUseBlocks.length === 0 || finalStopReason === 'end_turn') {
375
+ break;
376
+ }
377
+
378
+ for (const toolUse of toolUseBlocks) {
379
+ if (this.abortController.signal.aborted) {
380
+ throw new CancelledError('Agent loop cancelled');
381
+ }
382
+
383
+ const result = await this.executeTool(
384
+ sessionId,
385
+ toolUse.id,
386
+ toolUse.name,
387
+ toolUse.input,
388
+ config
389
+ );
390
+
391
+ const truncatedContent = truncateToolResult(
392
+ typeof result.content === 'string' ? result.content : JSON.stringify(result.content)
393
+ );
394
+
395
+ sessionManager.addMessage(sessionId, 'tool', [
396
+ {
397
+ type: 'tool_result',
398
+ tool_use_id: toolUse.id,
399
+ content: truncatedContent,
400
+ is_error: result.isError,
401
+ } as ToolResultContent,
402
+ ]);
403
+ }
404
+
405
+ this.options.onTurnEnd?.(turnsCompleted);
406
+ const newTurn = sessionManager.incrementTurn(sessionId);
407
+ if (sessionManager.getState(sessionId) === 'completed') {
408
+ break;
409
+ }
410
+ }
411
+
412
+ sessionManager.setState(sessionId, 'idle');
413
+ return {
414
+ messages: sessionManager.getMessages(sessionId),
415
+ turnsCompleted,
416
+ totalInputTokens,
417
+ totalOutputTokens,
418
+ stopReason: finalStopReason,
419
+ };
420
+ } catch (err) {
421
+ sessionManager.setState(sessionId, 'error');
422
+ throw err;
423
+ } finally {
424
+ this.isRunning = false;
425
+ this.abortController = null;
426
+ }
427
+ }
428
+
429
+ /** Cancel the running agent loop */
430
+ cancel(): void {
431
+ this.abortController?.abort();
432
+ }
433
+
434
+ /** Check if the loop is running */
435
+ isActive(): boolean {
436
+ return this.isRunning;
437
+ }
438
+
439
+ /** Execute a single tool */
440
+ private async executeTool(
441
+ sessionId: SessionId,
442
+ toolCallId: string,
443
+ toolName: string,
444
+ toolInput: Record<string, unknown>,
445
+ config: SessionConfig
446
+ ): Promise<ToolHandlerOutput> {
447
+ // Check if tool requires approval
448
+ if (this.toolRegistry.requiresApproval(toolName, toolInput, config.approvalMode)) {
449
+ if (this.options.onApprovalRequired) {
450
+ const request: ApprovalRequest = {
451
+ id: toolCallId,
452
+ sessionId,
453
+ toolName,
454
+ toolInput,
455
+ risk: this.toolRegistry.getRiskLevel(toolName),
456
+ description: `Tool "${toolName}" with input: ${JSON.stringify(toolInput).slice(0, 200)}`,
457
+ timestamp: Date.now(),
458
+ };
459
+
460
+ const response = await this.options.onApprovalRequired(request);
461
+ if (!response.approved) {
462
+ return {
463
+ content: `Tool execution denied by user: ${response.reason || 'No reason provided'}`,
464
+ isError: true,
465
+ };
466
+ }
467
+
468
+ // Use modified input if provided
469
+ if (response.modifiedInput) {
470
+ toolInput = response.modifiedInput;
471
+ }
472
+ }
473
+ }
474
+
475
+ const context: ToolHandlerInput = {
476
+ params: toolInput,
477
+ context: {
478
+ sessionId,
479
+ workingDirectory: config.workingDirectory,
480
+ environment: {},
481
+ model: config.model,
482
+ approvalMode: config.approvalMode,
483
+ },
484
+ abortSignal: this.abortController?.signal,
485
+ };
486
+
487
+ try {
488
+ const startTime = Date.now();
489
+ const result = await this.toolRegistry.execute(toolName, context);
490
+ const duration = Date.now() - startTime;
491
+
492
+ this.options.onToolComplete?.(toolName, toolCallId, result);
493
+ return result;
494
+ } catch (err) {
495
+ if (err instanceof ToolError) {
496
+ return { content: `[${err.code}] ${err.message}`, isError: true };
497
+ }
498
+ return { content: `Tool error: ${(err as Error).message}`, isError: true };
499
+ }
500
+ }
501
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionManager.d.ts","sourceRoot":"","sources":["SessionManager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,183 @@
1
+ // ============================================================================
2
+ // SessionManager Tests
3
+ // ============================================================================
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { SessionManager } from './SessionManager.js';
7
+ import type { Message } from '../types/session.js';
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
11
+
12
+ describe('SessionManager', () => {
13
+ let sessionManager: SessionManager;
14
+ const testSessionDir = path.join(os.tmpdir(), 'nova-test-sessions-' + Date.now());
15
+
16
+ beforeEach(() => {
17
+ if (!fs.existsSync(testSessionDir)) {
18
+ fs.mkdirSync(testSessionDir, { recursive: true });
19
+ }
20
+ sessionManager = new SessionManager(testSessionDir);
21
+ });
22
+
23
+ afterEach(() => {
24
+ if (fs.existsSync(testSessionDir)) {
25
+ fs.rmSync(testSessionDir, { recursive: true, force: true });
26
+ }
27
+ });
28
+
29
+ describe('create()', () => {
30
+ it('should create a new session with unique ID', () => {
31
+ const session1 = sessionManager.create({ workingDirectory: process.cwd() });
32
+ const session2 = sessionManager.create({ workingDirectory: process.cwd() });
33
+
34
+ expect(session1.id).toBeDefined();
35
+ expect(session2.id).toBeDefined();
36
+ expect(session1.id).not.toBe(session2.id);
37
+ });
38
+
39
+ it('should create session with messages array', () => {
40
+ const session = sessionManager.create({ workingDirectory: process.cwd() });
41
+ const messages = session.conversation.messages;
42
+
43
+ expect(messages).toBeDefined();
44
+ expect(Array.isArray(messages)).toBe(true);
45
+ });
46
+ });
47
+
48
+ describe('get()', () => {
49
+ it('should return undefined for non-existent session', () => {
50
+ const session = sessionManager.get('non-existent-id' as any);
51
+ expect(session).toBeUndefined();
52
+ });
53
+
54
+ it('should return session for valid ID', () => {
55
+ const session = sessionManager.create({ workingDirectory: process.cwd() });
56
+ const retrieved = sessionManager.get(session.id);
57
+
58
+ expect(retrieved).toBeDefined();
59
+ expect(retrieved?.id).toBe(session.id);
60
+ });
61
+ });
62
+
63
+ describe('addMessage()', () => {
64
+ it('should add message to session', () => {
65
+ const session = sessionManager.create({ workingDirectory: process.cwd() });
66
+
67
+ sessionManager.addMessage(session.id, 'user', 'Hello, world!');
68
+ const messages = session.conversation.messages;
69
+
70
+ expect(messages).toHaveLength(1);
71
+ expect(messages[0].content).toBe('Hello, world!');
72
+ });
73
+
74
+ it('should maintain message order', () => {
75
+ const session = sessionManager.create({ workingDirectory: process.cwd() });
76
+
77
+ sessionManager.addMessage(session.id, 'user', 'First');
78
+ sessionManager.addMessage(session.id, 'assistant', 'Second');
79
+ sessionManager.addMessage(session.id, 'user', 'Third');
80
+
81
+ const messages = session.conversation.messages;
82
+ expect(messages).toHaveLength(3);
83
+ expect(messages[0].content).toBe('First');
84
+ expect(messages[1].content).toBe('Second');
85
+ expect(messages[2].content).toBe('Third');
86
+ });
87
+
88
+ it('should throw for non-existent session', () => {
89
+ expect(() => {
90
+ sessionManager.addMessage('non-existent' as any, 'user', 'test');
91
+ }).toThrow();
92
+ });
93
+ });
94
+
95
+ describe('getMessages()', () => {
96
+ it('should return empty array for new session', () => {
97
+ const session = sessionManager.create({ workingDirectory: process.cwd() });
98
+ const messages = session.conversation.messages;
99
+
100
+ expect(messages).toHaveLength(0);
101
+ });
102
+
103
+ it('should return messages array', () => {
104
+ const session = sessionManager.create({ workingDirectory: process.cwd() });
105
+ sessionManager.addMessage(session.id, 'user', 'test');
106
+
107
+ const messages = session.conversation.messages;
108
+ expect(messages).toHaveLength(1);
109
+ expect(messages[0].content).toBe('test');
110
+ });
111
+ });
112
+
113
+ describe('persist() and loadLatestSession()', () => {
114
+ it('should persist and load session', async () => {
115
+ const session = sessionManager.create({ workingDirectory: process.cwd() });
116
+ sessionManager.addMessage(session.id, 'user', 'Test message');
117
+
118
+ await sessionManager.persist(session.id);
119
+
120
+ // Create new SessionManager instance
121
+ const sessionManager2 = new SessionManager(testSessionDir);
122
+ const loadedSession = await sessionManager2.loadLatestSession();
123
+
124
+ expect(loadedSession).toBeDefined();
125
+ const messages = loadedSession!.conversation.messages;
126
+ expect(messages).toHaveLength(1);
127
+ expect(messages[0].content).toBe('Test message');
128
+ });
129
+
130
+ it('should return null when no persisted sessions', async () => {
131
+ const loadedSession = await sessionManager.loadLatestSession();
132
+ expect(loadedSession).toBeNull();
133
+ });
134
+
135
+ it('should load most recent session', async () => {
136
+ const session1 = sessionManager.create({ workingDirectory: process.cwd() });
137
+ sessionManager.addMessage(session1.id, 'user', 'Session 1');
138
+ await sessionManager.persist(session1.id);
139
+
140
+ // Wait a bit to ensure different mtime
141
+ await new Promise(resolve => setTimeout(resolve, 100));
142
+
143
+ const session2 = sessionManager.create({ workingDirectory: process.cwd() });
144
+ sessionManager.addMessage(session2.id, 'user', 'Session 2');
145
+ await sessionManager.persist(session2.id);
146
+
147
+ const loadedSession = await sessionManager.loadLatestSession();
148
+ const messages = loadedSession!.conversation.messages;
149
+
150
+ expect(messages[0].content).toBe('Session 2');
151
+ });
152
+ });
153
+
154
+ describe('listPersistedSessions()', () => {
155
+ it('should return empty array when no sessions', async () => {
156
+ const sessions = await sessionManager.listPersistedSessions();
157
+ expect(sessions).toHaveLength(0);
158
+ });
159
+
160
+ it('should list persisted sessions', async () => {
161
+ const session1 = sessionManager.create({ workingDirectory: process.cwd() });
162
+ sessionManager.addMessage(session1.id, 'user', 'Test 1');
163
+ await sessionManager.persist(session1.id);
164
+
165
+ const session2 = sessionManager.create({ workingDirectory: process.cwd() });
166
+ sessionManager.addMessage(session2.id, 'user', 'Test 2');
167
+ await sessionManager.persist(session2.id);
168
+
169
+ const sessions = await sessionManager.listPersistedSessions();
170
+ expect(sessions.length).toBeGreaterThanOrEqual(2);
171
+ });
172
+ });
173
+
174
+ describe('delete()', () => {
175
+ it('should delete session from memory', () => {
176
+ const session = sessionManager.create({ workingDirectory: process.cwd() });
177
+ expect(sessionManager.get(session.id)).toBeDefined();
178
+
179
+ sessionManager.delete(session.id);
180
+ expect(sessionManager.get(session.id)).toBeUndefined();
181
+ });
182
+ });
183
+ });