agentlas 0.7.0 → 0.9.2
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/CHANGELOG.md +199 -0
- package/README.md +161 -18
- package/bin/agentlas.cjs +8 -8
- package/engine/agentlas-core-harness.cjs +212 -0
- package/engine/agentlas-desktop-loadout.cjs +527 -0
- package/engine/agentlas-doctor.cjs +1 -1
- package/engine/agentlas-experience-exchange.cjs +835 -85
- package/engine/agentlas-experience-intake.cjs +444 -0
- package/engine/agentlas-experience-mcp.cjs +580 -18
- package/engine/agentlas-i18n.cjs +10 -10
- package/engine/agentlas-input.cjs +5 -4
- package/engine/agentlas-mcp-env.cjs +219 -0
- package/engine/agentlas-mcp-wrapper.cjs +51 -0
- package/engine/agentlas-memory-governance.cjs +1029 -0
- package/engine/agentlas-native-host.cjs +129 -39
- package/engine/agentlas-parity.cjs +339 -154
- package/engine/agentlas-repl.cjs +306 -31
- package/engine/agentlas-workforce.cjs +2991 -0
- package/engine/agentlas-workload-routing.cjs +523 -0
- package/engine/agentlas.cjs +1619 -234
- package/engine/bootstrap-schema.sql +1 -1
- package/engine/experience-taxonomy-v1.json +49 -0
- package/package.json +8 -4
- package/scripts/gen-bootstrap-schema.sh +0 -23
- package/test/bootstrap-race.cjs +0 -47
- package/test/capture-runtime-guard.cjs +0 -122
- package/test/cloud-asset-restore.cjs +0 -423
- package/test/cloud-cas-client.cjs +0 -333
- package/test/cloud-owner-restore.cjs +0 -183
- package/test/cloud-runtime-paths.cjs +0 -40
- package/test/cloud-save-publish.cjs +0 -487
- package/test/credential-env-regression.cjs +0 -52
- package/test/engine-hardening-regression.cjs +0 -74
- package/test/experience-exchange-contract.cjs +0 -569
- package/test/experience-mcp-contract.cjs +0 -391
- package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
- package/test/login-loopback-security.cjs +0 -115
- package/test/mcp-config-isolation.cjs +0 -36
- package/test/permission-mapping.cjs +0 -180
- package/test/route-regression.cjs +0 -357
- package/test/run-api-regression.cjs +0 -322
- package/test/runtime-env-protection.cjs +0 -89
- package/test/semver-precedence.cjs +0 -39
- package/test/smoke.sh +0 -93
- package/test/sqlite-driver-probe.cjs +0 -22
- package/test/terminal-ui-regression.cjs +0 -477
- package/test/timeout-regression.cjs +0 -218
- package/test/tool-workspace-boundary.cjs +0 -165
- package/test/update-safety.cjs +0 -376
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* agentlas-parity: 데스크탑 앱 전용이던 기능의 터미널 패리티 구현.
|
|
4
4
|
*
|
|
5
|
-
* storm — Hephaestus
|
|
5
|
+
* storm — Agentlas-owned Goal/UltraCode harness (Hephaestus route evidence + local parallel workers)
|
|
6
6
|
* swarm — emergent 에이전트 스웜 (블랙보드 + `## Spawn` 그래프 성장 + 종합)
|
|
7
7
|
* automation — 앱 스케줄러가 실행하는 자동화의 등록/목록/토글 (같은 SQLite)
|
|
8
8
|
* usage — 로컬 실행/자동화/세션 집계
|
|
@@ -18,6 +18,12 @@ const fs = require("node:fs");
|
|
|
18
18
|
const crypto = require("node:crypto");
|
|
19
19
|
const { spawn } = require("node:child_process");
|
|
20
20
|
const { Ui } = require("./agentlas-ui.cjs");
|
|
21
|
+
const workloadRouting = require("./agentlas-workload-routing.cjs");
|
|
22
|
+
const {
|
|
23
|
+
loadCoreStormbreakerHarness,
|
|
24
|
+
resolveCoreRuntimeRoot,
|
|
25
|
+
spawnCoreModule,
|
|
26
|
+
} = require("./agentlas-core-harness.cjs");
|
|
21
27
|
|
|
22
28
|
// ── 스웜 상수 (앱 mcp/swarm-run.ts 와 동일한 안전 상한) ──
|
|
23
29
|
const SWARM_MAX_TASKS = 24;
|
|
@@ -36,7 +42,7 @@ const MAX_LOGIN_SESSION_BYTES = 16 * 1024;
|
|
|
36
42
|
|
|
37
43
|
function createLoginState(randomBytes = crypto.randomBytes) {
|
|
38
44
|
const bytes = Buffer.from(randomBytes(32));
|
|
39
|
-
if (bytes.length !== 32) throw new Error("
|
|
45
|
+
if (bytes.length !== 32) throw new Error("Could not create the login state.");
|
|
40
46
|
return bytes.toString("base64url");
|
|
41
47
|
}
|
|
42
48
|
|
|
@@ -59,7 +65,7 @@ function createLoginCallbackGuard(expectedState) {
|
|
|
59
65
|
try {
|
|
60
66
|
url = new URL(String(rawUrl || "/"), "http://127.0.0.1");
|
|
61
67
|
} catch {
|
|
62
|
-
return { handled: true, final: false, ok: false, statusCode: 400, message: "
|
|
68
|
+
return { handled: true, final: false, ok: false, statusCode: 400, message: "Invalid login callback." };
|
|
63
69
|
}
|
|
64
70
|
if (url.pathname !== LOGIN_CALLBACK_PATH) {
|
|
65
71
|
return { handled: false, final: false, ok: false, statusCode: 404, message: "not found" };
|
|
@@ -68,7 +74,7 @@ function createLoginCallbackGuard(expectedState) {
|
|
|
68
74
|
return { handled: true, final: false, ok: false, statusCode: 405, message: "method not allowed" };
|
|
69
75
|
}
|
|
70
76
|
if (consumed) {
|
|
71
|
-
return { handled: true, final: false, ok: false, statusCode: 410, message: "
|
|
77
|
+
return { handled: true, final: false, ok: false, statusCode: 410, message: "Login callback has already been used." };
|
|
72
78
|
}
|
|
73
79
|
consumed = true;
|
|
74
80
|
|
|
@@ -78,7 +84,7 @@ function createLoginCallbackGuard(expectedState) {
|
|
|
78
84
|
final: true,
|
|
79
85
|
ok: false,
|
|
80
86
|
statusCode: 400,
|
|
81
|
-
message: "
|
|
87
|
+
message: "Login callback state validation failed. Run agentlas login again.",
|
|
82
88
|
};
|
|
83
89
|
}
|
|
84
90
|
const oauthError = url.searchParams.get("error");
|
|
@@ -89,7 +95,7 @@ function createLoginCallbackGuard(expectedState) {
|
|
|
89
95
|
final: true,
|
|
90
96
|
ok: false,
|
|
91
97
|
statusCode: 400,
|
|
92
|
-
message: `Agentlas
|
|
98
|
+
message: `Agentlas login denied: ${safeCode}`,
|
|
93
99
|
};
|
|
94
100
|
}
|
|
95
101
|
const value = url.searchParams.get("session") || url.searchParams.get("token") || "";
|
|
@@ -99,7 +105,7 @@ function createLoginCallbackGuard(expectedState) {
|
|
|
99
105
|
final: true,
|
|
100
106
|
ok: false,
|
|
101
107
|
statusCode: 400,
|
|
102
|
-
message: "
|
|
108
|
+
message: "The callback did not include a session value.",
|
|
103
109
|
};
|
|
104
110
|
}
|
|
105
111
|
if (Buffer.byteLength(value, "utf8") > MAX_LOGIN_SESSION_BYTES) {
|
|
@@ -108,10 +114,10 @@ function createLoginCallbackGuard(expectedState) {
|
|
|
108
114
|
final: true,
|
|
109
115
|
ok: false,
|
|
110
116
|
statusCode: 400,
|
|
111
|
-
message: "
|
|
117
|
+
message: "The login session value is too large.",
|
|
112
118
|
};
|
|
113
119
|
}
|
|
114
|
-
return { handled: true, final: true, ok: true, statusCode: 200, value, message: "Agentlas
|
|
120
|
+
return { handled: true, final: true, ok: true, statusCode: 200, value, message: "Agentlas login complete" };
|
|
115
121
|
},
|
|
116
122
|
isConsumed() { return consumed; },
|
|
117
123
|
};
|
|
@@ -130,7 +136,7 @@ function create(deps) {
|
|
|
130
136
|
process.env.HEPHAESTUS_BIN,
|
|
131
137
|
path.join(os.homedir(), ".agentlas", "runtime", "current", "bin", "hephaestus"),
|
|
132
138
|
];
|
|
133
|
-
for (const c of candidates) {
|
|
139
|
+
for (const c of process.platform === "win32" ? [] : candidates) {
|
|
134
140
|
try {
|
|
135
141
|
if (c && fs.existsSync(c)) {
|
|
136
142
|
fs.accessSync(c, fs.constants.X_OK);
|
|
@@ -138,15 +144,8 @@ function create(deps) {
|
|
|
138
144
|
}
|
|
139
145
|
} catch { /* 다음 후보 */ }
|
|
140
146
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (process.resourcesPath) roots.push(path.join(process.resourcesPath, "Hephaestus"));
|
|
144
|
-
if (process.platform === "darwin") roots.push("/Applications/Agentlas.app/Contents/Resources/Hephaestus");
|
|
145
|
-
for (const root of roots) {
|
|
146
|
-
try {
|
|
147
|
-
if (fs.existsSync(path.join(root, "agentlas_cloud", "__main__.py"))) return { kind: "python", root };
|
|
148
|
-
} catch { /* 다음 후보 */ }
|
|
149
|
-
}
|
|
147
|
+
const root = resolveCoreRuntimeRoot();
|
|
148
|
+
if (root) return { kind: "python", root };
|
|
150
149
|
return null;
|
|
151
150
|
}
|
|
152
151
|
|
|
@@ -179,26 +178,17 @@ function create(deps) {
|
|
|
179
178
|
return null;
|
|
180
179
|
}
|
|
181
180
|
|
|
182
|
-
const PY_BOOTSTRAP =
|
|
183
|
-
"import os, runpy, sys; " +
|
|
184
|
-
'cwd=os.getcwd(); root=os.environ["HEPHAESTUS_RUNTIME_ROOT"]; ' +
|
|
185
|
-
'sys.path=[p for p in sys.path if p not in ("", cwd, root)]; ' +
|
|
186
|
-
"sys.path.insert(0, root); " +
|
|
187
|
-
"sys.argv=sys.argv[1:]; " +
|
|
188
|
-
'runpy.run_module(sys.argv[0], run_name="__main__", alter_sys=True)';
|
|
189
|
-
|
|
190
181
|
function spawnHephaestus(args, opts) {
|
|
191
182
|
const found = hephaestusBin();
|
|
192
183
|
if (!found) return null;
|
|
193
184
|
if (found.kind === "bin") return spawn(found.exec, args, opts);
|
|
194
|
-
return
|
|
195
|
-
...opts,
|
|
196
|
-
env: { ...(opts && opts.env ? opts.env : process.env), HEPHAESTUS_RUNTIME_ROOT: found.root },
|
|
197
|
-
});
|
|
185
|
+
return spawnCoreModule("agentlas_cloud", args, opts, found.root);
|
|
198
186
|
}
|
|
199
187
|
|
|
200
|
-
// ── storm —
|
|
201
|
-
//
|
|
188
|
+
// ── storm — Agentlas 자체 Goal/UltraCode 하네스 ──
|
|
189
|
+
// Core owns the exact Goal/UltraCode prompt. Terminal supplies only host
|
|
190
|
+
// runtime inventory, worker context, and execution; no local prompt fallback.
|
|
191
|
+
// ctx: { ui?, cwd?, research?, background?, runtimeOverride? }
|
|
202
192
|
async function stormRun(db, goal, ctx = {}) {
|
|
203
193
|
const ui = ctx.ui || newUi();
|
|
204
194
|
goal = String(goal || "").trim();
|
|
@@ -207,40 +197,49 @@ function create(deps) {
|
|
|
207
197
|
return { ok: false };
|
|
208
198
|
}
|
|
209
199
|
if (goal.startsWith("-")) {
|
|
210
|
-
ui.error("goal
|
|
200
|
+
ui.error("goal cannot start with '-'.");
|
|
211
201
|
return { ok: false };
|
|
212
202
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
203
|
+
const cwd = ctx.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : D.runCwd());
|
|
204
|
+
let executionHarness;
|
|
205
|
+
try {
|
|
206
|
+
executionHarness = await loadCoreStormbreakerHarness(cwd);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
ui.error(`Stormbreaker Core harness unavailable: ${String((error && error.message) || error).slice(0, 400)}`);
|
|
209
|
+
return { ok: false, error: "stormbreaker-core-harness-unavailable" };
|
|
217
210
|
}
|
|
218
|
-
const
|
|
219
|
-
const args = ["route", goal, "--project", cwd, "--runtime", "terminal", "--auto-run"];
|
|
211
|
+
const args = ["route", goal, "--project", cwd, "--runtime", "terminal"];
|
|
220
212
|
if (ctx.research) args.push("--research-evidence");
|
|
221
|
-
if (ctx.background)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
213
|
+
if (ctx.background) {
|
|
214
|
+
ui.warn(ui.lang === "ko"
|
|
215
|
+
? "Agentlas 자체 Stormbreaker 하네스는 현재 포그라운드에서 실행합니다. 세션이 끝나도 영수증으로 재개 지점을 보존합니다."
|
|
216
|
+
: "The Agentlas-owned Stormbreaker harness currently runs in the foreground and preserves resume receipts.");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let result = { code: 0, stdout: "", stderr: "" };
|
|
220
|
+
if (hephaestusBin()) {
|
|
221
|
+
ui.beginTurn();
|
|
222
|
+
ui.startSpinner(ui.lang === "ko" ? "Stormbreaker 라우팅 근거 수집 중…" : "Stormbreaker gathering route evidence…");
|
|
223
|
+
result = await new Promise((resolve) => {
|
|
224
|
+
const child = spawnHephaestus(args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
225
|
+
let stdout = "";
|
|
226
|
+
let stderrTail = [];
|
|
227
|
+
child.stdout.on("data", (c) => { stdout += c.toString(); });
|
|
228
|
+
child.stderr.on("data", (c) => {
|
|
229
|
+
for (const ln of c.toString().split("\n")) {
|
|
230
|
+
const line = ln.trim();
|
|
231
|
+
if (!line) continue;
|
|
232
|
+
stderrTail.push(line);
|
|
233
|
+
if (stderrTail.length > 30) stderrTail.shift();
|
|
234
|
+
ui.updateSpinner(line.slice(0, 100));
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
child.on("error", (err) => resolve({ code: 1, stdout, stderr: String(err.message) }));
|
|
238
|
+
child.on("close", (code) => resolve({ code: code ?? 0, stdout, stderr: stderrTail.join("\n") }));
|
|
238
239
|
});
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
ui.stopSpinner();
|
|
243
|
-
ui.endTurn();
|
|
240
|
+
ui.stopSpinner();
|
|
241
|
+
ui.endTurn();
|
|
242
|
+
}
|
|
244
243
|
|
|
245
244
|
let json = null;
|
|
246
245
|
try {
|
|
@@ -249,6 +248,7 @@ function create(deps) {
|
|
|
249
248
|
if (s >= 0 && e > s) json = JSON.parse(result.stdout.slice(s, e + 1));
|
|
250
249
|
} catch { /* 비JSON 출력 */ }
|
|
251
250
|
|
|
251
|
+
let routeContext = "";
|
|
252
252
|
if (json) {
|
|
253
253
|
const action = json.action || json.route_action || (json.route_decision && json.route_decision.action) || json.status || "?";
|
|
254
254
|
ui.line("");
|
|
@@ -273,6 +273,11 @@ function create(deps) {
|
|
|
273
273
|
// 파이프라인 패킷 요약
|
|
274
274
|
const packets = json.execution_fabric && json.execution_fabric.packets;
|
|
275
275
|
if (Array.isArray(packets)) {
|
|
276
|
+
routeContext = packets.slice(0, 24).map((p) => {
|
|
277
|
+
const title = String(p.title || p.id || "packet").replace(/\s+/g, " ").slice(0, 160);
|
|
278
|
+
const card = p.card ? ` [agent:${String(p.card).slice(0, 100)}]` : "";
|
|
279
|
+
return `- ${title}${card}`;
|
|
280
|
+
}).join("\n");
|
|
276
281
|
for (const p of packets.slice(0, 12)) {
|
|
277
282
|
ui.line(" " + ui.c.emerald("▸ ") + ui.c.text(String(p.title || p.id || "packet")) + (p.card ? ui.c.dim(" " + p.card) : ""));
|
|
278
283
|
}
|
|
@@ -293,13 +298,29 @@ function create(deps) {
|
|
|
293
298
|
const raw = (result.stdout || result.stderr || "").trim();
|
|
294
299
|
if (raw) ui.markdown(raw.slice(0, 4000));
|
|
295
300
|
}
|
|
296
|
-
if (result.code !== 0 && !json)
|
|
297
|
-
|
|
301
|
+
if (result.code !== 0 && !json) {
|
|
302
|
+
ui.warn(`Hephaestus route evidence unavailable (${result.code}); Agentlas parent planner will continue from the original goal.`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const harnessResult = await swarmRun(db, goal, {
|
|
306
|
+
...ctx,
|
|
307
|
+
ui,
|
|
308
|
+
cwd,
|
|
309
|
+
runtimeOverride: ctx.runtimeOverride,
|
|
310
|
+
stormbreaker: true,
|
|
311
|
+
executionHarness,
|
|
312
|
+
routeContext,
|
|
313
|
+
});
|
|
314
|
+
return { ...harnessResult, routeDecision: json };
|
|
298
315
|
}
|
|
299
316
|
|
|
300
|
-
async function cmdStorm(db, args, runtimeOverride) {
|
|
317
|
+
async function cmdStorm(db, args, runtimeOverride, executionContext = {}) {
|
|
301
318
|
const rest = [];
|
|
302
|
-
const ctx = {
|
|
319
|
+
const ctx = {
|
|
320
|
+
...executionContext,
|
|
321
|
+
cwd: executionContext.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : D.runCwd()),
|
|
322
|
+
runtimeOverride,
|
|
323
|
+
};
|
|
303
324
|
for (let i = 0; i < args.length; i++) {
|
|
304
325
|
if (args[i] === "--research" || args[i] === "--research-evidence") ctx.research = true;
|
|
305
326
|
else if (args[i] === "--background") ctx.background = true;
|
|
@@ -333,14 +354,14 @@ function create(deps) {
|
|
|
333
354
|
const child =
|
|
334
355
|
found.kind === "bin"
|
|
335
356
|
? spawn(found.exec, isCareerGraph ? args.slice(1) : args, { cwd, stdio: "inherit" })
|
|
336
|
-
:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
357
|
+
: spawnCoreModule(moduleName, moduleArgs, { cwd, stdio: "inherit" }, found.root);
|
|
358
|
+
if (!child) {
|
|
359
|
+
process.stderr.write("Hephaestus failed: Python 3.9+ was not found.\n");
|
|
360
|
+
return Promise.resolve(1);
|
|
361
|
+
}
|
|
341
362
|
return new Promise((resolve) => {
|
|
342
363
|
child.on("error", (e) => {
|
|
343
|
-
process.stderr.write(`Hephaestus
|
|
364
|
+
process.stderr.write(`Hephaestus failed: ${e.message}\n`);
|
|
344
365
|
resolve(1);
|
|
345
366
|
});
|
|
346
367
|
child.on("close", (code) => resolve(code == null ? 0 : code));
|
|
@@ -371,12 +392,17 @@ function create(deps) {
|
|
|
371
392
|
}
|
|
372
393
|
|
|
373
394
|
// ── swarm — 앱 swarm-run.ts 프로토콜의 CLI 포트 ──
|
|
374
|
-
function swarmProtocol(goal, tasks, task) {
|
|
395
|
+
function swarmProtocol(goal, tasks, task, liveRuntimeInventory) {
|
|
375
396
|
const doneList = tasks
|
|
376
397
|
.filter((t) => t.status === "done")
|
|
377
398
|
.slice(-8)
|
|
378
399
|
.map((t) => `- ${t.title}`)
|
|
379
400
|
.join("\n");
|
|
401
|
+
const assignedList = tasks
|
|
402
|
+
.filter((t) => t.id !== task.id && t.status !== "failed")
|
|
403
|
+
.slice(0, 24)
|
|
404
|
+
.map((t) => `- [${t.status}] ${t.title}${t.brief ? ` — ${t.brief}` : ""}`)
|
|
405
|
+
.join("\n");
|
|
380
406
|
return [
|
|
381
407
|
"You are one worker in an EMERGENT AGENT SWARM collaborating on a shared goal.",
|
|
382
408
|
`SHARED GOAL: ${goal}`,
|
|
@@ -386,15 +412,18 @@ function create(deps) {
|
|
|
386
412
|
task.brief ? `- Details: ${task.brief}` : "",
|
|
387
413
|
"",
|
|
388
414
|
doneList ? `Already completed by peers (recent):\n${doneList}` : "No peer results yet — you may be first.",
|
|
415
|
+
assignedList ? `WORK ALREADY ASSIGNED TO PEERS (never duplicate these packets):\n${assignedList}` : "",
|
|
389
416
|
"",
|
|
390
417
|
"RULES:",
|
|
391
418
|
"1. Do your task concretely with available tools/files in the current working folder.",
|
|
419
|
+
`LIVE_RUNTIME_INVENTORY=${JSON.stringify(liveRuntimeInventory || [])}`,
|
|
392
420
|
"2. If the goal needs MORE work beyond your task — split into concrete next steps — end your",
|
|
393
|
-
" message with a `## Spawn` block
|
|
421
|
+
" message with a `## Spawn` block. Every child MUST be one JSON object with a higher-level AI allocation. Choose runtimeId and exactModelId only from LIVE_RUNTIME_INVENTORY:",
|
|
394
422
|
" ## Spawn",
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
"
|
|
423
|
+
' - {"role":"webmaster","brief":"build the landing page structure","allocation":{"schema":"agentlas.workload-allocation.v1","runtimeId":"runtime-1","exactModelId":"model-from-inventory","tier":"balanced","effort":"high","phase":"delegate","reasonCodes":["complex-reasoning"],"rationale":"requires coordinated implementation","requiredCapabilities":["code","tools"]}}',
|
|
424
|
+
' - {"brief":"run focused tests","allocation":{"schema":"agentlas.workload-allocation.v1","runtimeId":"runtime-2","exactModelId":"model-from-inventory","tier":"economy","effort":"low","phase":"delegate","reasonCodes":["bounded-scope"],"rationale":"bounded verification","requiredCapabilities":["code","tools"]}}',
|
|
425
|
+
" Choose each exact runtime/model and effort from the actual child difficulty; do not copy one allocation to every child.",
|
|
426
|
+
" (role is optional. Do NOT spawn if the goal is already met or another pending/running/done packet already owns that work.)",
|
|
398
427
|
"3. Do NOT restate the whole goal. Do NOT invent work that isn't needed — over-spawning wastes the user's money.",
|
|
399
428
|
"4. Everything above the `## Spawn` block is your result and is shared with peers on the blackboard.",
|
|
400
429
|
]
|
|
@@ -415,6 +444,23 @@ function create(deps) {
|
|
|
415
444
|
continue;
|
|
416
445
|
}
|
|
417
446
|
const body = line.replace(/^-\s*/, "");
|
|
447
|
+
if (body.startsWith("{")) {
|
|
448
|
+
const item = workloadRouting.extractJsonObject(body);
|
|
449
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
450
|
+
const brief = String(item.brief || "").trim();
|
|
451
|
+
const allocation = workloadRouting.normalizeAllocation(item.allocation || item, "delegate");
|
|
452
|
+
if (brief) {
|
|
453
|
+
spawn.push({
|
|
454
|
+
title: String(item.title || brief).trim().slice(0, 80),
|
|
455
|
+
brief: brief.slice(0, 8_000),
|
|
456
|
+
role: item.role ? String(item.role).trim().slice(0, 80) : undefined,
|
|
457
|
+
allocation,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (spawn.length >= SWARM_SPAWN_PER_TURN) break;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
418
464
|
const parts = body.split("|");
|
|
419
465
|
let role;
|
|
420
466
|
let brief;
|
|
@@ -424,7 +470,9 @@ function create(deps) {
|
|
|
424
470
|
} else {
|
|
425
471
|
brief = body.trim();
|
|
426
472
|
}
|
|
427
|
-
|
|
473
|
+
// Legacy text remains parseable, but it has no AI-authored allocation and
|
|
474
|
+
// therefore runs on the current model with an observable fallback receipt.
|
|
475
|
+
if (brief) spawn.push({ title: brief.slice(0, 80), brief, role, allocation: null });
|
|
428
476
|
if (spawn.length >= SWARM_SPAWN_PER_TURN) break;
|
|
429
477
|
}
|
|
430
478
|
return { result, spawn };
|
|
@@ -439,8 +487,23 @@ function create(deps) {
|
|
|
439
487
|
return { ok: false };
|
|
440
488
|
}
|
|
441
489
|
const runtime = ctx.runtime || D.resolveRuntime(db, ctx.runtimeOverride);
|
|
490
|
+
const stormbreaker = ctx.stormbreaker === true;
|
|
491
|
+
const executionHarness = stormbreaker ? ctx.executionHarness : null;
|
|
492
|
+
if (stormbreaker && (!executionHarness || typeof executionHarness.system_prompt !== "string")) {
|
|
493
|
+
ui.error("Stormbreaker requires the canonical Goal + UltraCode harness from Agentlas Core.");
|
|
494
|
+
return { ok: false, error: "stormbreaker-core-harness-unavailable" };
|
|
495
|
+
}
|
|
496
|
+
const coreHarnessPrompt = executionHarness && executionHarness.system_prompt;
|
|
442
497
|
const permission = ctx.permission || "write";
|
|
443
|
-
const
|
|
498
|
+
const discoveredRuntimes = ctx.runtimes && ctx.runtimes.length
|
|
499
|
+
? ctx.runtimes
|
|
500
|
+
: typeof D.listAvailableRuntimes === "function"
|
|
501
|
+
? D.listAvailableRuntimes(db, runtime)
|
|
502
|
+
: [runtime];
|
|
503
|
+
const runtimes = discoveredRuntimes
|
|
504
|
+
.map((candidate, index) => ({ ...candidate, runtimeId: candidate.runtimeId || `runtime-${index + 1}` }));
|
|
505
|
+
const liveRuntimeInventory = workloadRouting.runtimeInventory(runtimes);
|
|
506
|
+
const cwd = ctx.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : D.runCwd());
|
|
444
507
|
const concurrency = Math.max(1, Math.min(8, Number(ctx.concurrency) || 3));
|
|
445
508
|
const env = await D.buildChildEnvCli(db, {
|
|
446
509
|
projectPath: ctx.projectPath || null,
|
|
@@ -450,22 +513,125 @@ function create(deps) {
|
|
|
450
513
|
lang: ui.lang,
|
|
451
514
|
});
|
|
452
515
|
|
|
453
|
-
async function
|
|
516
|
+
async function runBaseWorker(system, prompt) {
|
|
454
517
|
if (runtime.mode === "cli") {
|
|
455
|
-
return await D.captureRuntime(runtime.kind, system, prompt, {
|
|
518
|
+
return await D.captureRuntime(runtime.kind, system, prompt, {
|
|
519
|
+
cwd,
|
|
520
|
+
env,
|
|
521
|
+
permission,
|
|
522
|
+
model: ctx.modelPin || runtime.model || null,
|
|
523
|
+
effort: ctx.effortPin === undefined ? null : ctx.effortPin,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const text = await D.runApi(runtime.backend, ctx.modelPin || runtime.model, system, prompt);
|
|
527
|
+
return typeof text === "string" ? text : (text && text.text) || "";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function recordAllocation(task, stage, decision, resolution, parentTaskId = null) {
|
|
531
|
+
const receipt = workloadRouting.createDecisionReceipt({
|
|
532
|
+
taskId: `${stage}-${task.id || "synthesis"}`,
|
|
533
|
+
parentTaskId,
|
|
534
|
+
taskText: task.brief || task.title || goal,
|
|
535
|
+
stage,
|
|
536
|
+
decision,
|
|
537
|
+
resolution,
|
|
538
|
+
});
|
|
539
|
+
try {
|
|
540
|
+
workloadRouting.appendDecisionReceipt(
|
|
541
|
+
receipt,
|
|
542
|
+
ctx.receiptFile || (D.modelRoutingReceiptPath && D.modelRoutingReceiptPath()),
|
|
543
|
+
);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
ui.warn(`model routing receipt failed: ${String((error && error.message) || error).slice(0, 120)}`);
|
|
546
|
+
}
|
|
547
|
+
return receipt;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function runAllocatedWorker(system, prompt, task, stage, parentTaskId = null) {
|
|
551
|
+
const resolution = workloadRouting.resolveAllocationAcrossRuntimes({
|
|
552
|
+
runtimes,
|
|
553
|
+
fallbackRuntime: runtime,
|
|
554
|
+
decision: task.allocation,
|
|
555
|
+
modelPin: ctx.modelPin,
|
|
556
|
+
effortPin: ctx.effortPin,
|
|
557
|
+
availableModels: ctx.availableModels,
|
|
558
|
+
maxTier: ctx.maxTier || process.env.AGENTLAS_MODEL_MAX_TIER,
|
|
559
|
+
});
|
|
560
|
+
recordAllocation(task, stage, task.allocation, resolution, parentTaskId);
|
|
561
|
+
if (!resolution.ok) {
|
|
562
|
+
throw new Error(`model allocation failed closed: ${resolution.fallbackReason || "no compliant live model"}`);
|
|
563
|
+
}
|
|
564
|
+
if (resolution.fallbackReason) {
|
|
565
|
+
ui.info(`model route: ${resolution.source} · ${resolution.runtimeId || "current"} · ${resolution.model || runtime.kind || runtime.backend} · ${resolution.fallbackReason}`);
|
|
566
|
+
}
|
|
567
|
+
const selectedRuntime = resolution.runtime || runtime;
|
|
568
|
+
task.resolvedAllocation = {
|
|
569
|
+
runtimeId: resolution.runtimeId || selectedRuntime.runtimeId || null,
|
|
570
|
+
runtimeKind: selectedRuntime.kind || selectedRuntime.backend || null,
|
|
571
|
+
model: resolution.model || selectedRuntime.model || null,
|
|
572
|
+
effort: resolution.effort ?? null,
|
|
573
|
+
source: resolution.source,
|
|
574
|
+
fallbackReason: resolution.fallbackReason || null,
|
|
575
|
+
};
|
|
576
|
+
if (selectedRuntime.mode === "cli") {
|
|
577
|
+
return await D.captureRuntime(selectedRuntime.kind, system, prompt, {
|
|
578
|
+
cwd,
|
|
579
|
+
env,
|
|
580
|
+
permission,
|
|
581
|
+
model: resolution.model,
|
|
582
|
+
effort: resolution.effort,
|
|
583
|
+
});
|
|
456
584
|
}
|
|
457
|
-
const text = await D.runApi(
|
|
585
|
+
const text = await D.runApi(selectedRuntime.backend, resolution.model || selectedRuntime.model, system, prompt);
|
|
458
586
|
return typeof text === "string" ? text : (text && text.text) || "";
|
|
459
587
|
}
|
|
460
588
|
|
|
461
589
|
const label = runtime.mode === "cli" ? runtime.kind : runtime.backend;
|
|
462
590
|
ui.line("");
|
|
463
|
-
ui.line(ui.c.paw("◤ ") + ui.c.bold(ui.c.text("swarm")) + ui.c.dim(` ${label} · x${concurrency} · max ${SWARM_MAX_TASKS} tasks`));
|
|
591
|
+
ui.line(ui.c.paw("◤ ") + ui.c.bold(ui.c.text(stormbreaker ? "stormbreaker" : "swarm")) + ui.c.dim(` Agentlas harness · ${label} · x${concurrency} · max ${SWARM_MAX_TASKS} tasks`));
|
|
464
592
|
ui.info(goal.slice(0, 120));
|
|
465
593
|
|
|
594
|
+
ui.startSpinner(ui.lang === "ko" ? "상위 AI가 작업별 모델 비용을 배정 중…" : "Higher-level AI is allocating task models…");
|
|
595
|
+
let planned = null;
|
|
596
|
+
try {
|
|
597
|
+
const plannerText = await runBaseWorker(
|
|
598
|
+
[
|
|
599
|
+
coreHarnessPrompt,
|
|
600
|
+
workloadRouting.plannerSystemPrompt({
|
|
601
|
+
language: ui.lang === "ko" ? "Korean" : "English",
|
|
602
|
+
maxTasks: Math.min(SWARM_SPAWN_PER_TURN, SWARM_MAX_TASKS),
|
|
603
|
+
mode: stormbreaker ? "stormbreaker-goal-ultracode" : "swarm",
|
|
604
|
+
liveRuntimeInventory,
|
|
605
|
+
}),
|
|
606
|
+
].filter(Boolean).join("\n\n"),
|
|
607
|
+
ctx.routeContext
|
|
608
|
+
? `${goal}\n\nHEPHAESTUS ROUTE EVIDENCE (advisory; the Agentlas parent owns the final plan):\n${ctx.routeContext}`
|
|
609
|
+
: goal,
|
|
610
|
+
);
|
|
611
|
+
planned = workloadRouting.normalizePlan(plannerText, { maxTasks: SWARM_SPAWN_PER_TURN });
|
|
612
|
+
} catch (error) {
|
|
613
|
+
ui.warn(`workload planner failed: ${String((error && error.message) || error).slice(0, 160)}`);
|
|
614
|
+
}
|
|
615
|
+
ui.stopSpinner();
|
|
616
|
+
if (!planned) ui.warn(ui.lang === "ko" ? "모델 배정 JSON이 유효하지 않아 현재 모델로 투명하게 폴백합니다." : "Invalid allocation JSON; transparently falling back to the current model.");
|
|
617
|
+
if (planned) {
|
|
618
|
+
ui.line("");
|
|
619
|
+
ui.info(stormbreaker
|
|
620
|
+
? (ui.lang === "ko" ? "Stormbreaker Goal/UltraCode 실행 계획:" : "Stormbreaker Goal/UltraCode execution plan:")
|
|
621
|
+
: (ui.lang === "ko" ? "스웜 실행 계획:" : "Swarm execution plan:"));
|
|
622
|
+
for (const task of planned.tasks) {
|
|
623
|
+
const allocation = task.allocation;
|
|
624
|
+
ui.line(` ${ui.c.emerald("▸ ")}${ui.c.text(task.title)}${ui.c.dim(` ${allocation.runtimeId || "current"} · ${allocation.exactModelId || allocation.tier} · ${allocation.effort}`)}`);
|
|
625
|
+
}
|
|
626
|
+
ui.line(` ${ui.c.emerald("◆ ")}${ui.c.text("synthesis")}${ui.c.dim(` ${planned.synthesis.runtimeId || "current"} · ${planned.synthesis.exactModelId || planned.synthesis.tier} · ${planned.synthesis.effort}`)}`);
|
|
627
|
+
}
|
|
628
|
+
|
|
466
629
|
let seq = 0;
|
|
467
|
-
const
|
|
468
|
-
|
|
630
|
+
const initialTasks = planned
|
|
631
|
+
? planned.tasks
|
|
632
|
+
: [{ title: goal.slice(0, 80), brief: goal, role: undefined, allocation: null }];
|
|
633
|
+
const tasks = initialTasks.map((task) => ({ id: ++seq, ...task, status: "pending", result: "", parentTaskId: null }));
|
|
634
|
+
const seen = new Set(tasks.map((task) => task.title.toLowerCase()));
|
|
469
635
|
let active = 0;
|
|
470
636
|
let failed = 0;
|
|
471
637
|
|
|
@@ -478,17 +644,23 @@ function create(deps) {
|
|
|
478
644
|
task.status = "running";
|
|
479
645
|
active++;
|
|
480
646
|
ui.tool(`⚑ ${task.title}` + (task.role ? ` (${task.role})` : ""));
|
|
481
|
-
|
|
647
|
+
runAllocatedWorker(
|
|
648
|
+
[coreHarnessPrompt, swarmProtocol(goal, tasks, task, liveRuntimeInventory)].filter(Boolean).join("\n\n"),
|
|
649
|
+
task.brief || task.title,
|
|
650
|
+
task,
|
|
651
|
+
"worker",
|
|
652
|
+
task.parentTaskId,
|
|
653
|
+
)
|
|
482
654
|
.then((text) => {
|
|
483
655
|
const parsed = parseSwarmOutput(text);
|
|
484
656
|
task.status = "done";
|
|
485
657
|
task.result = parsed.result;
|
|
486
|
-
ui.toolResult(parsed.result.split("\n").slice(0, 3).join("\n") || "(
|
|
658
|
+
ui.toolResult(parsed.result.split("\n").slice(0, 3).join("\n") || "(empty result)", true);
|
|
487
659
|
for (const s of parsed.spawn) {
|
|
488
660
|
const key = s.title.toLowerCase();
|
|
489
661
|
if (tasks.length >= SWARM_MAX_TASKS || seen.has(key)) continue;
|
|
490
662
|
seen.add(key);
|
|
491
|
-
tasks.push({ id: ++seq, title: s.title, brief: s.brief, role: s.role, status: "pending", result: "" });
|
|
663
|
+
tasks.push({ id: ++seq, title: s.title, brief: s.brief, role: s.role, allocation: s.allocation, status: "pending", result: "", parentTaskId: `worker-${task.id}` });
|
|
492
664
|
ui.info(`+ spawn: ${s.title}`);
|
|
493
665
|
}
|
|
494
666
|
})
|
|
@@ -515,11 +687,22 @@ function create(deps) {
|
|
|
515
687
|
}
|
|
516
688
|
|
|
517
689
|
ui.startSpinner(ui.lang === "ko" ? "스웜 결과 종합 중…" : "Synthesizing swarm results…");
|
|
518
|
-
const pieces = done.map((t, i) =>
|
|
690
|
+
const pieces = done.map((t, i) => [
|
|
691
|
+
`### ${i + 1}. ${t.title}`,
|
|
692
|
+
`HOST-VERIFIED ALLOCATION: ${JSON.stringify(t.resolvedAllocation || null)}`,
|
|
693
|
+
t.result,
|
|
694
|
+
].join("\n")).join("\n\n");
|
|
519
695
|
let finalText;
|
|
520
696
|
try {
|
|
521
|
-
|
|
697
|
+
const synthesisTask = {
|
|
698
|
+
id: "final",
|
|
699
|
+
title: "swarm synthesis",
|
|
700
|
+
brief: goal,
|
|
701
|
+
allocation: planned && planned.synthesis,
|
|
702
|
+
};
|
|
703
|
+
finalText = await runAllocatedWorker(
|
|
522
704
|
[
|
|
705
|
+
coreHarnessPrompt,
|
|
523
706
|
"You are the synthesizer of an agent swarm. Below are the results your peers produced for the shared goal.",
|
|
524
707
|
"Integrate them into ONE coherent final answer for the user. Reconcile overlaps, note anything incomplete.",
|
|
525
708
|
"Do not just concatenate. Do not include a `## Spawn` block.",
|
|
@@ -527,10 +710,12 @@ function create(deps) {
|
|
|
527
710
|
`Answer in the user's language (${ui.lang === "ko" ? "Korean" : "English"}).`,
|
|
528
711
|
].join("\n"),
|
|
529
712
|
pieces,
|
|
713
|
+
synthesisTask,
|
|
714
|
+
"synthesis",
|
|
530
715
|
);
|
|
531
716
|
} catch (e) {
|
|
532
717
|
ui.stopSpinner();
|
|
533
|
-
ui.error("
|
|
718
|
+
ui.error("Synthesis failed: " + String((e && e.message) || e).slice(0, 200));
|
|
534
719
|
finalText = pieces;
|
|
535
720
|
}
|
|
536
721
|
ui.stopSpinner();
|
|
@@ -539,14 +724,14 @@ function create(deps) {
|
|
|
539
724
|
return { ok: true, finalText, taskCount: tasks.length, doneCount: done.length };
|
|
540
725
|
}
|
|
541
726
|
|
|
542
|
-
async function cmdSwarm(db, args, runtimeOverride) {
|
|
727
|
+
async function cmdSwarm(db, args, runtimeOverride, executionContext = {}) {
|
|
543
728
|
const rest = [];
|
|
544
729
|
let concurrency;
|
|
545
730
|
for (let i = 0; i < args.length; i++) {
|
|
546
731
|
if (args[i] === "--parallel" || args[i] === "-n") concurrency = Number(args[++i]);
|
|
547
732
|
else rest.push(args[i]);
|
|
548
733
|
}
|
|
549
|
-
const r = await swarmRun(db, rest.join(" "), { concurrency, runtimeOverride });
|
|
734
|
+
const r = await swarmRun(db, rest.join(" "), { ...executionContext, concurrency, runtimeOverride });
|
|
550
735
|
if (!r.ok) process.exitCode = 1;
|
|
551
736
|
}
|
|
552
737
|
|
|
@@ -716,7 +901,7 @@ function create(deps) {
|
|
|
716
901
|
const rows = db.prepare(
|
|
717
902
|
"SELECT id, name, schedule, target_type, target_id, enabled, next_run_at, last_run_at, run_count, trigger_type FROM automations ORDER BY created_at DESC",
|
|
718
903
|
).all();
|
|
719
|
-
if (!rows.length) return D.out("
|
|
904
|
+
if (!rows.length) return D.out("No automations. Run agentlas automation add --help.");
|
|
720
905
|
for (const r of rows) {
|
|
721
906
|
const target = r.target_type + ":" + String(r.target_id).slice(0, 24);
|
|
722
907
|
D.out(
|
|
@@ -726,8 +911,8 @@ function create(deps) {
|
|
|
726
911
|
);
|
|
727
912
|
}
|
|
728
913
|
D.out("");
|
|
729
|
-
D.out("
|
|
730
|
-
D.out("
|
|
914
|
+
D.out("Run now: agentlas automation run <id> · daemon: agentlas automation daemon");
|
|
915
|
+
D.out("The Desktop scheduler also runs when the app is open; leases prevent duplicate runs.");
|
|
731
916
|
return;
|
|
732
917
|
}
|
|
733
918
|
|
|
@@ -745,7 +930,7 @@ function create(deps) {
|
|
|
745
930
|
else if (a === "--disabled") flags.disabled = true;
|
|
746
931
|
}
|
|
747
932
|
if (!flags.cron || !flags.prompt || (!flags.agent && !flags.firm)) {
|
|
748
|
-
D.out('usage: agentlas automation add --name "
|
|
933
|
+
D.out('usage: agentlas automation add --name "name" --agent <slug>|--firm <slug> --cron "0 9 * * *" --prompt "instructions" [--tz Asia/Seoul] [--disabled]');
|
|
749
934
|
process.exitCode = 1;
|
|
750
935
|
return;
|
|
751
936
|
}
|
|
@@ -754,19 +939,19 @@ function create(deps) {
|
|
|
754
939
|
let targetLabel;
|
|
755
940
|
if (flags.agent) {
|
|
756
941
|
const a = D.resolveAgent(db, flags.agent);
|
|
757
|
-
if (!a) return D.fail(
|
|
942
|
+
if (!a) return D.fail(`Agent not found: ${flags.agent}`);
|
|
758
943
|
targetType = "agent";
|
|
759
944
|
targetId = a.id;
|
|
760
945
|
targetLabel = a.name;
|
|
761
946
|
} else {
|
|
762
947
|
const f = D.resolveFirm(db, flags.firm);
|
|
763
|
-
if (!f) return D.fail(
|
|
948
|
+
if (!f) return D.fail(`Company not found: ${flags.firm}`);
|
|
764
949
|
targetType = "firm";
|
|
765
950
|
targetId = f.id;
|
|
766
951
|
targetLabel = f.name;
|
|
767
952
|
}
|
|
768
953
|
const next = nextCronRun(flags.cron, new Date(), flags.tz || null);
|
|
769
|
-
if (!next) return D.fail(`
|
|
954
|
+
if (!next) return D.fail(`Could not parse cron expression: "${flags.cron}" (5 fields: minute hour day month weekday)`);
|
|
770
955
|
const id = crypto.randomUUID();
|
|
771
956
|
db.prepare(
|
|
772
957
|
`INSERT INTO automations (id, name, schedule, target_type, target_id, prompt_template, enabled, created_by,
|
|
@@ -774,7 +959,7 @@ function create(deps) {
|
|
|
774
959
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)`,
|
|
775
960
|
).run(
|
|
776
961
|
id,
|
|
777
|
-
flags.name || `${targetLabel}
|
|
962
|
+
flags.name || `${targetLabel} automation`,
|
|
778
963
|
flags.cron,
|
|
779
964
|
targetType,
|
|
780
965
|
targetId,
|
|
@@ -788,8 +973,8 @@ function create(deps) {
|
|
|
788
973
|
"auto",
|
|
789
974
|
"hub-allowed",
|
|
790
975
|
);
|
|
791
|
-
D.out(
|
|
792
|
-
D.out(
|
|
976
|
+
D.out(`Created: ${id.slice(0, 8)} ${flags.name || targetLabel} next=${next.toISOString().slice(0, 16)}`);
|
|
977
|
+
D.out(`Run now: agentlas automation run ${id.slice(0, 8)} · scheduled: agentlas automation daemon (or Desktop)`);
|
|
793
978
|
return;
|
|
794
979
|
}
|
|
795
980
|
|
|
@@ -797,14 +982,14 @@ function create(deps) {
|
|
|
797
982
|
const idPrefix = args[1];
|
|
798
983
|
if (!idPrefix) return D.fail(`usage: agentlas automation ${sub} <id>`);
|
|
799
984
|
const row = db.prepare("SELECT id, name, schedule, schedule_json, timezone FROM automations WHERE id LIKE ?").get(idPrefix + "%");
|
|
800
|
-
if (!row) return D.fail(
|
|
985
|
+
if (!row) return D.fail(`Automation not found: ${idPrefix}`);
|
|
801
986
|
if (sub === "on") {
|
|
802
987
|
const next = nextAutomationRun(row) || null;
|
|
803
988
|
db.prepare("UPDATE automations SET enabled=1, next_run_at=? WHERE id=?").run(next ? next.toISOString() : null, row.id);
|
|
804
989
|
} else {
|
|
805
990
|
db.prepare("UPDATE automations SET enabled=0 WHERE id=?").run(row.id);
|
|
806
991
|
}
|
|
807
|
-
D.out(`${sub === "on" ? "
|
|
992
|
+
D.out(`${sub === "on" ? "Enabled" : "Disabled"}: ${row.id.slice(0, 8)} ${row.name}`);
|
|
808
993
|
return;
|
|
809
994
|
}
|
|
810
995
|
|
|
@@ -812,9 +997,9 @@ function create(deps) {
|
|
|
812
997
|
const idPrefix = args[1];
|
|
813
998
|
if (!idPrefix) return D.fail("usage: agentlas automation remove <id>");
|
|
814
999
|
const row = db.prepare("SELECT id, name FROM automations WHERE id LIKE ?").get(idPrefix + "%");
|
|
815
|
-
if (!row) return D.fail(
|
|
1000
|
+
if (!row) return D.fail(`Automation not found: ${idPrefix}`);
|
|
816
1001
|
db.prepare("DELETE FROM automations WHERE id=?").run(row.id);
|
|
817
|
-
D.out(
|
|
1002
|
+
D.out(`Deleted: ${row.id.slice(0, 8)} ${row.name}`);
|
|
818
1003
|
return;
|
|
819
1004
|
}
|
|
820
1005
|
|
|
@@ -824,10 +1009,10 @@ function create(deps) {
|
|
|
824
1009
|
LEFT JOIN automations a ON a.id = h.automation_id
|
|
825
1010
|
ORDER BY h.ran_at DESC LIMIT 15`,
|
|
826
1011
|
).all();
|
|
827
|
-
if (!rows.length) return D.out("
|
|
1012
|
+
if (!rows.length) return D.out("No run history.");
|
|
828
1013
|
for (const r of rows) {
|
|
829
1014
|
D.out(
|
|
830
|
-
`${(r.ran_at || "").slice(0, 16).padEnd(17)} ${(r.status || "?").padEnd(9)} ${(r.name || "(
|
|
1015
|
+
`${(r.ran_at || "").slice(0, 16).padEnd(17)} ${(r.status || "?").padEnd(9)} ${(r.name || "(deleted)").slice(0, 30).padEnd(31)} ${r.error ? String(r.error).slice(0, 40) : ""}`,
|
|
831
1016
|
);
|
|
832
1017
|
}
|
|
833
1018
|
return;
|
|
@@ -837,7 +1022,7 @@ function create(deps) {
|
|
|
837
1022
|
const idPrefix = args[1];
|
|
838
1023
|
if (!idPrefix) return D.fail("usage: agentlas automation run <id>");
|
|
839
1024
|
const row = db.prepare("SELECT * FROM automations WHERE id LIKE ?").get(idPrefix + "%");
|
|
840
|
-
if (!row) return D.fail(
|
|
1025
|
+
if (!row) return D.fail(`Automation not found: ${idPrefix}`);
|
|
841
1026
|
const ui = newUi();
|
|
842
1027
|
// run-now는 스케줄을 건드리지 않는다 (앱의 advanceSchedule=false와 동일).
|
|
843
1028
|
const r = await runAutomationOnce(db, row, { ui, advanceSchedule: false, runtimeOverride });
|
|
@@ -888,7 +1073,7 @@ function create(deps) {
|
|
|
888
1073
|
const ui = ctx.ui || newUi();
|
|
889
1074
|
// run-now도 리스를 잡는다 — 앱 스케줄러가 같은 행을 동시에 돌리는 것을 방지.
|
|
890
1075
|
if (!claimAutomation(db, row.id)) {
|
|
891
|
-
ui.warn(
|
|
1076
|
+
ui.warn(`Another runner holds this automation (lease TTL 15 minutes): ${row.name}`);
|
|
892
1077
|
return { ok: false, skipped: true };
|
|
893
1078
|
}
|
|
894
1079
|
ui.line("");
|
|
@@ -900,11 +1085,11 @@ function create(deps) {
|
|
|
900
1085
|
let agentId = null;
|
|
901
1086
|
if (row.target_type === "firm") {
|
|
902
1087
|
const firm = db.prepare("SELECT * FROM firms WHERE id = ?").get(row.target_id);
|
|
903
|
-
if (!firm) throw new Error(
|
|
1088
|
+
if (!firm) throw new Error(`Company not found: ${row.target_id}`);
|
|
904
1089
|
system = D.firmSystemPrompt(db, firm);
|
|
905
1090
|
} else {
|
|
906
1091
|
const agent = db.prepare("SELECT * FROM installed_agents WHERE id = ?").get(row.target_id);
|
|
907
|
-
if (!agent) throw new Error(
|
|
1092
|
+
if (!agent) throw new Error(`Agent not found: ${row.target_id}`);
|
|
908
1093
|
system = agent.system_prompt || `You are ${agent.name}.`;
|
|
909
1094
|
agentId = agent.id;
|
|
910
1095
|
}
|
|
@@ -972,7 +1157,7 @@ function create(deps) {
|
|
|
972
1157
|
process.on("SIGTERM", () => { stopping = true; });
|
|
973
1158
|
|
|
974
1159
|
ui.line("");
|
|
975
|
-
ui.ok(`automation daemon — ${intervalSec}s
|
|
1160
|
+
ui.ok(`automation daemon — polling every ${intervalSec}s · owner ${AUTOMATION_LEASE_OWNER}`);
|
|
976
1161
|
ui.info(ui.lang === "ko" ? "Ctrl-C로 종료. (데스크탑 앱 스케줄러와 리스를 공유해 중복 실행되지 않습니다.)" : "Ctrl-C to stop.");
|
|
977
1162
|
|
|
978
1163
|
while (!stopping) {
|
|
@@ -983,7 +1168,7 @@ function create(deps) {
|
|
|
983
1168
|
"SELECT * FROM automations WHERE enabled = 1 AND trigger_type = 'schedule' AND next_run_at IS NOT NULL AND next_run_at <= ? ORDER BY next_run_at ASC LIMIT 5",
|
|
984
1169
|
).all(nowIso);
|
|
985
1170
|
} catch (e) {
|
|
986
|
-
ui.error("due
|
|
1171
|
+
ui.error("Failed to query due automations: " + String((e && e.message) || e));
|
|
987
1172
|
}
|
|
988
1173
|
for (const row of due) {
|
|
989
1174
|
if (stopping) break;
|
|
@@ -1007,12 +1192,12 @@ function create(deps) {
|
|
|
1007
1192
|
try {
|
|
1008
1193
|
rows = db.prepare("SELECT id, name, name_en, transport, enabled FROM mcp_servers ORDER BY installed_at ASC").all();
|
|
1009
1194
|
} catch { /* 테이블 없음 */ }
|
|
1010
|
-
if (!rows.length) return D.out("
|
|
1195
|
+
if (!rows.length) return D.out("No MCP servers are installed. Configure them in Desktop or an agent package.");
|
|
1011
1196
|
for (const r of rows) {
|
|
1012
1197
|
D.out(`${r.enabled ? "●" : "○"} ${String(r.name || r.name_en || r.id).padEnd(28).slice(0, 28)} ${String(r.transport || "stdio").padEnd(8)} ${String(r.id).slice(0, 12)}`);
|
|
1013
1198
|
}
|
|
1014
1199
|
D.out("");
|
|
1015
|
-
D.out("full
|
|
1200
|
+
D.out("Only full-access turns wire active (●) stdio servers into the runtime. In the REPL, use /mcp.");
|
|
1016
1201
|
}
|
|
1017
1202
|
|
|
1018
1203
|
function cmdChats(db, args) {
|
|
@@ -1026,12 +1211,12 @@ function create(deps) {
|
|
|
1026
1211
|
ORDER BY c.updated_at DESC LIMIT ?`,
|
|
1027
1212
|
).all(limit);
|
|
1028
1213
|
} catch { /* 스키마 차이 */ }
|
|
1029
|
-
if (!rows.length) return D.out("
|
|
1214
|
+
if (!rows.length) return D.out("No chats.");
|
|
1030
1215
|
for (const r of rows) {
|
|
1031
|
-
D.out(`${String(r.updated_at || "").slice(0, 16).padEnd(17)} ${String(r.
|
|
1216
|
+
D.out(`${String(r.updated_at || "").slice(0, 16).padEnd(17)} ${String(r.agent_name_en || r.agent_name || "-").slice(0, 18).padEnd(19)} ${String(r.title || "(untitled)").slice(0, 60)}`);
|
|
1032
1217
|
}
|
|
1033
1218
|
D.out("");
|
|
1034
|
-
D.out("
|
|
1219
|
+
D.out("These chats are shared with Desktop. Resume terminal sessions with /resume in the REPL.");
|
|
1035
1220
|
}
|
|
1036
1221
|
|
|
1037
1222
|
// ── usage — 로컬 집계 ──
|
|
@@ -1048,14 +1233,14 @@ function create(deps) {
|
|
|
1048
1233
|
const msg7 = q("SELECT COUNT(*) AS n FROM chat_messages WHERE created_at > ?", week);
|
|
1049
1234
|
const auto = q("SELECT COUNT(*) AS n FROM automations WHERE enabled=1");
|
|
1050
1235
|
const runs7 = q("SELECT COUNT(*) AS n, SUM(CASE WHEN status='error' OR error IS NOT NULL THEN 1 ELSE 0 END) AS err FROM run_history WHERE ran_at > ?", week);
|
|
1051
|
-
D.out(
|
|
1052
|
-
D.out(
|
|
1053
|
-
D.out(
|
|
1054
|
-
D.out(
|
|
1055
|
-
D.out(
|
|
1056
|
-
D.out(
|
|
1236
|
+
D.out(`Active runtime ${ar.kind || "(none)"}`);
|
|
1237
|
+
D.out(`Installed agents ${agents.n ?? "?"}`);
|
|
1238
|
+
D.out(`Active chats ${chats.n ?? "?"}`);
|
|
1239
|
+
D.out(`Messages 24h ${msg24.n ?? 0} · 7d ${msg7.n ?? 0}`);
|
|
1240
|
+
D.out(`Automations ${auto.n ?? 0}`);
|
|
1241
|
+
D.out(`Runs (7d) ${runs7.n ?? 0}${runs7.err ? ` (${runs7.err} failed)` : ""}`);
|
|
1057
1242
|
D.out("");
|
|
1058
|
-
D.out("
|
|
1243
|
+
D.out("Session tokens/cost: /cost in chat. Provider quota dashboards are in Desktop.");
|
|
1059
1244
|
}
|
|
1060
1245
|
|
|
1061
1246
|
// ── telegram — 바인딩 현황 (읽기 전용) ──
|
|
@@ -1067,17 +1252,17 @@ function create(deps) {
|
|
|
1067
1252
|
rows = [];
|
|
1068
1253
|
}
|
|
1069
1254
|
if (!rows.length) {
|
|
1070
|
-
D.out("
|
|
1255
|
+
D.out("No Telegram bindings. Pair devices from Desktop Connect.");
|
|
1071
1256
|
return;
|
|
1072
1257
|
}
|
|
1073
1258
|
for (const r of rows) {
|
|
1074
|
-
const bot = r.bot_username ? "@" + r.bot_username : "(
|
|
1075
|
-
const chat = r.telegram_chat_title || r.telegram_chat_id || "(
|
|
1259
|
+
const bot = r.bot_username ? "@" + r.bot_username : "(bot not set)";
|
|
1260
|
+
const chat = r.telegram_chat_title || r.telegram_chat_id || "(chat not connected)";
|
|
1076
1261
|
const status = r.status || (r.telegram_chat_id ? "paired" : "pending");
|
|
1077
1262
|
D.out(`${String(r.id).slice(0, 8)} ${r.target_kind}:${String(r.target_id).slice(0, 20).padEnd(21)} ${String(bot).padEnd(24)} ${String(chat).slice(0, 28).padEnd(29)} ${status}`);
|
|
1078
1263
|
}
|
|
1079
1264
|
D.out("");
|
|
1080
|
-
D.out("
|
|
1265
|
+
D.out("Pairing and bot issuance happen in Desktop Connect; this command only shows status.");
|
|
1081
1266
|
}
|
|
1082
1267
|
|
|
1083
1268
|
// ── login / logout / whoami — Agentlas Cloud 세션 (데스크탑과 동일한 loopback 브라우저 플로우) ──
|
|
@@ -1098,15 +1283,15 @@ function create(deps) {
|
|
|
1098
1283
|
|
|
1099
1284
|
async function fetchSessionMeta(cookie) {
|
|
1100
1285
|
const resp = await fetch(`${webBaseUrl()}/api/auth/session`, { headers: { cookie }, signal: AbortSignal.timeout(8000) });
|
|
1101
|
-
if (!resp.ok) throw new Error(
|
|
1286
|
+
if (!resp.ok) throw new Error(`Session check returned ${resp.status}`);
|
|
1102
1287
|
return resp.json();
|
|
1103
1288
|
}
|
|
1104
1289
|
|
|
1105
1290
|
function loginCallbackHtml(ok) {
|
|
1106
|
-
const title = ok ? "Agentlas
|
|
1291
|
+
const title = ok ? "Agentlas login complete" : "Agentlas login failed";
|
|
1107
1292
|
const body = ok
|
|
1108
|
-
? "
|
|
1109
|
-
: "
|
|
1293
|
+
? "Return to the terminal. You can close this window."
|
|
1294
|
+
: "Return to the terminal and run agentlas login again.";
|
|
1110
1295
|
return `<!doctype html><html><head><meta charset="utf-8"><title>Agentlas</title></head><body style="font-family:-apple-system,system-ui,sans-serif;padding:40px"><h3>${title}</h3><p>${body}</p></body></html>`;
|
|
1111
1296
|
}
|
|
1112
1297
|
|
|
@@ -1117,7 +1302,7 @@ function create(deps) {
|
|
|
1117
1302
|
const state = createLoginState(options.randomBytes || crypto.randomBytes);
|
|
1118
1303
|
const guard = createLoginCallbackGuard(state);
|
|
1119
1304
|
const onLoginUrl = options.onLoginUrl || ((url) => {
|
|
1120
|
-
D.out("
|
|
1305
|
+
D.out("Sign in to Agentlas in the browser (opening automatically):");
|
|
1121
1306
|
D.out(" " + url);
|
|
1122
1307
|
openInBrowser(url);
|
|
1123
1308
|
});
|
|
@@ -1156,7 +1341,7 @@ function create(deps) {
|
|
|
1156
1341
|
const address = server.address();
|
|
1157
1342
|
const port = address && typeof address === "object" ? address.port : 0;
|
|
1158
1343
|
if (!port) {
|
|
1159
|
-
finish(new Error("
|
|
1344
|
+
finish(new Error("Could not open the login loopback port."));
|
|
1160
1345
|
return;
|
|
1161
1346
|
}
|
|
1162
1347
|
const callback = new URL(`http://127.0.0.1:${port}${LOGIN_CALLBACK_PATH}`);
|
|
@@ -1165,13 +1350,13 @@ function create(deps) {
|
|
|
1165
1350
|
try {
|
|
1166
1351
|
loginUrl = new URL("/account", `${options.baseUrl || webBaseUrl()}/`);
|
|
1167
1352
|
} catch {
|
|
1168
|
-
finish(new Error("Agentlas
|
|
1353
|
+
finish(new Error("The Agentlas login URL is invalid."));
|
|
1169
1354
|
return;
|
|
1170
1355
|
}
|
|
1171
1356
|
loginUrl.searchParams.set("desktop", "1");
|
|
1172
1357
|
loginUrl.searchParams.set("callback", callback.toString());
|
|
1173
1358
|
timer = setTimeout(
|
|
1174
|
-
() => finish(new Error(
|
|
1359
|
+
() => finish(new Error(`Login timed out after ${Math.ceil(timeoutMs / 1000)} seconds. Try: agentlas login`)),
|
|
1175
1360
|
timeoutMs,
|
|
1176
1361
|
);
|
|
1177
1362
|
if (timer.unref) timer.unref();
|
|
@@ -1190,7 +1375,7 @@ function create(deps) {
|
|
|
1190
1375
|
async function cmdWhoami() {
|
|
1191
1376
|
const cookie = await D.cloudSessionCookieCli();
|
|
1192
1377
|
if (!cookie) {
|
|
1193
|
-
D.out("
|
|
1378
|
+
D.out("You are signed out. Sign in with agentlas login.");
|
|
1194
1379
|
process.exitCode = 1;
|
|
1195
1380
|
return;
|
|
1196
1381
|
}
|
|
@@ -1199,13 +1384,13 @@ function create(deps) {
|
|
|
1199
1384
|
if (j && j.authenticated) {
|
|
1200
1385
|
const email = (j.user && j.user.email) || "?";
|
|
1201
1386
|
const ws = j.workspace || {};
|
|
1202
|
-
D.out(
|
|
1387
|
+
D.out(`Signed in: ${email} · workspace: ${ws.name || "?"} (${ws.plan || "free"})`);
|
|
1203
1388
|
} else {
|
|
1204
|
-
D.out("
|
|
1389
|
+
D.out("The session is expired or invalid. Sign in again with agentlas login.");
|
|
1205
1390
|
process.exitCode = 1;
|
|
1206
1391
|
}
|
|
1207
1392
|
} catch (e) {
|
|
1208
|
-
D.fail("
|
|
1393
|
+
D.fail("Session check failed: " + String((e && e.message) || e));
|
|
1209
1394
|
}
|
|
1210
1395
|
}
|
|
1211
1396
|
|
|
@@ -1219,7 +1404,7 @@ function create(deps) {
|
|
|
1219
1404
|
try {
|
|
1220
1405
|
const j = await fetchSessionMeta(existing);
|
|
1221
1406
|
if (j && j.authenticated) {
|
|
1222
|
-
D.out(
|
|
1407
|
+
D.out(`Already signed in (${(j.user && j.user.email) || "?"}). Re-authenticate with agentlas login --force`);
|
|
1223
1408
|
return;
|
|
1224
1409
|
}
|
|
1225
1410
|
} catch { /* 확인 실패 — 새로 로그인 진행 */ }
|
|
@@ -1237,18 +1422,18 @@ function create(deps) {
|
|
|
1237
1422
|
fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
|
|
1238
1423
|
fs.writeFileSync(p, JSON.stringify({ version: 1, value, updatedAt: new Date().toISOString() }, null, 2) + "\n", { mode: 0o600 });
|
|
1239
1424
|
try { fs.chmodSync(p, 0o600); } catch { /* win32 */ }
|
|
1240
|
-
D.out(
|
|
1425
|
+
D.out(`Session saved: ${p}`);
|
|
1241
1426
|
await cmdWhoami();
|
|
1242
1427
|
}
|
|
1243
1428
|
|
|
1244
1429
|
function cmdLogout() {
|
|
1245
1430
|
const p = D.cliSessionPath();
|
|
1246
1431
|
if (fs.existsSync(p)) {
|
|
1247
|
-
try { fs.rmSync(p); D.out("
|
|
1432
|
+
try { fs.rmSync(p); D.out("Signed out (CLI session deleted)."); } catch (e) { return D.fail("Could not delete the session file: " + e.message); }
|
|
1248
1433
|
} else {
|
|
1249
|
-
D.out("
|
|
1434
|
+
D.out("No saved CLI session.");
|
|
1250
1435
|
}
|
|
1251
|
-
if (process.env.AGENTLAS_SESSION) D.out("
|
|
1436
|
+
if (process.env.AGENTLAS_SESSION) D.out("Warning: AGENTLAS_SESSION is still set, so the CLI may appear signed in.");
|
|
1252
1437
|
}
|
|
1253
1438
|
|
|
1254
1439
|
// ── cloud search — 마켓플레이스 검색 ──
|
|
@@ -1260,8 +1445,8 @@ function create(deps) {
|
|
|
1260
1445
|
else rest.push(args[i]);
|
|
1261
1446
|
}
|
|
1262
1447
|
const query = rest.join(" ").trim();
|
|
1263
|
-
if (!query) return D.fail('usage: agentlas cloud search "
|
|
1264
|
-
if (typeof fetch !== "function") return D.fail("
|
|
1448
|
+
if (!query) return D.fail('usage: agentlas cloud search "<task>" [--limit 10]');
|
|
1449
|
+
if (typeof fetch !== "function") return D.fail("fetch is not available in this runtime.");
|
|
1265
1450
|
const base = process.env.AGENTLAS_MCP_BASE_URL || "https://agentlas.cloud/api/mcp/v1";
|
|
1266
1451
|
const headers = { "content-type": "application/json" };
|
|
1267
1452
|
try {
|
|
@@ -1280,9 +1465,9 @@ function create(deps) {
|
|
|
1280
1465
|
}),
|
|
1281
1466
|
});
|
|
1282
1467
|
} catch (e) {
|
|
1283
|
-
return D.fail(
|
|
1468
|
+
return D.fail(`Marketplace connection failed: ${(e && e.message) || e}`);
|
|
1284
1469
|
}
|
|
1285
|
-
if (!resp.ok) return D.fail(
|
|
1470
|
+
if (!resp.ok) return D.fail(`Marketplace returned ${resp.status}`);
|
|
1286
1471
|
const json = await resp.json();
|
|
1287
1472
|
if (json.error) return D.fail(json.error.message || "marketplace error");
|
|
1288
1473
|
const result = json.result || {};
|
|
@@ -1290,7 +1475,7 @@ function create(deps) {
|
|
|
1290
1475
|
// 엔진 내부 에이전트(researcher-<n>, research-intelligence-desk, hephaestus-*)는 제품이 아니므로 숨긴다.
|
|
1291
1476
|
const items = Array.isArray(rawItems) ? rawItems.filter((it) => !isInternalAgentSlug(it && (it.slug || it.id))) : rawItems;
|
|
1292
1477
|
if (!Array.isArray(items) || !items.length) {
|
|
1293
|
-
D.out(
|
|
1478
|
+
D.out(`No results for "${query}"`);
|
|
1294
1479
|
return;
|
|
1295
1480
|
}
|
|
1296
1481
|
for (const it of items.slice(0, limit)) {
|
|
@@ -1301,7 +1486,7 @@ function create(deps) {
|
|
|
1301
1486
|
D.out(`${String(slug).padEnd(34).slice(0, 34)} ${String(name).slice(0, 26).padEnd(27)} ${String(kind).padEnd(14)} ${String(tagline).slice(0, 60)}`);
|
|
1302
1487
|
}
|
|
1303
1488
|
D.out("");
|
|
1304
|
-
D.out("
|
|
1489
|
+
D.out("Install: agentlas install <slug>");
|
|
1305
1490
|
}
|
|
1306
1491
|
|
|
1307
1492
|
return {
|