agentlas 1.0.28 → 1.0.29

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +5 -2
  3. package/engine/agentlas-i18n.cjs +4 -4
  4. package/engine/agentlas-input.cjs +0 -2
  5. package/engine/agentlas-onboard.cjs +20 -0
  6. package/engine/agentlas-workforce.cjs +41 -8
  7. package/engine/agentlas.cjs +7 -1
  8. package/engine/commands/billing.cjs +3 -0
  9. package/engine/commands/creds.cjs +49 -1
  10. package/engine/commands/doctor.cjs +46 -3
  11. package/engine/commands/graph.cjs +1150 -0
  12. package/engine/commands/help.cjs +82 -6
  13. package/engine/commands/hep-cloud.cjs +9 -23
  14. package/engine/commands/hep-hub.cjs +9 -22
  15. package/engine/commands/hep-local.cjs +9 -24
  16. package/engine/commands/hep-network.cjs +9 -35
  17. package/engine/commands/index.cjs +49 -25
  18. package/engine/commands/mcp.cjs +6 -2
  19. package/engine/commands/native.cjs +18 -2
  20. package/engine/commands/plugin.cjs +22 -0
  21. package/engine/commands/roles.cjs +202 -0
  22. package/engine/commands/workforce.cjs +63 -12
  23. package/engine/graph/ask-model.cjs +131 -0
  24. package/engine/graph/interview.cjs +875 -0
  25. package/engine/graph/layout.cjs +137 -0
  26. package/engine/graph/package.cjs +223 -0
  27. package/engine/graph/vocabulary.generated.cjs +30 -0
  28. package/engine/hephaestus/local-core.cjs +159 -0
  29. package/engine/hephaestus/runtime.cjs +4 -8
  30. package/engine/runtimes/auth-evidence.cjs +78 -0
  31. package/engine/sessions/prompt.cjs +16 -0
  32. package/engine/sessions/session.cjs +9 -0
  33. package/engine/tools/access-notice.cjs +86 -0
  34. package/engine/ui/palette.cjs +6 -3
  35. package/engine/ui/repl.cjs +8 -2
  36. package/engine/workforce/deps.cjs +13 -0
  37. package/engine/workforce/local-core-transport.cjs +298 -0
  38. package/package.json +4 -3
  39. package/engine/commands/legacy-network.cjs +0 -29
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ /*
3
+ * roles — 오케스트레이터/워커 모델 역할 조회·설정.
4
+ *
5
+ * 배경(2026-08-05 감사, 결함 C): `list`와 `doctor`는 model_roles를 보여주는데
6
+ * 터미널 어디에도 쓰기 경로가 없었다(전 코드베이스 SELECT만). 바꾸려면 데스크탑을
7
+ * 열어야 했고, REPL `/model`은 지역 변수 대입이라 검증도 저장도 안 됐다.
8
+ *
9
+ * 계약:
10
+ * - 스키마 소유권은 데스크탑(v79)에 있다. 여기서는 테이블·컬럼을 만들지 않는다 —
11
+ * 테이블이 없으면 정직 정지 + 데스크탑 안내. 있는 행에 UPSERT만 한다.
12
+ * - kind는 데스크탑과 같은 어휘(RUNTIME_BIN + byok 계열)로 검증한다. PATH에
13
+ * 없는 CLI도 저장은 허용하되(미리 설정) 그 자리에서 알린다 — 조용한 저장 금지.
14
+ * - worker --inherit 는 스키마 CHECK(worker만 inherit 가능)를 그대로 따른다.
15
+ * - 모델 id는 제공자마다 열린 어휘라 존재 검증하지 않는다(워크로드 라우팅의
16
+ * EFFORTS 주석과 같은 원칙: 화이트리스트를 게이트로 쓰지 않는다).
17
+ */
18
+ const { RUNTIME_BIN, whichSync } = require("../runtimes/detect.cjs");
19
+ const { runtimeAuthEvidence } = require("../runtimes/auth-evidence.cjs");
20
+ const { MODEL_ROLE_TABLE, VALID_ROLES, resolvedModelRole, roleMembers } = require("../runtimes/roles.cjs");
21
+ const { runWriteTransaction } = require("../agentlas-sqlite-policy.cjs");
22
+ const { EFFORTS } = require("../agentlas-workload-routing.cjs");
23
+
24
+ // 데스크탑 RuntimeKind 어휘(runtimes/detect.cjs 주석과 동일). CLI가 아닌 종류는
25
+ // which 검사 대상이 아니다.
26
+ const KNOWN_KINDS = new Set([...Object.keys(RUNTIME_BIN), "byok", "ollama", "lmstudio", "mlx"]);
27
+
28
+ function fmt(selection, en) {
29
+ if (!selection) return en ? "not set" : "미설정";
30
+ const bits = [
31
+ `${selection.kind}${selection.model ? `/${selection.model}` : ""}`,
32
+ selection.effort ? `effort=${selection.effort}` : null,
33
+ selection.inherit ? (en ? "(inherits orchestrator)" : "(오케스트레이터 상속)") : null,
34
+ ];
35
+ return bits.filter(Boolean).join(" ");
36
+ }
37
+
38
+ function show(ctx) {
39
+ const en = ctx.lang === "en";
40
+ const db = ctx.db();
41
+ ctx.out(ctx.ui.bold(en ? "Model roles" : "모델 역할"));
42
+ for (const role of ["orchestrator", "worker"]) {
43
+ const resolved = resolvedModelRole(db, role);
44
+ ctx.out(` ${role.padEnd(13)} ${fmt(resolved, en)}`);
45
+ const pool = roleMembers(db, role);
46
+ if (pool.length) {
47
+ ctx.out(ctx.ui.dim(` ${" ".repeat(13)} ${en ? "pool: " : "풀: "}${pool.map((m) => `${m.position}.${m.kind}${m.model ? `/${m.model}` : ""}`).join(" ")}`));
48
+ }
49
+ }
50
+ ctx.out("");
51
+ ctx.out(ctx.ui.dim(en
52
+ ? 'Change: agentlas roles set <orchestrator|worker> <runtime> [--model <id>] [--effort <level>] · worker inherit: agentlas roles set worker --inherit'
53
+ : '변경: agentlas roles set <orchestrator|worker> <runtime> [--model <id>] [--effort <level>] · 워커 상속: agentlas roles set worker --inherit'));
54
+ return 0;
55
+ }
56
+
57
+ function parseSetFlags(args) {
58
+ const rest = [];
59
+ const flags = {};
60
+ for (let i = 0; i < args.length; i += 1) {
61
+ const arg = args[i];
62
+ if (arg === "--model") { flags.model = String(args[++i] ?? "").trim(); continue; }
63
+ if (arg.startsWith("--model=")) { flags.model = arg.slice(8).trim(); continue; }
64
+ if (arg === "--effort") { flags.effort = String(args[++i] ?? "").trim().toLowerCase(); continue; }
65
+ if (arg.startsWith("--effort=")) { flags.effort = arg.slice(9).trim().toLowerCase(); continue; }
66
+ if (arg === "--inherit") { flags.inherit = true; continue; }
67
+ rest.push(arg);
68
+ }
69
+ return { rest, flags };
70
+ }
71
+
72
+ function set(ctx, args) {
73
+ const en = ctx.lang === "ko" ? false : true;
74
+ const ko = !en;
75
+ const { rest, flags } = parseSetFlags(args);
76
+ const role = String(rest[0] || "").toLowerCase();
77
+ const kindArg = rest[1] ? String(rest[1]).toLowerCase() : null;
78
+
79
+ const usage = ko
80
+ ? "사용법: agentlas roles set <orchestrator|worker> <runtime> [--model <id>] [--effort <level>] | agentlas roles set worker --inherit"
81
+ : "Usage: agentlas roles set <orchestrator|worker> <runtime> [--model <id>] [--effort <level>] | agentlas roles set worker --inherit";
82
+
83
+ if (!VALID_ROLES.has(role)) { ctx.err(usage); return 1; }
84
+ if (flags.inherit && role !== "worker") {
85
+ ctx.err(ko ? "--inherit 는 worker 역할에만 씁니다 (스키마 계약)." : "--inherit applies to the worker role only (schema contract).");
86
+ return 1;
87
+ }
88
+ if (!flags.inherit && !kindArg) { ctx.err(usage); return 1; }
89
+ if (kindArg && !KNOWN_KINDS.has(kindArg)) {
90
+ ctx.err((ko ? "모르는 런타임 종류: " : "unknown runtime kind: ") + kindArg);
91
+ ctx.err(ko
92
+ ? `가능한 값: ${[...KNOWN_KINDS].join(" · ")}`
93
+ : `valid kinds: ${[...KNOWN_KINDS].join(" · ")}`);
94
+ return 1;
95
+ }
96
+ if (flags.effort !== undefined && !EFFORTS.includes(flags.effort)) {
97
+ ctx.err((ko ? "모르는 effort 값: " : "unknown effort level: ") + flags.effort + ` (${EFFORTS.join("|")})`);
98
+ return 1;
99
+ }
100
+
101
+ const db = ctx.db();
102
+ // 스키마 창조 금지 — 테이블은 데스크탑 마이그레이션 v79가 만든다.
103
+ if (!ctx.tableExists(db, MODEL_ROLE_TABLE)) {
104
+ ctx.err(ko
105
+ ? "model_roles 테이블이 아직 없습니다. 스키마는 데스크탑 앱이 소유합니다 — 데스크탑을 한 번 실행하면 생성됩니다."
106
+ : "The model_roles table does not exist yet. Desktop owns this schema — run the desktop app once to create it.");
107
+ return 1;
108
+ }
109
+
110
+ let kind = kindArg;
111
+ let model = flags.model !== undefined ? (flags.model || null) : undefined;
112
+ let inherit = 0;
113
+ if (flags.inherit) {
114
+ // 상속 = 워커가 오케스트레이터를 따른다. 스키마상 kind NOT NULL이라
115
+ // 현재 오케스트레이터의 좌표를 복사해 두되 inherit=1로 표시한다(리더 부재 시 정직 정지).
116
+ const orchestrator = resolvedModelRole(db, "orchestrator");
117
+ if (!orchestrator) {
118
+ ctx.err(ko ? "상속할 오케스트레이터 설정이 없습니다. 먼저 orchestrator를 설정하세요." : "No orchestrator to inherit from. Set the orchestrator first.");
119
+ return 1;
120
+ }
121
+ kind = orchestrator.kind;
122
+ if (model === undefined) model = orchestrator.model;
123
+ inherit = 1;
124
+ }
125
+
126
+ const now = new Date().toISOString();
127
+ runWriteTransaction(db, () => {
128
+ const existing = db.prepare("SELECT * FROM model_roles WHERE role=?").get(role);
129
+ if (existing) {
130
+ // kind가 바뀌면 이전 모델 id는 새 런타임의 어휘가 아니다(예: kimi에 opus).
131
+ // --model 미지정 시 유지가 아니라 초기화 — 무의미한 좌표를 승계하지 않는다.
132
+ const keepModel = existing.kind === kind ? existing.model : null;
133
+ db.prepare(
134
+ "UPDATE model_roles SET kind=?, model=?, effort=?, inherit=?, updated_at=? WHERE role=?",
135
+ ).run(
136
+ kind,
137
+ model === undefined ? keepModel : model,
138
+ flags.effort === undefined ? existing.effort : (flags.effort === "none" ? null : flags.effort),
139
+ inherit,
140
+ now,
141
+ role,
142
+ );
143
+ } else {
144
+ db.prepare(
145
+ "INSERT INTO model_roles (role, kind, model, effort, inherit, updated_at) VALUES (?,?,?,?,?,?)",
146
+ ).run(role, kind, model === undefined ? null : model, flags.effort === undefined || flags.effort === "none" ? null : flags.effort, inherit, now);
147
+ }
148
+ });
149
+
150
+ const saved = resolvedModelRole(db, role);
151
+ ctx.out(`${ctx.ui.green("✓")} ${role} = ${fmt(saved, en)}`);
152
+
153
+ // 저장은 됐지만 실행이 안 될 수 있는 상태는 그 자리에서 말한다 — 조용한 저장 금지.
154
+ const bin = RUNTIME_BIN[kind];
155
+ if (bin && !whichSync(bin)) {
156
+ ctx.out(ctx.ui.dim(ko
157
+ ? `참고: '${bin}' 실행 파일이 PATH에 없습니다. 설치 전에는 이 역할의 실행이 실패합니다.`
158
+ : `Note: '${bin}' is not on PATH. Runs with this role will fail until it is installed.`));
159
+ } else if (bin) {
160
+ const evidence = runtimeAuthEvidence(kind);
161
+ if (evidence.status === "none") {
162
+ ctx.out(ctx.ui.dim(ko
163
+ ? `참고: ${kind} 로그인 흔적이 없습니다. 로그인 전에는 실행이 실패할 수 있습니다.`
164
+ : `Note: no local sign-in evidence for ${kind}. Runs may fail until you sign in.`));
165
+ }
166
+ }
167
+ return 0;
168
+ }
169
+
170
+ function run(ctx, args = []) {
171
+ const en = ctx.lang === "en";
172
+ const [sub, ...rest] = args;
173
+ if (!sub || sub === "show" || sub === "list") return show(ctx);
174
+ if (sub === "set") return set(ctx, rest);
175
+ if (sub === "help" || sub === "--help" || sub === "-h") {
176
+ // SELF_HELP_COMMANDS 계약: --help 는 스텁이 아니라 실제 안내여야 한다.
177
+ ctx.out(en
178
+ ? [
179
+ "agentlas roles — orchestrator/worker model roles (persisted, shared with Desktop)",
180
+ " roles show both roles and their pools",
181
+ " roles set <orchestrator|worker> <runtime> [--model <id>] [--effort <level>]",
182
+ " roles set worker --inherit worker follows the orchestrator",
183
+ "",
184
+ ` runtimes: ${[...KNOWN_KINDS].join(" · ")}`,
185
+ " REPL /model and /runtime are session-scoped — this command is the persistent path.",
186
+ ].join("\n")
187
+ : [
188
+ "agentlas roles — 오케스트레이터/워커 모델 역할 (영구 저장, 데스크탑과 공유)",
189
+ " roles 두 역할과 풀 조회",
190
+ " roles set <orchestrator|worker> <runtime> [--model <id>] [--effort <level>]",
191
+ " roles set worker --inherit 워커가 오케스트레이터를 따름",
192
+ "",
193
+ ` 런타임: ${[...KNOWN_KINDS].join(" · ")}`,
194
+ " REPL /model·/runtime 은 세션 한정입니다 — 영구 설정은 이 명령입니다.",
195
+ ].join("\n"));
196
+ return 0;
197
+ }
198
+ ctx.err(en ? `unknown roles subcommand: ${sub} (show · set)` : `모르는 roles 하위 명령: ${sub} (show · set)`);
199
+ return 1;
200
+ }
201
+
202
+ module.exports = { run };
@@ -7,9 +7,13 @@
7
7
  * runtimeOverride, { cwd, projectPath, permission })
8
8
  *
9
9
  * 라우팅 불변식: 이 표면은 Agent Workforce Ontology 전용 fail-closed 경로다.
10
- * 어휘 라우터/hep-network 조용히 폴백하지 않는다. legacy-network 는 v1에서
11
- * parity().cmdHep(["hep-network", ...]) 호환 탈출구였는데, 그 parity 모듈이 아직
12
- * v2에 없으므로 정직 정지로 안내만 한다(가짜 성공 금지).
10
+ * 어휘 라우터로 조용히 폴백하지 않는다.
11
+ *
12
+ * 스코프 고지(2026-08-05): cmdWorkforce sourceScope 싣지 않고
13
+ * AGENTLAS_MCP_BASE_URL 을 직접 친다. 서버는 sourceScope 부재를 "hub" 로 잡으므로
14
+ * 이 명령이 실제로 보는 메뉴는 **공개 Hub 뿐**이다. 로컬·오너 Cloud 를 포함한
15
+ * 연합 편성은 MCP 호스트의 /hep-network 가 한다 — 이 표면에는 그 계층이 없어서
16
+ * hep-network/hep-local/hep-cloud/hep-hub/legacy-network 를 전부 삭제했다.
13
17
  *
14
18
  * Write-capable first contact bootstraps the exact current project through the
15
19
  * canonical Core merge-only boundary. Read-only inspection remains passive.
@@ -79,15 +83,62 @@ async function dispatch(ctx, command, args) {
79
83
  });
80
84
  return result && result.ok ? 0 : 1;
81
85
  }
82
- case "legacy-network": {
83
- // v1의 명시적 호환 탈출구(parity cmdHep → hep-network)는 아직 v2에 없다.
84
- // 조용히 workforce 로 대체 실행하지 않는다 — 두 경로는 라우팅 권한 모델이 다르다.
85
- ctx.err(
86
- "'legacy-network' (hep-network compatibility) is not wired into the v2 engine yet.\n" +
87
- "Use `agentlas workforce <request>` for the fail-closed Agent Workforce Ontology route,\n" +
88
- "or the v1 build: git tag legacy-v1-engine-snapshot.",
89
- );
90
- return 1;
86
+ /*
87
+ * 소스 스코프 편성 4종 2026-08-05 네이티브 배선.
88
+ *
89
+ * 역사: 7/28에 "별칭이 스코프를 버린다"를 고치며 hep-*를 외부 CLI 패스스루로
90
+ * 승격했는데, CLI는 exit 3 + host_llm_required만 반환하는 스텁이었다(8/5
91
+ * 삭제). 재조사 결과 편성의 조각(4,578줄 루프·로컬 Core MCP·주입 지점)이
92
+ * 전부 머신에 있었고, 빠진 것은 배선뿐이었다. 이제 이 표면이 직접
93
+ * 호스트다: 루프의 리더 LLM이 WorkOrder를 작성하고, 로컬 Core가 선언된
94
+ * 스코프의 메뉴를 연합하며, 같은 LLM이 정확 릴리스를 고른다.
95
+ *
96
+ * 폴백 금지: 로컬 Core가 없으면 원격 Hub로 조용히 내려가지 않는다 — 이름이
97
+ * 약속한 스코프가 거짓이 된다(7/28 결함의 재발). 정직 정지 + 설치 안내.
98
+ */
99
+ case "hep-network":
100
+ case "hep-local":
101
+ case "hep-cloud":
102
+ case "hep-hub": {
103
+ const sourceScope = command.slice(4); // network|local|cloud|hub
104
+ const ko = ctx.lang !== "en";
105
+ const { rest, runtimeOverride } = splitRuntimeOverride(args);
106
+ const hasTask = rest.some((token, i) =>
107
+ !["--benchmark", "--json", "--parallel", "-n"].includes(String(token))
108
+ && !(i > 0 && ["--parallel", "-n"].includes(String(rest[i - 1]))));
109
+ if (!hasTask) {
110
+ ctx.err(`usage: agentlas ${command} "<task>" [--parallel N] [--json] [--runtime <kind>]`);
111
+ return 1;
112
+ }
113
+ const { localCoreBin } = require("../hephaestus/local-core.cjs");
114
+ if (!localCoreBin()) {
115
+ ctx.err(ko
116
+ ? `${sourceScope} 스코프 편성은 로컬 Agentlas-OS Core(hephaestus)가 연합을 소유합니다 — 설치되어 있지 않습니다.`
117
+ : `${sourceScope}-scope staffing is federated by the local Agentlas-OS core (hephaestus), which is not installed.`);
118
+ ctx.err(ko
119
+ ? "설치: Agentlas 데스크탑 앱 또는 https://agentlas.cloud 안내를 따르세요. 공개 Hub 메뉴만이라면 지금도: agentlas workforce \"<요청>\""
120
+ : "Install Agentlas-OS (Desktop app or https://agentlas.cloud). For the public Hub menu only, this works today: agentlas workforce \"<request>\"");
121
+ return 1;
122
+ }
123
+ const db = ctx.db();
124
+ const cwd = projectCwd();
125
+ const permission = resolvePermission(ctx);
126
+ const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, permission, `terminal-${command}`) || cwd;
127
+ const { createLocalCoreHubTool } = require("../workforce/local-core-transport.cjs");
128
+ const { createLocalCoreWorkforceRuntime } = require("../workforce/deps.cjs");
129
+ const transport = createLocalCoreHubTool({ sourceScope, projectDir: projectPath, cwd });
130
+ try {
131
+ const runtime = createLocalCoreWorkforceRuntime({ lang: ctx.lang, out: ctx.out }, transport);
132
+ const result = await runtime.cmdWorkforce(db, rest, runtimeOverride, {
133
+ cwd,
134
+ projectPath,
135
+ permission,
136
+ sourceScope,
137
+ });
138
+ return result && result.ok ? 0 : 1;
139
+ } finally {
140
+ transport.close();
141
+ }
91
142
  }
92
143
  default: {
93
144
  ctx.err(`unknown workforce command: ${command}`);
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ /*
3
+ * 인터뷰 한 턴을 모델에게 묻는 최소 경로.
4
+ *
5
+ * 세션·영속·메모리 티켓 없이 **한 번 묻고 최종 텍스트만** 받는다. 인터뷰는 사람이 화면 앞에
6
+ * 앉아 기다리는 대화라, 채팅 세션의 부수효과(대화 저장·메모리 방출·펜스 처리)가 끼어들면
7
+ * 사용자가 요청하지 않은 기록이 남는다.
8
+ *
9
+ * 실패를 삼키지 않는다. 모델이 안 돌면 그 사실과 다음 행동을 그대로 돌려준다 —
10
+ * 조용히 빈 문자열을 주면 파서가 "읽지 못했습니다"로 바꿔 원인이 사라진다.
11
+ */
12
+ const nativeHost = require("../agentlas-native-host.cjs");
13
+ // ★resolveRuntime(resolve.cjs)이 아니라 resolveRuntimeForAgent를 쓴다.
14
+ // resolveRuntime은 `role`을 **아예 읽지 않아서**, 사용자가 모델 역할에 gemini를 지정해 둬도
15
+ // active_runtime(codex)으로 흘렀다(실측: 인터뷰가 사용자가 고르지 않은 런타임에서 돌았고,
16
+ // 그쪽 사용 한도가 소진돼 있었다). 사용자가 정한 역할 사다리를 따르는 쪽이 정본이다.
17
+ const { resolveRuntimeForAgent } = require("../runtimes/overrides.cjs");
18
+ const { listAvailableCliRuntimes } = require("../runtimes/detect.cjs");
19
+
20
+ /**
21
+ * native-host가 부르는 이벤트 싱크 — 인터뷰 중에는 화면에 아무것도 흘리지 않는다.
22
+ *
23
+ * ★메서드를 손으로 골라 적으면 안 된다. native-host가 부르는 이름이 하나라도 빠지면
24
+ * 턴 중간에 TypeError로 죽는다(실측: streamStart 누락으로 프로세스가 통째로 죽었다).
25
+ * 실제 EventSink의 표면을 그대로 덮어써서, 저쪽이 늘어나도 여기가 따라간다.
26
+ */
27
+ function quietSink() {
28
+ const { EventSink } = require("../sessions/sink.cjs");
29
+ const sink = new EventSink(() => {}, () => {});
30
+ const quiet = Object.create(Object.getPrototypeOf(sink));
31
+ for (const name of Object.getOwnPropertyNames(Object.getPrototypeOf(sink))) {
32
+ if (name === "constructor") continue;
33
+ quiet[name] = () => {};
34
+ }
35
+ // native-host가 색/번역 헬퍼도 만진다.
36
+ quiet.c = new Proxy({}, { get: () => (text) => String(text ?? "") });
37
+ quiet.t = (_key, fallback) => String(fallback ?? "");
38
+ quiet.replaceTasks = () => {};
39
+ return quiet;
40
+ }
41
+
42
+ /**
43
+ * 인터뷰 한 턴을 묻는다.
44
+ *
45
+ * 사용자가 정한 역할(orchestrator)을 **먼저** 쓴다. 그게 실패하면 조용히 포기하지 않고
46
+ * 이 컴퓨터에 있는 다른 런타임으로 이어서 물어보되, **어느 것이 답했는지 말한다** —
47
+ * 조용히 다른 모델로 바꾸면 사용자는 자기가 고른 모델이 돈 줄 안다.
48
+ * 전부 실패하면 각각의 사유를 그대로 돌려준다(하나로 뭉뚱그리면 원인이 사라진다).
49
+ *
50
+ * @returns {Promise<{ok:true,text:string,runtime:string,fellBackFrom?:string}
51
+ * |{ok:false,reason:string,nextAction:string}>}
52
+ */
53
+ async function askModel(ctx, prompt, opts = {}) {
54
+ const db = ctx.db();
55
+ let primary = null;
56
+ try {
57
+ primary = resolveRuntimeForAgent({
58
+ db,
59
+ prefs: ctx.prefs,
60
+ role: "orchestrator",
61
+ ...(opts.runtime ? { explicit: opts.runtime } : {}),
62
+ ...(opts.model ? { model: opts.model } : {}),
63
+ });
64
+ } catch (err) {
65
+ primary = null;
66
+ var primaryError = (err && err.message) || String(err);
67
+ }
68
+
69
+ // 시도 순서: 사용자가 정한 것 먼저, 그다음 이 컴퓨터에 있는 나머지.
70
+ const candidates = [];
71
+ if (primary && primary.kind && primary.bin) candidates.push(primary);
72
+ if (!opts.runtime) {
73
+ // listAvailableCliRuntimes()는 문자열이 아니라 {kind, bin, path} 객체를 돌려준다.
74
+ // 문자열로 읽으면 후보가 하나도 안 쌓여 폴백이 통째로 죽는다(실측).
75
+ for (const found of listAvailableCliRuntimes()) {
76
+ if (!found || !found.kind || !found.path) continue;
77
+ if (candidates.some((c) => c.kind === found.kind)) continue;
78
+ candidates.push({ kind: found.kind, bin: found.path });
79
+ }
80
+ }
81
+ if (!candidates.length) {
82
+ return {
83
+ ok: false,
84
+ reason: primaryError
85
+ ? `실행할 AI 런타임을 찾지 못했습니다: ${primaryError}`
86
+ : "이 컴퓨터에서 쓸 수 있는 AI 런타임이 없습니다.",
87
+ nextAction: "`agentlas doctor`로 런타임 상태를 확인한 뒤 다시 시도해 주세요.",
88
+ };
89
+ }
90
+
91
+ const failures = [];
92
+ for (const runtime of candidates) {
93
+ let res;
94
+ try {
95
+ res = await nativeHost.runNativeTurn({
96
+ kind: runtime.kind,
97
+ bin: runtime.bin,
98
+ ui: quietSink(),
99
+ cwd: opts.cwd || process.cwd(),
100
+ prompt,
101
+ // 읽기 권한 — 인터뷰는 사람에게 묻고 형식을 만드는 일이라 파일을 바꿀 이유가 없다.
102
+ permission: "read",
103
+ session: {},
104
+ model: runtime.model,
105
+ effort: runtime.effort,
106
+ mcpServers: [],
107
+ mcpAllowlistMode: "exact",
108
+ });
109
+ } catch (err) {
110
+ failures.push(`${runtime.kind}: ${(err && err.message) || err}`);
111
+ continue;
112
+ }
113
+ const text = String((res && (res.finalText || res.text)) || "");
114
+ if (text.trim()) {
115
+ const out = { ok: true, text, runtime: runtime.kind };
116
+ if (candidates[0] !== runtime) out.fellBackFrom = candidates[0].kind;
117
+ return out;
118
+ }
119
+ failures.push(res && res.error
120
+ ? `${runtime.kind}: ${String(res.error).replace(/\s+/g, " ").slice(0, 200)}`
121
+ : `${runtime.kind}: 빈 답`);
122
+ }
123
+
124
+ return {
125
+ ok: false,
126
+ reason: `AI가 답하지 못했습니다.\n ${failures.join("\n ")}`,
127
+ nextAction: "`agentlas doctor`로 런타임 상태를 확인하거나, 로그인이 필요한 런타임에 다시 로그인해 주세요.",
128
+ };
129
+ }
130
+
131
+ module.exports = { askModel };