evolclaw 2.2.0 → 2.3.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 +49 -27
- package/data/evolclaw.sample.json +6 -3
- package/dist/agents/claude-runner.js +125 -52
- package/dist/agents/codex-runner.js +10 -5
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +247 -84
- package/dist/channels/feishu.js +556 -96
- package/dist/channels/wechat.js +98 -74
- package/dist/cli.js +132 -50
- package/dist/config.js +185 -31
- package/dist/core/channel-loader.js +11 -4
- package/dist/core/command-handler.js +750 -209
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/{message-processor.js → message/message-processor.js} +386 -105
- package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
- package/dist/{utils → core/message}/stream-debouncer.js +1 -1
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/core/permission.js +212 -11
- package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
- package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/{utils → core/session}/session-file-health.js +1 -1
- package/dist/core/{session-manager.js → session/session-manager.js} +57 -11
- package/dist/index.js +138 -54
- package/dist/{core/ipc-server.js → ipc.js} +36 -1
- package/dist/types.js +3 -0
- package/dist/utils/cross-platform.js +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +55 -150
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/{core → utils}/stats-collector.js +16 -0
- package/package.json +3 -3
- package/dist/core/message-bridge.js +0 -187
- package/dist/utils/init-feishu.js +0 -263
- package/dist/utils/init-wechat.js +0 -172
- package/dist/utils/ipc-client.js +0 -36
- package/dist/utils/permission-utils.js +0 -71
- /package/dist/{utils → core/message}/message-cache.js +0 -0
- /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
- /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini Agent Runner
|
|
3
|
+
*
|
|
4
|
+
* Integrates Google Gemini CLI as a backend via subprocess.
|
|
5
|
+
* Each runQuery spawns `gemini -p` with --output-format stream-json,
|
|
6
|
+
* parsing the JSONL event stream into EvolClaw AgentEvent.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* GeminiRunner → spawn `gemini -p ...` → stdout JSONL stream
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { createInterface } from 'readline';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
import { resolveGoogleConfig } from '../config.js';
|
|
17
|
+
import { GeminiSessionFileAdapter } from '../core/session/adapters/gemini-session-file-adapter.js';
|
|
18
|
+
import { logger } from '../utils/logger.js';
|
|
19
|
+
// Strip ANSI escape codes from Gemini CLI text output.
|
|
20
|
+
// Gemini embeds raw terminal colors from tool stdout (e.g. vitest, npm)
|
|
21
|
+
// into its assistant text, unlike Claude SDK which strips them internally.
|
|
22
|
+
// eslint-disable-next-line no-control-regex
|
|
23
|
+
const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?(?:\x1b\\|\x07)/g;
|
|
24
|
+
function stripAnsi(text) {
|
|
25
|
+
return text.replace(ANSI_RE, '');
|
|
26
|
+
}
|
|
27
|
+
// ── MIME → 扩展名映射 ──
|
|
28
|
+
const MIME_EXT = {
|
|
29
|
+
'image/png': '.png',
|
|
30
|
+
'image/jpeg': '.jpg',
|
|
31
|
+
'image/gif': '.gif',
|
|
32
|
+
'image/webp': '.webp',
|
|
33
|
+
};
|
|
34
|
+
// ── Gemini 模型列表 ──
|
|
35
|
+
const GEMINI_MODELS = [
|
|
36
|
+
'gemini-2.5-pro',
|
|
37
|
+
'gemini-2.5-flash',
|
|
38
|
+
'gemini-2.5-flash-lite',
|
|
39
|
+
];
|
|
40
|
+
// ── Gemini Runner ──
|
|
41
|
+
export class GeminiRunner {
|
|
42
|
+
name = 'gemini';
|
|
43
|
+
capabilities = { clear: true, compact: false, fork: false };
|
|
44
|
+
resolved;
|
|
45
|
+
model;
|
|
46
|
+
activeProcesses = new Map();
|
|
47
|
+
activeStreams = new Map();
|
|
48
|
+
activeSessions = new Map(); // sessionId → geminiSessionId
|
|
49
|
+
onSessionIdUpdate;
|
|
50
|
+
currentMode = 'auto';
|
|
51
|
+
constructor(config, callbacks) {
|
|
52
|
+
this.resolved = resolveGoogleConfig(config);
|
|
53
|
+
this.model = this.resolved.model;
|
|
54
|
+
this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
|
|
55
|
+
}
|
|
56
|
+
// ── ModelSwitcher ──
|
|
57
|
+
setModel(model) { this.model = model; }
|
|
58
|
+
getModel() { return this.model; }
|
|
59
|
+
listModels() { return GEMINI_MODELS; }
|
|
60
|
+
// ── Effort (not applicable) ──
|
|
61
|
+
setEffort(_effort) { }
|
|
62
|
+
getEffort() { return undefined; }
|
|
63
|
+
// ── Permission ──
|
|
64
|
+
setMode(mode) { this.currentMode = mode; }
|
|
65
|
+
getMode() { return this.currentMode; }
|
|
66
|
+
listModes() {
|
|
67
|
+
return [
|
|
68
|
+
{ key: 'auto', nameZh: '自动', description: '全部自动(--yolo 模式)', available: true },
|
|
69
|
+
{ key: 'bypass', nameZh: '放行', description: '全部自动(--yolo 模式)', available: true },
|
|
70
|
+
{ key: 'edit', nameZh: '编辑', description: '仅 Claude 支持', available: false, unavailableReason: 'Gemini CLI 不支持此模式' },
|
|
71
|
+
{ key: 'plan', nameZh: '规划', description: 'Gemini 规划模式', available: true },
|
|
72
|
+
{ key: 'noask', nameZh: '静默', description: '仅 Claude 支持', available: false, unavailableReason: 'Gemini CLI 不支持此模式' },
|
|
73
|
+
{ key: 'readonly', nameZh: '只读', description: '禁止修改项目文件,可在临时目录生成文件', available: true },
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
setSendPrompt(_fn) { }
|
|
77
|
+
setPermissionGateway(_gw) { }
|
|
78
|
+
// ── Stream management ──
|
|
79
|
+
registerStream(key, stream) {
|
|
80
|
+
this.activeStreams.set(key, stream);
|
|
81
|
+
}
|
|
82
|
+
cleanupStream(key) {
|
|
83
|
+
this.activeStreams.delete(key);
|
|
84
|
+
this.activeProcesses.delete(key);
|
|
85
|
+
}
|
|
86
|
+
hasActiveStream(key) {
|
|
87
|
+
return this.activeStreams.has(key) || this.activeProcesses.has(key);
|
|
88
|
+
}
|
|
89
|
+
// ── Core: runQuery ──
|
|
90
|
+
async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager) {
|
|
91
|
+
let geminiSessionId = initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
92
|
+
// Safe mode: skip resume
|
|
93
|
+
if (geminiSessionId && sessionManager) {
|
|
94
|
+
const health = await sessionManager.getHealthStatus(sessionId);
|
|
95
|
+
if (health.safeMode) {
|
|
96
|
+
geminiSessionId = undefined;
|
|
97
|
+
logger.warn(`[GeminiRunner] Safe mode enabled for ${sessionId}, not resuming session`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Build CLI args
|
|
101
|
+
const args = [];
|
|
102
|
+
// Only inject system context on first turn (no resume).
|
|
103
|
+
// Resumed sessions already have the context from the first turn;
|
|
104
|
+
// repeating it pollutes the conversation history.
|
|
105
|
+
let fullPrompt = prompt;
|
|
106
|
+
if (systemPromptAppend && !geminiSessionId) {
|
|
107
|
+
fullPrompt = prompt + '\n\n--- [SYSTEM_PROMPT_END] ---\n' + systemPromptAppend;
|
|
108
|
+
}
|
|
109
|
+
// Handle images: write to temp files, prepend @file references
|
|
110
|
+
const tempFiles = [];
|
|
111
|
+
if (images?.length) {
|
|
112
|
+
const tmpDir = os.tmpdir();
|
|
113
|
+
const fileParts = [];
|
|
114
|
+
for (let i = 0; i < images.length; i++) {
|
|
115
|
+
const img = images[i];
|
|
116
|
+
const ext = MIME_EXT[img.mimeType || ''] || '.jpg';
|
|
117
|
+
const tmpPath = path.join(tmpDir, `evolclaw-gemini-img-${Date.now()}-${i}${ext}`);
|
|
118
|
+
fs.writeFileSync(tmpPath, Buffer.from(img.data, 'base64'));
|
|
119
|
+
tempFiles.push(tmpPath);
|
|
120
|
+
fileParts.push(`@${tmpPath}`);
|
|
121
|
+
}
|
|
122
|
+
fullPrompt = fileParts.join(' ') + ' ' + fullPrompt;
|
|
123
|
+
logger.info(`[GeminiRunner] Attached ${images.length} image(s) via @file reference`);
|
|
124
|
+
}
|
|
125
|
+
args.push('-p', fullPrompt);
|
|
126
|
+
args.push('--output-format', 'stream-json');
|
|
127
|
+
args.push('-m', this.model);
|
|
128
|
+
// Permission mode
|
|
129
|
+
if (this.currentMode === 'plan') {
|
|
130
|
+
args.push('--approval-mode=plan');
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
args.push('--yolo');
|
|
134
|
+
}
|
|
135
|
+
// Resume session
|
|
136
|
+
if (geminiSessionId) {
|
|
137
|
+
args.push('-r', geminiSessionId);
|
|
138
|
+
}
|
|
139
|
+
// Spawn subprocess
|
|
140
|
+
const env = {
|
|
141
|
+
...process.env,
|
|
142
|
+
};
|
|
143
|
+
if (this.resolved.apiKey) {
|
|
144
|
+
env.GOOGLE_API_KEY = this.resolved.apiKey;
|
|
145
|
+
}
|
|
146
|
+
const child = spawn(this.resolved.cliPath, args, {
|
|
147
|
+
cwd: projectPath,
|
|
148
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
149
|
+
env,
|
|
150
|
+
});
|
|
151
|
+
this.activeProcesses.set(sessionId, child);
|
|
152
|
+
// Log stderr
|
|
153
|
+
child.stderr?.on('data', (data) => {
|
|
154
|
+
const msg = data.toString().trim();
|
|
155
|
+
if (msg)
|
|
156
|
+
logger.debug(`[GeminiRunner:stderr] ${msg}`);
|
|
157
|
+
});
|
|
158
|
+
return this.transformStream(child, sessionId, tempFiles);
|
|
159
|
+
}
|
|
160
|
+
// ── Event stream transformation ──
|
|
161
|
+
async *transformStream(child, sessionId, tempFiles) {
|
|
162
|
+
const pendingToolNames = new Map(); // toolId → toolName
|
|
163
|
+
const startTime = Date.now();
|
|
164
|
+
const rl = createInterface({ input: child.stdout });
|
|
165
|
+
// Build async queue from readline
|
|
166
|
+
const queue = [];
|
|
167
|
+
let resolve = null;
|
|
168
|
+
let rlClosed = false;
|
|
169
|
+
let processExited = false;
|
|
170
|
+
let exitCode = null;
|
|
171
|
+
// We need both rl 'close' AND child 'exit' before considering stream done.
|
|
172
|
+
// rl 'close' guarantees all buffered lines are emitted.
|
|
173
|
+
// child 'exit' gives us the exit code.
|
|
174
|
+
const isStreamDone = () => rlClosed && processExited;
|
|
175
|
+
rl.on('line', (line) => {
|
|
176
|
+
queue.push(line);
|
|
177
|
+
resolve?.();
|
|
178
|
+
});
|
|
179
|
+
rl.on('close', () => {
|
|
180
|
+
rlClosed = true;
|
|
181
|
+
if (isStreamDone())
|
|
182
|
+
resolve?.();
|
|
183
|
+
});
|
|
184
|
+
child.on('exit', (code) => {
|
|
185
|
+
processExited = true;
|
|
186
|
+
exitCode = code;
|
|
187
|
+
if (isStreamDone())
|
|
188
|
+
resolve?.();
|
|
189
|
+
});
|
|
190
|
+
// Handle race: process may have already exited before we registered the listener.
|
|
191
|
+
// Real ChildProcess sets exitCode (number) on exit; null means still running.
|
|
192
|
+
if (!processExited && child.exitCode != null) {
|
|
193
|
+
processExited = true;
|
|
194
|
+
exitCode = child.exitCode;
|
|
195
|
+
}
|
|
196
|
+
child.on('error', (err) => {
|
|
197
|
+
logger.error(`[GeminiRunner] Process error: ${err.message}`);
|
|
198
|
+
rlClosed = true;
|
|
199
|
+
processExited = true;
|
|
200
|
+
resolve?.();
|
|
201
|
+
});
|
|
202
|
+
try {
|
|
203
|
+
let done = false;
|
|
204
|
+
let accumulatedText = '';
|
|
205
|
+
// TextBuffer: accumulate streaming text tokens, flush as a single
|
|
206
|
+
// text event on boundary signals (tool_use / result / error / exit).
|
|
207
|
+
// Prevents StreamFlusher from splitting a single reply into multiple messages.
|
|
208
|
+
let textBuffer = '';
|
|
209
|
+
const flushTextBuffer = function* () {
|
|
210
|
+
if (textBuffer) {
|
|
211
|
+
yield { type: 'text', text: stripAnsi(textBuffer) };
|
|
212
|
+
textBuffer = '';
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
while (!done) {
|
|
216
|
+
// Process queued lines
|
|
217
|
+
while (queue.length > 0) {
|
|
218
|
+
const line = queue.shift();
|
|
219
|
+
if (!line.trim())
|
|
220
|
+
continue;
|
|
221
|
+
let event;
|
|
222
|
+
try {
|
|
223
|
+
event = JSON.parse(line);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
logger.debug(`[GeminiRunner] Non-JSON line: ${line.substring(0, 200)}`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
switch (event.type) {
|
|
230
|
+
case 'init': {
|
|
231
|
+
// Extract session_id from init event
|
|
232
|
+
const geminiId = event.session_id;
|
|
233
|
+
if (geminiId) {
|
|
234
|
+
this.activeSessions.set(sessionId, geminiId);
|
|
235
|
+
this.onSessionIdUpdate?.(sessionId, geminiId);
|
|
236
|
+
yield { type: 'session_id', sessionId: geminiId };
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case 'message': {
|
|
241
|
+
// Skip user message echo
|
|
242
|
+
if (event.role === 'user')
|
|
243
|
+
break;
|
|
244
|
+
// Assistant message (delta=true → streaming)
|
|
245
|
+
// Accumulate into textBuffer; will flush on boundary event
|
|
246
|
+
if (event.role === 'assistant' && event.content) {
|
|
247
|
+
accumulatedText += event.content;
|
|
248
|
+
textBuffer += event.content;
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case 'tool_use': {
|
|
253
|
+
// Boundary: flush accumulated text before tool call
|
|
254
|
+
yield* flushTextBuffer();
|
|
255
|
+
const toolName = event.tool_name || 'unknown';
|
|
256
|
+
if (event.tool_id) {
|
|
257
|
+
pendingToolNames.set(event.tool_id, toolName);
|
|
258
|
+
}
|
|
259
|
+
yield {
|
|
260
|
+
type: 'tool_use',
|
|
261
|
+
name: toolName,
|
|
262
|
+
input: event.parameters || {},
|
|
263
|
+
};
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case 'tool_result': {
|
|
267
|
+
const toolName = (event.tool_id && pendingToolNames.get(event.tool_id)) || 'unknown';
|
|
268
|
+
if (event.tool_id)
|
|
269
|
+
pendingToolNames.delete(event.tool_id);
|
|
270
|
+
yield {
|
|
271
|
+
type: 'tool_result',
|
|
272
|
+
name: toolName,
|
|
273
|
+
result: stripAnsi(event.output || ''),
|
|
274
|
+
isError: event.status !== 'success',
|
|
275
|
+
};
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case 'result': {
|
|
279
|
+
// Boundary: flush accumulated text before complete
|
|
280
|
+
yield* flushTextBuffer();
|
|
281
|
+
const durationMs = event.stats?.duration_ms || (Date.now() - startTime);
|
|
282
|
+
const isError = event.status !== 'success';
|
|
283
|
+
// Extract error message from event.error.message (Gemini CLI structure)
|
|
284
|
+
const errorMessage = event.error?.message || event.message;
|
|
285
|
+
yield {
|
|
286
|
+
type: 'complete',
|
|
287
|
+
result: accumulatedText || undefined,
|
|
288
|
+
isError,
|
|
289
|
+
errors: isError ? [errorMessage || event.status || '任务执行失败'] : undefined,
|
|
290
|
+
durationMs,
|
|
291
|
+
costUsd: undefined,
|
|
292
|
+
};
|
|
293
|
+
done = true;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case 'error': {
|
|
297
|
+
// Boundary: flush accumulated text before error
|
|
298
|
+
yield* flushTextBuffer();
|
|
299
|
+
yield {
|
|
300
|
+
type: 'error',
|
|
301
|
+
error: event.message || 'Unknown Gemini error',
|
|
302
|
+
errorType: 'unknown',
|
|
303
|
+
};
|
|
304
|
+
if (event.fatal) {
|
|
305
|
+
yield { type: 'complete', result: '', isError: true, durationMs: Date.now() - startTime };
|
|
306
|
+
done = true;
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
default:
|
|
311
|
+
logger.debug(`[GeminiRunner] Unknown event type: ${event.type}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (!done) {
|
|
315
|
+
if (isStreamDone()) {
|
|
316
|
+
// Boundary: flush accumulated text before exit
|
|
317
|
+
yield* flushTextBuffer();
|
|
318
|
+
// Process exited without result event
|
|
319
|
+
if (exitCode !== 0) {
|
|
320
|
+
yield { type: 'error', error: `Gemini CLI exited with code ${exitCode}`, errorType: 'unknown' };
|
|
321
|
+
}
|
|
322
|
+
yield { type: 'complete', result: '', isError: exitCode !== 0, durationMs: Date.now() - startTime };
|
|
323
|
+
done = true;
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
// Wait for more data
|
|
327
|
+
await new Promise((r) => { resolve = r; });
|
|
328
|
+
resolve = null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
rl.close();
|
|
335
|
+
this.activeProcesses.delete(sessionId);
|
|
336
|
+
// Kill process if still running
|
|
337
|
+
if (!child.killed && !processExited) {
|
|
338
|
+
child.kill('SIGTERM');
|
|
339
|
+
}
|
|
340
|
+
// Cleanup temp image files
|
|
341
|
+
if (tempFiles?.length) {
|
|
342
|
+
for (const f of tempFiles) {
|
|
343
|
+
try {
|
|
344
|
+
fs.unlinkSync(f);
|
|
345
|
+
}
|
|
346
|
+
catch { /* ignore */ }
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// ── Interrupt ──
|
|
352
|
+
async interrupt(sessionKey) {
|
|
353
|
+
const child = this.activeProcesses.get(sessionKey);
|
|
354
|
+
if (child && !child.killed) {
|
|
355
|
+
child.kill('SIGINT');
|
|
356
|
+
setTimeout(() => {
|
|
357
|
+
if (!child.killed) {
|
|
358
|
+
child.kill('SIGTERM');
|
|
359
|
+
logger.info(`[GeminiRunner] SIGTERM fallback for: ${sessionKey}`);
|
|
360
|
+
}
|
|
361
|
+
}, 3000);
|
|
362
|
+
logger.info(`[GeminiRunner] Interrupted session: ${sessionKey} (SIGINT, SIGTERM fallback in 3s)`);
|
|
363
|
+
}
|
|
364
|
+
this.activeProcesses.delete(sessionKey);
|
|
365
|
+
this.activeStreams.delete(sessionKey);
|
|
366
|
+
}
|
|
367
|
+
// ── Session commands ──
|
|
368
|
+
updateSessionId(sessionId, agentSessionId) {
|
|
369
|
+
if (agentSessionId) {
|
|
370
|
+
this.activeSessions.set(sessionId, agentSessionId);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
this.activeSessions.delete(sessionId);
|
|
374
|
+
}
|
|
375
|
+
this.onSessionIdUpdate?.(sessionId, agentSessionId);
|
|
376
|
+
}
|
|
377
|
+
async closeSession(sessionId) {
|
|
378
|
+
this.activeSessions.delete(sessionId);
|
|
379
|
+
this.activeStreams.delete(sessionId);
|
|
380
|
+
this.activeProcesses.delete(sessionId);
|
|
381
|
+
}
|
|
382
|
+
resolveSessionFile(agentSessionId, projectPath) {
|
|
383
|
+
const adapter = new GeminiSessionFileAdapter();
|
|
384
|
+
return adapter.findSessionFile(projectPath, agentSessionId);
|
|
385
|
+
}
|
|
386
|
+
async clearSession(sessionId, _agentSessionId, _projectPath) {
|
|
387
|
+
// Clear = don't pass -r next time → fresh session
|
|
388
|
+
this.activeSessions.delete(sessionId);
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
async compactSession(_sessionId, _agentSessionId, _projectPath) {
|
|
392
|
+
logger.info('[GeminiRunner] Compact not supported, Gemini CLI handles context internally');
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
async compact(_sessionId, _agentSessionId, _projectPath) {
|
|
396
|
+
return this.compactSession(_sessionId, _agentSessionId, _projectPath);
|
|
397
|
+
}
|
|
398
|
+
setCompactStartCallback(_callback) { }
|
|
399
|
+
// ── Cleanup ──
|
|
400
|
+
async dispose() {
|
|
401
|
+
for (const [, child] of this.activeProcesses) {
|
|
402
|
+
if (!child.killed)
|
|
403
|
+
child.kill('SIGTERM');
|
|
404
|
+
}
|
|
405
|
+
this.activeProcesses.clear();
|
|
406
|
+
this.activeStreams.clear();
|
|
407
|
+
this.activeSessions.clear();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// ── Plugin ──
|
|
411
|
+
export class GeminiAgentPlugin {
|
|
412
|
+
name = 'gemini';
|
|
413
|
+
isEnabled(config) {
|
|
414
|
+
try {
|
|
415
|
+
const resolved = resolveGoogleConfig(config);
|
|
416
|
+
return !!resolved.cliPath;
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
createAgent(config, callbacks) {
|
|
423
|
+
return { agent: new GeminiRunner(config, callbacks) };
|
|
424
|
+
}
|
|
425
|
+
}
|