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.
- package/README.md +24 -11
- package/dist/commands/curate.js +1 -1
- package/dist/commands/hook-prompt-submit.d.ts +27 -0
- package/dist/commands/hook-prompt-submit.js +39 -0
- package/dist/commands/main.d.ts +13 -0
- package/dist/commands/main.js +53 -2
- package/dist/commands/query.js +1 -1
- package/dist/commands/status.js +8 -3
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +2 -2
- package/dist/core/domain/cipher/llm/registry.js +53 -2
- package/dist/core/domain/cipher/llm/types.d.ts +2 -0
- package/dist/core/domain/cipher/process/types.d.ts +7 -0
- package/dist/core/domain/cipher/session/session-metadata.d.ts +178 -0
- package/dist/core/domain/cipher/session/session-metadata.js +147 -0
- package/dist/core/domain/cipher/tools/constants.d.ts +1 -0
- package/dist/core/domain/cipher/tools/constants.js +1 -0
- package/dist/core/domain/entities/agent.d.ts +16 -0
- package/dist/core/domain/entities/agent.js +24 -0
- package/dist/core/domain/entities/connector-type.d.ts +9 -0
- package/dist/core/domain/entities/connector-type.js +8 -0
- package/dist/core/domain/entities/event.d.ts +1 -1
- package/dist/core/domain/entities/event.js +2 -0
- package/dist/core/domain/errors/task-error.d.ts +4 -0
- package/dist/core/domain/errors/task-error.js +7 -0
- package/dist/core/domain/knowledge/markdown-writer.d.ts +15 -18
- package/dist/core/domain/knowledge/markdown-writer.js +232 -34
- package/dist/core/domain/knowledge/relation-parser.d.ts +25 -39
- package/dist/core/domain/knowledge/relation-parser.js +39 -61
- package/dist/core/domain/transport/schemas.d.ts +77 -2
- package/dist/core/domain/transport/schemas.js +51 -2
- package/dist/core/interfaces/cipher/i-session-persistence.d.ts +133 -0
- package/dist/core/interfaces/cipher/i-session-persistence.js +7 -0
- package/dist/core/interfaces/cipher/message-types.d.ts +6 -0
- package/dist/core/interfaces/connectors/connector-types.d.ts +57 -0
- package/dist/core/interfaces/connectors/i-connector-manager.d.ts +72 -0
- package/dist/core/interfaces/connectors/i-connector.d.ts +54 -0
- package/dist/core/interfaces/connectors/i-connector.js +1 -0
- package/dist/core/interfaces/executor/i-curate-executor.d.ts +2 -2
- package/dist/core/interfaces/i-context-file-reader.d.ts +3 -0
- package/dist/core/interfaces/i-file-service.d.ts +7 -0
- package/dist/core/interfaces/usecase/i-connectors-use-case.d.ts +3 -0
- package/dist/core/interfaces/usecase/i-connectors-use-case.js +1 -0
- package/dist/core/interfaces/usecase/{i-clear-use-case.d.ts → i-reset-use-case.d.ts} +1 -1
- package/dist/core/interfaces/usecase/i-reset-use-case.js +1 -0
- package/dist/hooks/init/update-notifier.d.ts +1 -0
- package/dist/hooks/init/update-notifier.js +10 -1
- package/dist/infra/cipher/agent/agent-schemas.d.ts +6 -6
- package/dist/infra/cipher/agent/service-initializer.js +4 -4
- package/dist/infra/cipher/file-system/binary-utils.d.ts +7 -12
- package/dist/infra/cipher/file-system/binary-utils.js +46 -31
- package/dist/infra/cipher/file-system/context-tree-file-system-factory.js +3 -2
- package/dist/infra/cipher/file-system/file-system-service.js +1 -0
- package/dist/infra/cipher/http/internal-llm-http-service.js +3 -5
- package/dist/infra/cipher/interactive-loop.js +3 -1
- package/dist/infra/cipher/llm/context/context-manager.d.ts +2 -2
- package/dist/infra/cipher/llm/context/context-manager.js +63 -18
- package/dist/infra/cipher/llm/formatters/gemini-formatter.d.ts +13 -0
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +146 -15
- package/dist/infra/cipher/llm/generators/byterover-content-generator.js +6 -2
- package/dist/infra/cipher/llm/internal-llm-service.js +2 -2
- package/dist/infra/cipher/llm/thought-parser.d.ts +21 -0
- package/dist/infra/cipher/llm/thought-parser.js +27 -0
- package/dist/infra/cipher/llm/tool-output-processor.d.ts +10 -0
- package/dist/infra/cipher/llm/tool-output-processor.js +80 -7
- package/dist/infra/cipher/process/process-service.js +11 -3
- package/dist/infra/cipher/session/chat-session.d.ts +7 -2
- package/dist/infra/cipher/session/chat-session.js +90 -52
- package/dist/infra/cipher/session/session-metadata-store.d.ts +52 -0
- package/dist/infra/cipher/session/session-metadata-store.js +406 -0
- package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.d.ts +6 -7
- package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.js +57 -18
- package/dist/infra/cipher/tools/implementations/curate-tool.js +132 -36
- package/dist/infra/cipher/tools/implementations/read-file-tool.js +38 -17
- package/dist/infra/cipher/tools/implementations/search-knowledge-tool.d.ts +7 -0
- package/dist/infra/cipher/tools/implementations/search-knowledge-tool.js +303 -0
- package/dist/infra/cipher/tools/implementations/task-tool.js +1 -0
- package/dist/infra/cipher/tools/index.d.ts +1 -0
- package/dist/infra/cipher/tools/index.js +1 -0
- package/dist/infra/cipher/tools/tool-manager.js +1 -0
- package/dist/infra/cipher/tools/tool-registry.js +7 -0
- package/dist/infra/connectors/connector-manager.d.ts +32 -0
- package/dist/infra/connectors/connector-manager.js +156 -0
- package/dist/infra/connectors/hook/hook-connector-config.d.ts +52 -0
- package/dist/infra/connectors/hook/hook-connector-config.js +41 -0
- package/dist/infra/connectors/hook/hook-connector.d.ts +46 -0
- package/dist/infra/connectors/hook/hook-connector.js +231 -0
- package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.d.ts +2 -2
- package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.js +1 -1
- package/dist/infra/connectors/rules/rules-connector-config.d.ts +95 -0
- package/dist/infra/{rule/agent-rule-config.js → connectors/rules/rules-connector-config.js} +10 -10
- package/dist/infra/connectors/rules/rules-connector.d.ts +41 -0
- package/dist/infra/connectors/rules/rules-connector.js +204 -0
- package/dist/infra/{rule/rule-template-service.d.ts → connectors/shared/template-service.d.ts} +3 -3
- package/dist/infra/{rule/rule-template-service.js → connectors/shared/template-service.js} +1 -1
- package/dist/infra/context-tree/file-context-file-reader.js +4 -0
- package/dist/infra/context-tree/file-context-tree-writer-service.d.ts +5 -2
- package/dist/infra/context-tree/file-context-tree-writer-service.js +20 -5
- package/dist/infra/core/executors/curate-executor.d.ts +2 -2
- package/dist/infra/core/executors/curate-executor.js +7 -7
- package/dist/infra/core/executors/query-executor.d.ts +12 -0
- package/dist/infra/core/executors/query-executor.js +62 -1
- package/dist/infra/core/task-processor.d.ts +2 -2
- package/dist/infra/file/fs-file-service.d.ts +7 -0
- package/dist/infra/file/fs-file-service.js +15 -1
- package/dist/infra/process/agent-worker.d.ts +2 -2
- package/dist/infra/process/agent-worker.js +626 -142
- package/dist/infra/process/constants.d.ts +1 -1
- package/dist/infra/process/constants.js +1 -1
- package/dist/infra/process/ipc-types.d.ts +17 -4
- package/dist/infra/process/ipc-types.js +3 -3
- package/dist/infra/process/parent-heartbeat.d.ts +47 -0
- package/dist/infra/process/parent-heartbeat.js +118 -0
- package/dist/infra/process/process-manager.d.ts +89 -1
- package/dist/infra/process/process-manager.js +293 -9
- package/dist/infra/process/task-queue-manager.d.ts +13 -0
- package/dist/infra/process/task-queue-manager.js +19 -0
- package/dist/infra/process/transport-handlers.d.ts +3 -0
- package/dist/infra/process/transport-handlers.js +82 -5
- package/dist/infra/process/transport-worker.js +9 -69
- package/dist/infra/repl/commands/connectors-command.d.ts +8 -0
- package/dist/infra/repl/commands/{gen-rules-command.js → connectors-command.js} +21 -10
- package/dist/infra/repl/commands/index.js +8 -4
- package/dist/infra/repl/commands/init-command.js +11 -7
- package/dist/infra/repl/commands/new-command.d.ts +14 -0
- package/dist/infra/repl/commands/new-command.js +61 -0
- package/dist/infra/repl/commands/query-command.js +22 -2
- package/dist/infra/repl/commands/{clear-command.d.ts → reset-command.d.ts} +2 -2
- package/dist/infra/repl/commands/{clear-command.js → reset-command.js} +11 -11
- package/dist/infra/transport/socket-io-transport-client.d.ts +68 -0
- package/dist/infra/transport/socket-io-transport-client.js +283 -7
- package/dist/infra/usecase/connectors-use-case.d.ts +59 -0
- package/dist/infra/usecase/connectors-use-case.js +203 -0
- package/dist/infra/usecase/init-use-case.d.ts +8 -43
- package/dist/infra/usecase/init-use-case.js +29 -253
- package/dist/infra/usecase/logout-use-case.js +2 -2
- package/dist/infra/usecase/pull-use-case.js +5 -5
- package/dist/infra/usecase/push-use-case.js +5 -5
- package/dist/infra/usecase/{clear-use-case.d.ts → reset-use-case.d.ts} +5 -5
- package/dist/infra/usecase/{clear-use-case.js → reset-use-case.js} +7 -8
- package/dist/infra/usecase/space-list-use-case.js +3 -3
- package/dist/infra/usecase/space-switch-use-case.js +3 -3
- package/dist/resources/prompts/curate.yml +75 -13
- package/dist/resources/prompts/explore.yml +34 -0
- package/dist/resources/prompts/query-orchestrator.yml +112 -0
- package/dist/resources/prompts/system-prompt.yml +12 -2
- package/dist/resources/tools/curate.txt +60 -15
- package/dist/resources/tools/search_knowledge.txt +32 -0
- package/dist/templates/sections/brv-instructions.md +98 -0
- package/dist/tui/components/inline-prompts/inline-confirm.js +2 -2
- package/dist/tui/components/onboarding/onboarding-flow.js +14 -10
- package/dist/tui/components/onboarding/welcome-box.js +1 -1
- package/dist/tui/contexts/onboarding-context.d.ts +4 -0
- package/dist/tui/contexts/onboarding-context.js +14 -2
- package/dist/tui/views/command-view.js +19 -0
- package/dist/utils/file-validator.d.ts +1 -1
- package/dist/utils/file-validator.js +34 -35
- package/dist/utils/type-guards.d.ts +5 -0
- package/dist/utils/type-guards.js +7 -0
- package/oclif.manifest.json +32 -6
- package/package.json +4 -1
- package/dist/config/context-tree-domains.d.ts +0 -29
- package/dist/config/context-tree-domains.js +0 -29
- package/dist/core/interfaces/usecase/i-generate-rules-use-case.d.ts +0 -3
- package/dist/infra/repl/commands/gen-rules-command.d.ts +0 -7
- package/dist/infra/rule/agent-rule-config.d.ts +0 -19
- package/dist/infra/usecase/generate-rules-use-case.d.ts +0 -61
- package/dist/infra/usecase/generate-rules-use-case.js +0 -285
- /package/dist/core/interfaces/{usecase/i-clear-use-case.js → connectors/connector-types.js} +0 -0
- /package/dist/core/interfaces/{usecase/i-generate-rules-use-case.js → connectors/i-connector-manager.js} +0 -0
- /package/dist/infra/{rule → connectors/shared}/constants.d.ts +0 -0
- /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.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 =
|
|
79
|
-
?
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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[]) -
|
|
178
|
-
|
|
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
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
//
|
|
215
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
307
|
+
// Try JSON.stringify with proper handling for special types
|
|
250
308
|
return JSON.stringify(value, (_, val) => {
|
|
251
|
-
//
|
|
252
|
-
if (typeof val === '
|
|
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
|
|
260
|
-
|
|
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(
|
|
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(
|
|
461
|
+
await this.sleep(gracePeriodMs);
|
|
454
462
|
if (child.exitCode === null) {
|
|
455
463
|
child.kill('SIGKILL');
|
|
456
464
|
}
|