byterover-cli 1.0.4 → 1.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.
Files changed (172) hide show
  1. package/README.md +24 -11
  2. package/dist/commands/curate.js +1 -1
  3. package/dist/commands/hook-prompt-submit.d.ts +27 -0
  4. package/dist/commands/hook-prompt-submit.js +39 -0
  5. package/dist/commands/main.d.ts +13 -0
  6. package/dist/commands/main.js +53 -2
  7. package/dist/commands/query.js +1 -1
  8. package/dist/commands/status.js +8 -3
  9. package/dist/constants.d.ts +2 -2
  10. package/dist/constants.js +2 -2
  11. package/dist/core/domain/cipher/llm/registry.js +53 -2
  12. package/dist/core/domain/cipher/llm/types.d.ts +2 -0
  13. package/dist/core/domain/cipher/process/types.d.ts +7 -0
  14. package/dist/core/domain/cipher/session/session-metadata.d.ts +178 -0
  15. package/dist/core/domain/cipher/session/session-metadata.js +147 -0
  16. package/dist/core/domain/cipher/tools/constants.d.ts +1 -0
  17. package/dist/core/domain/cipher/tools/constants.js +1 -0
  18. package/dist/core/domain/entities/agent.d.ts +16 -0
  19. package/dist/core/domain/entities/agent.js +24 -0
  20. package/dist/core/domain/entities/connector-type.d.ts +9 -0
  21. package/dist/core/domain/entities/connector-type.js +8 -0
  22. package/dist/core/domain/entities/event.d.ts +1 -1
  23. package/dist/core/domain/entities/event.js +2 -0
  24. package/dist/core/domain/errors/task-error.d.ts +4 -0
  25. package/dist/core/domain/errors/task-error.js +7 -0
  26. package/dist/core/domain/knowledge/markdown-writer.d.ts +15 -18
  27. package/dist/core/domain/knowledge/markdown-writer.js +232 -34
  28. package/dist/core/domain/knowledge/relation-parser.d.ts +25 -39
  29. package/dist/core/domain/knowledge/relation-parser.js +39 -61
  30. package/dist/core/domain/transport/schemas.d.ts +77 -2
  31. package/dist/core/domain/transport/schemas.js +51 -2
  32. package/dist/core/interfaces/cipher/i-session-persistence.d.ts +133 -0
  33. package/dist/core/interfaces/cipher/i-session-persistence.js +7 -0
  34. package/dist/core/interfaces/cipher/message-types.d.ts +6 -0
  35. package/dist/core/interfaces/connectors/connector-types.d.ts +57 -0
  36. package/dist/core/interfaces/connectors/i-connector-manager.d.ts +72 -0
  37. package/dist/core/interfaces/connectors/i-connector.d.ts +54 -0
  38. package/dist/core/interfaces/connectors/i-connector.js +1 -0
  39. package/dist/core/interfaces/executor/i-curate-executor.d.ts +2 -2
  40. package/dist/core/interfaces/i-context-file-reader.d.ts +3 -0
  41. package/dist/core/interfaces/i-file-service.d.ts +7 -0
  42. package/dist/core/interfaces/usecase/i-connectors-use-case.d.ts +3 -0
  43. package/dist/core/interfaces/usecase/i-connectors-use-case.js +1 -0
  44. package/dist/core/interfaces/usecase/{i-clear-use-case.d.ts → i-reset-use-case.d.ts} +1 -1
  45. package/dist/core/interfaces/usecase/i-reset-use-case.js +1 -0
  46. package/dist/hooks/init/update-notifier.d.ts +1 -0
  47. package/dist/hooks/init/update-notifier.js +10 -1
  48. package/dist/infra/cipher/agent/agent-schemas.d.ts +6 -6
  49. package/dist/infra/cipher/agent/service-initializer.js +4 -4
  50. package/dist/infra/cipher/file-system/binary-utils.d.ts +7 -12
  51. package/dist/infra/cipher/file-system/binary-utils.js +46 -31
  52. package/dist/infra/cipher/file-system/context-tree-file-system-factory.js +3 -2
  53. package/dist/infra/cipher/file-system/file-system-service.js +1 -0
  54. package/dist/infra/cipher/http/internal-llm-http-service.js +3 -5
  55. package/dist/infra/cipher/interactive-loop.js +3 -1
  56. package/dist/infra/cipher/llm/context/context-manager.d.ts +2 -2
  57. package/dist/infra/cipher/llm/context/context-manager.js +63 -18
  58. package/dist/infra/cipher/llm/formatters/gemini-formatter.d.ts +13 -0
  59. package/dist/infra/cipher/llm/formatters/gemini-formatter.js +146 -15
  60. package/dist/infra/cipher/llm/generators/byterover-content-generator.js +6 -2
  61. package/dist/infra/cipher/llm/internal-llm-service.js +2 -2
  62. package/dist/infra/cipher/llm/thought-parser.d.ts +21 -0
  63. package/dist/infra/cipher/llm/thought-parser.js +27 -0
  64. package/dist/infra/cipher/llm/tool-output-processor.d.ts +10 -0
  65. package/dist/infra/cipher/llm/tool-output-processor.js +80 -7
  66. package/dist/infra/cipher/process/process-service.js +11 -3
  67. package/dist/infra/cipher/session/chat-session.d.ts +7 -2
  68. package/dist/infra/cipher/session/chat-session.js +90 -52
  69. package/dist/infra/cipher/session/session-metadata-store.d.ts +52 -0
  70. package/dist/infra/cipher/session/session-metadata-store.js +406 -0
  71. package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.d.ts +6 -7
  72. package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.js +57 -18
  73. package/dist/infra/cipher/tools/implementations/curate-tool.js +132 -36
  74. package/dist/infra/cipher/tools/implementations/read-file-tool.js +38 -17
  75. package/dist/infra/cipher/tools/implementations/search-knowledge-tool.d.ts +7 -0
  76. package/dist/infra/cipher/tools/implementations/search-knowledge-tool.js +303 -0
  77. package/dist/infra/cipher/tools/implementations/task-tool.js +1 -0
  78. package/dist/infra/cipher/tools/index.d.ts +1 -0
  79. package/dist/infra/cipher/tools/index.js +1 -0
  80. package/dist/infra/cipher/tools/tool-manager.js +1 -0
  81. package/dist/infra/cipher/tools/tool-registry.js +7 -0
  82. package/dist/infra/connectors/connector-manager.d.ts +32 -0
  83. package/dist/infra/connectors/connector-manager.js +156 -0
  84. package/dist/infra/connectors/hook/hook-connector-config.d.ts +52 -0
  85. package/dist/infra/connectors/hook/hook-connector-config.js +41 -0
  86. package/dist/infra/connectors/hook/hook-connector.d.ts +46 -0
  87. package/dist/infra/connectors/hook/hook-connector.js +231 -0
  88. package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.d.ts +2 -2
  89. package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.js +1 -1
  90. package/dist/infra/connectors/rules/rules-connector-config.d.ts +95 -0
  91. package/dist/infra/{rule/agent-rule-config.js → connectors/rules/rules-connector-config.js} +10 -10
  92. package/dist/infra/connectors/rules/rules-connector.d.ts +41 -0
  93. package/dist/infra/connectors/rules/rules-connector.js +204 -0
  94. package/dist/infra/{rule/rule-template-service.d.ts → connectors/shared/template-service.d.ts} +3 -3
  95. package/dist/infra/{rule/rule-template-service.js → connectors/shared/template-service.js} +1 -1
  96. package/dist/infra/context-tree/file-context-file-reader.js +4 -0
  97. package/dist/infra/context-tree/file-context-tree-writer-service.d.ts +5 -2
  98. package/dist/infra/context-tree/file-context-tree-writer-service.js +20 -5
  99. package/dist/infra/core/executors/curate-executor.d.ts +2 -2
  100. package/dist/infra/core/executors/curate-executor.js +7 -7
  101. package/dist/infra/core/executors/query-executor.d.ts +12 -0
  102. package/dist/infra/core/executors/query-executor.js +62 -1
  103. package/dist/infra/core/task-processor.d.ts +2 -2
  104. package/dist/infra/file/fs-file-service.d.ts +7 -0
  105. package/dist/infra/file/fs-file-service.js +15 -1
  106. package/dist/infra/process/agent-worker.d.ts +2 -2
  107. package/dist/infra/process/agent-worker.js +626 -142
  108. package/dist/infra/process/constants.d.ts +1 -1
  109. package/dist/infra/process/constants.js +1 -1
  110. package/dist/infra/process/ipc-types.d.ts +17 -4
  111. package/dist/infra/process/ipc-types.js +3 -3
  112. package/dist/infra/process/parent-heartbeat.d.ts +47 -0
  113. package/dist/infra/process/parent-heartbeat.js +118 -0
  114. package/dist/infra/process/process-manager.d.ts +89 -1
  115. package/dist/infra/process/process-manager.js +293 -9
  116. package/dist/infra/process/task-queue-manager.d.ts +13 -0
  117. package/dist/infra/process/task-queue-manager.js +19 -0
  118. package/dist/infra/process/transport-handlers.d.ts +3 -0
  119. package/dist/infra/process/transport-handlers.js +82 -5
  120. package/dist/infra/process/transport-worker.js +9 -69
  121. package/dist/infra/repl/commands/connectors-command.d.ts +8 -0
  122. package/dist/infra/repl/commands/{gen-rules-command.js → connectors-command.js} +21 -10
  123. package/dist/infra/repl/commands/index.js +8 -4
  124. package/dist/infra/repl/commands/init-command.js +11 -7
  125. package/dist/infra/repl/commands/new-command.d.ts +14 -0
  126. package/dist/infra/repl/commands/new-command.js +61 -0
  127. package/dist/infra/repl/commands/query-command.js +22 -2
  128. package/dist/infra/repl/commands/{clear-command.d.ts → reset-command.d.ts} +2 -2
  129. package/dist/infra/repl/commands/{clear-command.js → reset-command.js} +11 -11
  130. package/dist/infra/transport/socket-io-transport-client.d.ts +68 -0
  131. package/dist/infra/transport/socket-io-transport-client.js +283 -7
  132. package/dist/infra/usecase/connectors-use-case.d.ts +59 -0
  133. package/dist/infra/usecase/connectors-use-case.js +203 -0
  134. package/dist/infra/usecase/init-use-case.d.ts +8 -43
  135. package/dist/infra/usecase/init-use-case.js +29 -253
  136. package/dist/infra/usecase/logout-use-case.js +2 -2
  137. package/dist/infra/usecase/pull-use-case.js +5 -5
  138. package/dist/infra/usecase/push-use-case.js +5 -5
  139. package/dist/infra/usecase/{clear-use-case.d.ts → reset-use-case.d.ts} +5 -5
  140. package/dist/infra/usecase/{clear-use-case.js → reset-use-case.js} +7 -8
  141. package/dist/infra/usecase/space-list-use-case.js +3 -3
  142. package/dist/infra/usecase/space-switch-use-case.js +3 -3
  143. package/dist/resources/prompts/curate.yml +75 -13
  144. package/dist/resources/prompts/explore.yml +34 -0
  145. package/dist/resources/prompts/query-orchestrator.yml +112 -0
  146. package/dist/resources/prompts/system-prompt.yml +12 -2
  147. package/dist/resources/tools/curate.txt +60 -15
  148. package/dist/resources/tools/search_knowledge.txt +32 -0
  149. package/dist/templates/sections/brv-instructions.md +98 -0
  150. package/dist/tui/components/inline-prompts/inline-confirm.js +2 -2
  151. package/dist/tui/components/onboarding/onboarding-flow.js +14 -10
  152. package/dist/tui/components/onboarding/welcome-box.js +1 -1
  153. package/dist/tui/contexts/onboarding-context.d.ts +4 -0
  154. package/dist/tui/contexts/onboarding-context.js +14 -2
  155. package/dist/tui/views/command-view.js +19 -0
  156. package/dist/utils/file-validator.d.ts +1 -1
  157. package/dist/utils/file-validator.js +34 -35
  158. package/dist/utils/type-guards.d.ts +5 -0
  159. package/dist/utils/type-guards.js +7 -0
  160. package/oclif.manifest.json +32 -6
  161. package/package.json +4 -1
  162. package/dist/config/context-tree-domains.d.ts +0 -29
  163. package/dist/config/context-tree-domains.js +0 -29
  164. package/dist/core/interfaces/usecase/i-generate-rules-use-case.d.ts +0 -3
  165. package/dist/infra/repl/commands/gen-rules-command.d.ts +0 -7
  166. package/dist/infra/rule/agent-rule-config.d.ts +0 -19
  167. package/dist/infra/usecase/generate-rules-use-case.d.ts +0 -61
  168. package/dist/infra/usecase/generate-rules-use-case.js +0 -285
  169. /package/dist/core/interfaces/{usecase/i-clear-use-case.js → connectors/connector-types.js} +0 -0
  170. /package/dist/core/interfaces/{usecase/i-generate-rules-use-case.js → connectors/i-connector-manager.js} +0 -0
  171. /package/dist/infra/{rule → connectors/shared}/constants.d.ts +0 -0
  172. /package/dist/infra/{rule → connectors/shared}/constants.js +0 -0
@@ -79,10 +79,14 @@ export class ContextManager {
79
79
  role: 'assistant',
80
80
  toolCalls,
81
81
  };
82
- this.messages.push(message);
83
- // Auto-save to persistent storage (non-blocking)
84
- this.persistHistory().catch((error) => {
85
- this.logger.error('Failed to persist history after assistant message', { error, sessionId: this.sessionId });
82
+ await this.mutex.withLock(async () => {
83
+ this.messages.push(message);
84
+ try {
85
+ await this.persistHistory();
86
+ }
87
+ catch (error) {
88
+ this.logger.error('Failed to persist history after assistant message', { error, sessionId: this.sessionId });
89
+ }
86
90
  });
87
91
  }
88
92
  /**
@@ -95,10 +99,14 @@ export class ContextManager {
95
99
  content,
96
100
  role: 'system',
97
101
  };
98
- this.messages.push(message);
99
- // Auto-save to persistent storage (non-blocking)
100
- this.persistHistory().catch((error) => {
101
- this.logger.error('Failed to persist history after system message', { error, sessionId: this.sessionId });
102
+ await this.mutex.withLock(async () => {
103
+ this.messages.push(message);
104
+ try {
105
+ await this.persistHistory();
106
+ }
107
+ catch (error) {
108
+ this.logger.error('Failed to persist history after system message', { error, sessionId: this.sessionId });
109
+ }
102
110
  });
103
111
  }
104
112
  /**
@@ -137,11 +145,32 @@ export class ContextManager {
137
145
  * @param _metadata.metadata - Execution metadata (duration, tokens, etc.)
138
146
  * @returns The content that was added
139
147
  */
140
- async addToolResult(toolCallId, toolName, result, _metadata) {
148
+ async addToolResult(toolCallId, toolName, result, _metadata, attachments) {
141
149
  // Sanitize result - convert to string representation (can be done outside lock)
142
150
  const sanitized = this.sanitizeToolResult(result);
151
+ // Build content: if attachments exist, create MessagePart array
152
+ const content = attachments && attachments.length > 0
153
+ ? [
154
+ { text: sanitized, type: 'text' },
155
+ ...attachments.map((att) => {
156
+ if (att.type === 'image') {
157
+ return {
158
+ image: att.data,
159
+ mimeType: att.mime,
160
+ type: 'image',
161
+ };
162
+ }
163
+ return {
164
+ data: att.data,
165
+ filename: att.filename,
166
+ mimeType: att.mime,
167
+ type: 'file',
168
+ };
169
+ }),
170
+ ]
171
+ : sanitized;
143
172
  const message = {
144
- content: sanitized,
173
+ content,
145
174
  name: toolName,
146
175
  role: 'tool',
147
176
  toolCallId,
@@ -167,16 +196,18 @@ export class ContextManager {
167
196
  * @param _fileData - Optional file data (not yet implemented)
168
197
  */
169
198
  async addUserMessage(content, _imageData, _fileData) {
170
- // Simple implementation: just use text content
171
- // Image and file support can be added later
172
199
  const message = {
173
200
  content,
174
201
  role: 'user',
175
202
  };
176
- this.messages.push(message);
177
- // Auto-save to persistent storage (non-blocking)
178
- this.persistHistory().catch((error) => {
179
- this.logger.error('Failed to persist history after user message', { error, sessionId: this.sessionId });
203
+ await this.mutex.withLock(async () => {
204
+ this.messages.push(message);
205
+ try {
206
+ await this.persistHistory();
207
+ }
208
+ catch (error) {
209
+ this.logger.error('Failed to persist history after user message', { error, sessionId: this.sessionId });
210
+ }
180
211
  });
181
212
  }
182
213
  /**
@@ -559,8 +590,22 @@ export class ContextManager {
559
590
  if (typeof result === 'string') {
560
591
  return result;
561
592
  }
562
- // Convert to JSON string
563
- const jsonString = JSON.stringify(result, null, 2);
593
+ // Convert to JSON string with special type handling
594
+ const jsonString = JSON.stringify(result, (_, val) => {
595
+ // Convert BigInt to string
596
+ if (typeof val === 'bigint') {
597
+ return val.toString();
598
+ }
599
+ // Convert functions to their string representation
600
+ if (typeof val === 'function') {
601
+ return `[Function: ${val.name || 'anonymous'}]`;
602
+ }
603
+ // Convert Symbols to string
604
+ if (typeof val === 'symbol') {
605
+ return val.toString();
606
+ }
607
+ return val;
608
+ }, 2);
564
609
  // Limit size to prevent extremely large results
565
610
  const MAX_RESULT_LENGTH = 50_000;
566
611
  if (jsonString.length > MAX_RESULT_LENGTH) {
@@ -34,6 +34,7 @@ export declare class GeminiMessageFormatter implements IMessageFormatter<Content
34
34
  /**
35
35
  * Formats assistant message to Gemini's Content format.
36
36
  * Maps 'assistant' role to 'model' and includes both text and tool calls.
37
+ * For Gemini 3+ models, includes thoughtSignature on function calls.
37
38
  */
38
39
  private formatAssistantMessage;
39
40
  /**
@@ -67,3 +68,15 @@ export declare class GeminiMessageFormatter implements IMessageFormatter<Content
67
68
  */
68
69
  private generateToolCallId;
69
70
  }
71
+ /**
72
+ * Ensures that function calls in the active conversation loop have thought signatures.
73
+ * Required for Gemini 3+ preview models.
74
+ *
75
+ * The "active loop" starts from the last user text message in the conversation.
76
+ * Only the first function call in each model turn needs a thought signature.
77
+ *
78
+ * @param contents Array of Content objects formatted for Gemini API
79
+ * @param model The model being used (only applies to Gemini 3+ models)
80
+ * @returns Modified contents with thought signatures added where needed
81
+ */
82
+ export declare function ensureActiveLoopHasThoughtSignatures(contents: Content[], model: string): Content[];
@@ -1,3 +1,10 @@
1
+ import { isGemini3Model, SYNTHETIC_THOUGHT_SIGNATURE } from '../thought-parser.js';
2
+ /**
3
+ * Type guard to check if a part has a thoughtSignature property.
4
+ */
5
+ function hasThoughtSignature(part) {
6
+ return 'thoughtSignature' in part;
7
+ }
1
8
  /**
2
9
  * Message formatter for Google Gemini API.
3
10
  *
@@ -64,24 +71,30 @@ export class GeminiMessageFormatter {
64
71
  return [];
65
72
  }
66
73
  const textParts = [];
67
- const functionCalls = [];
74
+ const functionCallsWithSignatures = [];
68
75
  // Extract text and function calls from response parts
69
76
  for (const part of candidate.content.parts) {
70
77
  if ('text' in part && part.text) {
71
78
  textParts.push(part.text);
72
79
  }
73
80
  if ('functionCall' in part && part.functionCall) {
74
- functionCalls.push(part.functionCall);
81
+ // Extract thoughtSignature if present (Gemini 3+ models)
82
+ const thoughtSignature = hasThoughtSignature(part) ? part.thoughtSignature : undefined;
83
+ functionCallsWithSignatures.push({
84
+ fc: part.functionCall,
85
+ thoughtSignature,
86
+ });
75
87
  }
76
88
  }
77
89
  // Convert to internal message format
78
- const toolCalls = functionCalls.length > 0
79
- ? functionCalls.map((fc) => ({
90
+ const toolCalls = functionCallsWithSignatures.length > 0
91
+ ? functionCallsWithSignatures.map(({ fc, thoughtSignature }) => ({
80
92
  function: {
81
93
  arguments: JSON.stringify(fc.args ?? {}),
82
94
  name: fc.name ?? '',
83
95
  },
84
96
  id: this.generateToolCallId(fc.name ?? ''),
97
+ thoughtSignature,
85
98
  type: 'function',
86
99
  }))
87
100
  : undefined;
@@ -98,30 +111,58 @@ export class GeminiMessageFormatter {
98
111
  * Required by Gemini API when assistant made multiple tool calls.
99
112
  */
100
113
  combineToolResults(toolMessages) {
114
+ const parts = [];
115
+ for (const msg of toolMessages) {
116
+ // Add the tool result part
117
+ parts.push(this.formatToolResultPart(msg));
118
+ // Extract image/file parts from MessagePart[] content
119
+ if (Array.isArray(msg.content)) {
120
+ for (const part of msg.content) {
121
+ if (part.type === 'image' || part.type === 'file') {
122
+ parts.push(this.formatUserContentPart(part));
123
+ }
124
+ }
125
+ }
126
+ }
101
127
  return {
102
- parts: toolMessages.map((msg) => this.formatToolResultPart(msg)),
128
+ parts,
103
129
  role: 'user',
104
130
  };
105
131
  }
106
132
  /**
107
133
  * Formats assistant message to Gemini's Content format.
108
134
  * Maps 'assistant' role to 'model' and includes both text and tool calls.
135
+ * For Gemini 3+ models, includes thoughtSignature on function calls.
109
136
  */
110
137
  formatAssistantMessage(msg) {
111
138
  const parts = [];
112
139
  // Add text content if present
113
140
  if (msg.content) {
114
- parts.push({ text: String(msg.content) });
141
+ if (typeof msg.content === 'string') {
142
+ parts.push({ text: msg.content });
143
+ }
144
+ else if (Array.isArray(msg.content)) {
145
+ for (const part of msg.content) {
146
+ if (part.type === 'text') {
147
+ parts.push({ text: part.text });
148
+ }
149
+ }
150
+ }
115
151
  }
116
152
  // Add tool calls if present
117
153
  if (msg.toolCalls) {
118
154
  for (const tc of msg.toolCalls) {
119
- parts.push({
155
+ const functionCallPart = {
120
156
  functionCall: {
121
157
  args: JSON.parse(tc.function.arguments),
122
158
  name: tc.function.name,
123
159
  },
124
- });
160
+ };
161
+ // Include thoughtSignature if present (required for Gemini 3+ models)
162
+ if (tc.thoughtSignature) {
163
+ functionCallPart.thoughtSignature = tc.thoughtSignature;
164
+ }
165
+ parts.push(functionCallPart);
125
166
  }
126
167
  }
127
168
  return {
@@ -174,8 +215,10 @@ export class GeminiMessageFormatter {
174
215
  responseObject = { result: null };
175
216
  }
176
217
  else if (Array.isArray(msg.content)) {
177
- // Array content (e.g., MessagePart[]) - wrap in result
178
- responseObject = { result: msg.content };
218
+ // Array content (e.g., MessagePart[]) - filter out file/image parts
219
+ // File/image parts are sent separately as inlineData to avoid duplicate tokenization
220
+ const textParts = msg.content.filter((p) => p.type === 'text');
221
+ responseObject = textParts.length > 0 ? { result: textParts } : { result: 'Attachment processed' };
179
222
  }
180
223
  else if (typeof msg.content === 'object') {
181
224
  // Already an object (shouldn't happen with current implementation, but handle it)
@@ -206,13 +249,28 @@ export class GeminiMessageFormatter {
206
249
  return { text: part.text };
207
250
  }
208
251
  if (part.type === 'image') {
209
- // Image support not yet implemented for Gemini
210
- // Gemini supports inline images via inlineData or fileData
211
- return { text: '[Image not yet supported]' };
252
+ // Convert image to Gemini inlineData format
253
+ const imageData = typeof part.image === 'string' ? part.image : String(part.image);
254
+ // Remove data URL prefix if present (e.g., "data:image/jpeg;base64,")
255
+ const base64Data = imageData.includes(',') ? imageData.split(',')[1] : imageData;
256
+ return {
257
+ inlineData: {
258
+ data: base64Data,
259
+ mimeType: part.mimeType ?? 'image/jpeg',
260
+ },
261
+ };
212
262
  }
213
263
  if (part.type === 'file') {
214
- // File support not yet implemented for Gemini
215
- return { text: '[File not yet supported]' };
264
+ // Convert file to Gemini inlineData format (supports PDFs)
265
+ const fileData = typeof part.data === 'string' ? part.data : String(part.data);
266
+ // Remove data URL prefix if present
267
+ const base64Data = fileData.includes(',') ? fileData.split(',')[1] : fileData;
268
+ return {
269
+ inlineData: {
270
+ data: base64Data,
271
+ mimeType: part.mimeType ?? 'application/pdf',
272
+ },
273
+ };
216
274
  }
217
275
  return { text: '[Unknown content type]' };
218
276
  }
@@ -251,3 +309,76 @@ export class GeminiMessageFormatter {
251
309
  return `call_${timestamp}_${random}_${toolName}`;
252
310
  }
253
311
  }
312
+ /**
313
+ * Ensures that function calls in the active conversation loop have thought signatures.
314
+ * Required for Gemini 3+ preview models.
315
+ *
316
+ * The "active loop" starts from the last user text message in the conversation.
317
+ * Only the first function call in each model turn needs a thought signature.
318
+ *
319
+ * @param contents Array of Content objects formatted for Gemini API
320
+ * @param model The model being used (only applies to Gemini 3+ models)
321
+ * @returns Modified contents with thought signatures added where needed
322
+ */
323
+ export function ensureActiveLoopHasThoughtSignatures(contents, model) {
324
+ // Only apply to Gemini 3+ models
325
+ if (!isGemini3Model(model)) {
326
+ return contents;
327
+ }
328
+ // Find the last user turn with text message (start of active loop)
329
+ let activeLoopStartIndex = -1;
330
+ for (let i = contents.length - 1; i >= 0; i--) {
331
+ const content = contents[i];
332
+ if (content.role === 'user' && content.parts?.some((part) => 'text' in part && part.text)) {
333
+ activeLoopStartIndex = i;
334
+ break;
335
+ }
336
+ }
337
+ // No user text message found - nothing to do
338
+ if (activeLoopStartIndex === -1) {
339
+ return contents;
340
+ }
341
+ // Create shallow copy to avoid mutating original
342
+ const newContents = [...contents];
343
+ // Process each content from active loop start to end
344
+ for (let i = activeLoopStartIndex; i < newContents.length; i++) {
345
+ const content = newContents[i];
346
+ // Only process model turns with parts
347
+ if (content.role !== 'model' || !content.parts) {
348
+ continue;
349
+ }
350
+ const newParts = [...content.parts];
351
+ const updatedContent = addThoughtSignatureToFirstFunctionCall(newParts, content);
352
+ if (updatedContent) {
353
+ newContents[i] = updatedContent;
354
+ }
355
+ }
356
+ return newContents;
357
+ }
358
+ /**
359
+ * Adds thought signature to the first function call in parts if missing.
360
+ * Returns updated content or null if no modification needed.
361
+ */
362
+ function addThoughtSignatureToFirstFunctionCall(parts, content) {
363
+ for (let j = 0; j < parts.length; j++) {
364
+ const part = parts[j];
365
+ if (!part || !('functionCall' in part) || !part.functionCall) {
366
+ continue;
367
+ }
368
+ // Check if thoughtSignature already exists using type guard
369
+ if (hasThoughtSignature(part) && part.thoughtSignature) {
370
+ return null; // Already has signature, no modification needed
371
+ }
372
+ // Add synthetic thought signature
373
+ const partWithSignature = {
374
+ ...part,
375
+ thoughtSignature: SYNTHETIC_THOUGHT_SIGNATURE,
376
+ };
377
+ parts[j] = partWithSignature;
378
+ return {
379
+ ...content,
380
+ parts,
381
+ };
382
+ }
383
+ return null; // No function call found
384
+ }
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { FunctionCallingConfigMode } from '@google/genai';
8
8
  import { ClaudeMessageFormatter } from '../formatters/claude-formatter.js';
9
- import { GeminiMessageFormatter } from '../formatters/gemini-formatter.js';
9
+ import { ensureActiveLoopHasThoughtSignatures, GeminiMessageFormatter } from '../formatters/gemini-formatter.js';
10
10
  import { ThinkingConfigManager } from '../thought-parser.js';
11
11
  import { ClaudeTokenizer } from '../tokenizers/claude-tokenizer.js';
12
12
  import { GeminiTokenizer } from '../tokenizers/gemini-tokenizer.js';
@@ -69,7 +69,11 @@ export class ByteRoverContentGenerator {
69
69
  */
70
70
  async generateContent(request) {
71
71
  // Format messages for provider
72
- const formattedMessages = this.formatter.format(request.contents);
72
+ let formattedMessages = this.formatter.format(request.contents);
73
+ // For Gemini 3+ models, ensure function calls in the active loop have thought signatures
74
+ if (this.providerType === 'gemini') {
75
+ formattedMessages = ensureActiveLoopHasThoughtSignatures(formattedMessages, this.config.model);
76
+ }
73
77
  // Build generation config
74
78
  const genConfig = this.buildGenerationConfig(request.tools ?? {}, request.systemPrompt ?? '', formattedMessages);
75
79
  // Call gRPC service
@@ -272,7 +272,7 @@ export class ByteRoverLLMService {
272
272
  errorType: toolResult.errorType,
273
273
  metadata: toolResult.metadata,
274
274
  success: toolResult.success,
275
- });
275
+ }, toolResult.processedOutput.attachments);
276
276
  }
277
277
  /**
278
278
  * Build generation request for the IContentGenerator.
@@ -624,7 +624,7 @@ export class ByteRoverLLMService {
624
624
  taskId,
625
625
  });
626
626
  // Process output (truncation and file saving if needed)
627
- const processedOutput = await this.outputProcessor.processOutput(toolName, result.content);
627
+ const processedOutput = await this.outputProcessor.processStructuredOutput(toolName, result.content);
628
628
  // Emit truncation event if output was truncated
629
629
  if (processedOutput.metadata?.truncated) {
630
630
  this.sessionEventBus.emit('llmservice:outputTruncated', {
@@ -39,13 +39,34 @@ export interface ThinkingConfig {
39
39
  }
40
40
  /**
41
41
  * Thinking levels for Gemini 3.x models
42
+ * Matches @google/genai ThinkingLevel enum values
42
43
  */
43
44
  export declare enum ThinkingLevel {
45
+ DISABLED = "DISABLED",
44
46
  HIGH = "HIGH",
45
47
  LOW = "LOW",
46
48
  MEDIUM = "MEDIUM",
47
49
  UNSPECIFIED = "THINKING_LEVEL_UNSPECIFIED"
48
50
  }
51
+ /**
52
+ * Check if model is a Gemini 2.x model
53
+ * @param model - Model identifier
54
+ * @returns True if model is Gemini 2.x
55
+ */
56
+ export declare function isGemini2Model(model: string): boolean;
57
+ /**
58
+ * Check if model is a Gemini 3.x model
59
+ * @param model - Model identifier
60
+ * @returns True if model is Gemini 3.x
61
+ */
62
+ export declare function isGemini3Model(model: string): boolean;
63
+ /**
64
+ * Check if model supports multimodal function responses
65
+ * This is supported in Gemini 3+ models
66
+ * @param model - Model identifier
67
+ * @returns True if model supports multimodal function responses
68
+ */
69
+ export declare function supportsMultimodalFunctionResponse(model: string): boolean;
49
70
  /**
50
71
  * Default thinking mode token budget
51
72
  */
@@ -6,14 +6,41 @@
6
6
  */
7
7
  /**
8
8
  * Thinking levels for Gemini 3.x models
9
+ * Matches @google/genai ThinkingLevel enum values
9
10
  */
10
11
  export var ThinkingLevel;
11
12
  (function (ThinkingLevel) {
13
+ ThinkingLevel["DISABLED"] = "DISABLED";
12
14
  ThinkingLevel["HIGH"] = "HIGH";
13
15
  ThinkingLevel["LOW"] = "LOW";
14
16
  ThinkingLevel["MEDIUM"] = "MEDIUM";
15
17
  ThinkingLevel["UNSPECIFIED"] = "THINKING_LEVEL_UNSPECIFIED";
16
18
  })(ThinkingLevel || (ThinkingLevel = {}));
19
+ /**
20
+ * Check if model is a Gemini 2.x model
21
+ * @param model - Model identifier
22
+ * @returns True if model is Gemini 2.x
23
+ */
24
+ export function isGemini2Model(model) {
25
+ return /^gemini-2(\.|$)/.test(model);
26
+ }
27
+ /**
28
+ * Check if model is a Gemini 3.x model
29
+ * @param model - Model identifier
30
+ * @returns True if model is Gemini 3.x
31
+ */
32
+ export function isGemini3Model(model) {
33
+ return model.startsWith('gemini-3-');
34
+ }
35
+ /**
36
+ * Check if model supports multimodal function responses
37
+ * This is supported in Gemini 3+ models
38
+ * @param model - Model identifier
39
+ * @returns True if model supports multimodal function responses
40
+ */
41
+ export function supportsMultimodalFunctionResponse(model) {
42
+ return model.startsWith('gemini-3-');
43
+ }
17
44
  /**
18
45
  * Default thinking mode token budget
19
46
  */
@@ -126,6 +126,16 @@ export declare class ToolOutputProcessor {
126
126
  * Extract text content from structured output.
127
127
  */
128
128
  private extractTextContent;
129
+ /**
130
+ * Fallback stringify for when JSON.stringify fails.
131
+ * Creates a meaningful string representation of objects/arrays.
132
+ *
133
+ * @param value - Value to stringify
134
+ * @param seen - Set to track circular references
135
+ * @param depth - Current depth for limiting recursion
136
+ * @returns String representation
137
+ */
138
+ private fallbackStringify;
129
139
  /**
130
140
  * Check if output is an MCP-style content array.
131
141
  */
@@ -185,6 +185,64 @@ export class ToolOutputProcessor {
185
185
  }
186
186
  return '';
187
187
  }
188
+ /**
189
+ * Fallback stringify for when JSON.stringify fails.
190
+ * Creates a meaningful string representation of objects/arrays.
191
+ *
192
+ * @param value - Value to stringify
193
+ * @param seen - Set to track circular references
194
+ * @param depth - Current depth for limiting recursion
195
+ * @returns String representation
196
+ */
197
+ fallbackStringify(value, seen = new WeakSet(), depth = 0) {
198
+ const MAX_DEPTH = 10;
199
+ // Handle primitives
200
+ if (value === null)
201
+ return 'null';
202
+ if (value === undefined)
203
+ return 'undefined';
204
+ if (typeof value === 'string')
205
+ return `"${value}"`;
206
+ if (typeof value === 'number' || typeof value === 'boolean')
207
+ return String(value);
208
+ if (typeof value === 'bigint')
209
+ return `${value}n`;
210
+ if (typeof value === 'symbol')
211
+ return value.toString();
212
+ if (typeof value === 'function')
213
+ return `[Function: ${value.name || 'anonymous'}]`;
214
+ // Handle objects and arrays
215
+ if (typeof value === 'object') {
216
+ // Check for circular reference
217
+ if (seen.has(value)) {
218
+ return '[Circular]';
219
+ }
220
+ seen.add(value);
221
+ // Depth limit
222
+ if (depth > MAX_DEPTH) {
223
+ return Array.isArray(value) ? '[Array]' : '[Object]';
224
+ }
225
+ // Handle arrays
226
+ if (Array.isArray(value)) {
227
+ const items = value.map((item) => this.fallbackStringify(item, seen, depth + 1));
228
+ return `[${items.join(', ')}]`;
229
+ }
230
+ // Handle Date
231
+ if (value instanceof Date) {
232
+ return value.toISOString();
233
+ }
234
+ // Handle Error
235
+ if (value instanceof Error) {
236
+ return `[Error: ${value.message}]`;
237
+ }
238
+ // Handle plain objects
239
+ const entries = Object.entries(value)
240
+ .map(([key, val]) => `"${key}": ${this.fallbackStringify(val, seen, depth + 1)}`);
241
+ return `{${entries.join(', ')}}`;
242
+ }
243
+ // Unknown type
244
+ return '[Unknown]';
245
+ }
188
246
  /**
189
247
  * Check if output is an MCP-style content array.
190
248
  */
@@ -246,18 +304,33 @@ export class ToolOutputProcessor {
246
304
  return String(value);
247
305
  }
248
306
  try {
249
- // Try JSON.stringify with circular reference handling
307
+ // Try JSON.stringify with proper handling for special types
250
308
  return JSON.stringify(value, (_, val) => {
251
- // Handle circular references by converting to string
252
- if (typeof val === 'object' && val !== null) {
253
- return val;
309
+ // Convert BigInt to string
310
+ if (typeof val === 'bigint') {
311
+ return val.toString();
312
+ }
313
+ // Convert functions to their string representation
314
+ if (typeof val === 'function') {
315
+ return `[Function: ${val.name || 'anonymous'}]`;
316
+ }
317
+ // Convert Symbols to string
318
+ if (typeof val === 'symbol') {
319
+ return val.toString();
254
320
  }
255
321
  return val;
256
322
  }, 2);
257
323
  }
258
- catch {
259
- // Fallback to String() if JSON.stringify fails
260
- return String(value);
324
+ catch (error) {
325
+ // Fallback: Try to create a meaningful string representation
326
+ // instead of [object Object]
327
+ try {
328
+ return this.fallbackStringify(value);
329
+ }
330
+ catch {
331
+ // Last resort: Return error description
332
+ return `[Serialization failed: ${error instanceof Error ? error.message : 'Unknown error'}]`;
333
+ }
261
334
  }
262
335
  }
263
336
  /**
@@ -20,6 +20,11 @@ const DEFAULT_MAX_CONCURRENT_PROCESSES = 5;
20
20
  * Default maximum output buffer size (bytes).
21
21
  */
22
22
  const DEFAULT_MAX_OUTPUT_BUFFER = 1024 * 1024; // 1MB
23
+ /**
24
+ * Default grace period for SIGTERM before SIGKILL (milliseconds).
25
+ * 5 seconds gives processes ample time to clean up gracefully.
26
+ */
27
+ const DEFAULT_KILL_GRACE_PERIOD = 5000;
23
28
  /**
24
29
  * Process service implementation.
25
30
  *
@@ -47,6 +52,7 @@ export class ProcessService {
47
52
  allowedCommands: config.allowedCommands || [],
48
53
  blockedCommands: config.blockedCommands || [],
49
54
  environment: config.environment || {},
55
+ killGracePeriod: config.killGracePeriod ?? DEFAULT_KILL_GRACE_PERIOD,
50
56
  maxConcurrentProcesses: config.maxConcurrentProcesses || DEFAULT_MAX_CONCURRENT_PROCESSES,
51
57
  maxOutputBuffer: config.maxOutputBuffer || DEFAULT_MAX_OUTPUT_BUFFER,
52
58
  maxTimeout: config.maxTimeout || DEFAULT_MAX_TIMEOUT,
@@ -303,7 +309,7 @@ export class ProcessService {
303
309
  */
304
310
  async executeInBackground(command, options) {
305
311
  // Check concurrent process limit
306
- const runningCount = [...this.backgroundProcesses.values()].filter(p => p.status === 'running').length;
312
+ const runningCount = [...this.backgroundProcesses.values()].filter((p) => p.status === 'running').length;
307
313
  if (runningCount >= this.config.maxConcurrentProcesses) {
308
314
  throw ProcessError.tooManyProcesses(runningCount, this.config.maxConcurrentProcesses);
309
315
  }
@@ -436,9 +442,11 @@ export class ProcessService {
436
442
  });
437
443
  }
438
444
  // Unix: kill process group using negative PID
445
+ // Grace period allows processes to clean up gracefully before SIGKILL
446
+ const gracePeriodMs = this.config.killGracePeriod;
439
447
  try {
440
448
  process.kill(-targetPid, 'SIGTERM');
441
- await this.sleep(5000);
449
+ await this.sleep(gracePeriodMs);
442
450
  try {
443
451
  process.kill(-targetPid, 'SIGKILL');
444
452
  }
@@ -450,7 +458,7 @@ export class ProcessService {
450
458
  // Fallback to direct kill if process group kill fails
451
459
  // (e.g., process wasn't started with detached: true)
452
460
  child.kill('SIGTERM');
453
- await this.sleep(5000);
461
+ await this.sleep(gracePeriodMs);
454
462
  if (child.exitCode === null) {
455
463
  child.kill('SIGKILL');
456
464
  }