byterover-cli 1.0.4 → 1.0.5
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 +13 -2
- package/dist/commands/curate.js +1 -1
- package/dist/commands/main.d.ts +13 -0
- package/dist/commands/main.js +53 -2
- package/dist/commands/query.js +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- 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/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 +37 -2
- package/dist/core/domain/transport/schemas.js +23 -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/executor/i-curate-executor.d.ts +2 -2
- package/dist/core/interfaces/i-context-file-reader.d.ts +3 -0
- package/dist/core/interfaces/usecase/{i-clear-use-case.d.ts → i-reset-use-case.d.ts} +1 -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/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.js +40 -16
- package/dist/infra/cipher/llm/formatters/gemini-formatter.d.ts +13 -0
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +98 -6
- package/dist/infra/cipher/llm/generators/byterover-content-generator.js +6 -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/tools/implementations/curate-tool.js +113 -35
- package/dist/infra/cipher/tools/implementations/task-tool.js +1 -0
- package/dist/infra/context-tree/file-context-file-reader.js +4 -0
- package/dist/infra/core/task-processor.d.ts +2 -2
- package/dist/infra/process/process-manager.d.ts +10 -1
- package/dist/infra/process/process-manager.js +16 -6
- package/dist/infra/process/transport-handlers.js +31 -0
- package/dist/infra/repl/commands/index.js +5 -2
- 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/{clear-command.d.ts → reset-command.d.ts} +2 -2
- package/dist/infra/repl/commands/{clear-command.js → reset-command.js} +10 -10
- package/dist/infra/usecase/generate-rules-use-case.js +2 -2
- package/dist/infra/usecase/init-use-case.js +4 -4
- package/dist/infra/usecase/logout-use-case.js +1 -1
- package/dist/infra/usecase/push-use-case.js +1 -1
- 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} +5 -5
- package/dist/resources/prompts/curate.yml +68 -13
- package/dist/resources/tools/curate.txt +60 -15
- package/dist/tui/components/inline-prompts/inline-confirm.js +2 -2
- package/dist/tui/components/onboarding/onboarding-flow.js +1 -0
- package/dist/tui/views/command-view.js +15 -0
- package/dist/utils/file-validator.js +9 -7
- package/oclif.manifest.json +3 -3
- package/package.json +1 -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-clear-use-case.js → i-reset-use-case.js} +0 -0
|
@@ -249,6 +249,8 @@ export const TransportLlmEventList = [
|
|
|
249
249
|
export const TransportAgentEventNames = {
|
|
250
250
|
CONNECTED: 'agent:connected',
|
|
251
251
|
DISCONNECTED: 'agent:disconnected',
|
|
252
|
+
NEW_SESSION: 'agent:newSession',
|
|
253
|
+
NEW_SESSION_CREATED: 'agent:newSessionCreated',
|
|
252
254
|
REGISTER: 'agent:register',
|
|
253
255
|
RESTART: 'agent:restart',
|
|
254
256
|
RESTARTED: 'agent:restarted',
|
|
@@ -272,10 +274,10 @@ export const TransportSessionEventNames = {
|
|
|
272
274
|
* Internal message, not exposed to external clients
|
|
273
275
|
*/
|
|
274
276
|
export const TaskExecuteSchema = z.object({
|
|
275
|
-
/** Client ID that created the task (for response routing) */
|
|
276
|
-
clientId: z.string(),
|
|
277
277
|
/** Client's working directory for file validation */
|
|
278
278
|
clientCwd: z.string().optional(),
|
|
279
|
+
/** Client ID that created the task (for response routing) */
|
|
280
|
+
clientId: z.string(),
|
|
279
281
|
/** Task content/prompt */
|
|
280
282
|
content: z.string(),
|
|
281
283
|
/** Optional file paths for curate --files */
|
|
@@ -560,3 +562,22 @@ export const AgentRestartResponseSchema = z.object({
|
|
|
560
562
|
/** Whether the restart was initiated successfully */
|
|
561
563
|
success: z.boolean(),
|
|
562
564
|
});
|
|
565
|
+
/**
|
|
566
|
+
* Request to create a new session (end current, start fresh).
|
|
567
|
+
* Used by /new command to start a fresh conversation.
|
|
568
|
+
*/
|
|
569
|
+
export const AgentNewSessionRequestSchema = z.object({
|
|
570
|
+
/** Optional reason for new session (for logging) */
|
|
571
|
+
reason: z.string().optional(),
|
|
572
|
+
});
|
|
573
|
+
/**
|
|
574
|
+
* Response after new session is created.
|
|
575
|
+
*/
|
|
576
|
+
export const AgentNewSessionResponseSchema = z.object({
|
|
577
|
+
/** Error message if session creation failed */
|
|
578
|
+
error: z.string().optional(),
|
|
579
|
+
/** The new session ID */
|
|
580
|
+
sessionId: z.string().optional(),
|
|
581
|
+
/** Whether the new session was created successfully */
|
|
582
|
+
success: z.boolean(),
|
|
583
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for Session Persistence Operations
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract for session metadata storage and retrieval.
|
|
5
|
+
* Implementation uses JSON files in .brv/sessions/ directory.
|
|
6
|
+
*/
|
|
7
|
+
import type { ActiveSessionPointer, SessionInfo, SessionMetadata } from '../../domain/cipher/session/session-metadata.js';
|
|
8
|
+
/**
|
|
9
|
+
* Session retention configuration for auto-cleanup.
|
|
10
|
+
*/
|
|
11
|
+
export interface SessionRetentionConfig {
|
|
12
|
+
/** Maximum age in days before auto-cleanup */
|
|
13
|
+
maxAgeDays: number;
|
|
14
|
+
/** Maximum number of sessions to keep */
|
|
15
|
+
maxCount: number;
|
|
16
|
+
/** Whether to run cleanup on startup */
|
|
17
|
+
runOnStartup: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Result of session cleanup operation.
|
|
21
|
+
*/
|
|
22
|
+
export interface SessionCleanupResult {
|
|
23
|
+
/** Number of corrupted session files removed */
|
|
24
|
+
corruptedRemoved: number;
|
|
25
|
+
/** Number of sessions deleted due to age */
|
|
26
|
+
deletedByAge: number;
|
|
27
|
+
/** Number of sessions deleted due to count limit */
|
|
28
|
+
deletedByCount: number;
|
|
29
|
+
/** Total sessions remaining after cleanup */
|
|
30
|
+
remaining: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Interface for session metadata persistence.
|
|
34
|
+
*
|
|
35
|
+
* Manages session metadata stored in .brv/sessions/ directory:
|
|
36
|
+
* - active.json: Current active session pointer
|
|
37
|
+
* - session-*.json: Individual session metadata files
|
|
38
|
+
*/
|
|
39
|
+
export interface ISessionPersistence {
|
|
40
|
+
/**
|
|
41
|
+
* Clean up expired sessions based on retention policy.
|
|
42
|
+
*
|
|
43
|
+
* @param config - Retention configuration
|
|
44
|
+
* @returns Cleanup result with counts
|
|
45
|
+
*/
|
|
46
|
+
cleanupSessions(config: SessionRetentionConfig): Promise<SessionCleanupResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Clear the active session pointer.
|
|
49
|
+
* Removes .brv/sessions/active.json
|
|
50
|
+
*/
|
|
51
|
+
clearActiveSession(): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Delete a session and its metadata.
|
|
54
|
+
*
|
|
55
|
+
* @param sessionId - Session ID to delete
|
|
56
|
+
* @returns True if session was deleted, false if not found
|
|
57
|
+
*/
|
|
58
|
+
deleteSession(sessionId: string): Promise<boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* Get the currently active session pointer.
|
|
61
|
+
*
|
|
62
|
+
* @returns Active session pointer or null if no active session
|
|
63
|
+
*/
|
|
64
|
+
getActiveSession(): Promise<ActiveSessionPointer | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Get session metadata by ID.
|
|
67
|
+
*
|
|
68
|
+
* @param sessionId - Session ID to look up
|
|
69
|
+
* @returns Session metadata or null if not found
|
|
70
|
+
*/
|
|
71
|
+
getSession(sessionId: string): Promise<null | SessionMetadata>;
|
|
72
|
+
/**
|
|
73
|
+
* Check if the active session pointer is stale (process not running).
|
|
74
|
+
*
|
|
75
|
+
* @returns True if active session exists but process is not running
|
|
76
|
+
*/
|
|
77
|
+
isActiveSessionStale(): Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Validate that a session belongs to the current working directory.
|
|
80
|
+
*
|
|
81
|
+
* @param sessionId - Session ID to validate
|
|
82
|
+
* @returns True if session belongs to current project
|
|
83
|
+
*/
|
|
84
|
+
isSessionForCurrentProject(sessionId: string): Promise<boolean>;
|
|
85
|
+
/**
|
|
86
|
+
* List all session metadata files.
|
|
87
|
+
*
|
|
88
|
+
* @returns Array of session info sorted by lastUpdated (newest first)
|
|
89
|
+
*/
|
|
90
|
+
listSessions(): Promise<SessionInfo[]>;
|
|
91
|
+
/**
|
|
92
|
+
* Mark a session as ended.
|
|
93
|
+
* Updates the session status to 'ended' and lastUpdated timestamp.
|
|
94
|
+
*
|
|
95
|
+
* @param sessionId - Session ID to mark as ended
|
|
96
|
+
*/
|
|
97
|
+
markSessionEnded(sessionId: string): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Mark a session as interrupted (e.g., process crashed).
|
|
100
|
+
* Updates the session status to 'interrupted'.
|
|
101
|
+
*
|
|
102
|
+
* @param sessionId - Session ID to mark as interrupted
|
|
103
|
+
*/
|
|
104
|
+
markSessionInterrupted(sessionId: string): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Save session metadata to disk.
|
|
107
|
+
* Creates or updates the session metadata file.
|
|
108
|
+
*
|
|
109
|
+
* @param metadata - Session metadata to save
|
|
110
|
+
*/
|
|
111
|
+
saveSession(metadata: SessionMetadata): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Set the active session pointer.
|
|
114
|
+
* Creates or updates .brv/sessions/active.json
|
|
115
|
+
*
|
|
116
|
+
* @param sessionId - Session ID to set as active
|
|
117
|
+
*/
|
|
118
|
+
setActiveSession(sessionId: string): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* Set session title from first user message.
|
|
121
|
+
*
|
|
122
|
+
* @param sessionId - Session ID to update
|
|
123
|
+
* @param title - Session title
|
|
124
|
+
*/
|
|
125
|
+
setSessionTitle(sessionId: string, title: string): Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Update session activity (lastUpdated timestamp and message count).
|
|
128
|
+
*
|
|
129
|
+
* @param sessionId - Session ID to update
|
|
130
|
+
* @param messageCount - Current message count
|
|
131
|
+
*/
|
|
132
|
+
updateSessionActivity(sessionId: string, messageCount: number): Promise<void>;
|
|
133
|
+
}
|
|
@@ -302,6 +302,12 @@ export interface ToolCall {
|
|
|
302
302
|
* Unique identifier for this tool call
|
|
303
303
|
*/
|
|
304
304
|
id: string;
|
|
305
|
+
/**
|
|
306
|
+
* Thought signature for Gemini 3+ models.
|
|
307
|
+
* Required by Gemini 3 preview models to validate function calls.
|
|
308
|
+
* Uses 'skip_thought_signature_validator' as synthetic signature.
|
|
309
|
+
*/
|
|
310
|
+
thoughtSignature?: string;
|
|
305
311
|
/**
|
|
306
312
|
* The type of tool call (currently only 'function' is supported)
|
|
307
313
|
*/
|
|
@@ -4,10 +4,10 @@ import type { ICipherAgent } from '../cipher/i-cipher-agent.js';
|
|
|
4
4
|
* Agent uses its default session (Single-Session pattern).
|
|
5
5
|
*/
|
|
6
6
|
export interface CurateExecuteOptions {
|
|
7
|
-
/** Context content to curate */
|
|
8
|
-
content: string;
|
|
9
7
|
/** Client's working directory for file validation (defaults to process.cwd() if not provided) */
|
|
10
8
|
clientCwd?: string;
|
|
9
|
+
/** Context content to curate */
|
|
10
|
+
content: string;
|
|
11
11
|
/** Optional file paths for --files flag */
|
|
12
12
|
files?: string[];
|
|
13
13
|
/** Task ID for event routing (required for concurrent task isolation) */
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import type { Narrative, RawConcept } from '../domain/knowledge/markdown-writer.js';
|
|
1
2
|
/**
|
|
2
3
|
* Represents the content of a context file with extracted metadata.
|
|
3
4
|
*/
|
|
4
5
|
export type ContextFileContent = {
|
|
5
6
|
/** The raw content of the file */
|
|
6
7
|
content: string;
|
|
8
|
+
narrative?: Narrative;
|
|
7
9
|
/** Relative path within the context tree (e.g., "structure/context.md") */
|
|
8
10
|
path: string;
|
|
11
|
+
rawConcept?: RawConcept;
|
|
9
12
|
/** Title extracted from the first heading, or the relative path if no heading found */
|
|
10
13
|
title: string;
|
|
11
14
|
};
|
|
@@ -49,13 +49,13 @@ export declare const FileSystemConfigSchema: z.ZodObject<{
|
|
|
49
49
|
maxFileSize: z.ZodOptional<z.ZodNumber>;
|
|
50
50
|
workingDirectory: z.ZodOptional<z.ZodString>;
|
|
51
51
|
}, "strict", z.ZodTypeAny, {
|
|
52
|
+
workingDirectory?: string | undefined;
|
|
52
53
|
allowedExtensions?: string[] | undefined;
|
|
53
54
|
maxFileSize?: number | undefined;
|
|
54
|
-
workingDirectory?: string | undefined;
|
|
55
55
|
}, {
|
|
56
|
+
workingDirectory?: string | undefined;
|
|
56
57
|
allowedExtensions?: string[] | undefined;
|
|
57
58
|
maxFileSize?: number | undefined;
|
|
58
|
-
workingDirectory?: string | undefined;
|
|
59
59
|
}>;
|
|
60
60
|
export type FileSystemConfig = z.input<typeof FileSystemConfigSchema>;
|
|
61
61
|
export type ValidatedFileSystemConfig = z.output<typeof FileSystemConfigSchema>;
|
|
@@ -105,13 +105,13 @@ export declare const AgentConfigSchema: z.ZodObject<{
|
|
|
105
105
|
maxFileSize: z.ZodOptional<z.ZodNumber>;
|
|
106
106
|
workingDirectory: z.ZodOptional<z.ZodString>;
|
|
107
107
|
}, "strict", z.ZodTypeAny, {
|
|
108
|
+
workingDirectory?: string | undefined;
|
|
108
109
|
allowedExtensions?: string[] | undefined;
|
|
109
110
|
maxFileSize?: number | undefined;
|
|
110
|
-
workingDirectory?: string | undefined;
|
|
111
111
|
}, {
|
|
112
|
+
workingDirectory?: string | undefined;
|
|
112
113
|
allowedExtensions?: string[] | undefined;
|
|
113
114
|
maxFileSize?: number | undefined;
|
|
114
|
-
workingDirectory?: string | undefined;
|
|
115
115
|
}>>;
|
|
116
116
|
httpReferer: z.ZodOptional<z.ZodString>;
|
|
117
117
|
llm: z.ZodDefault<z.ZodObject<{
|
|
@@ -182,9 +182,9 @@ export declare const AgentConfigSchema: z.ZodObject<{
|
|
|
182
182
|
storageDir: string;
|
|
183
183
|
} | undefined;
|
|
184
184
|
fileSystem?: {
|
|
185
|
+
workingDirectory?: string | undefined;
|
|
185
186
|
allowedExtensions?: string[] | undefined;
|
|
186
187
|
maxFileSize?: number | undefined;
|
|
187
|
-
workingDirectory?: string | undefined;
|
|
188
188
|
} | undefined;
|
|
189
189
|
httpReferer?: string | undefined;
|
|
190
190
|
openRouterApiKey?: string | undefined;
|
|
@@ -208,9 +208,9 @@ export declare const AgentConfigSchema: z.ZodObject<{
|
|
|
208
208
|
maxTotalSize?: number | undefined;
|
|
209
209
|
} | undefined;
|
|
210
210
|
fileSystem?: {
|
|
211
|
+
workingDirectory?: string | undefined;
|
|
211
212
|
allowedExtensions?: string[] | undefined;
|
|
212
213
|
maxFileSize?: number | undefined;
|
|
213
|
-
workingDirectory?: string | undefined;
|
|
214
214
|
} | undefined;
|
|
215
215
|
httpReferer?: string | undefined;
|
|
216
216
|
llm?: {
|
|
@@ -153,7 +153,7 @@ export async function createCipherAgentServices(config, agentEventBus) {
|
|
|
153
153
|
// - Existing sessions → BlobHistoryStorage (no migration)
|
|
154
154
|
historyStorage = new DualFormatHistoryStorage(blobHistoryStorage, granularStorage);
|
|
155
155
|
// Create CompactionService for context overflow management
|
|
156
|
-
const tokenizer = new GeminiTokenizer(config.model ?? 'gemini-
|
|
156
|
+
const tokenizer = new GeminiTokenizer(config.model ?? 'gemini-3-flash-preview');
|
|
157
157
|
compactionService = new CompactionService(messageStorage, tokenizer, {
|
|
158
158
|
overflowThreshold: 0.85, // 85% triggers compaction check
|
|
159
159
|
protectedTurns: 2, // Protect first 2 user turns from pruning
|
|
@@ -216,7 +216,7 @@ export function createSessionServices(sessionId, sharedServices, httpConfig, llm
|
|
|
216
216
|
httpReferer: llmConfig.httpReferer,
|
|
217
217
|
maxIterations: llmConfig.maxIterations ?? 50,
|
|
218
218
|
maxTokens: llmConfig.maxTokens ?? 8192,
|
|
219
|
-
model: llmConfig.model ?? 'google/gemini-
|
|
219
|
+
model: llmConfig.model ?? 'google/gemini-3-flash-preview',
|
|
220
220
|
siteName: llmConfig.siteName,
|
|
221
221
|
temperature: llmConfig.temperature ?? 0.7,
|
|
222
222
|
verbose: llmConfig.verbose ?? false,
|
|
@@ -244,7 +244,7 @@ export function createSessionServices(sessionId, sharedServices, httpConfig, llm
|
|
|
244
244
|
// Step 2: Create base content generator
|
|
245
245
|
let generator = new ByteRoverContentGenerator(httpService, {
|
|
246
246
|
maxTokens: llmConfig.maxTokens ?? 8192,
|
|
247
|
-
model: llmConfig.model ?? 'gemini-
|
|
247
|
+
model: llmConfig.model ?? 'gemini-3-flash-preview',
|
|
248
248
|
temperature: llmConfig.temperature ?? 0.7,
|
|
249
249
|
});
|
|
250
250
|
// Step 3: Wrap with retry decorator
|
|
@@ -263,7 +263,7 @@ export function createSessionServices(sessionId, sharedServices, httpConfig, llm
|
|
|
263
263
|
llmService = new ByteRoverLLMService(sessionId, generator, {
|
|
264
264
|
maxIterations: llmConfig.maxIterations ?? 50,
|
|
265
265
|
maxTokens: llmConfig.maxTokens ?? 8192,
|
|
266
|
-
model: llmConfig.model ?? 'gemini-
|
|
266
|
+
model: llmConfig.model ?? 'gemini-3-flash-preview',
|
|
267
267
|
temperature: llmConfig.temperature ?? 0.7,
|
|
268
268
|
verbose: llmConfig.verbose ?? false,
|
|
269
269
|
}, {
|
|
@@ -14,8 +14,9 @@ export function createContextTreeFileSystem(baseWorkingDirectory) {
|
|
|
14
14
|
allowedPaths: ['.'],
|
|
15
15
|
// Use default blocked extensions
|
|
16
16
|
blockedExtensions: ['.exe', '.dll', '.so', '.dylib'],
|
|
17
|
-
//
|
|
18
|
-
|
|
17
|
+
// No additional blocked paths needed - path traversal outside .brv/context-tree/
|
|
18
|
+
// is prevented by PathValidator.isPathTraversal() and isPathAllowed()
|
|
19
|
+
blockedPaths: [],
|
|
19
20
|
// Reasonable file size limit
|
|
20
21
|
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
21
22
|
// Restrict working directory to context tree
|
|
@@ -390,6 +390,7 @@ export class FileSystemService {
|
|
|
390
390
|
* - XML-wrapped output for clearer LLM parsing
|
|
391
391
|
* - Preview metadata (first 20 lines)
|
|
392
392
|
*/
|
|
393
|
+
// eslint-disable-next-line complexity -- Multiple file type handling paths (image/PDF, binary, text) are inherent to the functionality
|
|
393
394
|
async readFile(filePath, options = {}) {
|
|
394
395
|
this.ensureInitialized();
|
|
395
396
|
// Resolve relative paths against working directory
|
|
@@ -60,8 +60,8 @@ export class ByteRoverLlmHttpService {
|
|
|
60
60
|
const request = {
|
|
61
61
|
executionMetadata: JSON.stringify(executionMetadata ?? {}),
|
|
62
62
|
params: {
|
|
63
|
-
config
|
|
64
|
-
contents
|
|
63
|
+
config,
|
|
64
|
+
contents,
|
|
65
65
|
model,
|
|
66
66
|
},
|
|
67
67
|
project_id: this.config.projectId,
|
|
@@ -87,9 +87,7 @@ export class ByteRoverLlmHttpService {
|
|
|
87
87
|
const response = await httpClient.post(url, request, {
|
|
88
88
|
timeout: this.config.timeout,
|
|
89
89
|
});
|
|
90
|
-
|
|
91
|
-
const content = JSON.parse(response.data);
|
|
92
|
-
return content;
|
|
90
|
+
return response.data;
|
|
93
91
|
}
|
|
94
92
|
/**
|
|
95
93
|
* Detect LLM provider from model identifier.
|
|
@@ -128,6 +128,7 @@ function setupEventListeners(eventBus, spinnerState) {
|
|
|
128
128
|
}
|
|
129
129
|
process.stdout.write('\n' + chalk.red(payload.error) + '\n\n');
|
|
130
130
|
};
|
|
131
|
+
// eslint-disable-next-line no-warning-comments -- Tracked for v0.5.0 release
|
|
131
132
|
// TODO(v0.5.0): Move to outer scope for better performance
|
|
132
133
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
133
134
|
const uiListener = (payload) => {
|
|
@@ -167,6 +168,7 @@ function setupEventListeners(eventBus, spinnerState) {
|
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
};
|
|
171
|
+
// eslint-disable-next-line no-warning-comments -- Tracked for v0.5.0 release
|
|
170
172
|
// TODO(v0.5.0): Move to outer scope for better performance
|
|
171
173
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
172
174
|
const logListener = (payload) => {
|
|
@@ -272,7 +274,7 @@ async function executePrompt(prompt, agent, state, eventBus) {
|
|
|
272
274
|
*/
|
|
273
275
|
export async function startInteractiveLoop(agent, options) {
|
|
274
276
|
// Display welcome message
|
|
275
|
-
displayWelcome(options?.sessionId ?? 'cipher-agent-session', options?.model ?? 'gemini-
|
|
277
|
+
displayWelcome(options?.sessionId ?? 'cipher-agent-session', options?.model ?? 'gemini-3-flash-preview', options?.eventBus);
|
|
276
278
|
// Create readline interface
|
|
277
279
|
const rl = readline.createInterface({
|
|
278
280
|
input: process.stdin,
|
|
@@ -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
|
/**
|
|
@@ -167,16 +175,18 @@ export class ContextManager {
|
|
|
167
175
|
* @param _fileData - Optional file data (not yet implemented)
|
|
168
176
|
*/
|
|
169
177
|
async addUserMessage(content, _imageData, _fileData) {
|
|
170
|
-
// Simple implementation: just use text content
|
|
171
|
-
// Image and file support can be added later
|
|
172
178
|
const message = {
|
|
173
179
|
content,
|
|
174
180
|
role: 'user',
|
|
175
181
|
};
|
|
176
|
-
this.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
182
|
+
await this.mutex.withLock(async () => {
|
|
183
|
+
this.messages.push(message);
|
|
184
|
+
try {
|
|
185
|
+
await this.persistHistory();
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
this.logger.error('Failed to persist history after user message', { error, sessionId: this.sessionId });
|
|
189
|
+
}
|
|
180
190
|
});
|
|
181
191
|
}
|
|
182
192
|
/**
|
|
@@ -559,8 +569,22 @@ export class ContextManager {
|
|
|
559
569
|
if (typeof result === 'string') {
|
|
560
570
|
return result;
|
|
561
571
|
}
|
|
562
|
-
// Convert to JSON string
|
|
563
|
-
const jsonString = JSON.stringify(result,
|
|
572
|
+
// Convert to JSON string with special type handling
|
|
573
|
+
const jsonString = JSON.stringify(result, (_, val) => {
|
|
574
|
+
// Convert BigInt to string
|
|
575
|
+
if (typeof val === 'bigint') {
|
|
576
|
+
return val.toString();
|
|
577
|
+
}
|
|
578
|
+
// Convert functions to their string representation
|
|
579
|
+
if (typeof val === 'function') {
|
|
580
|
+
return `[Function: ${val.name || 'anonymous'}]`;
|
|
581
|
+
}
|
|
582
|
+
// Convert Symbols to string
|
|
583
|
+
if (typeof val === 'symbol') {
|
|
584
|
+
return val.toString();
|
|
585
|
+
}
|
|
586
|
+
return val;
|
|
587
|
+
}, 2);
|
|
564
588
|
// Limit size to prevent extremely large results
|
|
565
589
|
const MAX_RESULT_LENGTH = 50_000;
|
|
566
590
|
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;
|
|
@@ -106,6 +119,7 @@ export class GeminiMessageFormatter {
|
|
|
106
119
|
/**
|
|
107
120
|
* Formats assistant message to Gemini's Content format.
|
|
108
121
|
* Maps 'assistant' role to 'model' and includes both text and tool calls.
|
|
122
|
+
* For Gemini 3+ models, includes thoughtSignature on function calls.
|
|
109
123
|
*/
|
|
110
124
|
formatAssistantMessage(msg) {
|
|
111
125
|
const parts = [];
|
|
@@ -116,12 +130,17 @@ export class GeminiMessageFormatter {
|
|
|
116
130
|
// Add tool calls if present
|
|
117
131
|
if (msg.toolCalls) {
|
|
118
132
|
for (const tc of msg.toolCalls) {
|
|
119
|
-
|
|
133
|
+
const functionCallPart = {
|
|
120
134
|
functionCall: {
|
|
121
135
|
args: JSON.parse(tc.function.arguments),
|
|
122
136
|
name: tc.function.name,
|
|
123
137
|
},
|
|
124
|
-
}
|
|
138
|
+
};
|
|
139
|
+
// Include thoughtSignature if present (required for Gemini 3+ models)
|
|
140
|
+
if (tc.thoughtSignature) {
|
|
141
|
+
functionCallPart.thoughtSignature = tc.thoughtSignature;
|
|
142
|
+
}
|
|
143
|
+
parts.push(functionCallPart);
|
|
125
144
|
}
|
|
126
145
|
}
|
|
127
146
|
return {
|
|
@@ -251,3 +270,76 @@ export class GeminiMessageFormatter {
|
|
|
251
270
|
return `call_${timestamp}_${random}_${toolName}`;
|
|
252
271
|
}
|
|
253
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Ensures that function calls in the active conversation loop have thought signatures.
|
|
275
|
+
* Required for Gemini 3+ preview models.
|
|
276
|
+
*
|
|
277
|
+
* The "active loop" starts from the last user text message in the conversation.
|
|
278
|
+
* Only the first function call in each model turn needs a thought signature.
|
|
279
|
+
*
|
|
280
|
+
* @param contents Array of Content objects formatted for Gemini API
|
|
281
|
+
* @param model The model being used (only applies to Gemini 3+ models)
|
|
282
|
+
* @returns Modified contents with thought signatures added where needed
|
|
283
|
+
*/
|
|
284
|
+
export function ensureActiveLoopHasThoughtSignatures(contents, model) {
|
|
285
|
+
// Only apply to Gemini 3+ models
|
|
286
|
+
if (!isGemini3Model(model)) {
|
|
287
|
+
return contents;
|
|
288
|
+
}
|
|
289
|
+
// Find the last user turn with text message (start of active loop)
|
|
290
|
+
let activeLoopStartIndex = -1;
|
|
291
|
+
for (let i = contents.length - 1; i >= 0; i--) {
|
|
292
|
+
const content = contents[i];
|
|
293
|
+
if (content.role === 'user' && content.parts?.some((part) => 'text' in part && part.text)) {
|
|
294
|
+
activeLoopStartIndex = i;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// No user text message found - nothing to do
|
|
299
|
+
if (activeLoopStartIndex === -1) {
|
|
300
|
+
return contents;
|
|
301
|
+
}
|
|
302
|
+
// Create shallow copy to avoid mutating original
|
|
303
|
+
const newContents = [...contents];
|
|
304
|
+
// Process each content from active loop start to end
|
|
305
|
+
for (let i = activeLoopStartIndex; i < newContents.length; i++) {
|
|
306
|
+
const content = newContents[i];
|
|
307
|
+
// Only process model turns with parts
|
|
308
|
+
if (content.role !== 'model' || !content.parts) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const newParts = [...content.parts];
|
|
312
|
+
const updatedContent = addThoughtSignatureToFirstFunctionCall(newParts, content);
|
|
313
|
+
if (updatedContent) {
|
|
314
|
+
newContents[i] = updatedContent;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return newContents;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Adds thought signature to the first function call in parts if missing.
|
|
321
|
+
* Returns updated content or null if no modification needed.
|
|
322
|
+
*/
|
|
323
|
+
function addThoughtSignatureToFirstFunctionCall(parts, content) {
|
|
324
|
+
for (let j = 0; j < parts.length; j++) {
|
|
325
|
+
const part = parts[j];
|
|
326
|
+
if (!part || !('functionCall' in part) || !part.functionCall) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
// Check if thoughtSignature already exists using type guard
|
|
330
|
+
if (hasThoughtSignature(part) && part.thoughtSignature) {
|
|
331
|
+
return null; // Already has signature, no modification needed
|
|
332
|
+
}
|
|
333
|
+
// Add synthetic thought signature
|
|
334
|
+
const partWithSignature = {
|
|
335
|
+
...part,
|
|
336
|
+
thoughtSignature: SYNTHETIC_THOUGHT_SIGNATURE,
|
|
337
|
+
};
|
|
338
|
+
parts[j] = partWithSignature;
|
|
339
|
+
return {
|
|
340
|
+
...content,
|
|
341
|
+
parts,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return null; // No function call found
|
|
345
|
+
}
|