agentlas 1.0.22 → 1.0.24
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 +22 -0
- package/README.md +9 -26
- package/engine/agentlas-banner.cjs +0 -1
- package/engine/agentlas-capabilities.cjs +8 -71
- package/engine/agentlas-core-harness.cjs +18 -11
- package/engine/agentlas-i18n.cjs +21 -21
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-native-host.cjs +4 -31
- package/engine/agentlas-ui.cjs +13 -1
- package/engine/agentlas-workload-routing.cjs +11 -24
- package/engine/agentlas.cjs +3 -13
- package/engine/agents/router.cjs +0 -1
- package/engine/architecture.data.json +5 -5
- package/engine/bootstrap-schema.sql +3 -3
- package/engine/commands/doctor.cjs +3 -3
- package/engine/commands/firm.cjs +3 -1
- package/engine/commands/help.cjs +4 -7
- package/engine/commands/index.cjs +1 -6
- package/engine/commands/list.cjs +8 -2
- package/engine/firms/orchestrate.cjs +161 -12
- package/engine/sessions/orchestrator.cjs +4 -21
- package/engine/sessions/session.cjs +66 -5
- package/engine/sessions/sink.cjs +13 -3
- package/engine/sessions/store.cjs +2 -2
- package/engine/ui/palette.cjs +3 -12
- package/engine/ui/renderer.cjs +3 -6
- package/engine/ui/repl.cjs +41 -93
- package/engine/workforce/deps.cjs +9 -6
- package/package.json +4 -3
- package/engine/agentlas-doctor.cjs +0 -224
- package/engine/commands/chat.cjs +0 -12
- package/engine/commands/chats.cjs +0 -33
- package/engine/commands/open.cjs +0 -52
|
@@ -13,6 +13,9 @@ const crypto = require("node:crypto");
|
|
|
13
13
|
const { EventEmitter } = require("node:events");
|
|
14
14
|
const nativeHost = require("../agentlas-native-host.cjs");
|
|
15
15
|
const permissions = require("../agentlas-permissions.cjs");
|
|
16
|
+
const { RUNTIME_BIN, whichSync } = require("../runtimes/detect.cjs");
|
|
17
|
+
const { CLI_EXECUTABLE_KINDS } = require("../runtimes/resolve.cjs");
|
|
18
|
+
const { roleMembers } = require("../runtimes/roles.cjs");
|
|
16
19
|
const { EventSink } = require("./sink.cjs");
|
|
17
20
|
const store = require("./store.cjs");
|
|
18
21
|
|
|
@@ -37,6 +40,7 @@ class Session extends EventEmitter {
|
|
|
37
40
|
this.status = "idle"; // idle | running | done | failed | killed
|
|
38
41
|
this.lastLine = "";
|
|
39
42
|
this.lastError = null;
|
|
43
|
+
this._privateRecoveryEvidence = [];
|
|
40
44
|
this.startedAt = null;
|
|
41
45
|
this.endedAt = null;
|
|
42
46
|
this.usage = null;
|
|
@@ -52,7 +56,7 @@ class Session extends EventEmitter {
|
|
|
52
56
|
|
|
53
57
|
this.chatId = opts.chatId || store.createChat(this.db, {
|
|
54
58
|
agentId: this.agent.id,
|
|
55
|
-
title: opts.title || (opts.parent ? `sub: ${this.agent.slug}` : "New
|
|
59
|
+
title: opts.title || (opts.parent ? `sub: ${this.agent.slug}` : "New project task"),
|
|
56
60
|
kind: opts.parent ? "division" : "user",
|
|
57
61
|
parentChatId: opts.parent ? opts.parent.chatId : null,
|
|
58
62
|
workingFolder: this.cwd,
|
|
@@ -76,6 +80,11 @@ class Session extends EventEmitter {
|
|
|
76
80
|
this._sink = new EventSink({
|
|
77
81
|
lang: this.lang,
|
|
78
82
|
onEvent: (ev) => this._record(ev),
|
|
83
|
+
onPrivateEvidence: (text) => {
|
|
84
|
+
if (!text) return;
|
|
85
|
+
this._privateRecoveryEvidence.push(text.slice(0, 4000));
|
|
86
|
+
if (this._privateRecoveryEvidence.length > 16) this._privateRecoveryEvidence.shift();
|
|
87
|
+
},
|
|
79
88
|
});
|
|
80
89
|
}
|
|
81
90
|
|
|
@@ -143,16 +152,41 @@ class Session extends EventEmitter {
|
|
|
143
152
|
}
|
|
144
153
|
}
|
|
145
154
|
|
|
155
|
+
_nextRecoveryRuntime() {
|
|
156
|
+
const role = this.runtime.role === "worker" ? "worker" : "orchestrator";
|
|
157
|
+
const members = roleMembers(this.db, role);
|
|
158
|
+
const current = members.findIndex((member) =>
|
|
159
|
+
member.kind === this.runtime.kind &&
|
|
160
|
+
(member.model || null) === (this.runtime.model || null),
|
|
161
|
+
);
|
|
162
|
+
if (current < 0) return null;
|
|
163
|
+
for (const member of members.slice(current + 1)) {
|
|
164
|
+
if (!CLI_EXECUTABLE_KINDS.has(member.kind)) continue;
|
|
165
|
+
const bin = whichSync(RUNTIME_BIN[member.kind]);
|
|
166
|
+
if (!bin) continue;
|
|
167
|
+
return {
|
|
168
|
+
kind: member.kind,
|
|
169
|
+
bin,
|
|
170
|
+
model: member.model || undefined,
|
|
171
|
+
effort: member.effort || undefined,
|
|
172
|
+
role,
|
|
173
|
+
source: "model-role-pool-recovery",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
146
179
|
async _runTurn(prompt) {
|
|
147
180
|
this.status = "running";
|
|
148
181
|
this.startedAt = Date.now();
|
|
149
182
|
this.lastError = null;
|
|
183
|
+
this._privateRecoveryEvidence.length = 0;
|
|
150
184
|
this._record({ type: "turn-start", at: Date.now(), prompt });
|
|
151
185
|
store.appendMessage(this.db, this.chatId, "user", prompt);
|
|
152
186
|
// 데스크탑처럼 첫 프롬프트로 자동 제목 — "New chat"으로 남는 목록 방지(실사용 테스트 발견).
|
|
153
187
|
try {
|
|
154
188
|
const row = this.db.prepare("SELECT title FROM chats WHERE id=?").get(this.chatId);
|
|
155
|
-
if (row && (
|
|
189
|
+
if (row && (["New chat", "New project task"].includes(row.title) || !row.title)) {
|
|
156
190
|
store.retitleChat(this.db, this.chatId, prompt.slice(0, 60));
|
|
157
191
|
}
|
|
158
192
|
} catch { /* 제목은 장식 — 실패해도 턴 진행 */ }
|
|
@@ -234,6 +268,33 @@ class Session extends EventEmitter {
|
|
|
234
268
|
if (this._timeoutConfig) req.timeoutConfig = this._timeoutConfig;
|
|
235
269
|
try {
|
|
236
270
|
res = await nativeHost.runNativeTurn(req);
|
|
271
|
+
if (res && res.error && !res.text && !res.finalText) {
|
|
272
|
+
const nextRuntime = this._nextRecoveryRuntime();
|
|
273
|
+
if (nextRuntime) {
|
|
274
|
+
const privateEvidence = [...this._privateRecoveryEvidence, String(res.error)]
|
|
275
|
+
.filter(Boolean).join("\n").slice(0, 12000);
|
|
276
|
+
this.runtime = nextRuntime;
|
|
277
|
+
this.runtimeSession = {};
|
|
278
|
+
this.fingerprint = crypto.createHash("sha256")
|
|
279
|
+
.update(`${nextRuntime.kind}\n${this.agent.id}\n${this.agent.systemPrompt || ""}`)
|
|
280
|
+
.digest("hex");
|
|
281
|
+
res = await nativeHost.runNativeTurn({
|
|
282
|
+
...req,
|
|
283
|
+
kind: nextRuntime.kind,
|
|
284
|
+
bin: nextRuntime.bin,
|
|
285
|
+
model: nextRuntime.model,
|
|
286
|
+
effort: nextRuntime.effort,
|
|
287
|
+
session: {},
|
|
288
|
+
prompt: [
|
|
289
|
+
prompt,
|
|
290
|
+
"",
|
|
291
|
+
"Private recovery evidence follows. Never repeat it to the user.",
|
|
292
|
+
privateEvidence,
|
|
293
|
+
"Inspect the complete situation, apply safe reversible recovery within the granted authority, verify it, and finish the original request. Ask one concise question only if user identity or an irreversible choice is required.",
|
|
294
|
+
].join("\n"),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
237
298
|
} catch (e) {
|
|
238
299
|
res = { text: "", session: req.session, error: (e && e.message) || String(e) };
|
|
239
300
|
}
|
|
@@ -274,8 +335,8 @@ class Session extends EventEmitter {
|
|
|
274
335
|
}
|
|
275
336
|
if (res && res.error) {
|
|
276
337
|
this.status = "failed";
|
|
277
|
-
this.lastError =
|
|
278
|
-
this._record({ type: "turn-end", at: Date.now(), ok: false,
|
|
338
|
+
this.lastError = null;
|
|
339
|
+
this._record({ type: "turn-end", at: Date.now(), ok: false, recoveryRequired: true });
|
|
279
340
|
} else {
|
|
280
341
|
this.status = "done";
|
|
281
342
|
// 링버퍼 표시 이벤트에도 raw 대신 cleanText — 제어 블록이 패널/lastLine 에 새지 않게.
|
|
@@ -287,7 +348,7 @@ class Session extends EventEmitter {
|
|
|
287
348
|
try {
|
|
288
349
|
require("./apply-fences.cjs").applyReplyFences(this, parsedFences, { orch: this.orchestrator });
|
|
289
350
|
} catch (e) {
|
|
290
|
-
this.
|
|
351
|
+
this._privateRecoveryEvidence.push(`fence apply failed: ${(e && e.message) || String(e)}`.slice(0, 4000));
|
|
291
352
|
}
|
|
292
353
|
}
|
|
293
354
|
return res;
|
package/engine/sessions/sink.cjs
CHANGED
|
@@ -16,11 +16,12 @@ const i18n = require("../agentlas-i18n.cjs");
|
|
|
16
16
|
const NOOP_PALETTE = new Proxy({}, { get: () => (s) => String(s) });
|
|
17
17
|
|
|
18
18
|
class EventSink {
|
|
19
|
-
constructor({ lang = "en", onEvent } = {}) {
|
|
19
|
+
constructor({ lang = "en", onEvent, onPrivateEvidence } = {}) {
|
|
20
20
|
this.lang = lang;
|
|
21
21
|
this.t = (key, ...args) => i18n.t(this.lang, key, ...args);
|
|
22
22
|
this.c = NOOP_PALETTE;
|
|
23
23
|
this._onEvent = onEvent || (() => {});
|
|
24
|
+
this._onPrivateEvidence = onPrivateEvidence || (() => {});
|
|
24
25
|
this._streamOpen = false;
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -30,10 +31,19 @@ class EventSink {
|
|
|
30
31
|
|
|
31
32
|
status(text) { this._emit("status", { text: String(text || "") }); }
|
|
32
33
|
warn(text) { this._emit("warn", { text: String(text || "") }); }
|
|
33
|
-
|
|
34
|
+
// Runtime/provider failures are recovery evidence, never presentation copy.
|
|
35
|
+
// Session owns the recovery loop and may give this evidence to the controller;
|
|
36
|
+
// Renderer must never receive it first.
|
|
37
|
+
error(text) { this._onPrivateEvidence(String(text || "")); }
|
|
34
38
|
line(text) { this._emit("line", { text: String(text || "") }); }
|
|
35
39
|
tool(name, summary) { this._emit("tool", { name: String(name || "tool"), summary: String(summary || "") }); }
|
|
36
|
-
toolResult(text, ok) {
|
|
40
|
+
toolResult(text, ok) {
|
|
41
|
+
if (ok === false) {
|
|
42
|
+
this._onPrivateEvidence(String(text || ""));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this._emit("tool-result", { text: String(text || ""), ok: true });
|
|
46
|
+
}
|
|
37
47
|
streamStart() {
|
|
38
48
|
if (!this._streamOpen) { this._streamOpen = true; this._emit("stream-start", {}); }
|
|
39
49
|
}
|
|
@@ -21,7 +21,7 @@ function createChat(db, { agentId, title, kind = "user", parentChatId = null, wo
|
|
|
21
21
|
runWriteTransaction(db, () => {
|
|
22
22
|
db.prepare(
|
|
23
23
|
"INSERT INTO chats (id, agent_id, title, created_at, updated_at, kind, parent_chat_id, working_folder) VALUES (?,?,?,?,?,?,?,?)",
|
|
24
|
-
).run(id, agentId, title || "New
|
|
24
|
+
).run(id, agentId, title || "New project task", now, now, kind, parentChatId, workingFolder);
|
|
25
25
|
});
|
|
26
26
|
return id;
|
|
27
27
|
}
|
|
@@ -40,7 +40,7 @@ function appendMessage(db, chatId, role, text) {
|
|
|
40
40
|
|
|
41
41
|
function retitleChat(db, chatId, title) {
|
|
42
42
|
runWriteTransaction(db, () => {
|
|
43
|
-
db.prepare("UPDATE chats SET title=?, updated_at=? WHERE id=?").run(String(title || "New
|
|
43
|
+
db.prepare("UPDATE chats SET title=?, updated_at=? WHERE id=?").run(String(title || "New project task").slice(0, 120), nowIso(), chatId);
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
package/engine/ui/palette.cjs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* ui/palette — v2 슬래시 명령 정본 + readline 완성기.
|
|
4
4
|
*
|
|
5
5
|
* 왜 v1 input 모듈의 SLASH_COMMANDS를 쓰지 않는가: 그 목록은 v1 REPL 전용이라
|
|
6
|
-
* v2에 없는 명령(/status /team /model /effort …)을
|
|
7
|
-
*
|
|
6
|
+
* v2에 없는 명령(/status /team /model /effort …)을 광고한다. 프로젝트 Work에서는
|
|
7
|
+
* 컨트롤러만 서브에이전트를 배정하므로 사용자용 임의 spawn/steer/broadcast는 없다.
|
|
8
8
|
* 거짓말하면 사용자는 없는 기능을 부른다(실사용 Tab 테스트에서 실증).
|
|
9
9
|
* 경로 완성만 v1 모듈의 completePath를 재사용한다.
|
|
10
10
|
*/
|
|
@@ -17,15 +17,10 @@ const SLASH_COMMANDS = [
|
|
|
17
17
|
{ command: "/tree", args: "", ko: "세션 트리", en: "Session tree" },
|
|
18
18
|
{ command: "/s", args: "<n>", ko: "활성 세션 전환", en: "Switch active session" },
|
|
19
19
|
{ command: "/switch", args: "<n>", ko: "활성 세션 전환", en: "Switch active session" },
|
|
20
|
-
{ command: "/spawn", args: "<agent> [task]", ko: "서브에이전트 세션 생성", en: "Spawn a subagent session" },
|
|
21
|
-
{ command: "/steer", args: "<n> <msg>", ko: "해당 세션에 지시 큐잉", en: "Queue steering for a session" },
|
|
22
20
|
{ command: "/kill", args: "<n>", ko: "실행 중 턴 중단", en: "Interrupt a running turn" },
|
|
23
21
|
{ command: "/rm", args: "<n>", ko: "세션 제거", en: "Remove a session" },
|
|
24
|
-
{ command: "/broadcast", args: "<msg>", ko: "모든 세션에 지시", en: "Send to every session" },
|
|
25
|
-
{ command: "/use", args: "<agent>", ko: "메인 세션 에이전트 교체", en: "Switch the main agent" },
|
|
26
22
|
{ command: "/agents", args: "", ko: "설치 에이전트 목록", en: "List installed agents" },
|
|
27
23
|
{ command: "/list", args: "", ko: "설치 에이전트 목록", en: "List installed agents" },
|
|
28
|
-
{ command: "/chats", args: "[n]", ko: "최근 대화", en: "Recent conversations" },
|
|
29
24
|
{ command: "/mcp", args: "", ko: "MCP 서버 목록", en: "MCP servers" },
|
|
30
25
|
{ command: "/doctor", args: "", ko: "런타임·데이터 점검", en: "Health check" },
|
|
31
26
|
{ command: "/runtime", args: "<kind>", ko: "새 세션 런타임 지정", en: "Set runtime for new sessions" },
|
|
@@ -99,8 +94,7 @@ const RUNTIME_KINDS = ["claude-code", "codex", "gemini"];
|
|
|
99
94
|
const EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
100
95
|
const PERM_LEVELS = ["read", "write", "full"];
|
|
101
96
|
// 세션 인자를 받는 명령 — 완성 후보를 살아있는 세션 키(s1, s2…)로 채운다.
|
|
102
|
-
const SESSION_ARG_COMMANDS = new Set(["/s", "/switch", "/
|
|
103
|
-
const AGENT_ARG_COMMANDS = new Set(["/use", "/spawn"]);
|
|
97
|
+
const SESSION_ARG_COMMANDS = new Set(["/s", "/switch", "/kill", "/rm"]);
|
|
104
98
|
|
|
105
99
|
function uniqStartsWith(list, prefix) {
|
|
106
100
|
const p = String(prefix || "");
|
|
@@ -137,9 +131,6 @@ function makeCompleter(ctx = {}) {
|
|
|
137
131
|
if (cmd === "/effort") return [uniqStartsWith(EFFORT_LEVELS, last), last];
|
|
138
132
|
if (cmd === "/permission") return [uniqStartsWith(PERM_LEVELS, last), last];
|
|
139
133
|
if (SESSION_ARG_COMMANDS.has(cmd) && tokens.length === 2) return [uniqStartsWith(getSessions(), last), last];
|
|
140
|
-
if (AGENT_ARG_COMMANDS.has(cmd) && tokens.length === 2) {
|
|
141
|
-
return [uniqStartsWith(getAgents().concat(getFirms()), last), last];
|
|
142
|
-
}
|
|
143
134
|
return [[], last];
|
|
144
135
|
};
|
|
145
136
|
}
|
package/engine/ui/renderer.cjs
CHANGED
|
@@ -48,7 +48,6 @@ class Renderer {
|
|
|
48
48
|
}
|
|
49
49
|
const lines = text.trim().split(/\r?\n/).slice(-12);
|
|
50
50
|
for (const l of lines) this.ui.line(" " + l);
|
|
51
|
-
if (session.lastError) this.ui.error(session.lastError);
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
_render(ev) {
|
|
@@ -57,7 +56,6 @@ class Renderer {
|
|
|
57
56
|
case "turn-start": this._beginChrome(); return;
|
|
58
57
|
case "turn-end":
|
|
59
58
|
ui.endTurn();
|
|
60
|
-
if (!ev.ok && ev.error) ui.error(ev.error);
|
|
61
59
|
return;
|
|
62
60
|
case "status": ui.status(ev.text); return;
|
|
63
61
|
case "warn": ui.warn(ev.text); return;
|
|
@@ -65,7 +63,6 @@ class Renderer {
|
|
|
65
63
|
// 사용자가 의도적으로 중단(kill)한 턴의 종료 에러(SIGTERM exit 등)는 소음 —
|
|
66
64
|
// 실사용 테스트에서 확인된 UX 결함. 중단 안내는 REPL이 이미 출력했다.
|
|
67
65
|
if (this.session && this.session.status === "killed") return;
|
|
68
|
-
ui.error(ev.text);
|
|
69
66
|
return;
|
|
70
67
|
case "line": ui.line(ev.text); return;
|
|
71
68
|
case "tool": ui.tool(ev.name, ev.summary); return;
|
|
@@ -104,12 +101,12 @@ class Renderer {
|
|
|
104
101
|
ui.line(ui.c.dim(` ↳ agentlas automation list · agentlas automation off ${String(ev.id || "").slice(0, 8)}`));
|
|
105
102
|
return;
|
|
106
103
|
}
|
|
107
|
-
case "automation-refused":
|
|
104
|
+
case "automation-refused": return;
|
|
108
105
|
case "delegate-spawned":
|
|
109
106
|
ui.ok(`delegate spawned: ${ev.target} → ${ev.key}`);
|
|
110
107
|
return;
|
|
111
|
-
case "delegate-refused":
|
|
112
|
-
case "fence-error":
|
|
108
|
+
case "delegate-refused": return;
|
|
109
|
+
case "fence-error": return;
|
|
113
110
|
case "memory-curated":
|
|
114
111
|
ui.line(ui.c.dim(` ↳ memory: ${ev.written}/${ev.candidates} written (permission: ${ev.permission})`));
|
|
115
112
|
return;
|
package/engine/ui/repl.cjs
CHANGED
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* ui/repl —
|
|
3
|
+
* ui/repl — 프로젝트 Work 컨트롤러와 실행 트리.
|
|
4
4
|
*
|
|
5
5
|
* 원칙:
|
|
6
6
|
* - 실행 경로는 orchestrator 하나. 포그라운드 턴도 세션이다.
|
|
7
7
|
* - 화면은 활성 세션 하나만 스트리밍(Renderer). 백그라운드 턴 종료는 한 줄 notice.
|
|
8
8
|
* - 실행 중 입력: 텍스트는 다음 턴 스티어링 큐로, ctrl-c는 현재 턴 중단.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* /spawn <agent> [task] 백그라운드 서브세션 생성(+실행)
|
|
10
|
+
* 실행 관찰:
|
|
12
11
|
* /sessions · /tree 세션 표 / 부모-자식 트리
|
|
13
12
|
* /s <n> · /switch <n> 활성 세션 전환 (tail 재생 + 라이브 구독)
|
|
14
|
-
* /steer <n> <msg> 해당 세션 다음 턴 큐잉
|
|
15
13
|
* /kill <n> · /rm <n> 턴 중단 / 세션 제거
|
|
16
|
-
*
|
|
14
|
+
* 서브에이전트 배정과 지시는 프로젝트 컨트롤러만 수행한다.
|
|
17
15
|
*/
|
|
18
16
|
const readline = require("node:readline");
|
|
19
17
|
const { renderBanner, readVersion } = require("../agentlas-banner.cjs");
|
|
20
18
|
const { Orchestrator, maxParallel } = require("../sessions/orchestrator.cjs");
|
|
21
19
|
const { Renderer } = require("./renderer.cjs");
|
|
22
|
-
const {
|
|
20
|
+
const { listAgents } = require("../agents/registry.cjs");
|
|
23
21
|
const { resolveRuntimeForAgent } = require("../runtimes/overrides.cjs");
|
|
24
22
|
const { EFFORTS } = require("../agentlas-workload-routing.cjs");
|
|
25
23
|
const permissions = require("../agentlas-permissions.cjs");
|
|
26
24
|
const i18n = require("../agentlas-i18n.cjs");
|
|
27
25
|
const { tokenizeCommandLine } = require("../agentlas-input.cjs");
|
|
28
|
-
|
|
29
|
-
const DEFAULT_AGENT_SLUG = "agentlas-orchestrator";
|
|
26
|
+
const { resolveProjectController, withProjectControllerContext } = require("../project/controller.cjs");
|
|
30
27
|
|
|
31
28
|
/*
|
|
32
29
|
* Shift-Tab 권한 순환 — 배너(banner.location)와 permCycleHint/permFullArm/
|
|
@@ -83,12 +80,9 @@ function createPermissionShortcut(opts = {}) {
|
|
|
83
80
|
};
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
function
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const builtin = findAgent(db, DEFAULT_AGENT_SLUG);
|
|
90
|
-
if (builtin) return builtin;
|
|
91
|
-
return null;
|
|
83
|
+
function pickProjectController(db, cwd = process.cwd()) {
|
|
84
|
+
const resolved = resolveProjectController(db, cwd);
|
|
85
|
+
return withProjectControllerContext(resolved.controller, resolved.project);
|
|
92
86
|
}
|
|
93
87
|
|
|
94
88
|
async function startRepl(ctx, opts = {}) {
|
|
@@ -124,7 +118,7 @@ async function startRepl(ctx, opts = {}) {
|
|
|
124
118
|
} catch (e) {
|
|
125
119
|
// 온보딩 실패는 REPL 진입을 막지 않지만, 조용히 삼키면 마법사 프롬프트와
|
|
126
120
|
// REPL이 stdin을 경합하는 사고(실사용 테스트에서 실증)가 위장된다 — 표시한다.
|
|
127
|
-
ctx.err(ui.c.dim(
|
|
121
|
+
ctx.err(ui.c.dim(en ? "One is recovering setup." : "One이 설정을 복구하고 있습니다."));
|
|
128
122
|
} finally {
|
|
129
123
|
wizardRl.close();
|
|
130
124
|
}
|
|
@@ -137,6 +131,21 @@ async function startRepl(ctx, opts = {}) {
|
|
|
137
131
|
let modelOverride = opts.model || null;
|
|
138
132
|
let effortOverride = opts.effort || null;
|
|
139
133
|
|
|
134
|
+
const recoverPresentation = (operation, error) => {
|
|
135
|
+
const active = orch.active();
|
|
136
|
+
const evidence = String((error && error.message) || error || "").slice(0, 8000);
|
|
137
|
+
if (active && !active.isBusy()) {
|
|
138
|
+
orch.sendTo(active.key, [
|
|
139
|
+
`The user operation “${operation}” did not complete.`,
|
|
140
|
+
"Private evidence follows. Never quote codes, paths, provider text, or stack details to the user.",
|
|
141
|
+
evidence,
|
|
142
|
+
"Judge the situation, perform safe reversible recovery within current authority, verify the original outcome, then give only a concise useful result. Ask only when identity or an irreversible choice is required.",
|
|
143
|
+
].join("\n")).catch(() => {});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
ui.line(ui.c.dim(en ? "One is checking and recovering this operation." : "One이 상태를 확인하고 복구하고 있습니다."));
|
|
147
|
+
};
|
|
148
|
+
|
|
140
149
|
const resolveRt = (agentId = null) => resolveRuntimeForAgent({
|
|
141
150
|
db,
|
|
142
151
|
prefs: ctx.prefs,
|
|
@@ -148,12 +157,12 @@ async function startRepl(ctx, opts = {}) {
|
|
|
148
157
|
});
|
|
149
158
|
|
|
150
159
|
let resumeChatId = opts.chatId || null;
|
|
151
|
-
const ensureMainSession = (
|
|
152
|
-
const agent =
|
|
160
|
+
const ensureMainSession = () => {
|
|
161
|
+
const agent = orch.active() ? orch.active().agent : pickProjectController(db);
|
|
153
162
|
if (!agent) {
|
|
154
163
|
throw new Error(en
|
|
155
|
-
?
|
|
156
|
-
:
|
|
164
|
+
? "This project has no available controller. Configure its ordered team in Desktop Work."
|
|
165
|
+
: "이 프로젝트에 실행 가능한 컨트롤러가 없습니다. Desktop Work에서 순서가 있는 팀을 설정하세요.");
|
|
157
166
|
}
|
|
158
167
|
const active = orch.active();
|
|
159
168
|
if (active && active.agent.id === agent.id) return active;
|
|
@@ -181,13 +190,13 @@ async function startRepl(ctx, opts = {}) {
|
|
|
181
190
|
try { runtimeLabel = resolveRt().kind; } catch { /* no_runtime: 첫 턴에서 정직 정지 */ }
|
|
182
191
|
let subjectLabel;
|
|
183
192
|
try {
|
|
184
|
-
const subject =
|
|
193
|
+
const subject = pickProjectController(db);
|
|
185
194
|
if (subject) subjectLabel = subject.slug;
|
|
186
195
|
} catch { /* 표시용 — 못 정해도 배너는 그린다 */ }
|
|
187
196
|
renderBanner({ ui, version: readVersion(), runtimeLabel, subjectLabel, permission, cwd: process.cwd() });
|
|
188
197
|
} catch (e) {
|
|
189
198
|
ctx.out(`agentlas ${readVersion()}`);
|
|
190
|
-
|
|
199
|
+
void e;
|
|
191
200
|
}
|
|
192
201
|
|
|
193
202
|
// 배너 카드가 런타임·권한·작업 폴더와 명령 메뉴를 이미 보여준다 — 남은 것만.
|
|
@@ -195,15 +204,6 @@ async function startRepl(ctx, opts = {}) {
|
|
|
195
204
|
? `v2 engine · parallel ≤${maxParallel()}`
|
|
196
205
|
: `v2 엔진 · 동시 ≤${maxParallel()}`));
|
|
197
206
|
|
|
198
|
-
if (opts.agent) {
|
|
199
|
-
try {
|
|
200
|
-
const session = ensureMainSession(opts.agent);
|
|
201
|
-
ctx.out(ui.c.dim(`agent: ${session.agent.slug} · ${session.runtime.kind}`));
|
|
202
|
-
} catch (e) {
|
|
203
|
-
ctx.err(String((e && e.message) || e));
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
207
|
return new Promise((resolve) => {
|
|
208
208
|
// 히스토리는 v1 input 모듈 재사용, 완성기는 v2 팔레트(ui/palette)가 정본이다.
|
|
209
209
|
const input = require("../agentlas-input.cjs");
|
|
@@ -325,7 +325,7 @@ async function startRepl(ctx, opts = {}) {
|
|
|
325
325
|
const runForeground = (session, text) => {
|
|
326
326
|
const p = orch.sendTo(session.key, text);
|
|
327
327
|
if (p && typeof p.then === "function") {
|
|
328
|
-
p.then(() => prompt(), (e) => {
|
|
328
|
+
p.then(() => prompt(), (e) => { recoverPresentation("project task", e); prompt(); });
|
|
329
329
|
}
|
|
330
330
|
};
|
|
331
331
|
|
|
@@ -350,14 +350,14 @@ async function startRepl(ctx, opts = {}) {
|
|
|
350
350
|
});
|
|
351
351
|
if (quit === "quit") { rl.close(); return; }
|
|
352
352
|
} catch (e) {
|
|
353
|
-
|
|
353
|
+
recoverPresentation("command", e);
|
|
354
354
|
}
|
|
355
355
|
prompt();
|
|
356
356
|
return;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
if (input.startsWith("!")) {
|
|
360
|
-
runShell(ctx, input.slice(1).trim(), permission).then(prompt, (e) => {
|
|
360
|
+
runShell(ctx, input.slice(1).trim(), permission).then(prompt, (e) => { recoverPresentation("shell action", e); prompt(); });
|
|
361
361
|
return;
|
|
362
362
|
}
|
|
363
363
|
|
|
@@ -366,7 +366,7 @@ async function startRepl(ctx, opts = {}) {
|
|
|
366
366
|
session = orch.active() || ensureMainSession(null);
|
|
367
367
|
if (!renderer.session) renderer.attach(session, { replay: false });
|
|
368
368
|
} catch (e) {
|
|
369
|
-
|
|
369
|
+
recoverPresentation("project startup", e);
|
|
370
370
|
prompt();
|
|
371
371
|
return;
|
|
372
372
|
}
|
|
@@ -379,7 +379,7 @@ async function startRepl(ctx, opts = {}) {
|
|
|
379
379
|
try {
|
|
380
380
|
runForeground(session, input);
|
|
381
381
|
} catch (e) {
|
|
382
|
-
|
|
382
|
+
recoverPresentation("project task", e);
|
|
383
383
|
prompt();
|
|
384
384
|
}
|
|
385
385
|
});
|
|
@@ -473,7 +473,7 @@ async function runShell(ctx, cmd, permission) {
|
|
|
473
473
|
ui.streamStart(true);
|
|
474
474
|
child.stdout.on("data", onData);
|
|
475
475
|
child.stderr.on("data", onData);
|
|
476
|
-
child.on("error", (
|
|
476
|
+
child.on("error", () => { ui.streamEnd(); ui.error(); resolve(); });
|
|
477
477
|
child.on("close", (code) => {
|
|
478
478
|
ui.streamEnd();
|
|
479
479
|
if (code !== 0 && !capped) ui.line(ui.c.dim(`exit ${code}`));
|
|
@@ -499,7 +499,7 @@ function printSessions(ctx, orch) {
|
|
|
499
499
|
const ui = ctx.uiInstance;
|
|
500
500
|
const rows = orch.list();
|
|
501
501
|
if (!rows.length) {
|
|
502
|
-
ctx.out(ui.c.dim(ctx.lang === "en" ? "no
|
|
502
|
+
ctx.out(ui.c.dim(ctx.lang === "en" ? "no project run yet — type a task" : "아직 프로젝트 실행이 없습니다 — 작업을 입력하세요"));
|
|
503
503
|
return;
|
|
504
504
|
}
|
|
505
505
|
for (const r of rows) {
|
|
@@ -532,7 +532,7 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
532
532
|
* 인자는 따옴표를 인식해 쪼갠다. 공백 분해는 따옴표를 인자 안에 그대로 남겨,
|
|
533
533
|
* 팔레트가 안내하는 그대로 `/search "무엇이 필요한지"` 를 치면 따옴표째 검색어가 됐다.
|
|
534
534
|
* 최상위 CLI 와 같은 토크나이저를 쓴다. restStr 은 원문 꼬리를 그대로 넘기는 자리
|
|
535
|
-
*
|
|
535
|
+
* 일부 패스스루 명령은 원문 꼬리를 그대로 전달한다.
|
|
536
536
|
*/
|
|
537
537
|
const rawCmd = cmdline.split(/\s+/)[0] || "";
|
|
538
538
|
const rest = tokenizeCommandLine(cmdline).slice(1);
|
|
@@ -546,7 +546,7 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
546
546
|
case "help": {
|
|
547
547
|
require("../commands/help.cjs").run(ctx, rest);
|
|
548
548
|
ctx.out("");
|
|
549
|
-
ctx.out(ui.c.bold(en ? "
|
|
549
|
+
ctx.out(ui.c.bold(en ? "Project Work runs" : "프로젝트 Work 실행"));
|
|
550
550
|
// 팔레트는 Tab 완성과 같은 정본(ui/palette)에서 렌더한다 — 목록 드리프트 금지.
|
|
551
551
|
ctx.out(require("./palette.cjs").renderPalette(ctx.lang));
|
|
552
552
|
ctx.out("");
|
|
@@ -558,7 +558,6 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
558
558
|
return;
|
|
559
559
|
}
|
|
560
560
|
case "agents": case "list": require("../commands/list.cjs").run(ctx, rest); return;
|
|
561
|
-
case "chats": require("../commands/chats.cjs").run(ctx, rest); return;
|
|
562
561
|
case "doctor": require("../commands/doctor.cjs").run(ctx, rest); return;
|
|
563
562
|
case "mcp": require("../commands/mcp.cjs").run(ctx, rest); return;
|
|
564
563
|
|
|
@@ -571,36 +570,6 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
571
570
|
return;
|
|
572
571
|
}
|
|
573
572
|
|
|
574
|
-
case "spawn": {
|
|
575
|
-
const agentToken = rest[0];
|
|
576
|
-
if (!agentToken) throw new Error("Usage: /spawn <agent> [task]");
|
|
577
|
-
const agent = findAgent(ctx.db(), agentToken);
|
|
578
|
-
if (!agent) throw new Error((en ? "agent not found: " : "에이전트를 찾을 수 없음: ") + agentToken);
|
|
579
|
-
const task = restStr.slice(agentToken.length).trim();
|
|
580
|
-
const parent = orch.active();
|
|
581
|
-
const session = orch.spawn({
|
|
582
|
-
agent,
|
|
583
|
-
runtime: parent ? parent.runtime : api.resolveRt(),
|
|
584
|
-
permission: api.getPermission(),
|
|
585
|
-
cwd: process.cwd(),
|
|
586
|
-
parentKey: parent ? parent.key : null,
|
|
587
|
-
activate: false,
|
|
588
|
-
title: task ? `sub: ${task.slice(0, 60)}` : undefined,
|
|
589
|
-
});
|
|
590
|
-
if (task) orch.sendTo(session.key, task);
|
|
591
|
-
ctx.out(ui.c.dim(`${session.key} ${agent.slug} ${task ? (en ? "started" : "시작됨") : (en ? "ready (use /steer)" : "대기 (—/steer 로 지시)")}`));
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
case "steer": {
|
|
596
|
-
const key = sessionKeyArg(rest, `Usage: /${cmd} <n> <message>`);
|
|
597
|
-
const msg = restStr.slice(rest[0].length).trim();
|
|
598
|
-
if (!msg) throw new Error("Usage: /steer <n> <message>");
|
|
599
|
-
orch.sendTo(key, msg);
|
|
600
|
-
ctx.out(ui.c.dim(`→ ${key}`));
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
573
|
case "kill": {
|
|
605
574
|
const key = sessionKeyArg(rest, `Usage: /${cmd} <n>`);
|
|
606
575
|
orch.kill(key);
|
|
@@ -614,27 +583,6 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
614
583
|
return;
|
|
615
584
|
}
|
|
616
585
|
|
|
617
|
-
case "broadcast": {
|
|
618
|
-
if (!restStr) throw new Error("Usage: /broadcast <message>");
|
|
619
|
-
/*
|
|
620
|
-
* broadcast 는 {sent, skipped} 를 준다. 전달된 목록을 먼저 찍고 못 보낸 세션은
|
|
621
|
-
* 사유와 함께 경고로 덧붙인다 — 예전엔 상한 throw 가 이 catch 로 떨어져 에러 한
|
|
622
|
-
* 줄만 보였고, 이미 지시를 받아 돌기 시작한 세션이 화면에 전혀 안 나왔다.
|
|
623
|
-
*/
|
|
624
|
-
const { sent, skipped } = orch.broadcast(restStr);
|
|
625
|
-
ctx.out(ui.c.dim(`→ ${sent.join(", ") || "(none)"}`));
|
|
626
|
-
for (const s of skipped) ui.warn(`${s.key} ${en ? "not sent" : "미전송"}: ${s.error}`);
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
case "use": {
|
|
631
|
-
if (!rest[0]) throw new Error("Usage: /use <agent>");
|
|
632
|
-
const session = ensureMainSession(rest[0]);
|
|
633
|
-
renderer.attach(session, { replay: false });
|
|
634
|
-
ctx.out(ui.c.dim(`agent: ${session.agent.slug}`));
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
586
|
case "runtime": {
|
|
639
587
|
if (!rest[0]) throw new Error("Usage: /runtime claude-code|codex|gemini");
|
|
640
588
|
api.setRuntime(rest[0]);
|
|
@@ -679,8 +627,8 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
679
627
|
* 제외: 자기 자신을 다시 여는 대화형 명령(chat/open/firm/setup)과 run
|
|
680
628
|
* (REPL의 평문 입력이 곧 run이다).
|
|
681
629
|
*/
|
|
682
|
-
// help/agents/list/
|
|
683
|
-
const REPL_EXCLUDED = new Set(["
|
|
630
|
+
// help/agents/list/mcp/doctor 등은 위 케이스에서 이미 처리된다.
|
|
631
|
+
const REPL_EXCLUDED = new Set(["firm", "setup", "run"]);
|
|
684
632
|
if (!REPL_EXCLUDED.has(cmd) && commands.COMMANDS[cmd]) {
|
|
685
633
|
const result = commands.COMMANDS[cmd]().run(ctx, rest);
|
|
686
634
|
if (result && typeof result.then === "function") {
|
|
@@ -32,7 +32,7 @@ const path = require("node:path");
|
|
|
32
32
|
const { userDataDir } = require("../core/paths.cjs");
|
|
33
33
|
const hubClient = require("../cloud/hub-client.cjs");
|
|
34
34
|
const detect = require("../runtimes/detect.cjs");
|
|
35
|
-
const {
|
|
35
|
+
const { roleMembers } = require("../runtimes/roles.cjs");
|
|
36
36
|
const capture = require("./capture.cjs");
|
|
37
37
|
|
|
38
38
|
// ── 런타임 해석 (v1 resolveRuntime의 워크포스 어댑터) ─────────────────────
|
|
@@ -115,8 +115,7 @@ function legacyWorkforceRuntime(db, override) {
|
|
|
115
115
|
throw error;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
//
|
|
119
|
-
// worker가 비어 있거나 inherit=1이면 roles.cjs가 orchestrator로만 승격한다.
|
|
118
|
+
// 명시 override 또는 Dashboard에 저장된 role priority만 실행 권위다.
|
|
120
119
|
function resolveWorkforceRuntime(db, override) {
|
|
121
120
|
if (override) {
|
|
122
121
|
const exact = legacyWorkforceRuntime(db, override);
|
|
@@ -129,9 +128,13 @@ function resolveWorkforceRuntime(db, override) {
|
|
|
129
128
|
},
|
|
130
129
|
};
|
|
131
130
|
}
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
131
|
+
const orchestrator = runtimeFromSelection(roleMembers(db, "orchestrator")[0] || null);
|
|
132
|
+
const worker = runtimeFromSelection(roleMembers(db, "worker")[0] || null);
|
|
133
|
+
if (!orchestrator || !worker) {
|
|
134
|
+
const error = new Error("Dashboard orchestrator and worker priorities are required for Workforce execution.");
|
|
135
|
+
error.code = "no_runtime_role_priority";
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
135
138
|
return {
|
|
136
139
|
...orchestrator,
|
|
137
140
|
role: "orchestrator",
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Agentlas
|
|
3
|
+
"version": "1.0.24",
|
|
4
|
+
"description": "Agentlas project terminal — run project controllers and task-scoped agent teams from the terminal. Standalone: no desktop app process required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentlas": "bin/agentlas.cjs"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"smoke": "sh test/smoke.sh",
|
|
10
|
-
"test:release-contracts": "npm run smoke"
|
|
10
|
+
"test:release-contracts": "npm run smoke",
|
|
11
|
+
"sync:architecture": "node scripts/sync-architecture-from-desktop.cjs"
|
|
11
12
|
},
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=20"
|