@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.
- package/README.md +1 -1
- package/context/skills/create-extension/SKILL.md +2 -2
- package/context/skills/manage-extensions/SKILL.md +3 -3
- package/dist/channels/discord/discord-channel.js +2 -3
- package/dist/channels/discord/discord-channel.js.map +1 -1
- package/dist/channels/discord/message-formatter.d.ts +1 -1
- package/dist/channels/discord/message-formatter.js +1 -1
- package/dist/cli/commands/quota.js +13 -48
- package/dist/cli/commands/quota.js.map +1 -1
- package/dist/cli/commands/refresh.js +61 -9
- package/dist/cli/commands/refresh.js.map +1 -1
- package/dist/cli/commands/setup.js +192 -467
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/index.js +1 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/config/config.d.ts +5 -10
- package/dist/config/config.js +21 -27
- package/dist/config/config.js.map +1 -1
- package/dist/memory/knowledge-store.js +10 -1
- package/dist/memory/knowledge-store.js.map +1 -1
- package/dist/memory/memory-manager.d.ts +0 -3
- package/dist/memory/memory-manager.js +26 -34
- package/dist/memory/memory-manager.js.map +1 -1
- package/dist/scripts/debug-cli.js +2 -2
- package/dist/scripts/debug-cli.js.map +1 -1
- package/dist/supervisor/heartbeat-service.js +1 -1
- package/dist/supervisor/heartbeat-service.js.map +1 -1
- package/dist/supervisor/main.js +37 -79
- package/dist/supervisor/main.js.map +1 -1
- package/dist/supervisor/mcp-bridge.d.ts +25 -0
- package/dist/supervisor/mcp-bridge.js +157 -0
- package/dist/supervisor/mcp-bridge.js.map +1 -0
- package/dist/supervisor/session-manager.d.ts +1 -1
- package/dist/supervisor/session-manager.js +1 -1
- package/dist/supervisor/supervisor.d.ts +14 -7
- package/dist/supervisor/supervisor.js +87 -29
- package/dist/supervisor/supervisor.js.map +1 -1
- package/dist/supervisor/{gemini-engine.d.ts → tars-engine.d.ts} +39 -30
- package/dist/supervisor/tars-engine.js +698 -0
- package/dist/supervisor/tars-engine.js.map +1 -0
- package/dist/tools/get-quota.d.ts +38 -12
- package/dist/tools/get-quota.js +37 -94
- package/dist/tools/get-quota.js.map +1 -1
- package/dist/tools/send-notification.d.ts +32 -7
- package/dist/tools/send-notification.js +31 -37
- package/dist/tools/send-notification.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/utils/brain-audit.js +4 -4
- package/dist/utils/brain-audit.js.map +1 -1
- package/dist/utils/migration-manager.d.ts +4 -0
- package/dist/utils/migration-manager.js +205 -0
- package/dist/utils/migration-manager.js.map +1 -0
- package/extensions/memory/dist/store.js +29 -20
- package/extensions/memory/dist/store.js.map +1 -1
- package/extensions/memory/src/store.ts +33 -23
- package/package.json +4 -3
- package/src/prompts/system.md +3 -14
- package/context/agents/scaffolder.md +0 -22
- package/dist/auth/credential-manager.d.ts +0 -14
- package/dist/auth/credential-manager.js +0 -60
- package/dist/auth/credential-manager.js.map +0 -1
- package/dist/auth/oauth-service.d.ts +0 -24
- package/dist/auth/oauth-service.js +0 -89
- package/dist/auth/oauth-service.js.map +0 -1
- package/dist/auth/workspace-auth-service.d.ts +0 -10
- package/dist/auth/workspace-auth-service.js +0 -78
- package/dist/auth/workspace-auth-service.js.map +0 -1
- package/dist/cli/commands/swarm.d.ts +0 -13
- package/dist/cli/commands/swarm.js +0 -250
- package/dist/cli/commands/swarm.js.map +0 -1
- package/dist/inference/LlamaCppGenerator.d.ts +0 -25
- package/dist/inference/LlamaCppGenerator.js +0 -461
- package/dist/inference/LlamaCppGenerator.js.map +0 -1
- package/dist/scripts/test-local-llamacpp.d.ts +0 -1
- package/dist/scripts/test-local-llamacpp.js +0 -77
- package/dist/scripts/test-local-llamacpp.js.map +0 -1
- package/dist/supervisor/gemini-engine.js +0 -983
- package/dist/supervisor/gemini-engine.js.map +0 -1
- package/dist/swarm/agent-card.d.ts +0 -14
- package/dist/swarm/agent-card.js +0 -93
- package/dist/swarm/agent-card.js.map +0 -1
- package/dist/swarm/rpc-handler.d.ts +0 -27
- package/dist/swarm/rpc-handler.js +0 -235
- package/dist/swarm/rpc-handler.js.map +0 -1
- package/dist/swarm/swarm-service.d.ts +0 -47
- package/dist/swarm/swarm-service.js +0 -207
- package/dist/swarm/swarm-service.js.map +0 -1
- package/dist/swarm/types.d.ts +0 -109
- package/dist/swarm/types.js +0 -15
- package/dist/swarm/types.js.map +0 -1
- /package/extensions/memory/{gemini-extension.json → tars-extension.json} +0 -0
- /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
|