@openagents-org/agent-launcher 0.2.171 → 0.2.173
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/package.json +2 -2
- package/registry.json +49 -2
- package/src/adapters/antigravity-stream.js +242 -0
- package/src/adapters/antigravity.js +390 -0
- package/src/adapters/base.js +13 -0
- package/src/adapters/claude.js +10 -2
- package/src/adapters/index.js +4 -1
- package/src/cli.js +33 -0
- package/src/daemon.js +116 -1
- package/src/index.js +9 -0
- package/src/probe.js +318 -0
- package/src/registry.js +2 -0
- package/src/workspace-client.js +1 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity CLI (agy) adapter for OpenAgents workspace.
|
|
3
|
+
*
|
|
4
|
+
* Successor to the Gemini CLI adapter: Google transitioned Gemini CLI into
|
|
5
|
+
* Antigravity CLI in May 2026 and cut individual-account access to the old
|
|
6
|
+
* CLI that June. agy is a closed-source Go binary — no npm runtime prefix and
|
|
7
|
+
* no .cmd/.js shim resolution needed — driven headless via
|
|
8
|
+
* `agy -p <prompt> --output-format stream-json`.
|
|
9
|
+
*
|
|
10
|
+
* Stream interpretation, argv building, and failure classification live in
|
|
11
|
+
* ./antigravity-stream.js (pure, unit-tested); this file owns process
|
|
12
|
+
* lifecycle and workspace I/O, structurally mirroring gemini.js.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { execSync, spawn } = require('child_process');
|
|
21
|
+
|
|
22
|
+
const BaseAdapter = require('./base');
|
|
23
|
+
const { whereBinary } = require('../paths');
|
|
24
|
+
const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
|
|
25
|
+
const { buildClaudeSystemPrompt } = require('./workspace-prompt');
|
|
26
|
+
const {
|
|
27
|
+
buildAgyArgv,
|
|
28
|
+
parseAgyEvent,
|
|
29
|
+
AgyRunState,
|
|
30
|
+
classifyAgyFailure,
|
|
31
|
+
agyBinaryCandidates,
|
|
32
|
+
} = require('./antigravity-stream');
|
|
33
|
+
|
|
34
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
35
|
+
|
|
36
|
+
class AntigravityAdapter extends BaseAdapter {
|
|
37
|
+
/**
|
|
38
|
+
* @param {object} opts - BaseAdapter opts plus:
|
|
39
|
+
* @param {Set} [opts.disabledModules]
|
|
40
|
+
* @param {string} [opts.workingDir]
|
|
41
|
+
*/
|
|
42
|
+
constructor(opts) {
|
|
43
|
+
super(opts);
|
|
44
|
+
this.disabledModules = opts.disabledModules || new Set();
|
|
45
|
+
// Read-only pinned context, same as gemini: agy has no MCP bridge to the
|
|
46
|
+
// workspace knowledge tools, so it can see the decision log but not write it.
|
|
47
|
+
this._usesPinnedContext = true;
|
|
48
|
+
this._channelConversations = {}; // channel → agy conversation_id
|
|
49
|
+
this._channelProcesses = {}; // channel → child process
|
|
50
|
+
this._sessionsFile = path.join(
|
|
51
|
+
os.homedir(), '.openagents', 'sessions',
|
|
52
|
+
`${this.workspaceId}_${this.agentName}_antigravity.json`
|
|
53
|
+
);
|
|
54
|
+
this._loadSessions();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_loadSessions() {
|
|
58
|
+
try {
|
|
59
|
+
if (fs.existsSync(this._sessionsFile)) {
|
|
60
|
+
const data = JSON.parse(fs.readFileSync(this._sessionsFile, 'utf-8'));
|
|
61
|
+
if (data && typeof data === 'object') {
|
|
62
|
+
Object.assign(this._channelConversations, data);
|
|
63
|
+
this._log(`Loaded ${Object.keys(data).length} conversation(s)`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
this._log('Could not load sessions file, starting fresh');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_saveSessions() {
|
|
72
|
+
try {
|
|
73
|
+
const dir = path.dirname(this._sessionsFile);
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
fs.writeFileSync(this._sessionsFile, JSON.stringify(this._channelConversations));
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async _onControlAction(action, _payload) {
|
|
80
|
+
if (action === 'stop') {
|
|
81
|
+
await this._stopAllProcesses();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async _stopProcess(proc) {
|
|
86
|
+
if (!proc || proc.exitCode !== null) return;
|
|
87
|
+
try {
|
|
88
|
+
if (IS_WINDOWS) {
|
|
89
|
+
try { execSync(`taskkill /F /T /PID ${proc.pid}`, { timeout: 5000 }); } catch {}
|
|
90
|
+
} else {
|
|
91
|
+
try { process.kill(-proc.pid, 'SIGTERM'); } catch {
|
|
92
|
+
proc.kill('SIGTERM');
|
|
93
|
+
}
|
|
94
|
+
await new Promise((resolve) => {
|
|
95
|
+
const timeout = setTimeout(() => {
|
|
96
|
+
try { process.kill(-proc.pid, 'SIGKILL'); } catch {
|
|
97
|
+
proc.kill('SIGKILL');
|
|
98
|
+
}
|
|
99
|
+
resolve();
|
|
100
|
+
}, 5000);
|
|
101
|
+
proc.on('exit', () => { clearTimeout(timeout); resolve(); });
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async _stopAllProcesses() {
|
|
108
|
+
const entries = Object.entries(this._channelProcesses);
|
|
109
|
+
if (!entries.length) return;
|
|
110
|
+
this._log(`Stopping ${entries.length} running process(es)...`);
|
|
111
|
+
for (const [channel, proc] of entries) {
|
|
112
|
+
await this._stopProcess(proc);
|
|
113
|
+
delete this._channelProcesses[channel];
|
|
114
|
+
delete this._channelQueues[channel];
|
|
115
|
+
try {
|
|
116
|
+
await this.sendStatus(channel, 'Execution stopped by user');
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_findAgyBinary() {
|
|
122
|
+
// PATH first (codepage-safe lookup, see whereBinary), then the install
|
|
123
|
+
// script's fixed destinations.
|
|
124
|
+
const viaWhere = whereBinary('agy');
|
|
125
|
+
if (viaWhere) return viaWhere;
|
|
126
|
+
const candidates = agyBinaryCandidates({
|
|
127
|
+
home: os.homedir(),
|
|
128
|
+
isWindows: IS_WINDOWS,
|
|
129
|
+
localAppData: process.env.LOCALAPPDATA,
|
|
130
|
+
});
|
|
131
|
+
for (const c of candidates) {
|
|
132
|
+
if (fs.existsSync(c)) return c;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* agy ignores a bare GEMINI_API_KEY unless the settings file also selects
|
|
139
|
+
* the gemini provider — and, inversely, refuses to start when the provider
|
|
140
|
+
* is selected but the key is missing. When the user configured a key for
|
|
141
|
+
* this agent, select the provider for them; never touch the file otherwise
|
|
142
|
+
* (a keyring/OAuth sign-in needs no provider entry and must not gain one,
|
|
143
|
+
* or the CLI stops starting the day the key is removed).
|
|
144
|
+
*/
|
|
145
|
+
_ensureAgyAuth() {
|
|
146
|
+
if (this._agyAuthReady) return;
|
|
147
|
+
this._agyAuthReady = true;
|
|
148
|
+
try {
|
|
149
|
+
const env = this.agentEnv || process.env;
|
|
150
|
+
const apiKey = (env.GEMINI_API_KEY || '').trim();
|
|
151
|
+
if (!apiKey) return; // OAuth / keyring — leave the user's setup alone.
|
|
152
|
+
const dir = path.join(os.homedir(), '.gemini', 'antigravity-cli');
|
|
153
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
154
|
+
const file = path.join(dir, 'settings.json');
|
|
155
|
+
let settings = {};
|
|
156
|
+
try { settings = JSON.parse(fs.readFileSync(file, 'utf-8')) || {}; } catch {}
|
|
157
|
+
if (settings.modelProvider === 'gemini') return; // already selected.
|
|
158
|
+
settings.modelProvider = 'gemini';
|
|
159
|
+
fs.writeFileSync(file, JSON.stringify(settings, null, 2));
|
|
160
|
+
this._log('Configured Antigravity CLI for API-key auth (~/.gemini/antigravity-cli/settings.json)');
|
|
161
|
+
} catch (e) {
|
|
162
|
+
this._log(`agy auth setup skipped: ${e.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_buildAgyCmd(prompt, channelName, { skipResume = false } = {}) {
|
|
167
|
+
this._ensureAgyAuth();
|
|
168
|
+
const agyBin = this._findAgyBinary();
|
|
169
|
+
if (!agyBin) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
'agy CLI not found. Install with: curl -fsSL https://antigravity.google/cli/install.sh | bash'
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const pinned = this.pinnedPromptOpts(channelName);
|
|
176
|
+
const systemPrompt = '\n' + buildClaudeSystemPrompt({
|
|
177
|
+
agentName: this.agentName,
|
|
178
|
+
workspaceId: this.workspaceId,
|
|
179
|
+
channelName,
|
|
180
|
+
mode: this._mode,
|
|
181
|
+
decisionLog: pinned.decisionLog ? { ...pinned.decisionLog, writeAccess: false } : null,
|
|
182
|
+
glossary: pinned.glossary ? { ...pinned.glossary, writeAccess: false } : null,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// No system-prompt flag in agy's print mode either — prepend it.
|
|
186
|
+
const fullPrompt = `${systemPrompt}\n\n---\n\nUser message:\n${prompt}`;
|
|
187
|
+
|
|
188
|
+
const env = this.agentEnv || process.env;
|
|
189
|
+
const model = (env.ANTIGRAVITY_MODEL || env.AGY_MODEL || '').trim();
|
|
190
|
+
|
|
191
|
+
const args = buildAgyArgv({
|
|
192
|
+
prompt: fullPrompt,
|
|
193
|
+
model,
|
|
194
|
+
conversationId: this._channelConversations[channelName],
|
|
195
|
+
skipResume,
|
|
196
|
+
});
|
|
197
|
+
return { cmd: [agyBin, ...args] };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async _handleMessage(msg) {
|
|
201
|
+
let content = (msg.content || '').trim();
|
|
202
|
+
const attachments = msg.attachments || [];
|
|
203
|
+
|
|
204
|
+
const attText = formatAttachmentsForPrompt(attachments);
|
|
205
|
+
if (attText) {
|
|
206
|
+
content = content ? content + attText : attText.trim();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!content) return;
|
|
210
|
+
|
|
211
|
+
const msgChannel = msg.sessionId || this.channelName;
|
|
212
|
+
const sender = msg.senderName || msg.senderType || 'user';
|
|
213
|
+
this._log(`Processing message from ${sender} in ${msgChannel}: ${content.slice(0, 80)}...`);
|
|
214
|
+
|
|
215
|
+
if (!this._titledSessions.has(msgChannel)) {
|
|
216
|
+
this._titledSessions.add(msgChannel);
|
|
217
|
+
try {
|
|
218
|
+
const info = await this.client.getSession(this.workspaceId, msgChannel, this.token);
|
|
219
|
+
const resumeFrom = info.resumeFrom;
|
|
220
|
+
if (resumeFrom && !this._channelConversations[msgChannel]) {
|
|
221
|
+
const sourceConversation = this._channelConversations[resumeFrom];
|
|
222
|
+
if (sourceConversation) {
|
|
223
|
+
this._channelConversations[msgChannel] = sourceConversation;
|
|
224
|
+
this._saveSessions();
|
|
225
|
+
this._log(`Resuming channel ${msgChannel} from ${resumeFrom}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const title = generateSessionTitle(content);
|
|
229
|
+
if (title && !info.titleManuallySet && SESSION_DEFAULT_RE.test(info.title || '')) {
|
|
230
|
+
await this.client.updateSession(
|
|
231
|
+
this.workspaceId, msgChannel, this.token,
|
|
232
|
+
{ title, autoTitle: true }
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
} catch {}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await this.sendStatus(msgChannel, 'thinking...');
|
|
239
|
+
|
|
240
|
+
let cmd;
|
|
241
|
+
const cleanEnv = { ...(this.agentEnv || process.env) };
|
|
242
|
+
|
|
243
|
+
let _shouldRetry = false;
|
|
244
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
245
|
+
try {
|
|
246
|
+
const built = this._buildAgyCmd(content, msgChannel, { skipResume: attempt > 0 });
|
|
247
|
+
cmd = built.cmd;
|
|
248
|
+
} catch (e) {
|
|
249
|
+
await this.sendError(msgChannel, e.message);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
255
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
256
|
+
env: cleanEnv,
|
|
257
|
+
cwd: this.workingDir,
|
|
258
|
+
detached: !IS_WINDOWS,
|
|
259
|
+
windowsHide: true,
|
|
260
|
+
});
|
|
261
|
+
this._channelProcesses[msgChannel] = proc;
|
|
262
|
+
|
|
263
|
+
const run = new AgyRunState();
|
|
264
|
+
let stderrBuf = '';
|
|
265
|
+
let lineBuffer = '';
|
|
266
|
+
let _pendingLines = Promise.resolve();
|
|
267
|
+
|
|
268
|
+
if (proc.stderr) {
|
|
269
|
+
proc.stderr.on('data', (chunk) => { stderrBuf += chunk.toString('utf-8'); });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
_shouldRetry = await new Promise((resolve, reject) => {
|
|
273
|
+
let consecutiveTimeouts = 0;
|
|
274
|
+
let lastDataTime = Date.now();
|
|
275
|
+
let timeoutTimer = null;
|
|
276
|
+
|
|
277
|
+
const resetTimeout = () => {
|
|
278
|
+
consecutiveTimeouts = 0;
|
|
279
|
+
lastDataTime = Date.now();
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const startTimeoutMonitor = () => {
|
|
283
|
+
timeoutTimer = setInterval(async () => {
|
|
284
|
+
const elapsed = Date.now() - lastDataTime;
|
|
285
|
+
if (elapsed >= 15000) {
|
|
286
|
+
consecutiveTimeouts++;
|
|
287
|
+
lastDataTime = Date.now();
|
|
288
|
+
if (consecutiveTimeouts === 2) {
|
|
289
|
+
try { await this.sendStatus(msgChannel, 'Processing...'); } catch {}
|
|
290
|
+
}
|
|
291
|
+
if (consecutiveTimeouts >= 20) {
|
|
292
|
+
this._log(`Process idle for ${consecutiveTimeouts * 15}s, killing...`);
|
|
293
|
+
await this._stopProcess(proc);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}, 15000);
|
|
297
|
+
};
|
|
298
|
+
startTimeoutMonitor();
|
|
299
|
+
|
|
300
|
+
const processLine = async (line) => {
|
|
301
|
+
const event = parseAgyEvent(line);
|
|
302
|
+
if (!event) return;
|
|
303
|
+
resetTimeout();
|
|
304
|
+
for (const action of run.consume(event)) {
|
|
305
|
+
if (action.type === 'conversation') {
|
|
306
|
+
this._channelConversations[msgChannel] = action.id;
|
|
307
|
+
this._saveSessions();
|
|
308
|
+
} else if (action.type === 'thinking') {
|
|
309
|
+
try { await this.sendThinking(msgChannel, action.text); } catch {}
|
|
310
|
+
} else if (action.type === 'tool') {
|
|
311
|
+
try { await this.sendStatus(msgChannel, `${action.name} › ${action.preview}`); } catch {}
|
|
312
|
+
}
|
|
313
|
+
// 'result' is read from `run` after exit.
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
proc.on('exit', async (code) => {
|
|
318
|
+
if (timeoutTimer) clearInterval(timeoutTimer);
|
|
319
|
+
|
|
320
|
+
try { await _pendingLines; } catch {}
|
|
321
|
+
|
|
322
|
+
const lines = lineBuffer.split('\n');
|
|
323
|
+
for (const line of lines) {
|
|
324
|
+
try { await processLine(line); } catch {}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
delete this._channelProcesses[msgChannel];
|
|
328
|
+
|
|
329
|
+
if (code !== 0) {
|
|
330
|
+
this._log(`CLI exited with code ${code}`);
|
|
331
|
+
if (stderrBuf.trim()) {
|
|
332
|
+
this._log(`stderr: ${stderrBuf.trim().slice(0, 500)}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const response = run.finalResponse();
|
|
337
|
+
const failed = code !== 0 || run.status === 'ERROR';
|
|
338
|
+
|
|
339
|
+
if (response && !failed) {
|
|
340
|
+
try { await this.sendResponse(msgChannel, response); } catch {}
|
|
341
|
+
resolve(false);
|
|
342
|
+
} else if (failed && this._channelConversations[msgChannel] && attempt === 0) {
|
|
343
|
+
// A stored conversation id the CLI no longer knows fails the
|
|
344
|
+
// whole run — clear it and retry once from scratch.
|
|
345
|
+
this._log(`Stale conversation for ${msgChannel}, clearing and retrying without resume`);
|
|
346
|
+
delete this._channelConversations[msgChannel];
|
|
347
|
+
this._saveSessions();
|
|
348
|
+
resolve(true);
|
|
349
|
+
} else if (failed) {
|
|
350
|
+
// Surface the real reason (auth, provider config, bad model)
|
|
351
|
+
// instead of a generic retry prompt — the gemini adapter's
|
|
352
|
+
// silent-stderr dead end is what this branch fixes.
|
|
353
|
+
const { kind, message } = classifyAgyFailure({ code, stderr: stderrBuf, error: run.error });
|
|
354
|
+
this._log(`run failed (${kind})`);
|
|
355
|
+
try { await this.sendError(msgChannel, message); } catch {}
|
|
356
|
+
resolve(false);
|
|
357
|
+
} else {
|
|
358
|
+
if (!run.sawText()) {
|
|
359
|
+
try { await this.sendResponse(msgChannel, 'No response generated. Please try again.'); } catch {}
|
|
360
|
+
}
|
|
361
|
+
resolve(false);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
proc.on('error', (err) => {
|
|
366
|
+
if (timeoutTimer) clearInterval(timeoutTimer);
|
|
367
|
+
reject(err);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
proc.stdout.on('data', (chunk) => {
|
|
371
|
+
lineBuffer += chunk.toString('utf-8');
|
|
372
|
+
resetTimeout();
|
|
373
|
+
const lines = lineBuffer.split('\n');
|
|
374
|
+
lineBuffer = lines.pop();
|
|
375
|
+
for (const line of lines) {
|
|
376
|
+
_pendingLines = _pendingLines.then(() => processLine(line)).catch(() => {});
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
} catch (e) {
|
|
381
|
+
this._log(`Error handling message: ${e.message}`);
|
|
382
|
+
await this.sendError(msgChannel, `Error processing message: ${e.message}`);
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
if (!_shouldRetry) break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
module.exports = AntigravityAdapter;
|
package/src/adapters/base.js
CHANGED
|
@@ -81,6 +81,10 @@ class BaseAdapter {
|
|
|
81
81
|
this._processedIds = new Set();
|
|
82
82
|
this._titledSessions = new Set();
|
|
83
83
|
this._mode = 'execute';
|
|
84
|
+
// Workspace-selected model id (WorkspaceMember.model). Synced from
|
|
85
|
+
// discover at startup and via the `model.set` control event; null means
|
|
86
|
+
// the agent CLI runs with its own default.
|
|
87
|
+
this.workspaceModel = null;
|
|
84
88
|
this._lastControlId = null;
|
|
85
89
|
this._controlWake = null;
|
|
86
90
|
// Per-channel task tracking for parallel execution
|
|
@@ -212,6 +216,10 @@ class BaseAdapter {
|
|
|
212
216
|
this.disabledModules = skillsToDisabledModules(self.enabledSkills);
|
|
213
217
|
this._log(`Synced skills from workspace: disabled=[${[...this.disabledModules].join(',')}]`);
|
|
214
218
|
}
|
|
219
|
+
if (self && self.model) {
|
|
220
|
+
this.workspaceModel = self.model;
|
|
221
|
+
this._log(`Synced model from workspace: ${this.workspaceModel}`);
|
|
222
|
+
}
|
|
215
223
|
} catch (e) {
|
|
216
224
|
this._log(`Warning: skill sync failed (non-fatal): ${e.message}`);
|
|
217
225
|
}
|
|
@@ -360,6 +368,11 @@ class BaseAdapter {
|
|
|
360
368
|
await this._handleSkillInstall(payload);
|
|
361
369
|
} else if (action === 'skill.uninstall') {
|
|
362
370
|
await this._handleSkillUninstall(payload);
|
|
371
|
+
} else if (action === 'model.set') {
|
|
372
|
+
// Adapters read workspaceModel when (re)spawning their CLI; ones with
|
|
373
|
+
// a staleness check (claude.js) respawn on the next message.
|
|
374
|
+
this.workspaceModel = (payload && payload.model) || null;
|
|
375
|
+
this._log(`Workspace model set to ${this.workspaceModel || '(default)'}`);
|
|
363
376
|
}
|
|
364
377
|
}
|
|
365
378
|
|
package/src/adapters/claude.js
CHANGED
|
@@ -471,6 +471,10 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
471
471
|
const cmd = [claudeBin, '-p', prompt, '--output-format', 'stream-json', '--verbose'];
|
|
472
472
|
|
|
473
473
|
cmd.push('--append-system-prompt', systemPrompt);
|
|
474
|
+
// Workspace-picked model (agent profile panel). Absent = CLI's own default.
|
|
475
|
+
if (this.workspaceModel) {
|
|
476
|
+
cmd.push('--model', this.workspaceModel);
|
|
477
|
+
}
|
|
474
478
|
const disallowedTools = ['AskUserQuestion', 'CronCreate', 'CronDelete', 'CronList', 'ScheduleWakeup'];
|
|
475
479
|
if (browserEnabled) {
|
|
476
480
|
// Hard-ban the native WebFetch: it can't render JS and bypasses the
|
|
@@ -1165,13 +1169,16 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
1165
1169
|
// read-only plan process serving execute requests.
|
|
1166
1170
|
const modeStale = existingPP.spawnMode !== this._mode;
|
|
1167
1171
|
const pinnedStale = pinnedHash !== null && existingPP.pinnedHash !== pinnedHash;
|
|
1168
|
-
|
|
1172
|
+
const modelStale = (existingPP.spawnModel || null) !== (this.workspaceModel || null);
|
|
1173
|
+
if (modeStale || pinnedStale || modelStale) {
|
|
1169
1174
|
// Kill it and fall through to a fresh spawn: --resume keeps the
|
|
1170
1175
|
// conversation (it lives in the CLI transcript), while the new spawn
|
|
1171
1176
|
// carries the current mode and re-pins the current knowledge.
|
|
1172
1177
|
this._log(modeStale
|
|
1173
1178
|
? `Mode changed to ${this._mode} for ${msgChannel} — respawning with resume`
|
|
1174
|
-
:
|
|
1179
|
+
: modelStale
|
|
1180
|
+
? `Model changed to ${this.workspaceModel || '(default)'} for ${msgChannel} — respawning with resume`
|
|
1181
|
+
: `Pinned knowledge changed for ${msgChannel} — respawning with resume to re-pin it`);
|
|
1175
1182
|
await this._killPersistentProc(msgChannel);
|
|
1176
1183
|
} else {
|
|
1177
1184
|
this._log(`Reusing persistent process for ${msgChannel}`);
|
|
@@ -1258,6 +1265,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
1258
1265
|
{ entryId: glossaryOpt && glossaryOpt.entryId, content: glossaryOpt && glossaryOpt.content },
|
|
1259
1266
|
]);
|
|
1260
1267
|
pp.spawnMode = this._mode;
|
|
1268
|
+
pp.spawnModel = this.workspaceModel || null;
|
|
1261
1269
|
this._log(`Spawned persistent process for ${msgChannel} (attempt ${attempt + 1})`);
|
|
1262
1270
|
|
|
1263
1271
|
const result = await this._sendToPersistentProc(pp, effectiveContent);
|
package/src/adapters/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const AmpAdapter = require('./amp');
|
|
|
22
22
|
const MiniSweAgentAdapter = require('./mini');
|
|
23
23
|
const PiAdapter = require('./pi');
|
|
24
24
|
const DeepSeekAdapter = require('./deepseek');
|
|
25
|
+
const AntigravityAdapter = require('./antigravity');
|
|
25
26
|
|
|
26
27
|
const ADAPTER_MAP = {
|
|
27
28
|
openclaw: OpenClawAdapter,
|
|
@@ -41,11 +42,12 @@ const ADAPTER_MAP = {
|
|
|
41
42
|
'mini-swe-agent': MiniSweAgentAdapter,
|
|
42
43
|
pi: PiAdapter,
|
|
43
44
|
deepseek: DeepSeekAdapter,
|
|
45
|
+
antigravity: AntigravityAdapter,
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* Create an adapter instance for the given agent type.
|
|
48
|
-
* @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp, mini-swe-agent, pi, deepseek)
|
|
50
|
+
* @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp, mini-swe-agent, pi, deepseek, antigravity)
|
|
49
51
|
* @param {object} opts - Adapter constructor options
|
|
50
52
|
* @returns {BaseAdapter}
|
|
51
53
|
*/
|
|
@@ -76,6 +78,7 @@ module.exports = {
|
|
|
76
78
|
MiniSweAgentAdapter,
|
|
77
79
|
PiAdapter,
|
|
78
80
|
DeepSeekAdapter,
|
|
81
|
+
AntigravityAdapter,
|
|
79
82
|
createAdapter,
|
|
80
83
|
ADAPTER_MAP,
|
|
81
84
|
};
|
package/src/cli.js
CHANGED
|
@@ -806,6 +806,37 @@ async function cmdTestLLM(connector, _flags, positional) {
|
|
|
806
806
|
}
|
|
807
807
|
}
|
|
808
808
|
|
|
809
|
+
async function cmdProbe(connector, flags, positional) {
|
|
810
|
+
const target = positional[0];
|
|
811
|
+
if (!target) { print('Usage: agn probe <type|agent> [--json]'); return; }
|
|
812
|
+
|
|
813
|
+
// Accept an agent name as a convenience — probe its type.
|
|
814
|
+
let type = target;
|
|
815
|
+
try {
|
|
816
|
+
const agent = connector.config.getAgent(target);
|
|
817
|
+
if (agent) type = agent.type || 'openclaw';
|
|
818
|
+
} catch {}
|
|
819
|
+
|
|
820
|
+
const result = await connector.probeAgentType(type);
|
|
821
|
+
|
|
822
|
+
if (flags && flags.json) {
|
|
823
|
+
// Machine consumers (the daemon's probe_agent node command) read ONLY
|
|
824
|
+
// stdout JSON — same contract as `agn runtimes --json`.
|
|
825
|
+
process.stdout.write(JSON.stringify(result));
|
|
826
|
+
if (!result.ok) process.exitCode = 1;
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (result.ok) {
|
|
831
|
+
print(`OK (${result.method}, ${result.durationMs}ms)${result.reply ? ` — ${result.reply}` : ''}`);
|
|
832
|
+
if (result.code === 'static_only') print(result.message);
|
|
833
|
+
} else {
|
|
834
|
+
print(`FAILED [${result.code}] ${result.message || ''}`.trim());
|
|
835
|
+
for (const line of result.guidance || []) print(` → ${line}`);
|
|
836
|
+
process.exitCode = 1;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
809
840
|
async function cmdVersion() {
|
|
810
841
|
const pkg = require('../package.json');
|
|
811
842
|
print(`${pkg.name} v${pkg.version}`);
|
|
@@ -874,6 +905,7 @@ Commands:
|
|
|
874
905
|
tool-mode [agent] [mode] View/set tool mode (mcp or skills)
|
|
875
906
|
autostart [--disable] Enable/disable auto-start on login
|
|
876
907
|
test-llm <type> Test LLM connection
|
|
908
|
+
probe <type|agent> [--json] Smoke-test an agent (tiny end-to-end prompt)
|
|
877
909
|
logs [agent] [--lines N] View daemon logs
|
|
878
910
|
workspace create [name] Create a new workspace
|
|
879
911
|
workspace join <token> Join workspace with token
|
|
@@ -962,6 +994,7 @@ async function main() {
|
|
|
962
994
|
skills: () => cmdSkills(connector, flags, positional),
|
|
963
995
|
'tool-mode': () => cmdToolMode(connector, flags, positional),
|
|
964
996
|
'test-llm': () => cmdTestLLM(connector, flags, positional),
|
|
997
|
+
probe: () => cmdProbe(connector, flags, positional),
|
|
965
998
|
update: () => cmdUpdate(connector),
|
|
966
999
|
'mcp-server': () => {
|
|
967
1000
|
const { runMcpServer } = require('./mcp-server');
|