byterover-cli 1.3.0 → 1.5.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 (171) hide show
  1. package/README.md +71 -6
  2. package/dist/core/domain/cipher/errors/file-system-error.d.ts +11 -0
  3. package/dist/core/domain/cipher/errors/file-system-error.js +17 -0
  4. package/dist/core/domain/cipher/file-system/types.d.ts +40 -6
  5. package/dist/core/domain/cipher/process/types.d.ts +1 -1
  6. package/dist/core/domain/entities/agent.d.ts +1 -1
  7. package/dist/core/domain/entities/agent.js +5 -0
  8. package/dist/core/domain/entities/provider-config.d.ts +92 -0
  9. package/dist/core/domain/entities/provider-config.js +181 -0
  10. package/dist/core/domain/entities/provider-registry.d.ts +55 -0
  11. package/dist/core/domain/entities/provider-registry.js +74 -0
  12. package/dist/core/interfaces/cipher/cipher-services.d.ts +0 -3
  13. package/dist/core/interfaces/cipher/i-content-generator.d.ts +30 -0
  14. package/dist/core/interfaces/cipher/i-content-generator.js +12 -1
  15. package/dist/core/interfaces/cipher/index.d.ts +0 -2
  16. package/dist/core/interfaces/cipher/message-factory.d.ts +4 -1
  17. package/dist/core/interfaces/cipher/message-factory.js +5 -0
  18. package/dist/core/interfaces/cipher/message-types.d.ts +19 -1
  19. package/dist/core/interfaces/i-provider-config-store.d.ts +88 -0
  20. package/dist/core/interfaces/i-provider-keychain-store.d.ts +33 -0
  21. package/dist/infra/cipher/file-system/binary-utils.d.ts +15 -2
  22. package/dist/infra/cipher/file-system/binary-utils.js +26 -3
  23. package/dist/infra/cipher/file-system/file-system-service.d.ts +9 -0
  24. package/dist/infra/cipher/file-system/file-system-service.js +96 -13
  25. package/dist/infra/cipher/file-system/pdf-extractor.d.ts +100 -0
  26. package/dist/infra/cipher/file-system/pdf-extractor.js +226 -0
  27. package/dist/infra/cipher/http/internal-llm-http-service.d.ts +40 -0
  28. package/dist/infra/cipher/http/internal-llm-http-service.js +152 -2
  29. package/dist/infra/cipher/llm/formatters/gemini-formatter.js +8 -1
  30. package/dist/infra/cipher/llm/generators/byterover-content-generator.d.ts +2 -3
  31. package/dist/infra/cipher/llm/generators/byterover-content-generator.js +20 -11
  32. package/dist/infra/cipher/llm/generators/openrouter-content-generator.d.ts +1 -0
  33. package/dist/infra/cipher/llm/generators/openrouter-content-generator.js +26 -0
  34. package/dist/infra/cipher/llm/internal-llm-service.d.ts +13 -0
  35. package/dist/infra/cipher/llm/internal-llm-service.js +75 -4
  36. package/dist/infra/cipher/llm/model-capabilities.d.ts +74 -0
  37. package/dist/infra/cipher/llm/model-capabilities.js +157 -0
  38. package/dist/infra/cipher/llm/openrouter-llm-service.d.ts +35 -1
  39. package/dist/infra/cipher/llm/openrouter-llm-service.js +216 -28
  40. package/dist/infra/cipher/llm/stream-processor.d.ts +22 -2
  41. package/dist/infra/cipher/llm/stream-processor.js +78 -4
  42. package/dist/infra/cipher/llm/thought-parser.d.ts +1 -1
  43. package/dist/infra/cipher/llm/thought-parser.js +5 -5
  44. package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.d.ts +49 -0
  45. package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.js +272 -0
  46. package/dist/infra/cipher/llm/transformers/reasoning-extractor.d.ts +71 -0
  47. package/dist/infra/cipher/llm/transformers/reasoning-extractor.js +253 -0
  48. package/dist/infra/cipher/process/process-service.js +1 -1
  49. package/dist/infra/cipher/session/chat-session.d.ts +2 -0
  50. package/dist/infra/cipher/session/chat-session.js +13 -2
  51. package/dist/infra/cipher/storage/message-storage-service.js +4 -0
  52. package/dist/infra/cipher/tools/implementations/bash-exec-tool.js +3 -3
  53. package/dist/infra/cipher/tools/implementations/read-file-tool.js +24 -4
  54. package/dist/infra/cipher/tools/implementations/task-tool.js +1 -1
  55. package/dist/infra/connectors/rules/rules-connector-config.d.ts +4 -0
  56. package/dist/infra/connectors/rules/rules-connector-config.js +4 -0
  57. package/dist/infra/http/openrouter-api-client.d.ts +148 -0
  58. package/dist/infra/http/openrouter-api-client.js +161 -0
  59. package/dist/infra/mcp/tools/brv-curate-tool.d.ts +10 -4
  60. package/dist/infra/mcp/tools/brv-curate-tool.js +9 -4
  61. package/dist/infra/mcp/tools/task-result-waiter.js +9 -1
  62. package/dist/infra/process/agent-worker.js +178 -70
  63. package/dist/infra/process/transport-handlers.d.ts +25 -4
  64. package/dist/infra/process/transport-handlers.js +57 -10
  65. package/dist/infra/repl/commands/connectors-command.js +2 -2
  66. package/dist/infra/repl/commands/index.js +5 -0
  67. package/dist/infra/repl/commands/model-command.d.ts +13 -0
  68. package/dist/infra/repl/commands/model-command.js +212 -0
  69. package/dist/infra/repl/commands/provider-command.d.ts +13 -0
  70. package/dist/infra/repl/commands/provider-command.js +181 -0
  71. package/dist/infra/repl/commands/space/switch-command.js +0 -2
  72. package/dist/infra/repl/transport-client-helper.js +6 -2
  73. package/dist/infra/storage/file-provider-config-store.d.ts +83 -0
  74. package/dist/infra/storage/file-provider-config-store.js +157 -0
  75. package/dist/infra/storage/provider-keychain-store.d.ts +37 -0
  76. package/dist/infra/storage/provider-keychain-store.js +75 -0
  77. package/dist/infra/transport/socket-io-transport-client.d.ts +20 -0
  78. package/dist/infra/transport/socket-io-transport-client.js +88 -1
  79. package/dist/infra/usecase/curate-use-case.js +10 -4
  80. package/dist/infra/usecase/space-switch-use-case.d.ts +0 -10
  81. package/dist/infra/usecase/space-switch-use-case.js +7 -37
  82. package/dist/oclif/hooks/init/welcome.js +4 -17
  83. package/dist/resources/prompts/curate.yml +1 -0
  84. package/dist/resources/tools/bash_exec.txt +1 -1
  85. package/dist/resources/tools/read_file.txt +5 -2
  86. package/dist/tui/components/api-key-dialog.d.ts +39 -0
  87. package/dist/tui/components/api-key-dialog.js +94 -0
  88. package/dist/tui/components/execution/execution-changes.d.ts +3 -1
  89. package/dist/tui/components/execution/execution-changes.js +4 -4
  90. package/dist/tui/components/execution/execution-content.d.ts +1 -1
  91. package/dist/tui/components/execution/execution-content.js +4 -12
  92. package/dist/tui/components/execution/execution-input.js +1 -1
  93. package/dist/tui/components/execution/execution-progress.d.ts +10 -13
  94. package/dist/tui/components/execution/execution-progress.js +70 -17
  95. package/dist/tui/components/execution/execution-reasoning.d.ts +16 -0
  96. package/dist/tui/components/execution/execution-reasoning.js +34 -0
  97. package/dist/tui/components/execution/execution-tool.d.ts +23 -0
  98. package/dist/tui/components/execution/execution-tool.js +125 -0
  99. package/dist/tui/components/execution/expanded-log-view.js +3 -3
  100. package/dist/tui/components/execution/log-item.d.ts +2 -0
  101. package/dist/tui/components/execution/log-item.js +6 -4
  102. package/dist/tui/components/index.d.ts +2 -0
  103. package/dist/tui/components/index.js +2 -0
  104. package/dist/tui/components/inline-prompts/inline-select.js +3 -2
  105. package/dist/tui/components/model-dialog.d.ts +63 -0
  106. package/dist/tui/components/model-dialog.js +89 -0
  107. package/dist/tui/components/onboarding/onboarding-flow.js +8 -2
  108. package/dist/tui/components/provider-dialog.d.ts +27 -0
  109. package/dist/tui/components/provider-dialog.js +31 -0
  110. package/dist/tui/components/reasoning-text.d.ts +26 -0
  111. package/dist/tui/components/reasoning-text.js +49 -0
  112. package/dist/tui/components/selectable-list.d.ts +54 -0
  113. package/dist/tui/components/selectable-list.js +180 -0
  114. package/dist/tui/components/streaming-text.d.ts +30 -0
  115. package/dist/tui/components/streaming-text.js +52 -0
  116. package/dist/tui/contexts/tasks-context.d.ts +15 -0
  117. package/dist/tui/contexts/tasks-context.js +224 -40
  118. package/dist/tui/contexts/theme-context.d.ts +1 -0
  119. package/dist/tui/contexts/theme-context.js +3 -2
  120. package/dist/tui/hooks/use-activity-logs.js +7 -1
  121. package/dist/tui/types/messages.d.ts +32 -5
  122. package/dist/tui/utils/index.d.ts +1 -1
  123. package/dist/tui/utils/index.js +1 -1
  124. package/dist/tui/utils/log.d.ts +0 -9
  125. package/dist/tui/utils/log.js +2 -53
  126. package/dist/tui/views/command-view.js +4 -1
  127. package/dist/utils/file-validator.js +8 -4
  128. package/oclif.manifest.json +1 -54
  129. package/package.json +4 -2
  130. package/dist/core/interfaces/cipher/i-coding-agent-log-parser.d.ts +0 -20
  131. package/dist/core/interfaces/cipher/i-coding-agent-log-watcher.d.ts +0 -31
  132. package/dist/core/interfaces/i-file-watcher-service.d.ts +0 -41
  133. package/dist/core/interfaces/i-file-watcher-service.js +0 -1
  134. package/dist/core/interfaces/parser/i-clean-parser-service.d.ts +0 -18
  135. package/dist/core/interfaces/parser/i-clean-parser-service.js +0 -1
  136. package/dist/core/interfaces/parser/i-raw-parser-service.d.ts +0 -17
  137. package/dist/core/interfaces/parser/i-raw-parser-service.js +0 -1
  138. package/dist/core/interfaces/parser/i-session-normalizer.d.ts +0 -56
  139. package/dist/core/interfaces/parser/i-session-normalizer.js +0 -1
  140. package/dist/infra/cipher/parsers/coding-agent-log-parser.d.ts +0 -24
  141. package/dist/infra/cipher/parsers/coding-agent-log-parser.js +0 -51
  142. package/dist/infra/cipher/watcher/coding-agent-log-watcher.d.ts +0 -14
  143. package/dist/infra/cipher/watcher/coding-agent-log-watcher.js +0 -55
  144. package/dist/infra/parsers/clean/clean-claude-service.d.ts +0 -111
  145. package/dist/infra/parsers/clean/clean-claude-service.js +0 -271
  146. package/dist/infra/parsers/clean/clean-codex-service.d.ts +0 -231
  147. package/dist/infra/parsers/clean/clean-codex-service.js +0 -534
  148. package/dist/infra/parsers/clean/clean-copilot-service.d.ts +0 -255
  149. package/dist/infra/parsers/clean/clean-copilot-service.js +0 -729
  150. package/dist/infra/parsers/clean/clean-cursor-service.d.ts +0 -161
  151. package/dist/infra/parsers/clean/clean-cursor-service.js +0 -432
  152. package/dist/infra/parsers/clean/clean-parser-service-factory.d.ts +0 -54
  153. package/dist/infra/parsers/clean/clean-parser-service-factory.js +0 -80
  154. package/dist/infra/parsers/clean/shared.d.ts +0 -84
  155. package/dist/infra/parsers/clean/shared.js +0 -273
  156. package/dist/infra/parsers/raw/raw-claude-service.d.ts +0 -195
  157. package/dist/infra/parsers/raw/raw-claude-service.js +0 -548
  158. package/dist/infra/parsers/raw/raw-codex-service.d.ts +0 -313
  159. package/dist/infra/parsers/raw/raw-codex-service.js +0 -782
  160. package/dist/infra/parsers/raw/raw-copilot-service.d.ts +0 -196
  161. package/dist/infra/parsers/raw/raw-copilot-service.js +0 -558
  162. package/dist/infra/parsers/raw/raw-cursor-service.d.ts +0 -316
  163. package/dist/infra/parsers/raw/raw-cursor-service.js +0 -818
  164. package/dist/infra/parsers/raw/raw-parser-service-factory.d.ts +0 -54
  165. package/dist/infra/parsers/raw/raw-parser-service-factory.js +0 -81
  166. package/dist/infra/watcher/file-watcher-service.d.ts +0 -10
  167. package/dist/infra/watcher/file-watcher-service.js +0 -81
  168. package/dist/oclif/commands/watch.d.ts +0 -25
  169. package/dist/oclif/commands/watch.js +0 -175
  170. /package/dist/core/interfaces/{cipher/i-coding-agent-log-parser.js → i-provider-config-store.js} +0 -0
  171. /package/dist/core/interfaces/{cipher/i-coding-agent-log-watcher.js → i-provider-keychain-store.js} +0 -0
@@ -4,7 +4,10 @@ import { NoOpLogger } from '../../../core/interfaces/cipher/i-logger.js';
4
4
  import { getErrorMessage } from '../../../utils/error-helpers.js';
5
5
  import { ContextManager } from './context/context-manager.js';
6
6
  import { OpenRouterMessageFormatter } from './formatters/openrouter-formatter.js';
7
+ import { OpenRouterContentGenerator } from './generators/openrouter-content-generator.js';
8
+ import { createIdGenerator, StreamProcessor } from './stream-processor.js';
7
9
  import { OpenRouterTokenizer } from './tokenizers/openrouter-tokenizer.js';
10
+ import { transformGenerateContentChunksToStreamEvents } from './transformers/openrouter-stream-transformer.js';
8
11
  /**
9
12
  * OpenRouter LLM Service.
10
13
  *
@@ -25,11 +28,14 @@ import { OpenRouterTokenizer } from './tokenizers/openrouter-tokenizer.js';
25
28
  export class OpenRouterLLMService {
26
29
  client;
27
30
  config;
31
+ contentGenerator;
28
32
  contextManager;
29
33
  formatter;
30
34
  logger;
31
35
  memoryManager;
32
36
  sessionEventBus;
37
+ sessionId;
38
+ streamProcessor;
33
39
  systemPromptManager;
34
40
  tokenizer;
35
41
  toolManager;
@@ -65,6 +71,8 @@ export class OpenRouterLLMService {
65
71
  timeout: config.timeout,
66
72
  verbose: config.verbose,
67
73
  };
74
+ // Store sessionId for streaming context
75
+ this.sessionId = sessionId;
68
76
  // Initialize OpenAI client with OpenRouter base URL
69
77
  this.client = new OpenAI({
70
78
  apiKey: this.config.apiKey,
@@ -78,6 +86,19 @@ export class OpenRouterLLMService {
78
86
  // Initialize formatter and tokenizer
79
87
  this.formatter = new OpenRouterMessageFormatter();
80
88
  this.tokenizer = new OpenRouterTokenizer();
89
+ // Initialize content generator for streaming support
90
+ this.contentGenerator = new OpenRouterContentGenerator({
91
+ apiKey: this.config.apiKey,
92
+ baseUrl: this.config.baseUrl,
93
+ httpReferer: this.config.httpReferer,
94
+ maxTokens: this.config.maxTokens,
95
+ model: this.config.model,
96
+ siteName: this.config.siteName,
97
+ temperature: this.config.temperature,
98
+ timeout: this.config.timeout,
99
+ });
100
+ // Initialize stream processor for handling streaming events
101
+ this.streamProcessor = new StreamProcessor();
81
102
  // Initialize context manager with optional history storage
82
103
  this.contextManager = new ContextManager({
83
104
  formatter: this.formatter,
@@ -101,18 +122,23 @@ export class OpenRouterLLMService {
101
122
  * @param options.signal - Optional abort signal for cancellation
102
123
  * @param options.imageData - Optional image data
103
124
  * @param options.fileData - Optional file data
104
- * @param options.stream - Whether to stream response (not implemented yet)
125
+ * @param options.stream - Whether to stream response (emits llmservice:chunk events)
105
126
  * @param options.executionContext - Optional execution context (for JSON input mode, etc.)
106
127
  * @param options.taskId - Task ID for billing tracking
107
128
  * @returns Final assistant response
108
129
  */
109
130
  async completeTask(textInput, options) {
110
- // Extract options with defaults
111
- const { executionContext, fileData, imageData, signal } = options ?? {};
131
+ // Extract options with defaults - include taskId for concurrent task isolation
132
+ const { executionContext, fileData, imageData, signal, stream, taskId } = options ?? {};
112
133
  // Add user message to context
113
134
  await this.contextManager.addUserMessage(textInput, imageData, fileData);
114
135
  // Get filtered tools based on command type (e.g., only read-only tools for 'query')
115
136
  const toolSet = this.toolManager.getToolsForCommand(executionContext?.commandType);
137
+ // Route to streaming or non-streaming execution
138
+ if (stream) {
139
+ return this.completeTaskStreaming(toolSet, executionContext, signal, taskId);
140
+ }
141
+ // Non-streaming path: Build tools array for OpenAI format
116
142
  const tools = Object.entries(toolSet).map(([name, schema]) => ({
117
143
  function: {
118
144
  description: schema.description ?? '',
@@ -130,14 +156,14 @@ export class OpenRouterLLMService {
130
156
  }
131
157
  try {
132
158
  // eslint-disable-next-line no-await-in-loop -- Sequential iterations required for agentic loop
133
- const result = await this.executeAgenticIteration(iterationCount, tools, executionContext);
159
+ const result = await this.executeAgenticIteration(iterationCount, tools, executionContext, taskId);
134
160
  if (result !== null) {
135
161
  return result;
136
162
  }
137
163
  iterationCount++;
138
164
  }
139
165
  catch (error) {
140
- this.handleLLMError(error);
166
+ this.handleLLMError(error, taskId);
141
167
  }
142
168
  }
143
169
  // Max iterations exceeded
@@ -204,15 +230,65 @@ export class OpenRouterLLMService {
204
230
  }
205
231
  return lastMessage;
206
232
  }
233
+ /**
234
+ * Complete a task using streaming mode.
235
+ *
236
+ * Emits real-time llmservice:chunk events as tokens arrive.
237
+ * Follows the OpenCode pattern of delta-based streaming.
238
+ *
239
+ * @param toolSet - Available tools for the task
240
+ * @param executionContext - Optional execution context
241
+ * @param signal - Optional abort signal for cancellation
242
+ * @param taskId - Optional task ID for concurrent task isolation
243
+ * @returns Final accumulated response
244
+ */
245
+ async completeTaskStreaming(toolSet, executionContext, signal, taskId) {
246
+ let iterationCount = 0;
247
+ let finalResponse = '';
248
+ // Streaming agentic loop
249
+ while (iterationCount < this.config.maxIterations) {
250
+ // Check if aborted
251
+ if (signal?.aborted) {
252
+ throw new Error('Operation aborted');
253
+ }
254
+ try {
255
+ // eslint-disable-next-line no-await-in-loop -- Sequential iterations required for agentic loop
256
+ const result = await this.executeAgenticIterationStreaming(iterationCount, toolSet, executionContext, taskId);
257
+ // If no tool calls, we're done - emit final response
258
+ if (!result.hasToolCalls) {
259
+ finalResponse = result.response;
260
+ // Emit response event
261
+ this.sessionEventBus.emit('llmservice:response', {
262
+ content: finalResponse,
263
+ model: this.config.model,
264
+ provider: 'openrouter',
265
+ taskId: taskId || undefined,
266
+ });
267
+ // Add assistant message to context
268
+ // eslint-disable-next-line no-await-in-loop -- Must complete before returning
269
+ await this.contextManager.addAssistantMessage(finalResponse);
270
+ return finalResponse;
271
+ }
272
+ // Has tool calls - continue the loop
273
+ iterationCount++;
274
+ }
275
+ catch (error) {
276
+ this.handleLLMError(error, taskId);
277
+ }
278
+ }
279
+ // Max iterations exceeded
280
+ throw new LlmMaxIterationsError(this.config.maxIterations, 'openrouter', this.config.model);
281
+ }
207
282
  /**
208
283
  * Execute a single iteration of the agentic loop.
209
284
  *
210
285
  * @param iterationCount - Current iteration number
211
286
  * @param tools - Available tools for this iteration
212
287
  * @param executionContext - Optional execution context
288
+ * @param taskId - Optional task ID for concurrent task isolation
213
289
  * @returns Final response string if complete, null if more iterations needed
214
290
  */
215
- async executeAgenticIteration(iterationCount, tools, executionContext) {
291
+ async executeAgenticIteration(iterationCount, tools, executionContext, taskId) {
216
292
  // Build system prompt using SystemPromptManager (before compression for correct token accounting)
217
293
  // Use filtered tool names based on command type (e.g., only read-only tools for 'query')
218
294
  const availableTools = this.toolManager.getToolNamesForCommand(executionContext?.commandType);
@@ -250,53 +326,162 @@ export class OpenRouterLLMService {
250
326
  tokensUsed,
251
327
  });
252
328
  }
253
- // Emit thinking event
254
- this.sessionEventBus.emit('llmservice:thinking');
329
+ // Emit thinking event with taskId for concurrent task isolation
330
+ this.sessionEventBus.emit('llmservice:thinking', { taskId });
255
331
  // Call LLM and parse response
256
332
  const lastMessage = await this.callLLMAndParseResponse(tools, formattedMessages);
257
333
  // Check if there are tool calls
258
334
  if (!lastMessage.toolCalls || lastMessage.toolCalls.length === 0) {
259
- return this.handleFinalResponse(lastMessage);
335
+ return this.handleFinalResponse(lastMessage, taskId);
260
336
  }
261
337
  // Has tool calls - handle them
262
- await this.handleToolCalls(lastMessage);
338
+ await this.handleToolCalls(lastMessage, taskId);
263
339
  return null;
264
340
  }
341
+ /**
342
+ * Execute a single iteration of the agentic loop with streaming.
343
+ *
344
+ * This method uses the ContentGenerator's streaming API to provide
345
+ * real-time token-by-token output via the SessionEventBus.
346
+ *
347
+ * @param iterationCount - Current iteration number
348
+ * @param toolSet - Available tools for this iteration
349
+ * @param executionContext - Optional execution context
350
+ * @param taskId - Optional task ID for concurrent task isolation
351
+ * @returns Object with response text and whether tool calls were made
352
+ */
353
+ async executeAgenticIterationStreaming(iterationCount, toolSet, executionContext, taskId) {
354
+ // Build system prompt using SystemPromptManager
355
+ const availableTools = this.toolManager.getToolNamesForCommand(executionContext?.commandType);
356
+ const markersSet = this.toolManager.getAvailableMarkers();
357
+ const availableMarkers = {};
358
+ for (const marker of markersSet) {
359
+ availableMarkers[marker] = marker;
360
+ }
361
+ const systemPrompt = await this.systemPromptManager.build({
362
+ availableMarkers,
363
+ availableTools,
364
+ commandType: executionContext?.commandType,
365
+ conversationMetadata: executionContext?.conversationMetadata,
366
+ memoryManager: this.memoryManager,
367
+ });
368
+ // Get messages from context with compression
369
+ const { tokensUsed } = await this.contextManager.getFormattedMessagesWithCompression(systemPrompt);
370
+ if (this.config.verbose) {
371
+ this.logger.debug('Streaming iteration', {
372
+ iteration: `${iterationCount + 1}/${this.config.maxIterations}`,
373
+ maxInputTokens: this.config.maxInputTokens,
374
+ tokensUsed,
375
+ });
376
+ }
377
+ // Emit thinking event
378
+ this.sessionEventBus.emit('llmservice:thinking', { taskId });
379
+ // Get internal messages for content generator
380
+ const contents = this.contextManager.getMessages();
381
+ // Generate streaming response using ContentGenerator
382
+ const streamGenerator = this.contentGenerator.generateContentStream({
383
+ config: {
384
+ maxTokens: this.config.maxTokens,
385
+ temperature: this.config.temperature,
386
+ },
387
+ contents,
388
+ executionContext,
389
+ model: this.config.model,
390
+ systemPrompt,
391
+ taskId: taskId ?? `task-${Date.now()}`,
392
+ tools: toolSet,
393
+ });
394
+ // Transform chunks to StreamEvents and process
395
+ // Pass modelId for native reasoning extraction (OpenAI, Grok, Gemini)
396
+ const streamEvents = transformGenerateContentChunksToStreamEvents(streamGenerator, {
397
+ modelId: this.config.model,
398
+ stepIndex: iterationCount,
399
+ });
400
+ // Process stream and accumulate state
401
+ const generateId = createIdGenerator();
402
+ const processorState = await this.streamProcessor.process(streamEvents, {
403
+ eventBus: this.sessionEventBus,
404
+ generateId,
405
+ sessionId: this.sessionId,
406
+ taskId,
407
+ });
408
+ // Extract accumulated text and tool calls
409
+ const accumulatedText = processorState.textContent;
410
+ const toolParts = [...processorState.toolParts.values()];
411
+ const hasToolCalls = toolParts.length > 0;
412
+ // If there are tool calls, extract and execute them
413
+ if (hasToolCalls) {
414
+ // Convert tool parts to ToolCall format
415
+ const toolCalls = toolParts.map((part) => ({
416
+ function: {
417
+ arguments: JSON.stringify(part.state.status === 'pending' ? part.state.input : {}),
418
+ name: part.toolName,
419
+ },
420
+ id: part.callId,
421
+ type: 'function',
422
+ }));
423
+ // Add assistant message with tool calls to context
424
+ await this.contextManager.addAssistantMessage(accumulatedText, toolCalls);
425
+ // Execute tool calls in parallel (matching internal service behavior)
426
+ // This prevents long-running tools (e.g., subagent Tasks) from blocking others
427
+ await Promise.allSettled(toolCalls.map((toolCall) => this.executeToolCall(toolCall, taskId)));
428
+ }
429
+ return {
430
+ hasToolCalls,
431
+ response: accumulatedText,
432
+ };
433
+ }
265
434
  /**
266
435
  * Execute a single tool call.
267
436
  *
268
437
  * @param toolCall - Tool call to execute
438
+ * @param taskId - Optional task ID for concurrent task isolation
269
439
  */
270
- async executeToolCall(toolCall) {
440
+ async executeToolCall(toolCall, taskId) {
271
441
  try {
272
442
  const toolName = toolCall.function.name;
273
443
  const toolArgs = JSON.parse(toolCall.function.arguments);
274
- // Emit tool call event
444
+ // Emit tool call event with taskId for concurrent task isolation
275
445
  this.sessionEventBus.emit('llmservice:toolCall', {
276
446
  args: toolArgs,
277
447
  callId: toolCall.id,
448
+ taskId: taskId || undefined,
278
449
  toolName,
279
450
  });
280
451
  // Execute tool via ToolManager (handles approval, routing, etc.)
281
- const result = await this.toolManager.executeTool(toolName, toolArgs);
282
- // Emit tool result event (success)
452
+ // Pass sessionId and taskId context for sub-agent event routing
453
+ const result = await this.toolManager.executeTool(toolName, toolArgs, this.sessionId, {
454
+ sessionId: this.sessionId,
455
+ taskId,
456
+ });
457
+ // Extract content from ToolExecutionResult - the LLM needs the content string,
458
+ // not the full result object (which would be JSON-stringified and confuse the model)
459
+ const resultContent = result.content;
460
+ const isSuccess = result.success;
461
+ // Emit tool result event with taskId
283
462
  this.sessionEventBus.emit('llmservice:toolResult', {
284
463
  callId: toolCall.id,
285
- result,
286
- success: true,
464
+ ...(isSuccess ? { result: resultContent } : { error: result.errorMessage ?? String(resultContent) }),
465
+ errorType: result.errorType,
466
+ success: isSuccess,
467
+ taskId: taskId || undefined,
287
468
  toolName,
288
469
  });
289
470
  // Add tool result to context
290
- await this.contextManager.addToolResult(toolCall.id, toolName, result, { success: true });
471
+ await this.contextManager.addToolResult(toolCall.id, toolName, resultContent, {
472
+ errorType: result.errorType,
473
+ success: isSuccess,
474
+ });
291
475
  }
292
476
  catch (error) {
293
477
  // Add error result to context
294
478
  const errorMessage = error instanceof Error ? error.message : String(error);
295
- // Emit tool result event (error)
479
+ // Emit tool result event (error) with taskId
296
480
  this.sessionEventBus.emit('llmservice:toolResult', {
297
481
  callId: toolCall.id,
298
482
  error: errorMessage,
299
483
  success: false,
484
+ taskId: taskId || undefined,
300
485
  toolName: toolCall.function.name,
301
486
  });
302
487
  await this.contextManager.addToolResult(toolCall.id, toolCall.function.name, `Error: ${errorMessage}`, {
@@ -326,15 +511,17 @@ export class OpenRouterLLMService {
326
511
  * Handle final response when there are no tool calls.
327
512
  *
328
513
  * @param lastMessage - Last message from LLM
514
+ * @param taskId - Optional task ID for concurrent task isolation
329
515
  * @returns Final response content
330
516
  */
331
- async handleFinalResponse(lastMessage) {
517
+ async handleFinalResponse(lastMessage, taskId) {
332
518
  const content = this.extractTextContent(lastMessage);
333
- // Emit response event
519
+ // Emit response event with taskId for concurrent task isolation
334
520
  this.sessionEventBus.emit('llmservice:response', {
335
521
  content,
336
522
  model: this.config.model,
337
523
  provider: 'openrouter',
524
+ taskId: taskId || undefined,
338
525
  });
339
526
  // Add assistant message to context
340
527
  await this.contextManager.addAssistantMessage(content);
@@ -344,12 +531,14 @@ export class OpenRouterLLMService {
344
531
  * Handle LLM errors and re-throw or wrap appropriately.
345
532
  *
346
533
  * @param error - Error to handle
534
+ * @param taskId - Optional task ID for concurrent task isolation
347
535
  */
348
- handleLLMError(error) {
349
- // Emit error event
536
+ handleLLMError(error, taskId) {
537
+ // Emit error event with taskId for concurrent task isolation
350
538
  const errorMessage = error instanceof Error ? error.message : String(error);
351
539
  this.sessionEventBus.emit('llmservice:error', {
352
540
  error: errorMessage,
541
+ taskId: taskId || undefined,
353
542
  });
354
543
  // Re-throw LLM errors as-is
355
544
  if (error instanceof LlmResponseParsingError ||
@@ -367,18 +556,17 @@ export class OpenRouterLLMService {
367
556
  * Handle tool calls from LLM response.
368
557
  *
369
558
  * @param lastMessage - Last message containing tool calls
559
+ * @param taskId - Optional task ID for concurrent task isolation
370
560
  */
371
- async handleToolCalls(lastMessage) {
561
+ async handleToolCalls(lastMessage, taskId) {
372
562
  if (!lastMessage.toolCalls || lastMessage.toolCalls.length === 0) {
373
563
  return;
374
564
  }
375
565
  // Has tool calls - add assistant message with tool calls
376
566
  const assistantContent = this.extractTextContent(lastMessage);
377
567
  await this.contextManager.addAssistantMessage(assistantContent, lastMessage.toolCalls);
378
- // Execute tool calls via ToolManager
379
- for (const toolCall of lastMessage.toolCalls) {
380
- // eslint-disable-next-line no-await-in-loop -- Sequential tool execution required
381
- await this.executeToolCall(toolCall);
382
- }
568
+ // Execute tool calls in parallel (matching internal service behavior)
569
+ // This prevents long-running tools (e.g., subagent Tasks) from blocking others
570
+ await Promise.allSettled(lastMessage.toolCalls.map((toolCall) => this.executeToolCall(toolCall, taskId)));
383
571
  }
384
572
  }
@@ -11,10 +11,11 @@
11
11
  * - Part creation and updates with unique IDs
12
12
  */
13
13
  import type { StepTokenUsage } from '../../../core/domain/cipher/agent-events/types.js';
14
- import type { CompactionPart, PatchPart, RetryPart, SnapshotPart, StepFinishPart, StepStartPart, TextPart, ToolPart } from '../../../core/interfaces/cipher/message-types.js';
14
+ import type { CompactionPart, PatchPart, ReasoningPart, RetryPart, SnapshotPart, StepFinishPart, StepStartPart, TextPart, ToolPart } from '../../../core/interfaces/cipher/message-types.js';
15
15
  import type { SessionEventBus } from '../events/event-emitter.js';
16
16
  /**
17
17
  * Stream event types that the processor can handle.
18
+ * Following OpenCode's pattern with reasoning-start/delta/end lifecycle.
18
19
  */
19
20
  export type StreamEvent = {
20
21
  callId: string;
@@ -41,12 +42,25 @@ export type StreamEvent = {
41
42
  stepIndex: number;
42
43
  tokens: StepTokenUsage;
43
44
  type: 'step-finish';
45
+ } | {
46
+ delta: string;
47
+ id: string;
48
+ providerMetadata?: Record<string, unknown>;
49
+ type: 'reasoning-delta-v2';
44
50
  } | {
45
51
  delta: string;
46
52
  type: 'reasoning-delta';
47
53
  } | {
48
54
  delta: string;
49
55
  type: 'text-delta';
56
+ } | {
57
+ id: string;
58
+ providerMetadata?: Record<string, unknown>;
59
+ type: 'reasoning-end';
60
+ } | {
61
+ id: string;
62
+ providerMetadata?: Record<string, unknown>;
63
+ type: 'reasoning-start';
50
64
  } | {
51
65
  stepIndex: number;
52
66
  type: 'step-start';
@@ -63,6 +77,8 @@ export interface ProcessorContext {
63
77
  generateId: () => string;
64
78
  /** Session ID for event context */
65
79
  sessionId: string;
80
+ /** Task ID for event routing (required for chunk events to reach TUI) */
81
+ taskId?: string;
66
82
  }
67
83
  /**
68
84
  * Accumulated state during stream processing.
@@ -71,7 +87,11 @@ export interface ProcessorState {
71
87
  /** Current step index */
72
88
  currentStepIndex: number;
73
89
  /** Parts created during processing */
74
- parts: Array<CompactionPart | PatchPart | RetryPart | SnapshotPart | StepFinishPart | StepStartPart | TextPart | ToolPart>;
90
+ parts: Array<CompactionPart | PatchPart | ReasoningPart | RetryPart | SnapshotPart | StepFinishPart | StepStartPart | TextPart | ToolPart>;
91
+ /** Accumulated reasoning content (for legacy reasoning-delta events) */
92
+ reasoningContent: string;
93
+ /** Reasoning parts indexed by ID (for v2 reasoning events) */
94
+ reasoningParts: Map<string, ReasoningPart>;
75
95
  /** Accumulated text content */
76
96
  textContent: string;
77
97
  /** Tool parts indexed by call ID */
@@ -43,12 +43,24 @@ export class StreamProcessor {
43
43
  const state = {
44
44
  currentStepIndex: 0,
45
45
  parts: [],
46
+ reasoningContent: '',
47
+ reasoningParts: new Map(),
46
48
  textContent: '',
47
49
  toolParts: new Map(),
48
50
  };
51
+ let receivedFinish = false;
49
52
  for await (const event of stream) {
53
+ if (event.type === 'finish') {
54
+ receivedFinish = true;
55
+ }
50
56
  await this.handleEvent(event, state, context);
51
57
  }
58
+ // Safety net: if the stream ended without a 'finish' event (e.g., OpenRouter
59
+ // stream closed without setting finish_reason), finalize any pending text part
60
+ // so the TUI receives isComplete: true and stops showing a loading spinner.
61
+ if (!receivedFinish) {
62
+ this.finalizeTextPart(state, context);
63
+ }
52
64
  return state;
53
65
  }
54
66
  /**
@@ -65,6 +77,7 @@ export class StreamProcessor {
65
77
  context.eventBus.emit('llmservice:chunk', {
66
78
  content: '',
67
79
  isComplete: true,
80
+ taskId: context.taskId,
68
81
  type: 'text',
69
82
  });
70
83
  }
@@ -80,13 +93,65 @@ export class StreamProcessor {
80
93
  break;
81
94
  }
82
95
  case 'reasoning-delta': {
83
- // Emit reasoning chunk for UI streaming
96
+ // Legacy: Emit reasoning chunk for UI streaming (simple delta without ID tracking)
97
+ state.reasoningContent += event.delta;
84
98
  context.eventBus.emit('llmservice:chunk', {
85
99
  content: event.delta,
100
+ taskId: context.taskId,
86
101
  type: 'reasoning',
87
102
  });
88
103
  break;
89
104
  }
105
+ case 'reasoning-delta-v2': {
106
+ // V2: Emit reasoning chunk with ID tracking (following OpenCode pattern)
107
+ const reasoningPart = state.reasoningParts.get(event.id);
108
+ if (reasoningPart) {
109
+ reasoningPart.text += event.delta;
110
+ if (event.providerMetadata) {
111
+ reasoningPart.providerMetadata = event.providerMetadata;
112
+ }
113
+ context.eventBus.emit('llmservice:chunk', {
114
+ content: event.delta,
115
+ taskId: context.taskId,
116
+ type: 'reasoning',
117
+ });
118
+ }
119
+ break;
120
+ }
121
+ case 'reasoning-end': {
122
+ // Finalize reasoning part with end timestamp
123
+ const reasoningPart = state.reasoningParts.get(event.id);
124
+ if (reasoningPart) {
125
+ reasoningPart.text = reasoningPart.text.trimEnd();
126
+ reasoningPart.time.end = Date.now();
127
+ if (event.providerMetadata) {
128
+ reasoningPart.providerMetadata = event.providerMetadata;
129
+ }
130
+ // Emit completion signal
131
+ context.eventBus.emit('llmservice:chunk', {
132
+ content: '',
133
+ isComplete: true,
134
+ taskId: context.taskId,
135
+ type: 'reasoning',
136
+ });
137
+ }
138
+ break;
139
+ }
140
+ case 'reasoning-start': {
141
+ // Create new reasoning part and track it
142
+ const reasoningPart = {
143
+ id: event.id,
144
+ providerMetadata: event.providerMetadata,
145
+ text: '',
146
+ time: {
147
+ start: Date.now(),
148
+ },
149
+ type: 'reasoning',
150
+ };
151
+ state.reasoningParts.set(event.id, reasoningPart);
152
+ state.parts.push(reasoningPart);
153
+ break;
154
+ }
90
155
  case 'step-finish': {
91
156
  this.handleStepFinish({
92
157
  cost: event.cost,
@@ -113,7 +178,7 @@ export class StreamProcessor {
113
178
  break;
114
179
  }
115
180
  case 'tool-call-input': {
116
- this.handleToolCallInput(event.callId, event.input, state);
181
+ this.handleToolCallInput(event.callId, event.input, state, context);
117
182
  break;
118
183
  }
119
184
  case 'tool-call-running': {
@@ -173,6 +238,7 @@ export class StreamProcessor {
173
238
  // Emit chunk with delta for real-time UI update
174
239
  context.eventBus.emit('llmservice:chunk', {
175
240
  content: delta,
241
+ taskId: context.taskId,
176
242
  type: 'text',
177
243
  });
178
244
  }
@@ -215,13 +281,20 @@ export class StreamProcessor {
215
281
  /**
216
282
  * Handle tool call input received.
217
283
  */
218
- handleToolCallInput(callId, input, state) {
284
+ handleToolCallInput(callId, input, state, context) {
219
285
  const toolPart = state.toolParts.get(callId);
220
286
  if (toolPart && toolPart.state.status === 'pending') {
221
287
  toolPart.state = {
222
288
  input,
223
289
  status: 'pending',
224
290
  };
291
+ // Emit updated tool call event with args so TUI can display them
292
+ context.eventBus.emit('llmservice:toolCall', {
293
+ args: input,
294
+ callId,
295
+ taskId: context.taskId,
296
+ toolName: toolPart.toolName,
297
+ });
225
298
  }
226
299
  }
227
300
  /**
@@ -255,10 +328,11 @@ export class StreamProcessor {
255
328
  };
256
329
  state.toolParts.set(callId, toolPart);
257
330
  state.parts.push(toolPart);
258
- // Emit tool call event
331
+ // Emit tool call event with taskId for TUI routing
259
332
  context.eventBus.emit('llmservice:toolCall', {
260
333
  args: {},
261
334
  callId,
335
+ taskId: context.taskId,
262
336
  toolName,
263
337
  });
264
338
  }
@@ -70,7 +70,7 @@ export declare function supportsMultimodalFunctionResponse(model: string): boole
70
70
  /**
71
71
  * Default thinking mode token budget
72
72
  */
73
- export declare const DEFAULT_THINKING_BUDGET = 512;
73
+ export declare const DEFAULT_THINKING_BUDGET = 8192;
74
74
  /**
75
75
  * Synthetic thought signature used for Preview models
76
76
  */
@@ -44,7 +44,7 @@ export function supportsMultimodalFunctionResponse(model) {
44
44
  /**
45
45
  * Default thinking mode token budget
46
46
  */
47
- export const DEFAULT_THINKING_BUDGET = 512;
47
+ export const DEFAULT_THINKING_BUDGET = 8192;
48
48
  /**
49
49
  * Synthetic thought signature used for Preview models
50
50
  */
@@ -159,20 +159,20 @@ export const ThinkingConfigManager = {
159
159
  // Gemini 3.x models
160
160
  if (lowerModel.startsWith('gemini-3') || lowerModel.includes('gemini-3')) {
161
161
  return {
162
- includeThoughts: false,
163
- thinkingLevel: ThinkingLevel.HIGH,
162
+ includeThoughts: true,
163
+ thinkingLevel: ThinkingLevel.LOW,
164
164
  };
165
165
  }
166
166
  // Gemini 2.x models
167
167
  if (lowerModel.startsWith('gemini-2') || lowerModel.includes('gemini-2')) {
168
168
  return {
169
- includeThoughts: false,
169
+ includeThoughts: true,
170
170
  thinkingBudget: DEFAULT_THINKING_BUDGET,
171
171
  };
172
172
  }
173
173
  // Other Gemini models - use budget as default
174
174
  return {
175
- includeThoughts: false,
175
+ includeThoughts: true,
176
176
  thinkingBudget: DEFAULT_THINKING_BUDGET,
177
177
  };
178
178
  },