@openagents-org/agent-launcher 0.2.150 → 0.2.152
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 +95 -0
- package/src/adapters/aider.js +5 -3
- package/src/adapters/base.js +34 -6
- package/src/adapters/claude.js +358 -158
- package/src/adapters/codex.js +58 -4
- package/src/adapters/cursor.js +21 -3
- package/src/adapters/decision-log.js +181 -0
- package/src/adapters/index.js +4 -1
- package/src/adapters/mini.js +572 -0
- package/src/adapters/openclaw.js +39 -2
- package/src/adapters/opencode.js +58 -10
- package/src/adapters/workspace-prompt.js +255 -29
- package/src/mcp-server.js +112 -0
- package/src/skill-installer.js +218 -0
- package/src/workspace-client.js +59 -14
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mini-SWE-agent adapter for OpenAgents workspace.
|
|
3
|
+
*
|
|
4
|
+
* Bridges the mini-swe-agent CLI (https://mini-swe-agent.com) to an OpenAgents
|
|
5
|
+
* workspace as a plain shell-based coding agent. Each workspace message spawns
|
|
6
|
+
* ONE non-interactive `mini` run in the task's workspace directory and exits:
|
|
7
|
+
*
|
|
8
|
+
* mini [--model <model>] --yolo --exit-immediately \
|
|
9
|
+
* [--cost-limit <n>] --output <trajectory> --task <prompt>
|
|
10
|
+
*
|
|
11
|
+
* Design notes (verified against the mini CLI docs):
|
|
12
|
+
* - `mini` defaults to *confirm* mode (interactive) and, when the agent wants
|
|
13
|
+
* to finish, PROMPTS for the next step instead of exiting. `--yolo` runs LM
|
|
14
|
+
* commands without confirmation and `--exit-immediately` makes it exit on
|
|
15
|
+
* finish — BOTH are required for a headless one-shot run, or the daemon would
|
|
16
|
+
* hang waiting on stdin. stdin is also given /dev/null (stdio 'ignore') so a
|
|
17
|
+
* stray prompt (e.g. the first-run setup wizard) can never wedge the process.
|
|
18
|
+
* - mini is a Python process with no JSON event protocol: stdout is drained as
|
|
19
|
+
* a best-effort progress log (ANSI-stripped, relayed as `thinking`, capped)
|
|
20
|
+
* and the cleaned transcript tail is sent once as the final answer. The exit
|
|
21
|
+
* code decides success (non-zero is never reported as success). PYTHONUNBUFFERED
|
|
22
|
+
* is set so stdout streams instead of being fully buffered; NO_COLOR keeps
|
|
23
|
+
* rich/ANSI escapes out of the relayed log.
|
|
24
|
+
* - Each run gets its own --output trajectory file under ~/.openagents/sessions
|
|
25
|
+
* (never in the project), removed afterwards, so concurrent channel runs never
|
|
26
|
+
* clash on a shared trajectory.
|
|
27
|
+
* - Stateless per task: mini keeps no memory across workspace messages (there is
|
|
28
|
+
* no fabricated persistent session). The agent is auto-authorized to run shell
|
|
29
|
+
* commands via --yolo — consistent with how Goose (GOOSE_MODE=auto) and Copilot
|
|
30
|
+
* (--no-ask-user) already auto-execute; there is no shared per-command approval
|
|
31
|
+
* gate to hook into.
|
|
32
|
+
* - Config: mini's config is set via the MSWEA_MINI_CONFIG_PATH env var (which
|
|
33
|
+
* mini reads as its default config file), forwarded through the agent env. We
|
|
34
|
+
* intentionally do NOT pass --config: a single --config / MSWEA_MINI_CONFIG_PATH
|
|
35
|
+
* REPLACES mini's built-in mini.yaml (agent prompt templates, step/env config)
|
|
36
|
+
* rather than merging (verified in mini's run/mini.py), so the launcher field
|
|
37
|
+
* is documented as a FULL config file, not a partial override.
|
|
38
|
+
* - Multi-provider auth is left to mini/LiteLLM: the user supplies the model
|
|
39
|
+
* (MSWEA_MODEL_NAME or the Model field) and provider keys (ANTHROPIC_API_KEY /
|
|
40
|
+
* OPENAI_API_KEY / …) through the agent environment, which flows through
|
|
41
|
+
* unchanged. Keys never reach the command line or the logs.
|
|
42
|
+
*
|
|
43
|
+
* Reuses all shared connectivity / dispatch / state machinery in BaseAdapter;
|
|
44
|
+
* only the mini-specific subprocess invocation and parsing live here. Node-only
|
|
45
|
+
* adapter (like Cline / Copilot) — there is no Python mirror.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
'use strict';
|
|
49
|
+
|
|
50
|
+
const fs = require('fs');
|
|
51
|
+
const os = require('os');
|
|
52
|
+
const path = require('path');
|
|
53
|
+
const { execSync, spawn } = require('child_process');
|
|
54
|
+
|
|
55
|
+
const BaseAdapter = require('./base');
|
|
56
|
+
const { whichBinary, getEnhancedEnv, aiderBinDirs } = require('../paths');
|
|
57
|
+
const {
|
|
58
|
+
REASON,
|
|
59
|
+
classifySpawnError,
|
|
60
|
+
redactDiagnostic,
|
|
61
|
+
shouldUseShellForBinary,
|
|
62
|
+
} = require('./health-status');
|
|
63
|
+
|
|
64
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
65
|
+
// Terminate if mini produces no output for this long (a wedged turn). Resets on
|
|
66
|
+
// any stdout activity, so a slow-but-progressing task is never killed.
|
|
67
|
+
const IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
68
|
+
// Cap how many stdout lines are relayed as `thinking` progress, so a task that
|
|
69
|
+
// prints large command output (diffs, test logs) can't flood the channel. The
|
|
70
|
+
// full transcript is still collected for the final answer.
|
|
71
|
+
const MAX_STREAM_UPDATES = 60;
|
|
72
|
+
// Cap the size of the final transcript we post back. mini's own closing summary
|
|
73
|
+
// is at the END of stdout, so we keep the tail when truncating.
|
|
74
|
+
const FINAL_RESPONSE_MAX = 12000;
|
|
75
|
+
|
|
76
|
+
// eslint-disable-next-line no-control-regex
|
|
77
|
+
const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
78
|
+
const BLANKS_RE = /\n{3,}/g;
|
|
79
|
+
|
|
80
|
+
// Error signatures → an actionable message. mini routes model calls through
|
|
81
|
+
// LiteLLM, so the failure surface mirrors Aider's; a couple of mini-specific
|
|
82
|
+
// cases (cost limit, no model configured) are added.
|
|
83
|
+
const ERROR_SIGNATURES = [
|
|
84
|
+
[['authenticationerror', 'invalid api key', 'incorrect api key', 'no api key',
|
|
85
|
+
'missing these environment variables', 'api key not found', '401',
|
|
86
|
+
'unauthorized', 'permission denied to access model'],
|
|
87
|
+
'Authentication failed — configure a model API key (e.g. ANTHROPIC_API_KEY or '
|
|
88
|
+
+ 'OPENAI_API_KEY) for the selected model in the agent environment.'],
|
|
89
|
+
[['notfounderror', 'model_not_found', 'does not exist', 'unknown model',
|
|
90
|
+
'could not find model', 'you do not have access to model'],
|
|
91
|
+
'Model not found or not accessible — check MSWEA_MODEL_NAME / the Model field '
|
|
92
|
+
+ 'and that your key has access.'],
|
|
93
|
+
[['you must provide a model', 'no model', 'set up a model', 'model is required',
|
|
94
|
+
'run `mini-extra config setup`', 'run mini-extra config setup'],
|
|
95
|
+
'No model configured — set MSWEA_MODEL_NAME (or the Model field / a --config '
|
|
96
|
+
+ 'file), or run `mini-extra config setup`.'],
|
|
97
|
+
[['cost limit', 'costlimit', 'exceeded the cost', 'limitreached', 'limit reached'],
|
|
98
|
+
'Stopped: the configured cost limit (MSWEA_COST_LIMIT) was reached before the '
|
|
99
|
+
+ 'task finished.'],
|
|
100
|
+
[['rate limit', 'ratelimiterror', '429', 'quota', 'insufficient_quota'],
|
|
101
|
+
'Rate-limited or out of quota at the model provider — try again later.'],
|
|
102
|
+
[['connectionerror', 'timeout', 'could not connect', 'getaddrinfo',
|
|
103
|
+
'temporary failure in name resolution', 'network is unreachable',
|
|
104
|
+
'failed to establish a new connection'],
|
|
105
|
+
'Network error reaching the model provider — check connectivity and the base URL.'],
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
function miniInstallHint() {
|
|
109
|
+
return 'pip install mini-swe-agent';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function cleanOutput(text) {
|
|
113
|
+
return String(text || '').replace(ANSI_RE, '').replace(BLANKS_RE, '\n\n').trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function classifyError(stderr, stdout) {
|
|
117
|
+
const blob = `${stderr}\n${stdout}`.toLowerCase();
|
|
118
|
+
for (const [needles, message] of ERROR_SIGNATURES) {
|
|
119
|
+
if (needles.some((n) => blob.includes(n))) return message;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Wrap a workspace message as a self-contained coding task. mini works on the
|
|
126
|
+
* files directly via shell (it does NOT use the OpenAgents MCP tools), so the
|
|
127
|
+
* prompt is a plain instruction — no workspace/MCP context, and deliberately no
|
|
128
|
+
* SWE-bench / patch / benchmark wording.
|
|
129
|
+
*/
|
|
130
|
+
function buildTaskPrompt(userMessage) {
|
|
131
|
+
return [
|
|
132
|
+
'You are working in the current workspace directory.',
|
|
133
|
+
'',
|
|
134
|
+
'User request:',
|
|
135
|
+
userMessage,
|
|
136
|
+
'',
|
|
137
|
+
'Work directly on the files in the current workspace.',
|
|
138
|
+
'Inspect the codebase before editing.',
|
|
139
|
+
'Run relevant tests or checks where practical.',
|
|
140
|
+
'Do not merely describe a solution: make the changes in the workspace.',
|
|
141
|
+
'When finished, summarize:',
|
|
142
|
+
'- what you changed',
|
|
143
|
+
'- which checks/tests you ran',
|
|
144
|
+
'- any remaining limitations',
|
|
145
|
+
].join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class MiniSweAgentAdapter extends BaseAdapter {
|
|
149
|
+
constructor(opts) {
|
|
150
|
+
super(opts);
|
|
151
|
+
this.disabledModules = opts.disabledModules || new Set();
|
|
152
|
+
|
|
153
|
+
// channel -> running child process (for stop / cleanup)
|
|
154
|
+
this._channelProcesses = {};
|
|
155
|
+
// channels the user explicitly stopped (suppress "no response" noise)
|
|
156
|
+
this._stoppingChannels = new Set();
|
|
157
|
+
this._trajCounter = 0;
|
|
158
|
+
|
|
159
|
+
// Per-run trajectory files live here, never in the project directory.
|
|
160
|
+
this._sessionsDir = path.join(
|
|
161
|
+
os.homedir(), '.openagents', 'sessions', 'mini-swe-agent',
|
|
162
|
+
`${this.workspaceId}_${this.agentName}`,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
this._miniBin = this._findMiniBinary();
|
|
166
|
+
if (this._miniBin) {
|
|
167
|
+
this._log(`Using mini-SWE-agent CLI: ${this._miniBin}`);
|
|
168
|
+
} else {
|
|
169
|
+
this._log(`Warning: mini-SWE-agent CLI not found — install with: ${miniInstallHint()}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ------------------------------------------------------------------
|
|
174
|
+
// Binary resolution
|
|
175
|
+
// ------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
_findMiniBinary() {
|
|
178
|
+
// Shared cross-platform resolver runs `which`/`where` against an ENHANCED
|
|
179
|
+
// PATH (nvm/fnm/volta/homebrew + pip/pipx/uv user-install dirs) — covers the
|
|
180
|
+
// GUI/daemon "installed but not on PATH" case for a `pip install`.
|
|
181
|
+
const resolved = whichBinary('mini');
|
|
182
|
+
if (resolved) return this._resolveExePreference(resolved, IS_WINDOWS);
|
|
183
|
+
|
|
184
|
+
// Explicit fallback over the pip/pipx/uv user-install bin dirs plus the uv
|
|
185
|
+
// tool venv for mini-swe-agent.
|
|
186
|
+
const names = IS_WINDOWS ? ['mini.exe', 'mini.cmd', 'mini'] : ['mini'];
|
|
187
|
+
const dirs = [...aiderBinDirs()];
|
|
188
|
+
const home = os.homedir();
|
|
189
|
+
if (IS_WINDOWS) {
|
|
190
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
191
|
+
const uvTools = process.env.UV_TOOL_DIR || path.join(appData, 'uv', 'tools');
|
|
192
|
+
dirs.push(path.join(uvTools, 'mini-swe-agent', 'Scripts'));
|
|
193
|
+
} else {
|
|
194
|
+
const uvTools = process.env.UV_TOOL_DIR
|
|
195
|
+
|| path.join(home, '.local', 'share', 'uv', 'tools');
|
|
196
|
+
dirs.push(path.join(uvTools, 'mini-swe-agent', 'bin'));
|
|
197
|
+
}
|
|
198
|
+
for (const dir of dirs) {
|
|
199
|
+
for (const name of names) {
|
|
200
|
+
const c = path.join(dir, name);
|
|
201
|
+
if (fs.existsSync(c)) return c;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* On Windows, prefer a directly-executable `mini.exe` over a `mini.cmd`/`.bat`
|
|
209
|
+
* shim that would otherwise have to be launched through a shell. pip/pipx/uv
|
|
210
|
+
* install a console-script `.exe` wrapper, so this normally resolves to the
|
|
211
|
+
* exe; if the resolver landed on a .cmd/.bat and a sibling .exe exists, use the
|
|
212
|
+
* .exe. No-op off Windows. (A .cmd shim still runs correctly via
|
|
213
|
+
* shouldUseShellForBinary — this just avoids the shell hop.)
|
|
214
|
+
*/
|
|
215
|
+
_resolveExePreference(binPath, isWindows) {
|
|
216
|
+
if (!isWindows || !/\.(cmd|bat)$/i.test(String(binPath))) return binPath;
|
|
217
|
+
const exe = path.join(path.dirname(binPath), 'mini.exe');
|
|
218
|
+
return fs.existsSync(exe) ? exe : binPath;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Preflight gate (run by the daemon before join). mini needs a resolvable CLI
|
|
223
|
+
* to do anything useful; when none is found we surface 'runtime_missing' and
|
|
224
|
+
* skip the join. We deliberately do NOT block on a missing model here — mini
|
|
225
|
+
* can read a model from a global config we can't see, and a stray prompt is
|
|
226
|
+
* already made safe by the /dev/null stdin. A genuinely unconfigured model
|
|
227
|
+
* surfaces as a crisp error on the first run instead.
|
|
228
|
+
*/
|
|
229
|
+
preflight() {
|
|
230
|
+
if (!this._miniBin) this._miniBin = this._findMiniBinary();
|
|
231
|
+
if (!this._miniBin) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
reason: REASON.RUNTIME_MISSING,
|
|
235
|
+
message: `mini-SWE-agent CLI not found — install with: ${miniInstallHint()}`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
this._log(`mini-SWE-agent CLI resolved: ${this._miniBin}`);
|
|
239
|
+
return { ok: true };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ------------------------------------------------------------------
|
|
243
|
+
// Config + command construction
|
|
244
|
+
// ------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
_model() {
|
|
247
|
+
return String(
|
|
248
|
+
this.agentEnv.MSWEA_MODEL_NAME || this.agentEnv.MSWEA_MODEL
|
|
249
|
+
|| this.agentEnv.LLM_MODEL || '',
|
|
250
|
+
).trim();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
_costLimit() {
|
|
254
|
+
const raw = String(this.agentEnv.MSWEA_COST_LIMIT || '').trim();
|
|
255
|
+
if (!raw) return null;
|
|
256
|
+
return /^\d+(\.\d+)?$/.test(raw) ? raw : null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Build the mini argv. Order is stable: [--model], --yolo, --exit-immediately,
|
|
261
|
+
* [--cost-limit], [--output], --task <prompt>. The task is always the LAST
|
|
262
|
+
* argument and is passed as a single argv element (never string-concatenated),
|
|
263
|
+
* so quotes / spaces / newlines / shell metacharacters in the prompt are
|
|
264
|
+
* carried verbatim with no shell involvement. Config is NOT a flag here — it
|
|
265
|
+
* flows via the MSWEA_MINI_CONFIG_PATH env var (see the file header).
|
|
266
|
+
*/
|
|
267
|
+
_buildMiniArgs({ model, costLimit, trajectoryPath, task }) {
|
|
268
|
+
const args = [];
|
|
269
|
+
if (model) args.push('--model', model);
|
|
270
|
+
args.push('--yolo', '--exit-immediately');
|
|
271
|
+
if (costLimit) args.push('--cost-limit', String(costLimit));
|
|
272
|
+
if (trajectoryPath) args.push('--output', trajectoryPath);
|
|
273
|
+
args.push('--task', task);
|
|
274
|
+
return args;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
_buildSubprocessEnv() {
|
|
278
|
+
const base = getEnhancedEnv(this.agentEnv);
|
|
279
|
+
if (base.NO_COLOR === undefined) base.NO_COLOR = '1';
|
|
280
|
+
// mini is a Python process — force line-buffered stdout so progress streams
|
|
281
|
+
// instead of arriving all at once at exit.
|
|
282
|
+
base.PYTHONUNBUFFERED = '1';
|
|
283
|
+
// Skip mini's interactive first-run setup wizard. mini runs it whenever
|
|
284
|
+
// MSWEA_CONFIGURED is unset (see run/utilities/config.configure_if_first_time),
|
|
285
|
+
// which — under our /dev/null stdin — aborts with a useless "Aborted." even
|
|
286
|
+
// when the user HAS configured a model + key via the agent environment. In a
|
|
287
|
+
// daemon the wizard is never appropriate; the model/keys come from the env.
|
|
288
|
+
if (base.MSWEA_CONFIGURED === undefined) base.MSWEA_CONFIGURED = 'true';
|
|
289
|
+
return base;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ------------------------------------------------------------------
|
|
293
|
+
// Control actions (stop)
|
|
294
|
+
// ------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
async _onControlAction(action, payload) {
|
|
297
|
+
if (action === 'stop') {
|
|
298
|
+
for (const [channel, proc] of Object.entries(this._channelProcesses)) {
|
|
299
|
+
this._stoppingChannels.add(channel);
|
|
300
|
+
await this._stopProcess(proc);
|
|
301
|
+
delete this._channelProcesses[channel];
|
|
302
|
+
try { await this.sendStatus(channel, 'Execution stopped by user'); } catch {}
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
await super._onControlAction(action, payload);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async _stopProcess(proc) {
|
|
310
|
+
if (!proc || proc.exitCode !== null) return;
|
|
311
|
+
try {
|
|
312
|
+
if (IS_WINDOWS) {
|
|
313
|
+
try { execSync(`taskkill /F /T /PID ${proc.pid}`, { timeout: 5000 }); } catch {}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// POSIX: kill the whole process group (proc was detached) so the shell
|
|
317
|
+
// commands mini spawned under --yolo are reaped too — not left orphaned.
|
|
318
|
+
try { process.kill(-proc.pid, 'SIGTERM'); } catch { proc.kill('SIGTERM'); }
|
|
319
|
+
await new Promise((resolve) => {
|
|
320
|
+
const timeout = setTimeout(() => {
|
|
321
|
+
try { process.kill(-proc.pid, 'SIGKILL'); } catch { proc.kill('SIGKILL'); }
|
|
322
|
+
resolve();
|
|
323
|
+
}, 5000);
|
|
324
|
+
proc.on('exit', () => { clearTimeout(timeout); resolve(); });
|
|
325
|
+
});
|
|
326
|
+
} catch {}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** True when an error is a child_process spawn failure (vs. a runtime error). */
|
|
330
|
+
_isSpawnError(e) {
|
|
331
|
+
if (!e) return false;
|
|
332
|
+
const code = e.code || e.errno;
|
|
333
|
+
return (
|
|
334
|
+
e.syscall === 'spawn'
|
|
335
|
+
|| String(e.syscall || '').startsWith('spawn ')
|
|
336
|
+
|| code === 'ENOENT'
|
|
337
|
+
|| code === 'EACCES'
|
|
338
|
+
|| code === 'EPERM'
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ------------------------------------------------------------------
|
|
343
|
+
// Message handler
|
|
344
|
+
// ------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
async _handleMessage(msg) {
|
|
347
|
+
const content = (msg.content || '').trim();
|
|
348
|
+
if (!content) return;
|
|
349
|
+
|
|
350
|
+
const msgChannel = msg.sessionId || this.channelName;
|
|
351
|
+
const sender = msg.senderName || msg.senderType || 'user';
|
|
352
|
+
this._log(`Processing message from ${sender} in ${msgChannel}: ${content.length} chars`);
|
|
353
|
+
|
|
354
|
+
if (!this._miniBin) this._miniBin = this._findMiniBinary();
|
|
355
|
+
if (!this._miniBin) {
|
|
356
|
+
const message = `mini-SWE-agent CLI not found — install with: ${miniInstallHint()}`;
|
|
357
|
+
this._reportStatus(REASON.RUNTIME_MISSING, message);
|
|
358
|
+
await this.sendError(msgChannel, message);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
await this._autoTitleChannel(msgChannel, content);
|
|
363
|
+
this._stoppingChannels.delete(msgChannel);
|
|
364
|
+
await this.sendStatus(msgChannel, 'mini-SWE-agent is working...');
|
|
365
|
+
|
|
366
|
+
let result;
|
|
367
|
+
try {
|
|
368
|
+
result = await this._runMini(content, msgChannel);
|
|
369
|
+
} catch (e) {
|
|
370
|
+
// A failure to LAUNCH the process (ENOENT/EACCES) is a distinct, actionable
|
|
371
|
+
// failure — surface it as spawn_failed with the resolved path + code.
|
|
372
|
+
if (this._isSpawnError(e)) {
|
|
373
|
+
const { reason, message } = classifySpawnError(e, {
|
|
374
|
+
label: 'mini-SWE-agent',
|
|
375
|
+
bin: this._miniBin,
|
|
376
|
+
});
|
|
377
|
+
this._log(message);
|
|
378
|
+
this._reportStatus(reason, message);
|
|
379
|
+
await this.sendError(msgChannel, message);
|
|
380
|
+
} else {
|
|
381
|
+
this._log(`Error handling message: ${redactDiagnostic(e.message)}`);
|
|
382
|
+
await this.sendError(
|
|
383
|
+
msgChannel,
|
|
384
|
+
`Error processing message: ${redactDiagnostic(e.message)}`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (this._stoppingChannels.has(msgChannel)) {
|
|
391
|
+
this._stoppingChannels.delete(msgChannel);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const { text, error } = result;
|
|
395
|
+
if (error) {
|
|
396
|
+
await this.sendError(msgChannel, error);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
// A successful run proves the runtime is healthy — clear any prior error.
|
|
400
|
+
this._reportStatus(null);
|
|
401
|
+
if (text) {
|
|
402
|
+
await this.sendResponse(msgChannel, text);
|
|
403
|
+
} else {
|
|
404
|
+
await this.sendResponse(
|
|
405
|
+
msgChannel,
|
|
406
|
+
'mini-SWE-agent finished with no textual output (any file changes were applied '
|
|
407
|
+
+ 'to the workspace directory).',
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ------------------------------------------------------------------
|
|
413
|
+
// Subprocess execution
|
|
414
|
+
// ------------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
async _runMini(content, msgChannel) {
|
|
417
|
+
fs.mkdirSync(this._sessionsDir, { recursive: true });
|
|
418
|
+
this._trajCounter += 1;
|
|
419
|
+
// Per-run trajectory file — isolates concurrent channel runs from a shared
|
|
420
|
+
// default trajectory. Removed after the run (we rely on stdout, not the
|
|
421
|
+
// trajectory, for the result).
|
|
422
|
+
const trajectoryPath = path.join(
|
|
423
|
+
this._sessionsDir, `traj-${process.pid}-${this._trajCounter}.json`,
|
|
424
|
+
);
|
|
425
|
+
const cmd = [
|
|
426
|
+
this._miniBin,
|
|
427
|
+
...this._buildMiniArgs({
|
|
428
|
+
model: this._model(),
|
|
429
|
+
costLimit: this._costLimit(),
|
|
430
|
+
trajectoryPath,
|
|
431
|
+
task: buildTaskPrompt(content),
|
|
432
|
+
}),
|
|
433
|
+
];
|
|
434
|
+
let result;
|
|
435
|
+
try {
|
|
436
|
+
result = await this._spawnMini(cmd, msgChannel);
|
|
437
|
+
} catch (e) {
|
|
438
|
+
// Keep the trajectory on a launch failure — it is most useful exactly when
|
|
439
|
+
// something went wrong.
|
|
440
|
+
this._log(`mini-SWE-agent trajectory kept for debugging: ${trajectoryPath}`);
|
|
441
|
+
throw e;
|
|
442
|
+
}
|
|
443
|
+
// Retain the trajectory on failure / timeout / cancel (when it matters for
|
|
444
|
+
// debugging); only clean it up on a clean success. Timeout and cancel both
|
|
445
|
+
// surface here as result.error and/or a stopping channel.
|
|
446
|
+
const kept = !!result.error || this._stoppingChannels.has(msgChannel);
|
|
447
|
+
if (kept) {
|
|
448
|
+
this._log(`mini-SWE-agent trajectory kept for debugging: ${trajectoryPath}`);
|
|
449
|
+
} else {
|
|
450
|
+
try { fs.rmSync(trajectoryPath, { force: true }); } catch {}
|
|
451
|
+
}
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
_spawnMini(cmd, msgChannel) {
|
|
456
|
+
return new Promise((resolve, reject) => {
|
|
457
|
+
const env = this._buildSubprocessEnv();
|
|
458
|
+
|
|
459
|
+
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
460
|
+
// stdin is /dev/null: mini never has a terminal to prompt against, so a
|
|
461
|
+
// stray confirm / first-run wizard hits EOF and exits instead of hanging.
|
|
462
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
463
|
+
env,
|
|
464
|
+
cwd: this.workingDir,
|
|
465
|
+
detached: !IS_WINDOWS,
|
|
466
|
+
windowsHide: true,
|
|
467
|
+
// Args are always an array (no shell string); the shared helper only
|
|
468
|
+
// opts into a shell for Windows .cmd/.bat shims so the daemon, adapter
|
|
469
|
+
// and launcher probe all agree on the rule.
|
|
470
|
+
shell: shouldUseShellForBinary(cmd[0]),
|
|
471
|
+
});
|
|
472
|
+
this._channelProcesses[msgChannel] = proc;
|
|
473
|
+
// Log the flags only — never the task text or any secret. The task is the
|
|
474
|
+
// last argv element; everything before it is safe to log.
|
|
475
|
+
this._log(`Running mini-SWE-agent: ${cmd.slice(0, cmd.indexOf('--task') + 1).map(String).join(' ')} <task>`);
|
|
476
|
+
|
|
477
|
+
// Guard the child's stdio streams against a benign post-SIGKILL 'error'
|
|
478
|
+
// (EPIPE/ECONNRESET) that would otherwise crash the process.
|
|
479
|
+
if (proc.stdout) proc.stdout.on('error', () => {});
|
|
480
|
+
if (proc.stderr) proc.stderr.on('error', () => {});
|
|
481
|
+
|
|
482
|
+
let stdoutBuf = '';
|
|
483
|
+
let stderrBuf = '';
|
|
484
|
+
let lineBuffer = '';
|
|
485
|
+
let streamCount = 0;
|
|
486
|
+
let pending = Promise.resolve();
|
|
487
|
+
|
|
488
|
+
let idleTimer = null;
|
|
489
|
+
const armIdle = () => {
|
|
490
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
491
|
+
idleTimer = setTimeout(() => {
|
|
492
|
+
this._log(`mini-SWE-agent produced no output for ${IDLE_TIMEOUT_MS / 1000}s — terminating`);
|
|
493
|
+
this._stopProcess(proc);
|
|
494
|
+
}, IDLE_TIMEOUT_MS);
|
|
495
|
+
};
|
|
496
|
+
armIdle();
|
|
497
|
+
|
|
498
|
+
if (proc.stderr) {
|
|
499
|
+
proc.stderr.on('data', (chunk) => { stderrBuf += chunk.toString('utf-8'); });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const handleLine = async (raw) => {
|
|
503
|
+
const stripped = raw.replace(ANSI_RE, '').trim();
|
|
504
|
+
if (!stripped) return;
|
|
505
|
+
if (streamCount < MAX_STREAM_UPDATES) {
|
|
506
|
+
streamCount += 1;
|
|
507
|
+
try { await this.sendThinking(msgChannel, stripped.slice(0, 500)); } catch {}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
proc.stdout.on('data', (chunk) => {
|
|
512
|
+
armIdle();
|
|
513
|
+
const s = chunk.toString('utf-8');
|
|
514
|
+
stdoutBuf += s;
|
|
515
|
+
lineBuffer += s;
|
|
516
|
+
const lines = lineBuffer.split('\n');
|
|
517
|
+
lineBuffer = lines.pop();
|
|
518
|
+
for (const line of lines) {
|
|
519
|
+
pending = pending.then(() => handleLine(line)).catch(() => {});
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
proc.on('exit', async (code) => {
|
|
524
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
525
|
+
if (lineBuffer) {
|
|
526
|
+
pending = pending.then(() => handleLine(lineBuffer)).catch(() => {});
|
|
527
|
+
}
|
|
528
|
+
try { await pending; } catch {}
|
|
529
|
+
delete this._channelProcesses[msgChannel];
|
|
530
|
+
|
|
531
|
+
// User asked to stop — swallow output, emit no final answer here (the
|
|
532
|
+
// stop handler already sent the "stopped" status).
|
|
533
|
+
if (this._stoppingChannels.has(msgChannel)) {
|
|
534
|
+
resolve({ text: '', error: null });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const stdoutText = cleanOutput(stdoutBuf);
|
|
539
|
+
const stderrText = cleanOutput(stderrBuf);
|
|
540
|
+
if (stderrText) this._log(`mini-SWE-agent stderr: ${stderrText.length} chars`);
|
|
541
|
+
|
|
542
|
+
if (code !== 0) {
|
|
543
|
+
let diagnostic = classifyError(stderrText, stdoutText);
|
|
544
|
+
if (!diagnostic) {
|
|
545
|
+
const tail = (stderrText || stdoutText).split('\n').filter(Boolean);
|
|
546
|
+
const detail = tail.length ? tail[tail.length - 1] : '';
|
|
547
|
+
diagnostic = `mini-SWE-agent exited with code ${code}.${detail ? ` ${redactDiagnostic(detail)}` : ''}`;
|
|
548
|
+
}
|
|
549
|
+
resolve({ text: '', error: diagnostic });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Exit 0: surface the cleaned transcript (tail-capped — mini's closing
|
|
554
|
+
// summary is at the end). Success ≠ "task definitely fixed", just that
|
|
555
|
+
// mini ran to completion.
|
|
556
|
+
let text = stdoutText;
|
|
557
|
+
if (text.length > FINAL_RESPONSE_MAX) {
|
|
558
|
+
text = `…(earlier output truncated)…\n\n${text.slice(-FINAL_RESPONSE_MAX)}`;
|
|
559
|
+
}
|
|
560
|
+
resolve({ text, error: null });
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
proc.on('error', (err) => {
|
|
564
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
565
|
+
delete this._channelProcesses[msgChannel];
|
|
566
|
+
reject(err);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
module.exports = MiniSweAgentAdapter;
|
package/src/adapters/openclaw.js
CHANGED
|
@@ -18,7 +18,7 @@ const { spawn, execSync } = require('child_process');
|
|
|
18
18
|
|
|
19
19
|
const BaseAdapter = require('./base');
|
|
20
20
|
const { formatAttachmentsForPrompt } = require('./utils');
|
|
21
|
-
const { buildOpenclawSkillMd, buildOpenclawSystemPrompt } = require('./workspace-prompt');
|
|
21
|
+
const { buildOpenclawSkillMd, buildOpenclawSystemPrompt, workspaceSkillName } = require('./workspace-prompt');
|
|
22
22
|
const { getRuntimePrefix } = require('../paths');
|
|
23
23
|
|
|
24
24
|
const IS_WINDOWS = process.platform === 'win32';
|
|
@@ -151,7 +151,44 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
const skillName =
|
|
154
|
+
const skillName = workspaceSkillName(this.agentName);
|
|
155
|
+
// Migration — pre-normalization versions installed the skill under the
|
|
156
|
+
// RAW agent name. For names the normalizer rewrites (uppercase,
|
|
157
|
+
// underscores, …) that leaves a stale directory with an outdated
|
|
158
|
+
// channel/token/endpoint that OpenClaw would keep auto-loading alongside
|
|
159
|
+
// the new one (`always: true` metadata). Remove the exact legacy dir when
|
|
160
|
+
// its name differs from the current one; for already-valid names the two
|
|
161
|
+
// coincide and nothing is deleted.
|
|
162
|
+
// The raw name feeds a recursive delete, so it must be provably inert
|
|
163
|
+
// first — agent names are not validated at creation, and a name carrying
|
|
164
|
+
// path separators or `..` would otherwise let the join escape the skills
|
|
165
|
+
// root (rmSync then destroys whatever it lands on). Only names the safe
|
|
166
|
+
// charset allows are considered, and the resolved target must still be a
|
|
167
|
+
// DIRECT child of the skills root. Legacy dirs from weirder names are
|
|
168
|
+
// left in place — stale beats deleting an unproven path.
|
|
169
|
+
const skillsRoot = path.join(wsDir, 'skills');
|
|
170
|
+
const legacyName = `openagents-workspace-${this.agentName}`;
|
|
171
|
+
const legacyDir = path.join(skillsRoot, legacyName);
|
|
172
|
+
if (
|
|
173
|
+
legacyName !== skillName &&
|
|
174
|
+
/^[A-Za-z0-9._-]+$/.test(String(this.agentName)) &&
|
|
175
|
+
path.dirname(path.resolve(legacyDir)) === path.resolve(skillsRoot)
|
|
176
|
+
) {
|
|
177
|
+
// Ownership proof — the path checks above cannot see filesystem
|
|
178
|
+
// aliasing (case-insensitive lookup on Windows/macOS, trailing-dot
|
|
179
|
+
// stripping on Windows): agent Foo's legacy path can be the SAME
|
|
180
|
+
// directory as agent foo's CURRENT skill. The stored SKILL.md embeds
|
|
181
|
+
// its owner's identity in a quote-delimited phrase, so delete only
|
|
182
|
+
// when that identity is exactly this agent; otherwise leave the dir.
|
|
183
|
+
let owned = false;
|
|
184
|
+
try {
|
|
185
|
+
const stored = fs.readFileSync(path.join(legacyDir, 'SKILL.md'), 'utf-8');
|
|
186
|
+
owned = stored.includes(`agent '${this.agentName}'`);
|
|
187
|
+
} catch {}
|
|
188
|
+
if (owned) {
|
|
189
|
+
try { fs.rmSync(legacyDir, { recursive: true, force: true }); } catch {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
155
192
|
const skillDir = path.join(wsDir, 'skills', skillName);
|
|
156
193
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
157
194
|
|