@saccolabs/tars 1.31.0 → 1.32.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 (91) hide show
  1. package/context/skills/create-extension/SKILL.md +2 -2
  2. package/context/skills/manage-extensions/SKILL.md +3 -3
  3. package/dist/channels/discord/discord-channel.js +2 -3
  4. package/dist/channels/discord/discord-channel.js.map +1 -1
  5. package/dist/channels/discord/message-formatter.d.ts +1 -1
  6. package/dist/channels/discord/message-formatter.js +1 -1
  7. package/dist/cli/commands/quota.js +13 -48
  8. package/dist/cli/commands/quota.js.map +1 -1
  9. package/dist/cli/commands/refresh.js +40 -3
  10. package/dist/cli/commands/refresh.js.map +1 -1
  11. package/dist/cli/commands/setup.js +192 -467
  12. package/dist/cli/commands/setup.js.map +1 -1
  13. package/dist/cli/index.js +1 -13
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/config/config.d.ts +5 -10
  16. package/dist/config/config.js +21 -27
  17. package/dist/config/config.js.map +1 -1
  18. package/dist/memory/knowledge-store.js +10 -1
  19. package/dist/memory/knowledge-store.js.map +1 -1
  20. package/dist/memory/memory-manager.d.ts +0 -3
  21. package/dist/memory/memory-manager.js +26 -34
  22. package/dist/memory/memory-manager.js.map +1 -1
  23. package/dist/scripts/debug-cli.js +2 -2
  24. package/dist/scripts/debug-cli.js.map +1 -1
  25. package/dist/supervisor/heartbeat-service.js +1 -1
  26. package/dist/supervisor/heartbeat-service.js.map +1 -1
  27. package/dist/supervisor/main.js +37 -79
  28. package/dist/supervisor/main.js.map +1 -1
  29. package/dist/supervisor/mcp-bridge.d.ts +25 -0
  30. package/dist/supervisor/mcp-bridge.js +157 -0
  31. package/dist/supervisor/mcp-bridge.js.map +1 -0
  32. package/dist/supervisor/session-manager.d.ts +1 -1
  33. package/dist/supervisor/session-manager.js +1 -1
  34. package/dist/supervisor/supervisor.d.ts +14 -7
  35. package/dist/supervisor/supervisor.js +87 -29
  36. package/dist/supervisor/supervisor.js.map +1 -1
  37. package/dist/supervisor/{gemini-engine.d.ts → tars-engine.d.ts} +38 -33
  38. package/dist/supervisor/tars-engine.js +698 -0
  39. package/dist/supervisor/tars-engine.js.map +1 -0
  40. package/dist/tools/get-quota.d.ts +38 -12
  41. package/dist/tools/get-quota.js +37 -94
  42. package/dist/tools/get-quota.js.map +1 -1
  43. package/dist/tools/send-notification.d.ts +32 -7
  44. package/dist/tools/send-notification.js +31 -37
  45. package/dist/tools/send-notification.js.map +1 -1
  46. package/dist/types/index.d.ts +2 -2
  47. package/dist/utils/brain-audit.js +4 -4
  48. package/dist/utils/brain-audit.js.map +1 -1
  49. package/dist/utils/migration-manager.d.ts +4 -0
  50. package/dist/utils/migration-manager.js +205 -0
  51. package/dist/utils/migration-manager.js.map +1 -0
  52. package/extensions/memory/dist/store.js +29 -20
  53. package/extensions/memory/dist/store.js.map +1 -1
  54. package/extensions/memory/src/store.ts +33 -23
  55. package/package.json +4 -3
  56. package/src/prompts/system.md +3 -14
  57. package/context/agents/scaffolder.md +0 -22
  58. package/dist/auth/credential-manager.d.ts +0 -14
  59. package/dist/auth/credential-manager.js +0 -60
  60. package/dist/auth/credential-manager.js.map +0 -1
  61. package/dist/auth/oauth-service.d.ts +0 -24
  62. package/dist/auth/oauth-service.js +0 -89
  63. package/dist/auth/oauth-service.js.map +0 -1
  64. package/dist/auth/workspace-auth-service.d.ts +0 -10
  65. package/dist/auth/workspace-auth-service.js +0 -78
  66. package/dist/auth/workspace-auth-service.js.map +0 -1
  67. package/dist/cli/commands/swarm.d.ts +0 -13
  68. package/dist/cli/commands/swarm.js +0 -250
  69. package/dist/cli/commands/swarm.js.map +0 -1
  70. package/dist/inference/LlamaCppGenerator.d.ts +0 -25
  71. package/dist/inference/LlamaCppGenerator.js +0 -461
  72. package/dist/inference/LlamaCppGenerator.js.map +0 -1
  73. package/dist/scripts/test-local-llamacpp.d.ts +0 -1
  74. package/dist/scripts/test-local-llamacpp.js +0 -77
  75. package/dist/scripts/test-local-llamacpp.js.map +0 -1
  76. package/dist/supervisor/gemini-engine.js +0 -1056
  77. package/dist/supervisor/gemini-engine.js.map +0 -1
  78. package/dist/swarm/agent-card.d.ts +0 -14
  79. package/dist/swarm/agent-card.js +0 -93
  80. package/dist/swarm/agent-card.js.map +0 -1
  81. package/dist/swarm/rpc-handler.d.ts +0 -27
  82. package/dist/swarm/rpc-handler.js +0 -235
  83. package/dist/swarm/rpc-handler.js.map +0 -1
  84. package/dist/swarm/swarm-service.d.ts +0 -47
  85. package/dist/swarm/swarm-service.js +0 -207
  86. package/dist/swarm/swarm-service.js.map +0 -1
  87. package/dist/swarm/types.d.ts +0 -109
  88. package/dist/swarm/types.js +0 -15
  89. package/dist/swarm/types.js.map +0 -1
  90. /package/extensions/memory/{gemini-extension.json → tars-extension.json} +0 -0
  91. /package/extensions/tasks/{gemini-extension.json → tars-extension.json} +0 -0
@@ -1,1056 +0,0 @@
1
- import { Config as CoreConfig, GeminiEventType, AuthType, promptIdContext, Scheduler, ApprovalMode, PolicyDecision, SimpleExtensionLoader, MCPServerConfig, CompressionStatus, loadConversationRecord } from '@google/gemini-cli-core';
2
- import { EventEmitter } from 'events';
3
- import logger from '../utils/logger.js';
4
- import { v4 as uuidv4 } from 'uuid';
5
- import fs from 'fs';
6
- import path from 'path';
7
- import { SendNotificationTool } from '../tools/send-notification.js';
8
- import { GetQuotaTool } from '../tools/get-quota.js';
9
- import { LocalRateLimiter } from './rate-limiter.js';
10
- import { LlamaCppGenerator } from '../inference/LlamaCppGenerator.js';
11
- import { DLPService } from '../utils/dlp-service.js';
12
- /**
13
- * Detects the best authentication type based on environment variables.
14
- * (Local implementation since it's not exported from core index)
15
- */
16
- function getAuthTypeFromEnv() {
17
- if (process.env['GOOGLE_GENAI_USE_GCA'] === 'true') {
18
- return AuthType.LOGIN_WITH_GOOGLE;
19
- }
20
- if (process.env['GOOGLE_GENAI_USE_VERTEXAI'] === 'true') {
21
- return AuthType.USE_VERTEX_AI;
22
- }
23
- if (process.env['GEMINI_API_KEY']) {
24
- return AuthType.USE_GEMINI;
25
- }
26
- return undefined;
27
- }
28
- /**
29
- * GeminiEngine - Native replacement for GeminiCli subprocess
30
- *
31
- * Uses @google/gemini-cli-core directly to interact with Gemini models.
32
- * Operates within the ~/.tars isolated environment by overriding HOME.
33
- */
34
- export class GeminiEngine extends EventEmitter {
35
- tarsConfig;
36
- coreConfig;
37
- client;
38
- initialized = false;
39
- initializedWithFallback = false;
40
- currentSessionId = null;
41
- channelManager;
42
- sessionManager;
43
- rateLimiter;
44
- constructor(tarsConfig) {
45
- super();
46
- this.tarsConfig = tarsConfig;
47
- this.rateLimiter = new LocalRateLimiter(tarsConfig.maxRPM || 14, tarsConfig.maxTPM || 900000);
48
- }
49
- /**
50
- * Provide the ChannelManager instance to the engine so it can build proactive notification tools
51
- */
52
- setChannelManager(channelManager) {
53
- this.channelManager = channelManager;
54
- }
55
- /**
56
- * Provide the SessionManager instance to the engine for session-aware tools
57
- */
58
- setSessionManager(sessionManager) {
59
- this.sessionManager = sessionManager;
60
- }
61
- /**
62
- * Initializes the core Gemini client with proper auth and config.
63
- */
64
- async initialize(initialSessionId) {
65
- if (this.initialized)
66
- return;
67
- logger.info('🚀 Initializing Gemini Engine (Native Core)...');
68
- const savedHome = process.env.HOME;
69
- try {
70
- // Ensure home directory exists
71
- if (!fs.existsSync(this.tarsConfig.homeDir)) {
72
- fs.mkdirSync(this.tarsConfig.homeDir, { recursive: true });
73
- }
74
- // Isolating to ~/.tars
75
- process.env.HOME = this.tarsConfig.homeDir;
76
- process.env.GEMINI_CLI_HOME = this.tarsConfig.homeDir;
77
- // Tell the Gemini Core PromptProvider to use our custom system.md
78
- const systemMdPath = path.join(this.tarsConfig.homeDir, '.gemini', 'system.md');
79
- process.env.GEMINI_SYSTEM_MD = systemMdPath;
80
- let authType = getAuthTypeFromEnv() || AuthType.LOGIN_WITH_GOOGLE;
81
- // Prevent interactive Google login prompt if using local inference
82
- if (this.tarsConfig.inferenceBackend === 'llamacpp') {
83
- authType = AuthType.USE_GEMINI;
84
- if (!process.env.GEMINI_API_KEY) {
85
- process.env.GEMINI_API_KEY = 'dummy_llama_key_to_bypass_sdk_auth';
86
- }
87
- }
88
- const discoveredExtensions = await this.discoverExtensions();
89
- const extensionLoader = new SimpleExtensionLoader(discoveredExtensions);
90
- this.coreConfig = new CoreConfig({
91
- sessionId: initialSessionId || uuidv4(),
92
- targetDir: this.tarsConfig.homeDir,
93
- cwd: this.tarsConfig.homeDir,
94
- model: this.tarsConfig.geminiModel,
95
- debugMode: false,
96
- approvalMode: ApprovalMode.YOLO,
97
- disableModelRouterForAuth: this.tarsConfig.inferenceBackend === 'llamacpp'
98
- ? [AuthType.USE_GEMINI]
99
- : undefined,
100
- policyEngineConfig: {
101
- defaultDecision: PolicyDecision.ALLOW
102
- },
103
- interactive: true,
104
- enableHooks: true,
105
- mcpEnabled: true,
106
- extensionsEnabled: true,
107
- enableAgents: true,
108
- skillsSupport: true,
109
- adminSkillsEnabled: true,
110
- noBrowser: true,
111
- folderTrust: true,
112
- trustedFolder: true,
113
- extensionLoader
114
- });
115
- await this.coreConfig.refreshAuth(authType);
116
- await this.coreConfig.initialize();
117
- // Handle Local Inference Override
118
- if (this.tarsConfig.inferenceBackend === 'llamacpp') {
119
- logger.info(`🔌 Overriding Gemini Core with Local Inference: ${this.tarsConfig.localInferenceUrl}`);
120
- const localGenerator = new LlamaCppGenerator(this.tarsConfig.localInferenceUrl);
121
- // Override the content generator at runtime to bypass the SDK's internal Gemini calls
122
- this.coreConfig.contentGenerator = localGenerator;
123
- // We'll apply more overrides after the client is created
124
- }
125
- // Register system prompt template for tars-request
126
- const promptProvider = this.coreConfig.promptProvider;
127
- if (promptProvider) {
128
- promptProvider.registerPrompt('tars-request', {
129
- template: fs.readFileSync(systemMdPath, 'utf-8'),
130
- includeContext: true,
131
- includeTools: true,
132
- includeHistory: true
133
- });
134
- }
135
- // Inject native tools
136
- const toolRegistry = this.coreConfig.getToolRegistry();
137
- if (this.channelManager) {
138
- const notifyTool = new SendNotificationTool(this.channelManager);
139
- toolRegistry.registerTool(notifyTool);
140
- logger.info('🔌 Registered native tool: send_notification');
141
- }
142
- const getQuotaTool = new GetQuotaTool(this.coreConfig, this.sessionManager, {
143
- inferenceBackend: this.tarsConfig.inferenceBackend,
144
- contextWindowTokens: this.tarsConfig.contextWindowTokens,
145
- geminiModel: this.tarsConfig.geminiModel,
146
- localInferenceUrl: this.tarsConfig.localInferenceUrl
147
- });
148
- toolRegistry.registerTool(getQuotaTool);
149
- logger.info('🔌 Registered native tool: get_model_quota');
150
- this.client = this.coreConfig.getGeminiClient();
151
- this.applyClientOverrides(this.client);
152
- // Deregister plan-mode tools — they require interactive user confirmation
153
- // that Tars cannot provide (non-interactive agent). Without this, the model
154
- // calls enter_plan_mode which switches ApprovalMode to PLAN, but exit_plan_mode
155
- // silently fails ("Rejected (no feedback)"), leaving the agent permanently
156
- // stuck in PLAN mode. This forces all requests through the rate-limited
157
- // gemini-3.1-pro-preview model, exhausting quota instantly.
158
- try {
159
- toolRegistry.unregisterTool('enter_plan_mode');
160
- toolRegistry.unregisterTool('exit_plan_mode');
161
- logger.info('🔇 Deregistered plan-mode tools (non-interactive agent)');
162
- }
163
- catch (e) {
164
- logger.debug(`Plan-mode tool deregistration skipped: ${e.message}`);
165
- }
166
- this.initialized = true;
167
- this.currentSessionId = this.coreConfig.getSessionId();
168
- logger.info('✨ Gemini Engine initialized successfully.');
169
- }
170
- catch (error) {
171
- logger.error(`❌ Failed to initialize Gemini Engine: ${error.message}`);
172
- throw error;
173
- }
174
- finally {
175
- process.env.HOME = savedHome;
176
- }
177
- }
178
- /**
179
- * Discovers extensions from the ~/.tars/.gemini/extensions directory.
180
- */
181
- async discoverExtensions() {
182
- const extensionsDir = path.join(this.tarsConfig.homeDir, '.gemini', 'extensions');
183
- if (!fs.existsSync(extensionsDir))
184
- return [];
185
- const extensions = [];
186
- try {
187
- const entries = fs.readdirSync(extensionsDir, { withFileTypes: true });
188
- for (const entry of entries) {
189
- if (!entry.isDirectory() && !entry.isSymbolicLink())
190
- continue;
191
- const extPath = path.resolve(extensionsDir, entry.name);
192
- const configPath = path.join(extPath, 'gemini-extension.json');
193
- if (fs.existsSync(configPath)) {
194
- try {
195
- const content = fs.readFileSync(configPath, 'utf-8');
196
- const config = JSON.parse(content);
197
- // Ensure mcpServers are converted to MCPServerConfig instances if they exist
198
- const mcpServers = {};
199
- if (config.mcpServers) {
200
- for (const [name, srv] of Object.entries(config.mcpServers)) {
201
- const s = srv;
202
- // Manually resolve ${extensionPath} since we are constructing configs early
203
- const resolvedArgs = s.args?.map((arg) => arg.replace(/\${extensionPath}/g, extPath));
204
- const resolvedEnv = s.env ? { ...s.env } : {};
205
- for (const key in resolvedEnv) {
206
- resolvedEnv[key] = resolvedEnv[key].replace(/\${extensionPath}/g, extPath);
207
- }
208
- mcpServers[name] = new MCPServerConfig(s.command, resolvedArgs, resolvedEnv, s.cwd?.replace(/\${extensionPath}/g, extPath), s.url?.replace(/\${extensionPath}/g, extPath), s.httpUrl?.replace(/\${extensionPath}/g, extPath), s.headers, s.tcp, s.type, s.timeout, s.trust);
209
- }
210
- }
211
- extensions.push({
212
- ...config,
213
- id: config.name,
214
- path: extPath,
215
- isActive: true,
216
- mcpServers,
217
- contextFiles: config.contextFiles || []
218
- });
219
- logger.info(`🔌 Found extension: ${config.name}`);
220
- }
221
- catch (e) {
222
- logger.error(`Failed to parse extension at ${extPath}: ${e}`);
223
- }
224
- }
225
- }
226
- }
227
- catch (error) {
228
- logger.error(`Error during extension discovery: ${error}`);
229
- }
230
- return extensions;
231
- }
232
- /**
233
- * Executes a prompt and streams events back.
234
- */
235
- async run(prompt, onEvent, sessionId, attachments, onStatus) {
236
- if (!this.initialized) {
237
- await this.initialize(sessionId);
238
- }
239
- const sid = sessionId || this.coreConfig.getSessionId();
240
- const savedHome = process.env.HOME;
241
- try {
242
- process.env.HOME = this.tarsConfig.homeDir;
243
- process.env.GEMINI_CLI_HOME = this.tarsConfig.homeDir;
244
- // Session Swapping Logic or First Run
245
- // We must call startChat at least once to initialize the GeminiChat session,
246
- // even if the sessionId matches the coreConfig's initial ID.
247
- if (this.currentSessionId !== sid || !this.client.isInitialized()) {
248
- logger.debug(`🔄 Initializing/Swapping Gemini session to: ${sid}`);
249
- const resumedData = await this.loadResumedSessionData(sid);
250
- let history = undefined;
251
- if (resumedData && resumedData.conversation) {
252
- history = this.convertRecordToHistory(resumedData.conversation);
253
- }
254
- // @ts-ignore - access private to swap session
255
- await this.client.startChat(history, resumedData || undefined);
256
- // Sync: Read back the actual session ID that Core assigned.
257
- // Core may create a new session ID internally (e.g. if the project hash
258
- // changed or the old session was not found). We must keep Tars's
259
- // SessionManager in sync to prevent ID mismatch on next restart.
260
- const recordingService = this.client.getChatRecordingService();
261
- const actualCoreSessionId = recordingService?.sessionId || this.coreConfig.getSessionId();
262
- if (actualCoreSessionId && actualCoreSessionId !== sid) {
263
- logger.warn(`⚠️ Session ID mismatch detected: Tars=${sid}, Core=${actualCoreSessionId}. Syncing to Core's ID.`);
264
- this.currentSessionId = actualCoreSessionId;
265
- // Update SessionManager so the correct ID is persisted to disk
266
- if (this.sessionManager) {
267
- await this.sessionManager.save(actualCoreSessionId);
268
- }
269
- }
270
- else {
271
- this.currentSessionId = sid;
272
- }
273
- }
274
- let currentRequestParts = [{ text: prompt }];
275
- const reqPromptId = `req-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
276
- // Handle Multimodal Attachments
277
- if (attachments && attachments.length > 0) {
278
- for (const attachment of attachments) {
279
- try {
280
- const data = fs.readFileSync(attachment.path).toString('base64');
281
- currentRequestParts.push({
282
- inlineData: {
283
- data,
284
- mimeType: attachment.mimeType
285
- }
286
- });
287
- logger.debug(`📎 Attached file to prompt: ${attachment.path} (${attachment.mimeType})`);
288
- }
289
- catch (err) {
290
- logger.error(`Failed to read attachment ${attachment.path}: ${err.message}`);
291
- }
292
- }
293
- }
294
- let turnCount = 0;
295
- const maxTurns = 100; // Increased to handle complex autonomous tasks
296
- const abortController = new AbortController();
297
- let finalUsageStats = undefined;
298
- // Accumulate usage across multi-turn interactions.
299
- // For local models, each turn reports only its own tokens. For Gemini Cloud,
300
- // promptTokenCount is cumulative (total context). We keep the maximum
301
- // promptTokenCount seen (last turn = largest context) and sum outputTokens.
302
- let accumulatedInputTokens = 0;
303
- let accumulatedOutputTokens = 0;
304
- let accumulatedCachedTokens = 0;
305
- let loopDetected = false;
306
- let hasRealContent = false;
307
- // Track recent tool calls for live status updates
308
- const recentTools = [];
309
- while (turnCount < maxTurns) {
310
- turnCount++;
311
- const toolRequests = [];
312
- let stream;
313
- let retryCount = 0;
314
- const maxRetries = 8;
315
- let lastError = null;
316
- while (retryCount < maxRetries) {
317
- try {
318
- const estimatedTokens = Math.max(100, accumulatedInputTokens);
319
- const waitTime = this.rateLimiter.checkWaitTime(estimatedTokens);
320
- if (waitTime > 0) {
321
- logger.info(`⏳ Pre-emptive throttling: waiting ${Math.round(waitTime / 1000)}s to avoid rate limits...`);
322
- await new Promise((resolve) => setTimeout(resolve, waitTime));
323
- }
324
- this.rateLimiter.recordRequest(estimatedTokens);
325
- stream = await promptIdContext.run(sid, () => {
326
- // Combine manual abort signal with a 5-minute timeout to prevent deadlock
327
- const timeoutSignal = AbortSignal.timeout(5 * 60 * 1000);
328
- const combinedSignal = abortController.signal.aborted
329
- ? abortController.signal
330
- : timeoutSignal;
331
- // We listen for manual aborts to also abort the combined signal
332
- // (AbortSignal.any is available in Node 20+, but we can just pass timeoutSignal
333
- // and manual aborts are rare here, or use AbortSignal.any if supported)
334
- const signalToUse = typeof AbortSignal.any === 'function'
335
- ? AbortSignal.any([abortController.signal, timeoutSignal])
336
- : timeoutSignal;
337
- return this.client.sendMessageStream(currentRequestParts, signalToUse, reqPromptId);
338
- });
339
- break; // Success
340
- }
341
- catch (error) {
342
- retryCount++;
343
- lastError = error;
344
- const isTransient = error.message?.includes('429') ||
345
- error.message?.includes('503') ||
346
- error.message?.toLowerCase().includes('rate limit') ||
347
- error.message?.toLowerCase().includes('capacity') ||
348
- error.message?.toLowerCase().includes('quota') ||
349
- error.message?.toLowerCase().includes('overloaded');
350
- if (isTransient && retryCount < maxRetries) {
351
- const delay = Math.pow(2, retryCount) * 1000 + Math.random() * 1000;
352
- logger.warn(`⚠️ Gemini API transient error (attempt ${retryCount}/${maxRetries}): ${error.message}. Retrying in ${Math.round(delay)}ms...`);
353
- await new Promise((resolve) => setTimeout(resolve, delay));
354
- continue;
355
- }
356
- // Fallback logic for 'auto' model on permanent error or final retry
357
- if (this.tarsConfig.geminiModel === 'auto' &&
358
- !this.initializedWithFallback) {
359
- logger.warn(`🔄 'auto' model failed with error: ${error.message}. Attempting fallback to gemini-2.0-flash...`);
360
- this.initializedWithFallback = true;
361
- // @ts-ignore - modifying private config for fallback
362
- this.coreConfig.model = 'gemini-2.0-flash';
363
- // Re-initialize client with new model
364
- this.client = this.coreConfig.getGeminiClient();
365
- this.applyClientOverrides(this.client);
366
- await this.client.initialize();
367
- retryCount = 0; // Reset retries for the fallback model
368
- continue;
369
- }
370
- throw error;
371
- }
372
- }
373
- for await (const event of stream) {
374
- if (event.type === GeminiEventType.ToolCallRequest) {
375
- toolRequests.push(event.value);
376
- }
377
- if (event.type === GeminiEventType.LoopDetected) {
378
- loopDetected = true;
379
- }
380
- if (event.type === GeminiEventType.Finished) {
381
- const usage = event.value.usageMetadata;
382
- if (usage) {
383
- // promptTokenCount reflects total context size (cumulative)
384
- // so we always take the latest (highest) value
385
- if (usage.promptTokenCount) {
386
- accumulatedInputTokens = Math.max(accumulatedInputTokens, usage.promptTokenCount);
387
- }
388
- // candidatesTokenCount is per-turn, so we accumulate
389
- if (usage.candidatesTokenCount) {
390
- accumulatedOutputTokens += usage.candidatesTokenCount;
391
- }
392
- if (usage.cachedContentTokenCount) {
393
- accumulatedCachedTokens = Math.max(accumulatedCachedTokens, usage.cachedContentTokenCount);
394
- }
395
- finalUsageStats = usage;
396
- }
397
- continue; // Don't emit done yet
398
- }
399
- const normalized = this.normalizeEvent(event, sid);
400
- if (normalized) {
401
- if (normalized.type === 'text' && normalized.content?.trim()) {
402
- hasRealContent = true;
403
- }
404
- await onEvent(normalized);
405
- }
406
- }
407
- if (loopDetected) {
408
- logger.warn(`⚠️ Loop detected in Gemini Engine at turn ${turnCount}.`);
409
- break;
410
- }
411
- if (toolRequests.length === 0) {
412
- logger.debug(`✅ Interaction complete after ${turnCount} turns.`);
413
- break;
414
- }
415
- // ... (rest of the loop)
416
- if (turnCount >= maxTurns) {
417
- logger.warn(`⚠️ Hit maxTurns (${maxTurns}) limit. Force terminating interaction.`);
418
- await onEvent({
419
- type: 'text',
420
- role: 'assistant',
421
- content: '\n\n⚠️ *Task was complex and reached the maximum turn limit. I have executed as much as I could.*',
422
- sessionId: sid
423
- });
424
- break;
425
- }
426
- // Runtime Safety Filter: Prevent self-destructive commands and unauthorized path access
427
- const filteredToolRequests = [];
428
- const blockedResponses = new Map();
429
- const sensitiveCalls = new Set();
430
- for (const req of toolRequests) {
431
- const toolName = req.name;
432
- const args = req.args || {};
433
- const commandLine = args.CommandLine || args.command || '';
434
- const filePath = args.file_path || args.path || args.dir_path || '';
435
- // 1. Block self-destructive commands
436
- if ((toolName.includes('run_command') ||
437
- toolName.includes('run_shell_command')) &&
438
- (commandLine.includes('tars stop') ||
439
- /\bpm2\s+(stop|kill|delete)\b/.test(commandLine))) {
440
- logger.warn(`🛑 INTERCEPTED self-destructive command: ${commandLine}`);
441
- await onEvent({
442
- type: 'text',
443
- role: 'assistant',
444
- content: `\n\n⚠️ **Safety Interruption**: I attempted to run a command that would stop my own supervisor process (${commandLine}). To prevent a loss of connection or state, I have blocked this action. If you really want me to stop, please run \`tars stop\` manually in your terminal.`,
445
- sessionId: sid
446
- });
447
- blockedResponses.set(req.callId, 'Execution blocked: Self-destructive command detected.');
448
- continue;
449
- }
450
- // 2. Block blacklisted path access
451
- if (filePath && DLPService.isPathBlacklisted(filePath)) {
452
- logger.warn(`🛑 INTERCEPTED unauthorized path access: ${filePath}`);
453
- await onEvent({
454
- type: 'text',
455
- role: 'assistant',
456
- content: `\n\n⚠️ **Security Interruption**: I attempted to access a protected file or directory (${filePath}). Access to this path is restricted by the Tars Data Loss Prevention (DLP) policy.`,
457
- sessionId: sid
458
- });
459
- blockedResponses.set(req.callId, `Access to ${filePath} is restricted by DLP policy.`);
460
- continue;
461
- }
462
- // 3. Mark sensitive paths for aggressive scrubbing
463
- if ((filePath && DLPService.isSensitivePath(filePath)) ||
464
- (commandLine && DLPService.isSensitivePath(commandLine))) {
465
- sensitiveCalls.add(req.callId);
466
- }
467
- filteredToolRequests.push(req);
468
- }
469
- // Execute tools using Scheduler
470
- let completedCalls = [];
471
- if (filteredToolRequests.length > 0) {
472
- logger.debug(`🛠️ Executing ${filteredToolRequests.length} tool calls...`);
473
- const scheduler = new Scheduler({
474
- context: this.coreConfig,
475
- messageBus: this.coreConfig.getMessageBus(),
476
- getPreferredEditor: () => undefined,
477
- schedulerId: sid
478
- });
479
- completedCalls = await scheduler.schedule(filteredToolRequests, abortController.signal);
480
- // Emit tool responses so the Supervisor can log them
481
- for (const call of completedCalls) {
482
- const normalized = this.normalizeEvent({
483
- type: GeminiEventType.ToolCallResponse,
484
- value: call
485
- }, sid);
486
- if (normalized)
487
- await onEvent(normalized);
488
- }
489
- // Record results in chat recording service for persistence/memory
490
- const model = this.tarsConfig.geminiModel;
491
- this.client.getChat().recordCompletedToolCalls(model, completedCalls);
492
- }
493
- // Build tool status for live progress updates
494
- const turnToolStatuses = [];
495
- for (const call of completedCalls) {
496
- const req = toolRequests.find((r) => r.callId === call.request?.callId || r.callId === call.callId);
497
- if (!req)
498
- continue;
499
- const part = call.response?.responseParts?.find((p) => 'functionResponse' in p);
500
- const response = part?.functionResponse?.response;
501
- const responseStr = typeof response === 'string'
502
- ? response
503
- : typeof response === 'object'
504
- ? JSON.stringify(response)
505
- : String(response || '');
506
- turnToolStatuses.push({
507
- name: req.name,
508
- responsePreview: responseStr.substring(0, 120),
509
- responseSize: responseStr.length
510
- });
511
- }
512
- // Also include blocked tools
513
- for (const [callId, reason] of blockedResponses) {
514
- const req = toolRequests.find((r) => r.callId === callId);
515
- if (req) {
516
- turnToolStatuses.push({
517
- name: req.name,
518
- responsePreview: `⛔ ${reason}`,
519
- responseSize: reason.length
520
- });
521
- }
522
- }
523
- recentTools.push(...turnToolStatuses);
524
- const isMilestone = turnCount > 0 && turnCount % 20 === 0;
525
- // Fire status update after each tool batch or on a milestone
526
- if (onStatus && (turnToolStatuses.length > 0 || isMilestone)) {
527
- if (isMilestone) {
528
- logger.info(`[GeminiEngine] Milestone ${turnCount} — firing status update...`);
529
- }
530
- try {
531
- await onStatus(turnCount, recentTools.slice(-10), isMilestone);
532
- }
533
- catch (e) {
534
- logger.warn(`[GeminiEngine] Status update failed: ${e.message}`);
535
- }
536
- }
537
- // Prepare next request with tool results (Scrubbed via DLP and mapped back to 1:1)
538
- currentRequestParts = GeminiEngine.buildToolResponseParts(toolRequests, completedCalls, blockedResponses, sensitiveCalls, this.tarsConfig.inferenceBackend === 'llamacpp');
539
- if (currentRequestParts.length === 0) {
540
- logger.warn('⚠️ No tool responses generated after execution.');
541
- break;
542
- }
543
- // -------------------------------------------------------------------------
544
- // PROACTIVE COMPRESSION & USER CHECK-IN
545
- // -------------------------------------------------------------------------
546
- // 1. Check if we need to compress mid-loop to avoid context window crashes
547
- if (this.sessionManager && this.tarsConfig) {
548
- const stats = this.sessionManager.getStats();
549
- const threshold = this.tarsConfig.compressionThreshold || 0.8;
550
- const limit = this.tarsConfig.contextWindowTokens || 128000;
551
- if (stats && stats.lastInputTokens > limit * threshold) {
552
- logger.info(`[GeminiEngine] Mid-loop compression triggered (${stats.lastInputTokens}/${limit} tokens)`);
553
- try {
554
- const didCompress = await this.compressSession();
555
- if (didCompress) {
556
- await onEvent({
557
- type: 'text',
558
- role: 'assistant',
559
- content: '\n\n✨ *Mid-task memory compacted to optimally save context space.*',
560
- sessionId: sid
561
- });
562
- }
563
- }
564
- catch (e) {
565
- logger.warn(`[GeminiEngine] Mid-loop compression failed: ${e.message}`);
566
- }
567
- }
568
- }
569
- // 2. Max turns safety (handled by loop condition, but we break here if needed)
570
- }
571
- // If the loop finished without producing any content, notify the user
572
- if (!hasRealContent) {
573
- let fallbackMsg = '\n\n⚠️ **Model Interaction Issue**: The Gemini model failed to produce a valid text response.';
574
- if (loopDetected) {
575
- fallbackMsg +=
576
- ' A repetitive output loop was detected and terminated. This can sometimes happen with complex prompts or transient API glitches.';
577
- }
578
- fallbackMsg += '\n\nPlease try rephrasing your request or starting a new session.';
579
- await onEvent({
580
- type: 'text',
581
- role: 'assistant',
582
- content: fallbackMsg,
583
- sessionId: sid
584
- });
585
- }
586
- // Always emit final done event when exiting the loop
587
- await onEvent({
588
- type: 'done',
589
- usageStats: accumulatedInputTokens > 0 || accumulatedOutputTokens > 0
590
- ? {
591
- inputTokens: accumulatedInputTokens,
592
- outputTokens: accumulatedOutputTokens,
593
- cachedTokens: accumulatedCachedTokens
594
- }
595
- : finalUsageStats
596
- ? {
597
- inputTokens: finalUsageStats.promptTokenCount || 0,
598
- outputTokens: finalUsageStats.candidatesTokenCount || 0,
599
- cachedTokens: finalUsageStats.cachedContentTokenCount || 0
600
- }
601
- : undefined,
602
- sessionId: sid
603
- });
604
- }
605
- catch (error) {
606
- const errorMsg = error.message ||
607
- (typeof error === 'object' ? JSON.stringify(error) : String(error));
608
- logger.error(`❌ Gemini Engine run error: ${errorMsg}`);
609
- await onEvent({ type: 'error', error: errorMsg });
610
- throw error;
611
- }
612
- finally {
613
- process.env.HOME = savedHome;
614
- }
615
- }
616
- /**
617
- * Synchronous-style run for background tasks.
618
- */
619
- async runSync(prompt, sessionId) {
620
- let fullContent = '';
621
- await this.run(prompt, (event) => {
622
- if (event.content && event.role === 'assistant') {
623
- fullContent += event.content;
624
- }
625
- }, sessionId);
626
- return fullContent;
627
- }
628
- /**
629
- * Rebuilds the response array for Gemini ensuring 1:1 parity with function calls.
630
- */
631
- static buildToolResponseParts(toolRequests, completedCalls, blockedResponses, sensitiveCalls, isLocalInference = false) {
632
- return toolRequests
633
- .map((req) => {
634
- const callId = req.callId;
635
- if (blockedResponses.has(callId)) {
636
- return {
637
- functionResponse: {
638
- name: req.name,
639
- response: { error: blockedResponses.get(callId) }
640
- }
641
- };
642
- }
643
- const completedCall = completedCalls.find((c) => (c.request?.callId || c.callId) === callId);
644
- if (!completedCall)
645
- return null;
646
- const part = completedCall.response?.responseParts?.find((p) => 'functionResponse' in p);
647
- if (part?.functionResponse?.response) {
648
- let scrubbedResponse = DLPService.scrubDeep(part.functionResponse.response);
649
- if (sensitiveCalls.has(callId) &&
650
- typeof scrubbedResponse === 'object' &&
651
- scrubbedResponse !== null) {
652
- if (scrubbedResponse.content &&
653
- typeof scrubbedResponse.content === 'string') {
654
- scrubbedResponse.content = DLPService.scrubEnvContent(scrubbedResponse.content);
655
- }
656
- if (scrubbedResponse.stdout &&
657
- typeof scrubbedResponse.stdout === 'string') {
658
- scrubbedResponse.stdout = DLPService.scrubEnvContent(scrubbedResponse.stdout);
659
- }
660
- }
661
- part.functionResponse.response = scrubbedResponse;
662
- }
663
- // Inject the original callId so LlamaCppGenerator can map it to tool_call_id.
664
- // We ONLY do this for local inference as the Cloud Gemini API rejects unknown fields.
665
- if (isLocalInference) {
666
- part.id = callId;
667
- }
668
- return part;
669
- })
670
- .filter(Boolean);
671
- }
672
- /**
673
- * Proactively compress the session history to reclaim context window space.
674
- * Delegates to Gemini Core's built-in compression.
675
- */
676
- async compressSession(force = false) {
677
- if (!this.initialized || !this.client)
678
- return false;
679
- const sid = this.currentSessionId || 'unknown';
680
- logger.info(`🗜️ Triggering session compression (force=${force})...`);
681
- try {
682
- if (this.tarsConfig.inferenceBackend === 'llamacpp') {
683
- const history = this.client.getHistory();
684
- // We keep the most recent ~60% and ensure the boundary lands on a 'user' role
685
- // to maintain proper turn alternation (no orphaned tool responses).
686
- if (history && history.length > 20) {
687
- const keepCount = Math.ceil(history.length * 0.6);
688
- let cutIndex = history.length - keepCount;
689
- // Walk forward to find a 'user' role entry for clean boundary
690
- while (cutIndex < history.length && history[cutIndex]?.role !== 'user') {
691
- cutIndex++;
692
- }
693
- if (cutIndex < history.length) {
694
- const historyToCompress = history.slice(0, cutIndex);
695
- const tail = history.slice(cutIndex);
696
- logger.info(`🗜️ Local inference compaction: Summarizing oldest ${historyToCompress.length} turns...`);
697
- // Use the local generator non-streamed to summarize the truncated chunk
698
- const generator = this.coreConfig.contentGenerator;
699
- const hasPreviousSnapshot = historyToCompress.some((c) => c.parts?.some((p) => p.text?.includes('<state_snapshot>')));
700
- const anchorInstruction = hasPreviousSnapshot
701
- ? 'A previous <state_snapshot> exists in the history. You MUST integrate all still-relevant information from that snapshot into the new one, updating it with the more recent events.'
702
- : 'Generate a new <state_snapshot> based on the provided history.';
703
- const summaryPrompt = `${anchorInstruction}\nExtract all important constraints, configs, details and tool results from this chunk of history. Format your response cleanly.`;
704
- let summaryContent = '';
705
- try {
706
- const response = await generator.generateContent({
707
- model: this.tarsConfig.geminiModel,
708
- contents: [
709
- ...historyToCompress,
710
- { role: 'user', parts: [{ text: summaryPrompt }] }
711
- ]
712
- }, sid);
713
- summaryContent =
714
- response?.candidates?.[0]?.content?.parts?.[0]?.text || '';
715
- }
716
- catch (err) {
717
- logger.warn(`Semantic compression inference failed: ${err.message}`);
718
- }
719
- if (!summaryContent) {
720
- summaryContent =
721
- '*(Summary generation failed, falling back to raw truncation)*';
722
- }
723
- const newHistory = [
724
- {
725
- role: 'user',
726
- parts: [
727
- {
728
- text: `<state_snapshot>\n${summaryContent.trim()}\n</state_snapshot>`
729
- }
730
- ]
731
- },
732
- {
733
- role: 'model',
734
- parts: [
735
- { text: 'Got it. I will keep this historical context in mind.' }
736
- ]
737
- },
738
- ...tail
739
- ];
740
- this.client.setHistory(newHistory);
741
- logger.info(`🗜️ Local inference context compacted: retained tail of ${tail.length} turns + snapshot.`);
742
- return true;
743
- }
744
- }
745
- return false;
746
- }
747
- const result = await this.client.tryCompressChat(sid, force);
748
- logger.info(`🗜️ Compression result: status=${result.compressionStatus}, ` +
749
- `${result.originalTokenCount} → ${result.newTokenCount} tokens`);
750
- return String(result.compressionStatus) === 'COMPRESSED';
751
- }
752
- catch (e) {
753
- logger.warn(`⚠️ Compression failed: ${e.message}`);
754
- return false;
755
- }
756
- }
757
- /**
758
- * Refreshes the system instruction in-place without destroying the session.
759
- * Used after memory mutations so the model sees updated facts.
760
- */
761
- refreshSystemInstruction() {
762
- if (!this.initialized || !this.client)
763
- return;
764
- this.client.updateSystemInstruction();
765
- logger.debug('🔄 System instruction refreshed in-place');
766
- }
767
- /**
768
- * Maps native core events to Tars-compatible event format.
769
- */
770
- normalizeEvent(event, sessionId) {
771
- switch (event.type) {
772
- case GeminiEventType.Content:
773
- return {
774
- type: 'text',
775
- role: 'assistant',
776
- content: event.value,
777
- sessionId
778
- };
779
- case GeminiEventType.Thought:
780
- // ThoughtSummary has subject and description
781
- const thoughtText = event.value.subject
782
- ? `**${event.value.subject}** ${event.value.description}`
783
- : event.value.description;
784
- return {
785
- type: 'thought',
786
- content: DLPService.scrub(thoughtText),
787
- sessionId
788
- };
789
- case GeminiEventType.ToolCallRequest:
790
- return {
791
- type: 'tool_call',
792
- toolName: event.value.name,
793
- toolArgs: DLPService.scrubDeep(event.value.args),
794
- callId: event.value.callId,
795
- sessionId
796
- };
797
- case GeminiEventType.ToolCallResponse:
798
- // resultDisplay can be string | FileDiff | AnsiOutput | TodoList
799
- // Support both ToolCallResponseInfo and CompletedToolCall payloads
800
- const val = event.value;
801
- const callInfo = val.response ? val.response : val;
802
- const display = callInfo.resultDisplay;
803
- let content = '';
804
- if (typeof display === 'string') {
805
- content = display;
806
- }
807
- else if (display) {
808
- content = JSON.stringify(display);
809
- }
810
- else if (callInfo.error) {
811
- content = callInfo.error.message;
812
- }
813
- return {
814
- type: 'tool_response',
815
- toolName: val.request?.callId || callInfo.callId,
816
- content: DLPService.scrub(content),
817
- sessionId
818
- };
819
- case GeminiEventType.Finished:
820
- return {
821
- type: 'done',
822
- usageStats: event.value.usageMetadata
823
- ? {
824
- inputTokens: event.value.usageMetadata.promptTokenCount || 0,
825
- outputTokens: event.value.usageMetadata.candidatesTokenCount || 0,
826
- cachedTokens: event.value.usageMetadata.cachedContentTokenCount || 0
827
- }
828
- : undefined,
829
- sessionId
830
- };
831
- case GeminiEventType.Error:
832
- let errorDetails = '';
833
- if (event.value instanceof Error) {
834
- errorDetails = event.value.message;
835
- }
836
- else if (typeof event.value === 'object' && event.value !== null) {
837
- // Try to extract nested error message if it exists (common in Google API errors)
838
- const val = event.value;
839
- errorDetails = val.message || val.error?.message || JSON.stringify(event.value);
840
- }
841
- else {
842
- errorDetails = String(event.value);
843
- }
844
- return {
845
- type: 'error',
846
- error: errorDetails,
847
- sessionId
848
- };
849
- case GeminiEventType.LoopDetected:
850
- return {
851
- type: 'loop_detected',
852
- sessionId
853
- };
854
- case GeminiEventType.ChatCompressed: {
855
- const info = event.value;
856
- if (info && info.compressionStatus === CompressionStatus.COMPRESSED) {
857
- return {
858
- type: 'compressed',
859
- content: `🗜️ Session compressed: ${info.originalTokenCount.toLocaleString()} → ${info.newTokenCount.toLocaleString()} tokens`,
860
- sessionId
861
- };
862
- }
863
- return null;
864
- }
865
- case GeminiEventType.ContextWindowWillOverflow:
866
- logger.warn(`⚠️ Context window near overflow: ${event.value.estimatedRequestTokenCount} tokens, ${event.value.remainingTokenCount} remaining`);
867
- return {
868
- type: 'context_warning',
869
- content: `⚠️ Context window near capacity (${event.value.remainingTokenCount.toLocaleString()} tokens remaining)`,
870
- sessionId
871
- };
872
- case GeminiEventType.MaxSessionTurns:
873
- return {
874
- type: 'max_turns',
875
- content: '⚠️ Maximum session turns reached. Consider compressing the session.',
876
- sessionId
877
- };
878
- default:
879
- return null;
880
- }
881
- }
882
- /**
883
- * Attempts to find and load session history from the Core's history directory.
884
- */
885
- async loadResumedSessionData(sessionId) {
886
- try {
887
- // Core history is usually in ~/.gemini/tmp/<hash>/chats/
888
- // But we isolated HOME to ~/.tars, so it's in ~/.tars/.gemini/...
889
- const projectRoot = this.tarsConfig.homeDir;
890
- const geminiDir = path.join(this.tarsConfig.homeDir, '.gemini');
891
- const tmpDir = path.join(geminiDir, 'tmp');
892
- if (!fs.existsSync(tmpDir))
893
- return null;
894
- // 1. Try to find the exact project identifier from projects.json
895
- let projectIdentifier = null;
896
- const registryPath = path.join(geminiDir, 'projects.json');
897
- if (fs.existsSync(registryPath)) {
898
- try {
899
- const registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8'));
900
- projectIdentifier = registry.projects[projectRoot] || null;
901
- }
902
- catch (e) {
903
- logger.warn(`⚠️ Failed to read projects.json: ${e}`);
904
- }
905
- }
906
- // 2. Fallback: MD5 hash (used in some versions)
907
- if (!projectIdentifier) {
908
- const crypto = await import('node:crypto');
909
- projectIdentifier = crypto.createHash('md5').update(projectRoot).digest('hex');
910
- }
911
- // 3. Search for the session file in candidate directories
912
- // We search projectIdentifier first, then scan all if not found
913
- const searchDirs = [projectIdentifier];
914
- try {
915
- const allDirs = fs.readdirSync(tmpDir);
916
- for (const d of allDirs) {
917
- if (d !== projectIdentifier)
918
- searchDirs.push(d);
919
- }
920
- }
921
- catch (e) { }
922
- const shortId = sessionId.slice(0, 8);
923
- for (const dir of searchDirs) {
924
- if (!dir)
925
- continue;
926
- const chatsDir = path.join(tmpDir, dir, 'chats');
927
- if (!fs.existsSync(chatsDir))
928
- continue;
929
- const files = fs.readdirSync(chatsDir);
930
- const sessionFile = files.find((f) => f.includes(`-${shortId}.json`));
931
- if (sessionFile) {
932
- const filePath = path.join(chatsDir, sessionFile);
933
- const content = await loadConversationRecord(filePath);
934
- logger.info(`📂 Resumed session from exact match: ${sessionFile}`);
935
- return {
936
- conversation: content,
937
- filePath
938
- };
939
- }
940
- // Fallback: If no exact session ID match, use the most recently
941
- // modified chat file. This prevents a blank cold start when the
942
- // Tars session ID has drifted from Core's internal session ID.
943
- // We also check for .jsonl files used in newer Core versions.
944
- const jsonFiles = files.filter((f) => f.endsWith('.json') || f.endsWith('.jsonl'));
945
- if (jsonFiles.length > 0) {
946
- const sorted = jsonFiles
947
- .map((f) => ({
948
- name: f,
949
- mtime: fs.statSync(path.join(chatsDir, f)).mtimeMs
950
- }))
951
- .sort((a, b) => b.mtime - a.mtime);
952
- const latestFile = sorted[0].name;
953
- logger.warn(`⚠️ No exact session match for ${shortId}. Falling back to latest: ${latestFile}`);
954
- const filePath = path.join(chatsDir, latestFile);
955
- const content = await loadConversationRecord(filePath);
956
- return {
957
- conversation: content,
958
- filePath
959
- };
960
- }
961
- }
962
- return null;
963
- }
964
- catch (e) {
965
- logger.warn(`⚠️ Failed to load resumed session data: ${e}`);
966
- return null;
967
- }
968
- }
969
- /**
970
- * Converts a ConversationRecord from Core to Gemini API Content[] history.
971
- */
972
- convertRecordToHistory(conversation) {
973
- const history = [];
974
- if (!conversation || !conversation.messages)
975
- return history;
976
- for (const msg of conversation.messages) {
977
- if (msg.type === 'user') {
978
- let parts = [];
979
- if (typeof msg.content === 'string') {
980
- parts = [{ text: msg.content }];
981
- }
982
- else if (Array.isArray(msg.content)) {
983
- parts = msg.content;
984
- }
985
- history.push({ role: 'user', parts });
986
- }
987
- else if (msg.type === 'gemini') {
988
- let parts = [];
989
- if (typeof msg.content === 'string' && msg.content !== '') {
990
- parts.push({ text: msg.content });
991
- }
992
- else if (Array.isArray(msg.content)) {
993
- parts.push(...msg.content);
994
- }
995
- // Add function calls if any
996
- const functionResponseParts = [];
997
- if (msg.toolCalls) {
998
- for (const tc of msg.toolCalls) {
999
- parts.push({
1000
- functionCall: {
1001
- name: tc.name,
1002
- args: tc.args
1003
- }
1004
- });
1005
- // If the tool call has a result, we need to add a function response
1006
- if (tc.status === 'done' || tc.result) {
1007
- let responseObj = tc.result;
1008
- if (typeof responseObj === 'string') {
1009
- try {
1010
- responseObj = JSON.parse(responseObj);
1011
- }
1012
- catch (e) {
1013
- responseObj = { result: responseObj };
1014
- }
1015
- }
1016
- else if (responseObj === null || responseObj === undefined) {
1017
- responseObj = { result: 'Success' };
1018
- }
1019
- functionResponseParts.push({
1020
- functionResponse: {
1021
- name: tc.name,
1022
- response: responseObj
1023
- }
1024
- });
1025
- }
1026
- }
1027
- }
1028
- history.push({ role: 'model', parts });
1029
- // If we generated function response parts, they belong to the NEXT turn as 'user'
1030
- if (functionResponseParts.length > 0) {
1031
- history.push({ role: 'user', parts: functionResponseParts });
1032
- }
1033
- }
1034
- }
1035
- return history;
1036
- }
1037
- /**
1038
- * Applies runtime overrides to the Gemini client to ensure smooth operation
1039
- * in specific environments (e.g., local inference).
1040
- */
1041
- applyClientOverrides(client) {
1042
- if (this.tarsConfig.inferenceBackend === 'llamacpp') {
1043
- // The loop detector runs concurrently in the background and causes 400 crashes
1044
- // for local-only setups that don't have a valid Google API key.
1045
- const loopService = client.getLoopDetectionService();
1046
- if (loopService) {
1047
- logger.debug('🔇 Silencing LoopDetectionService for local inference...');
1048
- loopService.queryLoopDetectionModel = async () => {
1049
- logger.debug('Background loop verification skipped (Local Mode).');
1050
- return null;
1051
- };
1052
- }
1053
- }
1054
- }
1055
- }
1056
- //# sourceMappingURL=gemini-engine.js.map