@saccolabs/tars 1.30.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 (92) hide show
  1. package/README.md +1 -1
  2. package/context/skills/create-extension/SKILL.md +2 -2
  3. package/context/skills/manage-extensions/SKILL.md +3 -3
  4. package/dist/channels/discord/discord-channel.js +2 -3
  5. package/dist/channels/discord/discord-channel.js.map +1 -1
  6. package/dist/channels/discord/message-formatter.d.ts +1 -1
  7. package/dist/channels/discord/message-formatter.js +1 -1
  8. package/dist/cli/commands/quota.js +13 -48
  9. package/dist/cli/commands/quota.js.map +1 -1
  10. package/dist/cli/commands/refresh.js +61 -9
  11. package/dist/cli/commands/refresh.js.map +1 -1
  12. package/dist/cli/commands/setup.js +192 -467
  13. package/dist/cli/commands/setup.js.map +1 -1
  14. package/dist/cli/index.js +1 -13
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/config/config.d.ts +5 -10
  17. package/dist/config/config.js +21 -27
  18. package/dist/config/config.js.map +1 -1
  19. package/dist/memory/knowledge-store.js +10 -1
  20. package/dist/memory/knowledge-store.js.map +1 -1
  21. package/dist/memory/memory-manager.d.ts +0 -3
  22. package/dist/memory/memory-manager.js +26 -34
  23. package/dist/memory/memory-manager.js.map +1 -1
  24. package/dist/scripts/debug-cli.js +2 -2
  25. package/dist/scripts/debug-cli.js.map +1 -1
  26. package/dist/supervisor/heartbeat-service.js +1 -1
  27. package/dist/supervisor/heartbeat-service.js.map +1 -1
  28. package/dist/supervisor/main.js +37 -79
  29. package/dist/supervisor/main.js.map +1 -1
  30. package/dist/supervisor/mcp-bridge.d.ts +25 -0
  31. package/dist/supervisor/mcp-bridge.js +157 -0
  32. package/dist/supervisor/mcp-bridge.js.map +1 -0
  33. package/dist/supervisor/session-manager.d.ts +1 -1
  34. package/dist/supervisor/session-manager.js +1 -1
  35. package/dist/supervisor/supervisor.d.ts +14 -7
  36. package/dist/supervisor/supervisor.js +87 -29
  37. package/dist/supervisor/supervisor.js.map +1 -1
  38. package/dist/supervisor/{gemini-engine.d.ts → tars-engine.d.ts} +39 -30
  39. package/dist/supervisor/tars-engine.js +698 -0
  40. package/dist/supervisor/tars-engine.js.map +1 -0
  41. package/dist/tools/get-quota.d.ts +38 -12
  42. package/dist/tools/get-quota.js +37 -94
  43. package/dist/tools/get-quota.js.map +1 -1
  44. package/dist/tools/send-notification.d.ts +32 -7
  45. package/dist/tools/send-notification.js +31 -37
  46. package/dist/tools/send-notification.js.map +1 -1
  47. package/dist/types/index.d.ts +2 -2
  48. package/dist/utils/brain-audit.js +4 -4
  49. package/dist/utils/brain-audit.js.map +1 -1
  50. package/dist/utils/migration-manager.d.ts +4 -0
  51. package/dist/utils/migration-manager.js +205 -0
  52. package/dist/utils/migration-manager.js.map +1 -0
  53. package/extensions/memory/dist/store.js +29 -20
  54. package/extensions/memory/dist/store.js.map +1 -1
  55. package/extensions/memory/src/store.ts +33 -23
  56. package/package.json +4 -3
  57. package/src/prompts/system.md +3 -14
  58. package/context/agents/scaffolder.md +0 -22
  59. package/dist/auth/credential-manager.d.ts +0 -14
  60. package/dist/auth/credential-manager.js +0 -60
  61. package/dist/auth/credential-manager.js.map +0 -1
  62. package/dist/auth/oauth-service.d.ts +0 -24
  63. package/dist/auth/oauth-service.js +0 -89
  64. package/dist/auth/oauth-service.js.map +0 -1
  65. package/dist/auth/workspace-auth-service.d.ts +0 -10
  66. package/dist/auth/workspace-auth-service.js +0 -78
  67. package/dist/auth/workspace-auth-service.js.map +0 -1
  68. package/dist/cli/commands/swarm.d.ts +0 -13
  69. package/dist/cli/commands/swarm.js +0 -250
  70. package/dist/cli/commands/swarm.js.map +0 -1
  71. package/dist/inference/LlamaCppGenerator.d.ts +0 -25
  72. package/dist/inference/LlamaCppGenerator.js +0 -461
  73. package/dist/inference/LlamaCppGenerator.js.map +0 -1
  74. package/dist/scripts/test-local-llamacpp.d.ts +0 -1
  75. package/dist/scripts/test-local-llamacpp.js +0 -77
  76. package/dist/scripts/test-local-llamacpp.js.map +0 -1
  77. package/dist/supervisor/gemini-engine.js +0 -983
  78. package/dist/supervisor/gemini-engine.js.map +0 -1
  79. package/dist/swarm/agent-card.d.ts +0 -14
  80. package/dist/swarm/agent-card.js +0 -93
  81. package/dist/swarm/agent-card.js.map +0 -1
  82. package/dist/swarm/rpc-handler.d.ts +0 -27
  83. package/dist/swarm/rpc-handler.js +0 -235
  84. package/dist/swarm/rpc-handler.js.map +0 -1
  85. package/dist/swarm/swarm-service.d.ts +0 -47
  86. package/dist/swarm/swarm-service.js +0 -207
  87. package/dist/swarm/swarm-service.js.map +0 -1
  88. package/dist/swarm/types.d.ts +0 -109
  89. package/dist/swarm/types.js +0 -15
  90. package/dist/swarm/types.js.map +0 -1
  91. /package/extensions/memory/{gemini-extension.json → tars-extension.json} +0 -0
  92. /package/extensions/tasks/{gemini-extension.json → tars-extension.json} +0 -0
@@ -0,0 +1,698 @@
1
+ import { Agent } from '@earendil-works/pi-agent-core';
2
+ import { getModel } from '@earendil-works/pi-ai';
3
+ import { createCodingTools } from '@earendil-works/pi-coding-agent';
4
+ import { EventEmitter } from 'events';
5
+ import logger from '../utils/logger.js';
6
+ import { v4 as uuidv4 } from 'uuid';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { SendNotificationTool } from '../tools/send-notification.js';
10
+ import { GetQuotaTool } from '../tools/get-quota.js';
11
+ import { LocalRateLimiter } from './rate-limiter.js';
12
+ import { McpBridge } from './mcp-bridge.js';
13
+ /**
14
+ * TarsEngine - Wraps the Pi Agent SDK as a drop-in replacement.
15
+ *
16
+ * Interacts with configured providers (Google, OpenAI, Anthropic, or Custom).
17
+ * Operates within the ~/.tars isolated environment.
18
+ */
19
+ export class TarsEngine extends EventEmitter {
20
+ tarsConfig;
21
+ initialized = false;
22
+ currentSessionId = null;
23
+ channelManager;
24
+ sessionManager;
25
+ rateLimiter;
26
+ mcpBridge;
27
+ allTools = [];
28
+ activeTools = [];
29
+ constructor(tarsConfig) {
30
+ super();
31
+ this.tarsConfig = tarsConfig;
32
+ this.rateLimiter = new LocalRateLimiter(tarsConfig.maxRPM || 14, tarsConfig.maxTPM || 900000);
33
+ }
34
+ /**
35
+ * Provide the ChannelManager instance to the engine so it can build proactive notification tools
36
+ */
37
+ setChannelManager(channelManager) {
38
+ this.channelManager = channelManager;
39
+ }
40
+ /**
41
+ * Provide the SessionManager instance to the engine for session-aware tools
42
+ */
43
+ setSessionManager(sessionManager) {
44
+ this.sessionManager = sessionManager;
45
+ }
46
+ /**
47
+ * Initializes the Tars Engine and discovers MCP extensions.
48
+ */
49
+ async initialize(initialSessionId) {
50
+ if (this.initialized)
51
+ return;
52
+ logger.info('🚀 Initializing Tars Engine...');
53
+ if (!fs.existsSync(this.tarsConfig.homeDir)) {
54
+ fs.mkdirSync(this.tarsConfig.homeDir, { recursive: true });
55
+ }
56
+ // Initialize MCP bridge
57
+ this.mcpBridge = new McpBridge(this.tarsConfig.homeDir);
58
+ let mcpTools = [];
59
+ try {
60
+ mcpTools = await this.mcpBridge.initialize();
61
+ logger.info(`🔌 Loaded ${mcpTools.length} MCP tools.`);
62
+ }
63
+ catch (err) {
64
+ logger.error(`⚠️ Failed to initialize MCP bridge: ${err.message}`);
65
+ }
66
+ // Gather native tools
67
+ const nativeTools = [];
68
+ if (this.channelManager) {
69
+ nativeTools.push(new SendNotificationTool(this.channelManager));
70
+ logger.info('🔌 Registered native tool: send_notification');
71
+ }
72
+ nativeTools.push(new GetQuotaTool(this.sessionManager, {
73
+ piProvider: this.tarsConfig.piProvider,
74
+ contextWindowTokens: this.tarsConfig.contextWindowTokens,
75
+ piModel: this.tarsConfig.piModel,
76
+ piBaseUrl: this.tarsConfig.piBaseUrl
77
+ }));
78
+ logger.info('🔌 Registered native tool: get_model_quota');
79
+ // Gather coding tools
80
+ let codingTools = [];
81
+ try {
82
+ codingTools = createCodingTools(this.tarsConfig.homeDir);
83
+ logger.info(`🔌 Loaded ${codingTools.length} standard coding tools.`);
84
+ }
85
+ catch (err) {
86
+ logger.error(`⚠️ Failed to initialize coding tools: ${err.message}`);
87
+ }
88
+ this.allTools = [...mcpTools, ...nativeTools, ...codingTools];
89
+ this.initialized = true;
90
+ this.currentSessionId = initialSessionId || uuidv4();
91
+ logger.info('✨ Tars Engine initialized successfully.');
92
+ }
93
+ /**
94
+ * Returns the API key mapped to the provider name from process.env.
95
+ */
96
+ getApiKeyForProvider(providerName) {
97
+ if (providerName === 'google')
98
+ return process.env.TARS_API_KEY || process.env.GEMINI_API_KEY;
99
+ if (providerName === 'openai')
100
+ return process.env.OPENAI_API_KEY;
101
+ if (providerName === 'anthropic')
102
+ return process.env.ANTHROPIC_API_KEY;
103
+ if (providerName === 'local-stark')
104
+ return process.env.STARK_API_KEY;
105
+ if (providerName === 'custom')
106
+ return process.env.CUSTOM_API_KEY;
107
+ if (providerName === this.tarsConfig.piProvider) {
108
+ if (this.tarsConfig.piProvider === 'google')
109
+ return process.env.TARS_API_KEY || process.env.GEMINI_API_KEY;
110
+ if (this.tarsConfig.piProvider === 'openai')
111
+ return process.env.OPENAI_API_KEY;
112
+ if (this.tarsConfig.piProvider === 'anthropic')
113
+ return process.env.ANTHROPIC_API_KEY;
114
+ }
115
+ return undefined;
116
+ }
117
+ /**
118
+ * Executes the conversational agent loop using the Pi Agent SDK.
119
+ */
120
+ async run(prompt, onEvent, sessionId, attachments, onStatus) {
121
+ if (!this.initialized) {
122
+ await this.initialize(sessionId);
123
+ }
124
+ const sid = sessionId || this.currentSessionId || uuidv4();
125
+ this.currentSessionId = sid;
126
+ // Load history messages
127
+ const history = await this.loadHistory(sid);
128
+ // Get system prompt
129
+ const systemPromptPath = this.tarsConfig.systemPromptPath;
130
+ let systemPrompt = '';
131
+ if (fs.existsSync(systemPromptPath)) {
132
+ systemPrompt = fs.readFileSync(systemPromptPath, 'utf-8');
133
+ }
134
+ // Construct model config
135
+ let model;
136
+ const isBuiltIn = ['google', 'openai', 'anthropic'].includes(this.tarsConfig.piProvider);
137
+ if (isBuiltIn && !this.tarsConfig.piBaseUrl) {
138
+ model = getModel(this.tarsConfig.piProvider, this.tarsConfig.piModel);
139
+ }
140
+ else {
141
+ model = {
142
+ id: this.tarsConfig.piModel,
143
+ name: this.tarsConfig.piModel,
144
+ api: this.tarsConfig.piProvider === 'google'
145
+ ? 'google-generative-ai'
146
+ : 'openai-completions',
147
+ provider: this.tarsConfig.piProvider || 'custom',
148
+ baseUrl: this.tarsConfig.piBaseUrl ||
149
+ (this.tarsConfig.piProvider === 'google'
150
+ ? 'https://generativelanguage.googleapis.com'
151
+ : 'https://api.openai.com/v1'),
152
+ reasoning: false,
153
+ input: ['text'],
154
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
155
+ contextWindow: this.tarsConfig.contextWindowTokens || 128000,
156
+ maxTokens: 32000
157
+ };
158
+ }
159
+ // Build target Agent
160
+ const agent = new Agent({
161
+ initialState: {
162
+ systemPrompt,
163
+ model,
164
+ tools: this.allTools,
165
+ messages: history
166
+ },
167
+ getApiKey: (providerName) => this.getApiKeyForProvider(providerName)
168
+ });
169
+ // Track tool executions for status reporting
170
+ this.activeTools = [];
171
+ let turnCount = 0;
172
+ // Subscribe to agent event stream
173
+ agent.subscribe((event) => {
174
+ try {
175
+ if (event.type === 'message_update') {
176
+ const ame = event.assistantMessageEvent;
177
+ if (ame.type === 'text_delta') {
178
+ onEvent({
179
+ type: 'text',
180
+ role: 'assistant',
181
+ content: ame.delta,
182
+ sessionId: sid
183
+ });
184
+ }
185
+ else if (ame.type === 'thinking_delta') {
186
+ onEvent({
187
+ type: 'thought',
188
+ content: ame.delta,
189
+ sessionId: sid
190
+ });
191
+ }
192
+ }
193
+ else if (event.type === 'tool_execution_start') {
194
+ onEvent({
195
+ type: 'tool_call',
196
+ toolName: event.toolName,
197
+ toolArgs: event.args,
198
+ callId: event.toolCallId,
199
+ sessionId: sid
200
+ });
201
+ this.activeTools.push({
202
+ id: event.toolCallId,
203
+ name: event.toolName,
204
+ status: 'running'
205
+ });
206
+ if (onStatus) {
207
+ onStatus(turnCount, this.activeTools.slice(-10), turnCount % 20 === 0);
208
+ }
209
+ }
210
+ else if (event.type === 'tool_execution_end') {
211
+ const responseStr = typeof event.result === 'string'
212
+ ? event.result
213
+ : typeof event.result === 'object'
214
+ ? JSON.stringify(event.result)
215
+ : String(event.result || '');
216
+ onEvent({
217
+ type: 'tool_response',
218
+ toolName: event.toolCallId,
219
+ content: responseStr,
220
+ sessionId: sid
221
+ });
222
+ let cleanPreview = responseStr;
223
+ try {
224
+ const parsed = JSON.parse(responseStr);
225
+ if (parsed && Array.isArray(parsed.content)) {
226
+ cleanPreview = parsed.content
227
+ .filter((c) => c.type === 'text' && typeof c.text === 'string')
228
+ .map((c) => c.text)
229
+ .join(' ');
230
+ }
231
+ else if (parsed && typeof parsed.text === 'string') {
232
+ cleanPreview = parsed.text;
233
+ }
234
+ }
235
+ catch (e) { }
236
+ const runningTool = this.activeTools.find((t) => t.id === event.toolCallId);
237
+ if (runningTool) {
238
+ runningTool.status = 'completed';
239
+ runningTool.responsePreview = cleanPreview.substring(0, 500);
240
+ runningTool.responseSize = responseStr.length;
241
+ }
242
+ else {
243
+ this.activeTools.push({
244
+ id: event.toolCallId,
245
+ name: event.toolName,
246
+ status: 'completed',
247
+ responsePreview: cleanPreview.substring(0, 500),
248
+ responseSize: responseStr.length
249
+ });
250
+ }
251
+ turnCount++;
252
+ if (onStatus) {
253
+ onStatus(turnCount, this.activeTools.slice(-10), turnCount % 20 === 0);
254
+ }
255
+ }
256
+ }
257
+ catch (err) {
258
+ logger.error(`Error in event stream mapping: ${err.message}`);
259
+ }
260
+ });
261
+ // Prepare prompt with attachments if any
262
+ let promptContent = prompt;
263
+ if (attachments && attachments.length > 0) {
264
+ const parts = [{ type: 'text', text: prompt }];
265
+ for (const attachment of attachments) {
266
+ try {
267
+ const data = fs.readFileSync(attachment.path).toString('base64');
268
+ parts.push({
269
+ type: 'image',
270
+ data,
271
+ mimeType: attachment.mimeType
272
+ });
273
+ logger.debug(`📎 Attached image to prompt: ${attachment.path}`);
274
+ }
275
+ catch (err) {
276
+ logger.error(`Failed to read attachment ${attachment.path}: ${err.message}`);
277
+ }
278
+ }
279
+ promptContent = parts;
280
+ }
281
+ try {
282
+ if (typeof promptContent === 'string') {
283
+ await agent.prompt(promptContent);
284
+ }
285
+ else {
286
+ await agent.prompt(promptContent);
287
+ }
288
+ // Save history
289
+ await this.saveHistory(sid, agent.state.messages);
290
+ // Report done with usage stats
291
+ const finalMessage = agent.state.messages[agent.state.messages.length - 1];
292
+ let usageStats = undefined;
293
+ if (finalMessage && finalMessage.role === 'assistant' && finalMessage.usage) {
294
+ const u = finalMessage.usage;
295
+ usageStats = {
296
+ inputTokens: u.input || 0,
297
+ outputTokens: u.output || 0,
298
+ cachedTokens: u.cacheRead || 0
299
+ };
300
+ }
301
+ onEvent({
302
+ type: 'done',
303
+ usageStats,
304
+ sessionId: sid
305
+ });
306
+ }
307
+ catch (err) {
308
+ logger.error(`❌ Pi Agent execution error: ${err.message}`);
309
+ onEvent({
310
+ type: 'error',
311
+ error: err.message,
312
+ sessionId: sid
313
+ });
314
+ throw err;
315
+ }
316
+ }
317
+ /**
318
+ * Executes a prompt synchronously and returns the model response.
319
+ */
320
+ async runSync(prompt, sessionId) {
321
+ let fullContent = '';
322
+ await this.run(prompt, (event) => {
323
+ if (event.content && event.role === 'assistant') {
324
+ fullContent += event.content;
325
+ }
326
+ }, sessionId);
327
+ return fullContent;
328
+ }
329
+ /**
330
+ * Proactively compress the session history to reclaim context window space.
331
+ * Summarizes older messages into a <state_snapshot> block.
332
+ */
333
+ async compressSession(force = false) {
334
+ const sid = this.currentSessionId || 'unknown';
335
+ logger.info(`🗜️ Triggering session compression (force=${force})...`);
336
+ try {
337
+ const history = await this.loadHistory(sid);
338
+ if (history && history.length > 20) {
339
+ const keepCount = Math.ceil(history.length * 0.6);
340
+ let cutIndex = history.length - keepCount;
341
+ // Walk forward to find a 'user' role entry for clean boundary
342
+ while (cutIndex < history.length && history[cutIndex]?.role !== 'user') {
343
+ cutIndex++;
344
+ }
345
+ if (cutIndex < history.length) {
346
+ const historyToCompress = history.slice(0, cutIndex);
347
+ const tail = history.slice(cutIndex);
348
+ logger.info(`🗜️ Summarizing oldest ${historyToCompress.length} turns...`);
349
+ const hasPreviousSnapshot = historyToCompress.some((c) => {
350
+ if (typeof c.content === 'string') {
351
+ return c.content.includes('<state_snapshot>');
352
+ }
353
+ else if (Array.isArray(c.content)) {
354
+ return c.content.some((part) => part.text?.includes('<state_snapshot>'));
355
+ }
356
+ return false;
357
+ });
358
+ const anchorInstruction = hasPreviousSnapshot
359
+ ? '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.'
360
+ : 'Generate a new <state_snapshot> based on the provided history.';
361
+ const summaryPrompt = `${anchorInstruction}\nExtract all important constraints, configs, details and tool results from this chunk of history. Format your response cleanly.`;
362
+ // Construct model config
363
+ let model;
364
+ const isBuiltIn = ['google', 'openai', 'anthropic'].includes(this.tarsConfig.piProvider);
365
+ if (isBuiltIn && !this.tarsConfig.piBaseUrl) {
366
+ model = getModel(this.tarsConfig.piProvider, this.tarsConfig.piModel);
367
+ }
368
+ else {
369
+ model = {
370
+ id: this.tarsConfig.piModel,
371
+ name: this.tarsConfig.piModel,
372
+ api: this.tarsConfig.piProvider === 'google'
373
+ ? 'google-generative-ai'
374
+ : 'openai-completions',
375
+ provider: this.tarsConfig.piProvider || 'custom',
376
+ baseUrl: this.tarsConfig.piBaseUrl ||
377
+ (this.tarsConfig.piProvider === 'google'
378
+ ? 'https://generativelanguage.googleapis.com'
379
+ : 'https://api.openai.com/v1'),
380
+ reasoning: false,
381
+ input: ['text'],
382
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
383
+ contextWindow: this.tarsConfig.contextWindowTokens || 128000,
384
+ maxTokens: 32000
385
+ };
386
+ }
387
+ // Convert historyToCompress to Message[] for streamSimple
388
+ const llmMessages = historyToCompress.filter((m) => ['user', 'assistant', 'toolResult'].includes(m.role));
389
+ llmMessages.push({
390
+ role: 'user',
391
+ content: summaryPrompt,
392
+ timestamp: Date.now()
393
+ });
394
+ const { streamSimple } = await import('@earendil-works/pi-ai/base');
395
+ const apiKey = this.getApiKeyForProvider(model.provider);
396
+ const stream = streamSimple(model, { messages: llmMessages }, { apiKey });
397
+ let summaryContent = '';
398
+ for await (const event of stream) {
399
+ if (event.type === 'text_delta') {
400
+ summaryContent += event.delta;
401
+ }
402
+ }
403
+ if (!summaryContent) {
404
+ summaryContent =
405
+ '*(Summary generation failed, falling back to raw truncation)*';
406
+ }
407
+ const newHistory = [
408
+ {
409
+ role: 'user',
410
+ content: `<state_snapshot>\n${summaryContent.trim()}\n</state_snapshot>`,
411
+ timestamp: Date.now()
412
+ },
413
+ {
414
+ role: 'assistant',
415
+ content: [
416
+ {
417
+ type: 'text',
418
+ text: 'Got it. I will keep this historical context in mind.'
419
+ }
420
+ ],
421
+ api: model.api,
422
+ provider: model.provider,
423
+ model: model.id,
424
+ usage: {
425
+ input: 0,
426
+ output: 0,
427
+ cacheRead: 0,
428
+ cacheWrite: 0,
429
+ totalTokens: 0,
430
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
431
+ },
432
+ stopReason: 'stop',
433
+ timestamp: Date.now()
434
+ },
435
+ ...tail
436
+ ];
437
+ await this.saveHistory(sid, newHistory);
438
+ logger.info(`🗜️ Context compacted: retained tail of ${tail.length} turns + snapshot.`);
439
+ return true;
440
+ }
441
+ }
442
+ return false;
443
+ }
444
+ catch (e) {
445
+ logger.warn(`⚠️ Compression failed: ${e.message}`);
446
+ return false;
447
+ }
448
+ }
449
+ /**
450
+ * Refreshes the system instruction in-place.
451
+ */
452
+ refreshSystemInstruction() {
453
+ logger.debug('🔄 System instruction refreshed in-place (Pi SDK will load fresh content on next run)');
454
+ }
455
+ /**
456
+ * Attempts to find and load session history from either the Pi chats directory
457
+ * or the legacy Core history directory.
458
+ */
459
+ async loadHistory(sessionId) {
460
+ const chatsDir = path.join(this.tarsConfig.homeDir, 'chats');
461
+ const newChatPath = path.join(chatsDir, `${sessionId}.json`);
462
+ if (fs.existsSync(newChatPath)) {
463
+ try {
464
+ logger.info(`📂 Loading session history from Pi format: ${newChatPath}`);
465
+ const data = await fs.promises.readFile(newChatPath, 'utf-8');
466
+ return JSON.parse(data);
467
+ }
468
+ catch (err) {
469
+ logger.error(`Failed to load Pi session chat: ${err}`);
470
+ }
471
+ }
472
+ // Fallback: load and migrate from legacy format
473
+ const resumedData = await this.loadResumedSessionData(sessionId);
474
+ if (resumedData && resumedData.conversation) {
475
+ logger.info(`📂 Migrating legacy session to Pi format...`);
476
+ const migrated = this.migrateLegacyConversation(resumedData.conversation);
477
+ await this.saveHistory(sessionId, migrated);
478
+ return migrated;
479
+ }
480
+ return [];
481
+ }
482
+ /**
483
+ * Saves session history to the Pi chats directory.
484
+ */
485
+ async saveHistory(sessionId, messages) {
486
+ const chatsDir = path.join(this.tarsConfig.homeDir, 'chats');
487
+ if (!fs.existsSync(chatsDir)) {
488
+ await fs.promises.mkdir(chatsDir, { recursive: true });
489
+ }
490
+ const filePath = path.join(chatsDir, `${sessionId}.json`);
491
+ try {
492
+ await fs.promises.writeFile(filePath, JSON.stringify(messages, null, 2), 'utf-8');
493
+ }
494
+ catch (err) {
495
+ logger.error(`Failed to save session history: ${err}`);
496
+ }
497
+ }
498
+ /**
499
+ * Helper to load legacy conversation records.
500
+ */
501
+ async loadConversationRecord(filePath) {
502
+ const content = await fs.promises.readFile(filePath, 'utf-8');
503
+ try {
504
+ return JSON.parse(content);
505
+ }
506
+ catch {
507
+ const lines = content.split('\n').filter(Boolean);
508
+ const messages = lines.map((line) => JSON.parse(line));
509
+ return { messages };
510
+ }
511
+ }
512
+ /**
513
+ * Converts a legacy ConversationRecord to Pi SDK AgentMessage[] format.
514
+ */
515
+ migrateLegacyConversation(conversation) {
516
+ const messages = [];
517
+ if (!conversation || !conversation.messages)
518
+ return messages;
519
+ for (const msg of conversation.messages) {
520
+ if (msg.type === 'user') {
521
+ let content = '';
522
+ if (typeof msg.content === 'string') {
523
+ content = msg.content;
524
+ }
525
+ else if (Array.isArray(msg.content)) {
526
+ content = msg.content.map((p) => ({
527
+ type: 'text',
528
+ text: p.text || ''
529
+ }));
530
+ }
531
+ messages.push({
532
+ role: 'user',
533
+ content,
534
+ timestamp: msg.timestamp || Date.now()
535
+ });
536
+ }
537
+ else if (msg.type === 'gemini') {
538
+ const contentParts = [];
539
+ const toolResultMessages = [];
540
+ if (typeof msg.content === 'string' && msg.content !== '') {
541
+ contentParts.push({ type: 'text', text: msg.content });
542
+ }
543
+ else if (Array.isArray(msg.content)) {
544
+ for (const p of msg.content) {
545
+ if (p.text) {
546
+ contentParts.push({ type: 'text', text: p.text });
547
+ }
548
+ }
549
+ }
550
+ if (msg.toolCalls) {
551
+ for (const tc of msg.toolCalls) {
552
+ const callId = tc.id ||
553
+ tc.callId ||
554
+ `call-${Math.random().toString(36).substring(2, 9)}`;
555
+ contentParts.push({
556
+ type: 'toolCall',
557
+ id: callId,
558
+ name: tc.name,
559
+ arguments: tc.args || {}
560
+ });
561
+ if (tc.status === 'done' || tc.result) {
562
+ let responseObj = tc.result;
563
+ if (typeof responseObj === 'string') {
564
+ try {
565
+ responseObj = JSON.parse(responseObj);
566
+ }
567
+ catch {
568
+ responseObj = { result: responseObj };
569
+ }
570
+ }
571
+ const responseContent = typeof responseObj === 'object'
572
+ ? JSON.stringify(responseObj)
573
+ : String(responseObj);
574
+ toolResultMessages.push({
575
+ role: 'toolResult',
576
+ toolCallId: callId,
577
+ toolName: tc.name,
578
+ content: [{ type: 'text', text: responseContent }],
579
+ details: responseObj,
580
+ isError: tc.isError || false,
581
+ timestamp: msg.timestamp || Date.now()
582
+ });
583
+ }
584
+ }
585
+ }
586
+ messages.push({
587
+ role: 'assistant',
588
+ content: contentParts,
589
+ api: 'openai-completions',
590
+ provider: this.tarsConfig.piProvider || 'custom',
591
+ model: this.tarsConfig.piModel,
592
+ usage: {
593
+ input: 0,
594
+ output: 0,
595
+ cacheRead: 0,
596
+ cacheWrite: 0,
597
+ totalTokens: 0,
598
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
599
+ },
600
+ stopReason: 'stop',
601
+ timestamp: msg.timestamp || Date.now()
602
+ });
603
+ if (toolResultMessages.length > 0) {
604
+ messages.push(...toolResultMessages);
605
+ }
606
+ }
607
+ }
608
+ return messages;
609
+ }
610
+ /**
611
+ * Attempts to find and load legacy session history.
612
+ */
613
+ async loadResumedSessionData(sessionId) {
614
+ try {
615
+ const geminiDir = path.join(this.tarsConfig.homeDir, '.gemini');
616
+ const tmpDir = path.join(geminiDir, 'tmp');
617
+ if (!fs.existsSync(tmpDir))
618
+ return null;
619
+ let projectIdentifier = null;
620
+ const registryPath = path.join(geminiDir, 'projects.json');
621
+ if (fs.existsSync(registryPath)) {
622
+ try {
623
+ const registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8'));
624
+ projectIdentifier = registry.projects[this.tarsConfig.homeDir] || null;
625
+ }
626
+ catch (e) {
627
+ logger.warn(`⚠️ Failed to read projects.json: ${e}`);
628
+ }
629
+ }
630
+ if (!projectIdentifier) {
631
+ const crypto = await import('node:crypto');
632
+ projectIdentifier = crypto
633
+ .createHash('md5')
634
+ .update(this.tarsConfig.homeDir)
635
+ .digest('hex');
636
+ }
637
+ const searchDirs = [projectIdentifier];
638
+ try {
639
+ const allDirs = fs.readdirSync(tmpDir);
640
+ for (const d of allDirs) {
641
+ if (d !== projectIdentifier)
642
+ searchDirs.push(d);
643
+ }
644
+ }
645
+ catch (e) { }
646
+ const shortId = sessionId.slice(0, 8);
647
+ for (const dir of searchDirs) {
648
+ if (!dir)
649
+ continue;
650
+ const chatsDir = path.join(tmpDir, dir, 'chats');
651
+ if (!fs.existsSync(chatsDir))
652
+ continue;
653
+ const files = fs.readdirSync(chatsDir);
654
+ const sessionFile = files.find((f) => f.includes(`-${shortId}.json`));
655
+ if (sessionFile) {
656
+ const filePath = path.join(chatsDir, sessionFile);
657
+ const content = await this.loadConversationRecord(filePath);
658
+ logger.info(`📂 Resumed session from exact match: ${sessionFile}`);
659
+ return {
660
+ conversation: content,
661
+ filePath
662
+ };
663
+ }
664
+ const jsonFiles = files.filter((f) => f.endsWith('.json') || f.endsWith('.jsonl'));
665
+ if (jsonFiles.length > 0) {
666
+ const sorted = jsonFiles
667
+ .map((f) => ({
668
+ name: f,
669
+ mtime: fs.statSync(path.join(chatsDir, f)).mtimeMs
670
+ }))
671
+ .sort((a, b) => b.mtime - a.mtime);
672
+ const latestFile = sorted[0].name;
673
+ logger.warn(`⚠️ No exact session match for ${shortId}. Falling back to latest: ${latestFile}`);
674
+ const filePath = path.join(chatsDir, latestFile);
675
+ const content = await this.loadConversationRecord(filePath);
676
+ return {
677
+ conversation: content,
678
+ filePath
679
+ };
680
+ }
681
+ }
682
+ return null;
683
+ }
684
+ catch (e) {
685
+ logger.warn(`⚠️ Failed to load resumed session data: ${e}`);
686
+ return null;
687
+ }
688
+ }
689
+ /**
690
+ * Closes the MCP client bridge connections.
691
+ */
692
+ async shutdown() {
693
+ if (this.mcpBridge) {
694
+ await this.mcpBridge.shutdown();
695
+ }
696
+ }
697
+ }
698
+ //# sourceMappingURL=tars-engine.js.map