agentlas 1.0.11 → 1.0.12

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.
@@ -13,7 +13,13 @@
13
13
  */
14
14
  const { tableExists, columnExists } = require("../core/db.cjs");
15
15
  const { RUNTIME_BIN, whichSync } = require("./detect.cjs");
16
- const { resolveRuntime, EXECUTABLE_KINDS } = require("./resolve.cjs");
16
+ const {
17
+ resolveRuntime,
18
+ EXECUTABLE_KINDS,
19
+ CLI_EXECUTABLE_KINDS,
20
+ API_EXECUTABLE_KINDS,
21
+ } = require("./resolve.cjs");
22
+ const { pickRoleFromPool } = require("./roles.cjs");
17
23
 
18
24
  const OVERRIDE_TABLE = "agent_runtime_overrides";
19
25
  // 데스크탑 VALID_SCOPES 동형. v2 터미널 호출자는 주로 'agent'지만 firm/division도 읽을 수 있다.
@@ -78,7 +84,7 @@ function findRuntimeOverride(db, targets) {
78
84
 
79
85
  /**
80
86
  * 오버라이드를 얹은 런타임 해석.
81
- * @param {object} p { db, prefs, explicit, agentId, targets?, deps? }
87
+ * @param {object} p { db, prefs, explicit, model?, effort?, role?, agentId, targets?, deps? }
82
88
  * targets: [{scope,targetId}] — 주면 agentId 대신 이 우선순위로 오버라이드를 찾는다
83
89
  * (firm 경로: agent > division > firm).
84
90
  * deps: 테스트 주입 { which, resolve } — 상용 호출자는 사용하지 않는다.
@@ -86,10 +92,48 @@ function findRuntimeOverride(db, targets) {
86
92
  * { ..., unavailableOverride } (오버라이드가 있으나 이 머신/v2에서 실행 불가 —
87
93
  * 조용히 무시하지 않고 사유를 실어 보내며, 호출자가 note를 출력해야 한다)
88
94
  */
89
- function resolveRuntimeForAgent({ db, prefs, explicit, agentId, targets, deps } = {}) {
95
+ function resolveRuntimeForAgent({
96
+ db,
97
+ prefs,
98
+ explicit,
99
+ model,
100
+ effort,
101
+ role = "orchestrator",
102
+ agentId,
103
+ targets,
104
+ deps,
105
+ } = {}) {
90
106
  const resolveImpl = (deps && deps.resolve) || resolveRuntime;
107
+ const withPins = (runtime) => ({
108
+ ...runtime,
109
+ ...(cleanText(model) ? { model: cleanText(model) } : {}),
110
+ ...(cleanText(effort) ? { effort: cleanText(effort) } : {}),
111
+ });
112
+ const selectedRuntime = (selection, source) => {
113
+ const kind = selection && selection.kind;
114
+ if (API_EXECUTABLE_KINDS.has(kind)) {
115
+ return {
116
+ kind,
117
+ backend: selection.backend || kind,
118
+ model: cleanText(model) || cleanText(selection.model) || undefined,
119
+ effort: cleanText(effort) || cleanText(selection.effort) || undefined,
120
+ source,
121
+ };
122
+ }
123
+ if (!CLI_EXECUTABLE_KINDS.has(kind)) return null;
124
+ const which = (deps && deps.which) || whichSync;
125
+ const bin = which(RUNTIME_BIN[kind]);
126
+ if (!bin) return null;
127
+ return {
128
+ kind,
129
+ bin,
130
+ model: cleanText(model) || cleanText(selection.model) || undefined,
131
+ effort: cleanText(effort) || cleanText(selection.effort) || undefined,
132
+ source,
133
+ };
134
+ };
91
135
  // 명시(--runtime)가 항상 이긴다 — 오버라이드는 "사용자가 고르지 않았을 때"의 기본값이다.
92
- if (explicit) return resolveImpl({ db, prefs, explicit });
136
+ if (explicit) return withPins(resolveImpl({ db, prefs, explicit }));
93
137
 
94
138
  const override = targets && targets.length
95
139
  ? findRuntimeOverride(db, targets)
@@ -98,27 +142,43 @@ function resolveRuntimeForAgent({ db, prefs, explicit, agentId, targets, deps }
98
142
  : null;
99
143
  if (override) {
100
144
  const kind = override.selection.kind;
101
- // v2 스트리밍 드라이버가 있는 CLI 런타임만 실제 실행 대상이다. byok/ollama/kimi
102
- // 오버라이드는 데스크탑에서는 유효하지만 터미널 v2 실행 사다리에는 아직 없다 —
103
- // 조용히 다른 런타임으로 둔갑시키지 않고 unavailableOverride로 정직하게 알린다.
145
+ // CLI 스트리밍과 로컬 API(Ollama)는 같은 Session 경로를 쓴다. 아직 연결되지
146
+ // 않은 BYOK/kimi 등은 조용히 다른 런타임으로 둔갑시키지 않는다.
104
147
  if (EXECUTABLE_KINDS.has(kind)) {
105
- const which = (deps && deps.which) || whichSync;
106
- const bin = which(RUNTIME_BIN[kind]);
107
- if (bin) {
108
- return {
109
- kind,
110
- bin,
111
- model: override.selection.model,
112
- effort: override.selection.effort,
113
- source: "agent-override",
114
- override,
115
- };
116
- }
148
+ const selected = selectedRuntime(override.selection, "agent-override");
149
+ if (selected) return { ...selected, override, role };
150
+ }
151
+ const resolved = withPins(resolveImpl({ db, prefs, explicit: null }));
152
+ return { ...resolved, unavailableOverride: override, role };
153
+ }
154
+
155
+ // Role defaults sit above active_runtime/detected but below exact per-call
156
+ // pins and agent/firm/division overrides. v80 풀(순서=우선순위)이 있으면
157
+ // 이 Terminal에서 실행 가능한 첫 멤버를 쓰고, 스킵 내역은 결과에 남긴다.
158
+ // 풀이 없으면 pickRoleFromPool이 단일 행/레거시 해석으로 내려간다.
159
+ const roleSelection = pickRoleFromPool(db, role, (member) => {
160
+ if (!EXECUTABLE_KINDS.has(member.kind)) return false;
161
+ return Boolean(selectedRuntime(member, member.sourceLayer));
162
+ });
163
+ if (roleSelection && EXECUTABLE_KINDS.has(roleSelection.kind)) {
164
+ const selected = selectedRuntime(roleSelection, roleSelection.sourceLayer);
165
+ if (selected) {
166
+ return {
167
+ ...selected,
168
+ role,
169
+ inheritedRole: roleSelection.inherit,
170
+ ...(roleSelection.skipped?.length
171
+ ? { rolePoolSkipped: roleSelection.skipped }
172
+ : {}),
173
+ };
117
174
  }
118
- const resolved = resolveImpl({ db, prefs, explicit: null });
119
- return { ...resolved, unavailableOverride: override };
120
175
  }
121
- return resolveImpl({ db, prefs, explicit: null });
176
+ const resolved = withPins(resolveImpl({ db, prefs, explicit: null }));
177
+ return {
178
+ ...resolved,
179
+ role,
180
+ ...(roleSelection ? { unavailableRoleSelection: roleSelection } : {}),
181
+ };
122
182
  }
123
183
 
124
184
  /** 오버라이드 실행 불가 시 사용자에게 출력할 한 줄 (데스크탑 문구 동형). */
@@ -130,10 +190,19 @@ function unavailableOverrideNote(runtime, lang) {
130
190
  : `Assigned runtime (${kind}) is unavailable here — using the default runtime (${runtime.kind}).`;
131
191
  }
132
192
 
193
+ function unavailableRoleNote(runtime, lang) {
194
+ if (!runtime || !runtime.unavailableRoleSelection) return "";
195
+ const selected = runtime.unavailableRoleSelection;
196
+ return lang === "ko"
197
+ ? `${selected.role} 기본 런타임(${selected.kind})을 이 Terminal에서 실행할 수 없어 ${runtime.kind}으로 실행합니다.`
198
+ : `${selected.role} default runtime (${selected.kind}) is unavailable in this Terminal — using ${runtime.kind}.`;
199
+ }
200
+
133
201
  module.exports = {
134
202
  readAgentRuntimeOverride,
135
203
  readRuntimeOverride,
136
204
  findRuntimeOverride,
137
205
  resolveRuntimeForAgent,
138
206
  unavailableOverrideNote,
207
+ unavailableRoleNote,
139
208
  };
@@ -7,9 +7,25 @@
7
7
  */
8
8
  const { RUNTIME_BIN, whichSync, listAvailableCliRuntimes, activeRuntimeRow } = require("./detect.cjs");
9
9
 
10
- // native-host가 스트리밍 드라이버를 갖춘 런타임만 실행 대상으로 삼는다.
11
- // kimi/grok/cursor 드라이버가 포팅되면 여기에 추가한다 (조용한 오폭 방지).
12
- const EXECUTABLE_KINDS = new Set(["claude-code", "codex", "gemini"]);
10
+ // Session이 실제 드라이버를 갖춘 런타임만 실행 대상으로 삼는다.
11
+ // CLI는 native-host, Ollama는 로컬 API loop를 쓴다. 다른 드라이버가 포팅되면
12
+ // 해당 집합에 추가한다(조용한 오폭 방지).
13
+ const CLI_EXECUTABLE_KINDS = new Set(["claude-code", "codex", "gemini"]);
14
+ const API_EXECUTABLE_KINDS = new Set(["ollama"]);
15
+ const EXECUTABLE_KINDS = new Set([
16
+ ...CLI_EXECUTABLE_KINDS,
17
+ ...API_EXECUTABLE_KINDS,
18
+ ]);
19
+
20
+ function apiRuntime(kind, model, source) {
21
+ if (!API_EXECUTABLE_KINDS.has(kind)) return null;
22
+ return {
23
+ kind,
24
+ backend: kind,
25
+ ...(model ? { model } : {}),
26
+ source,
27
+ };
28
+ }
13
29
 
14
30
  class NoRuntimeError extends Error {
15
31
  constructor(message) {
@@ -24,9 +40,11 @@ class NoRuntimeError extends Error {
24
40
  */
25
41
  function resolveRuntime({ db, prefs, explicit }) {
26
42
  if (explicit) {
43
+ const api = apiRuntime(explicit, null, "explicit");
44
+ if (api) return api;
27
45
  const bin = RUNTIME_BIN[explicit];
28
46
  if (!bin) throw new NoRuntimeError(`unknown runtime: ${explicit}`);
29
- if (!EXECUTABLE_KINDS.has(explicit)) {
47
+ if (!CLI_EXECUTABLE_KINDS.has(explicit)) {
30
48
  throw new NoRuntimeError(`runtime '${explicit}' has no v2 streaming driver yet (available: ${[...EXECUTABLE_KINDS].join(", ")})`);
31
49
  }
32
50
  const p = whichSync(bin);
@@ -34,18 +52,24 @@ function resolveRuntime({ db, prefs, explicit }) {
34
52
  return { kind: explicit, bin: p, source: "explicit" };
35
53
  }
36
54
  const pref = prefs && prefs.runtime;
37
- if (pref && EXECUTABLE_KINDS.has(pref)) {
55
+ if (pref && API_EXECUTABLE_KINDS.has(pref)) {
56
+ return apiRuntime(pref, null, "prefs");
57
+ }
58
+ if (pref && CLI_EXECUTABLE_KINDS.has(pref)) {
38
59
  const p = whichSync(RUNTIME_BIN[pref]);
39
60
  if (p) return { kind: pref, bin: p, source: "prefs" };
40
61
  }
41
62
  if (db) {
42
63
  const active = activeRuntimeRow(db);
43
- if (active && EXECUTABLE_KINDS.has(active.kind)) {
64
+ if (active && API_EXECUTABLE_KINDS.has(active.kind)) {
65
+ return apiRuntime(active.kind, active.model || undefined, "active");
66
+ }
67
+ if (active && CLI_EXECUTABLE_KINDS.has(active.kind)) {
44
68
  const p = whichSync(RUNTIME_BIN[active.kind]);
45
69
  if (p) return { kind: active.kind, bin: p, model: active.model || undefined, source: "active" };
46
70
  }
47
71
  }
48
- const found = listAvailableCliRuntimes().filter((r) => EXECUTABLE_KINDS.has(r.kind));
72
+ const found = listAvailableCliRuntimes().filter((r) => CLI_EXECUTABLE_KINDS.has(r.kind));
49
73
  if (found.length) return { kind: found[0].kind, bin: found[0].path, source: "detected" };
50
74
  // 신규 사용자의 최빈 막다른 길: "설치하라"만 있고 방법이 없으면 여기서 이탈한다.
51
75
  // 실제 설치 명령을 그대로 준다(데스크탑 온보딩의 "Claude Code 무료로 설치하기"와 동형).
@@ -61,4 +85,10 @@ function resolveRuntime({ db, prefs, explicit }) {
61
85
  ].join("\n"));
62
86
  }
63
87
 
64
- module.exports = { resolveRuntime, NoRuntimeError, EXECUTABLE_KINDS };
88
+ module.exports = {
89
+ resolveRuntime,
90
+ NoRuntimeError,
91
+ EXECUTABLE_KINDS,
92
+ CLI_EXECUTABLE_KINDS,
93
+ API_EXECUTABLE_KINDS,
94
+ };
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /*
3
+ * Shared Desktop/Terminal model-role reader.
4
+ *
5
+ * Persistence is owned by Desktop migration v79. Terminal reads defensively:
6
+ * orchestrator row -> legacy active_runtime -> null
7
+ * worker direct row -> orchestrator row/legacy active_runtime
8
+ *
9
+ * The worker may inherit upward for quality; the orchestrator never falls
10
+ * downward to the worker row.
11
+ */
12
+ const { tableExists, columnExists } = require("../core/db.cjs");
13
+
14
+ const MODEL_ROLE_TABLE = "model_roles";
15
+ const VALID_ROLES = new Set(["orchestrator", "worker"]);
16
+
17
+ function cleanText(value) {
18
+ const trimmed = typeof value === "string" ? value.trim() : "";
19
+ return trimmed || null;
20
+ }
21
+
22
+ function roleRow(db, role) {
23
+ if (!db || !VALID_ROLES.has(role) || !tableExists(db, MODEL_ROLE_TABLE)) return null;
24
+ for (const column of ["role", "kind", "inherit"]) {
25
+ if (!columnExists(db, MODEL_ROLE_TABLE, column)) return null;
26
+ }
27
+ try {
28
+ return db.prepare("SELECT * FROM model_roles WHERE role=?").get(role) || null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ function legacyOrchestrator(db) {
35
+ if (!db || !tableExists(db, "active_runtime")) return null;
36
+ try {
37
+ const row = db
38
+ .prepare(
39
+ "SELECT kind, backend, source, model, long_context FROM active_runtime WHERE id=1",
40
+ )
41
+ .get();
42
+ if (!row || !cleanText(row.kind)) return null;
43
+ let effort = null;
44
+ if (tableExists(db, "meta")) {
45
+ effort = cleanText(
46
+ db.prepare("SELECT value FROM meta WHERE key='claude_effort'").get()
47
+ ?.value,
48
+ );
49
+ }
50
+ return {
51
+ role: "orchestrator",
52
+ kind: cleanText(row.kind),
53
+ backend: cleanText(row.backend),
54
+ source: cleanText(row.source),
55
+ model: cleanText(row.model),
56
+ effort,
57
+ longContext: Boolean(row.long_context),
58
+ inherit: false,
59
+ updatedAt: null,
60
+ sourceLayer: "active-runtime",
61
+ };
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function normalizedRow(row, role) {
68
+ if (!row || !cleanText(row.kind)) return null;
69
+ return {
70
+ role,
71
+ kind: cleanText(row.kind),
72
+ backend: cleanText(row.backend),
73
+ source: cleanText(row.source),
74
+ model: cleanText(row.model),
75
+ effort: cleanText(row.effort),
76
+ longContext: Boolean(row.long_context),
77
+ inherit: role === "worker" && Boolean(row.inherit),
78
+ updatedAt: cleanText(row.updated_at),
79
+ sourceLayer: "model-role",
80
+ };
81
+ }
82
+
83
+ const MODEL_ROLE_MEMBER_TABLE = "model_role_members";
84
+
85
+ /** Desktop v80 역할 풀(순서=우선순위). 테이블이 없거나 비면 []. */
86
+ function roleMembers(db, role) {
87
+ if (!db || !VALID_ROLES.has(role) || !tableExists(db, MODEL_ROLE_MEMBER_TABLE)) return [];
88
+ for (const column of ["role", "position", "kind"]) {
89
+ if (!columnExists(db, MODEL_ROLE_MEMBER_TABLE, column)) return [];
90
+ }
91
+ try {
92
+ return db
93
+ .prepare("SELECT * FROM model_role_members WHERE role=? ORDER BY position ASC")
94
+ .all(role)
95
+ .map((row) => ({
96
+ ...normalizedRow({ ...row, inherit: 0 }, role),
97
+ position: row.position,
98
+ sourceLayer: "model-role-pool",
99
+ }))
100
+ .filter((row) => row && row.kind);
101
+ } catch {
102
+ return [];
103
+ }
104
+ }
105
+
106
+ /**
107
+ * 풀에서 첫 가용 멤버를 고른다. isAvailable(member)가 없으면 순서 1위.
108
+ * 전원 불가면 조용한 대체 없이 1위를 그대로 쓰고 skipped를 남긴다.
109
+ * worker 풀이 비면 오케스트레이터 풀을 상속하며, 풀 자체가 없으면
110
+ * 기존 단일 행 해석(resolvedModelRole)으로 내려간다.
111
+ */
112
+ function pickRoleFromPool(db, role = "orchestrator", isAvailable = null) {
113
+ if (!VALID_ROLES.has(role)) throw new TypeError(`unknown model role: ${role}`);
114
+ const own = roleMembers(db, role);
115
+ const inherited = role === "worker" && own.length === 0;
116
+ const members = inherited ? roleMembers(db, "orchestrator") : own;
117
+ const skipped = [];
118
+ for (const member of members) {
119
+ if (typeof isAvailable === "function" && !isAvailable(member)) {
120
+ skipped.push({ position: member.position, kind: member.kind, reason: "runtime-unavailable" });
121
+ continue;
122
+ }
123
+ return { ...member, role, inherit: inherited, skipped };
124
+ }
125
+ if (members.length > 0) {
126
+ return { ...members[0], role, inherit: inherited, skipped };
127
+ }
128
+ const single = resolvedModelRole(db, role);
129
+ return single ? { ...single, position: null, skipped } : null;
130
+ }
131
+
132
+ function resolvedModelRole(db, role = "orchestrator") {
133
+ if (!VALID_ROLES.has(role)) throw new TypeError(`unknown model role: ${role}`);
134
+ if (role === "orchestrator") {
135
+ return normalizedRow(roleRow(db, "orchestrator"), role) || legacyOrchestrator(db);
136
+ }
137
+ const worker = normalizedRow(roleRow(db, "worker"), "worker");
138
+ if (worker && !worker.inherit) return worker;
139
+ const orchestrator = resolvedModelRole(db, "orchestrator");
140
+ if (!orchestrator) return null;
141
+ return {
142
+ ...orchestrator,
143
+ role: "worker",
144
+ inherit: true,
145
+ updatedAt: worker?.updatedAt || orchestrator.updatedAt,
146
+ sourceLayer:
147
+ worker?.sourceLayer === "model-role"
148
+ ? "model-role-inherit"
149
+ : "active-runtime-inherit",
150
+ };
151
+ }
152
+
153
+ module.exports = {
154
+ MODEL_ROLE_TABLE,
155
+ MODEL_ROLE_MEMBER_TABLE,
156
+ VALID_ROLES,
157
+ roleRow,
158
+ roleMembers,
159
+ pickRoleFromPool,
160
+ legacyOrchestrator,
161
+ resolvedModelRole,
162
+ };
@@ -43,7 +43,7 @@ class Orchestrator extends EventEmitter {
43
43
  * 세션 생성(+선택적 즉시 실행). parentKey를 주면 그 세션의 서브에이전트
44
44
  * (division 챗)로 붙는다.
45
45
  */
46
- spawn({ agent, runtime, permission, cwd, title, parentKey, activate = true, spawnImpl, timeoutConfig, chatId }) {
46
+ spawn({ agent, runtime, permission, cwd, title, parentKey, activate = true, spawnImpl, apiTurnImpl, timeoutConfig, chatId }) {
47
47
  const parent = parentKey ? this.sessions.get(parentKey) || null : null;
48
48
  const session = new Session({
49
49
  db: this.db,
@@ -55,6 +55,7 @@ class Orchestrator extends EventEmitter {
55
55
  parent,
56
56
  title,
57
57
  spawnImpl,
58
+ apiTurnImpl,
58
59
  timeoutConfig,
59
60
  chatId,
60
61
  });
@@ -21,7 +21,7 @@ const RING_LIMIT = 2000;
21
21
  class Session extends EventEmitter {
22
22
  /**
23
23
  * @param {object} opts
24
- * db, agent {id,slug,name,systemPrompt}, runtime {kind,bin,model?},
24
+ * db, agent {id,slug,name,systemPrompt}, runtime {kind,bin,model?,effort?},
25
25
  * permission, cwd, lang, parent (Session|null), title, chatId?(재개)
26
26
  */
27
27
  constructor(opts) {
@@ -46,7 +46,9 @@ class Session extends EventEmitter {
46
46
  this._turnPromise = null;
47
47
  // 계약 테스트용 spawn 주입(runNativeTurn의 req.spawn). 프로덕션 경로에선 null.
48
48
  this._spawnImpl = opts.spawnImpl || null;
49
+ this._apiTurnImpl = opts.apiTurnImpl || null;
49
50
  this._timeoutConfig = opts.timeoutConfig || null;
51
+ this._apiAbort = null;
50
52
 
51
53
  this.chatId = opts.chatId || store.createChat(this.db, {
52
54
  agentId: this.agent.id,
@@ -154,26 +156,55 @@ class Session extends EventEmitter {
154
156
  }, true, prompt);
155
157
  } catch { /* 프롬프트 증강 실패는 턴을 막지 않는다 — 원 프롬프트로 진행 */ }
156
158
 
157
- const req = {
158
- kind: this.runtime.kind,
159
- bin: this.runtime.bin,
160
- ui: this._sink,
161
- cwd: this.cwd,
162
- prompt,
163
- systemPrompt,
164
- permission: this.permission,
165
- session: { ...this.runtimeSession },
166
- model: this.runtime.model,
167
- onSpawn: (child) => { this._child = child; },
168
- };
169
- if (this._spawnImpl) req.spawn = this._spawnImpl;
170
- if (this._timeoutConfig) req.timeoutConfig = this._timeoutConfig;
171
-
172
159
  let res;
173
- try {
174
- res = await nativeHost.runNativeTurn(req);
175
- } catch (e) {
176
- res = { text: "", session: req.session, error: (e && e.message) || String(e) };
160
+ if (this.runtime.kind === "ollama") {
161
+ const ctrl = new AbortController();
162
+ this._apiAbort = ctrl;
163
+ const apiTurn = this._apiTurnImpl || require("../agentlas-api-agent.cjs").runApiTurn;
164
+ try {
165
+ const history = store.chatHistory(this.db, this.chatId).map((row) => ({
166
+ role: row.role,
167
+ content: row.text,
168
+ }));
169
+ res = await apiTurn({
170
+ backend: "ollama",
171
+ model: this.runtime.model || "llama3.1",
172
+ system: systemPrompt,
173
+ messages: history,
174
+ ctx: {
175
+ cwd: this.cwd,
176
+ permission: this.permission,
177
+ env: process.env,
178
+ },
179
+ ui: this._sink,
180
+ signal: ctrl.signal,
181
+ });
182
+ } catch (e) {
183
+ res = { text: "", error: (e && e.message) || String(e) };
184
+ } finally {
185
+ this._apiAbort = null;
186
+ }
187
+ } else {
188
+ const req = {
189
+ kind: this.runtime.kind,
190
+ bin: this.runtime.bin,
191
+ ui: this._sink,
192
+ cwd: this.cwd,
193
+ prompt,
194
+ systemPrompt,
195
+ permission: this.permission,
196
+ session: { ...this.runtimeSession },
197
+ model: this.runtime.model,
198
+ effort: this.runtime.effort,
199
+ onSpawn: (child) => { this._child = child; },
200
+ };
201
+ if (this._spawnImpl) req.spawn = this._spawnImpl;
202
+ if (this._timeoutConfig) req.timeoutConfig = this._timeoutConfig;
203
+ try {
204
+ res = await nativeHost.runNativeTurn(req);
205
+ } catch (e) {
206
+ res = { text: "", session: req.session, error: (e && e.message) || String(e) };
207
+ }
177
208
  }
178
209
  this._child = null;
179
210
 
@@ -233,6 +264,11 @@ class Session extends EventEmitter {
233
264
  /** 실행 중 턴을 중단한다. 큐는 비운다. */
234
265
  kill() {
235
266
  this.queue.length = 0;
267
+ if (this._apiAbort) {
268
+ this.status = "killed";
269
+ try { this._apiAbort.abort(new Error("session killed")); } catch { /* already aborted */ }
270
+ return;
271
+ }
236
272
  if (this._child) {
237
273
  this.status = "killed";
238
274
  try { nativeHost.terminateNativeChild(this._child); } catch { /* already dead */ }
@@ -191,7 +191,7 @@ function create(deps) {
191
191
  return typeof text === "string" ? text : (text && text.text) || "";
192
192
  }
193
193
 
194
- function recordAllocation(task, stage, decision, resolution, parentTaskId = null) {
194
+ function recordAllocation(task, stage, decision, resolution, parentTaskId = null, usage = null) {
195
195
  const receipt = workloadRouting.createDecisionReceipt({
196
196
  taskId: `${stage}-${task.id || "synthesis"}`,
197
197
  parentTaskId,
@@ -199,6 +199,7 @@ function create(deps) {
199
199
  stage,
200
200
  decision,
201
201
  resolution,
202
+ usage,
202
203
  });
203
204
  try {
204
205
  workloadRouting.appendDecisionReceipt(
@@ -221,9 +222,9 @@ function create(deps) {
221
222
  availableModels: ctx.availableModels,
222
223
  maxTier: ctx.maxTier || process.env.AGENTLAS_MODEL_MAX_TIER,
223
224
  });
224
- recordAllocation(task, stage, task.allocation, resolution, parentTaskId);
225
225
  // 할당 거부 후 CLI 기본 모델로 조용히 실행 금지 — fail-closed (계약 테스트 고정).
226
226
  if (!resolution.ok) {
227
+ recordAllocation(task, stage, task.allocation, resolution, parentTaskId);
227
228
  throw new Error(`model allocation failed closed: ${resolution.fallbackReason || "no compliant live model"}`);
228
229
  }
229
230
  if (resolution.fallbackReason) {
@@ -238,17 +239,32 @@ function create(deps) {
238
239
  source: resolution.source,
239
240
  fallbackReason: resolution.fallbackReason || null,
240
241
  };
241
- if (selectedRuntime.mode === "cli") {
242
- return await D.captureRuntime(selectedRuntime.kind, system, prompt, {
243
- cwd,
244
- env,
245
- permission,
246
- model: resolution.model,
247
- effort: resolution.effort,
248
- });
242
+ let observed;
243
+ try {
244
+ observed = selectedRuntime.mode === "cli"
245
+ ? await D.captureRuntime(selectedRuntime.kind, system, prompt, {
246
+ cwd,
247
+ env,
248
+ permission,
249
+ model: resolution.model,
250
+ effort: resolution.effort,
251
+ envelope: true,
252
+ })
253
+ : await D.runApi(
254
+ selectedRuntime.backend,
255
+ resolution.model || selectedRuntime.model,
256
+ system,
257
+ prompt,
258
+ { envelope: true },
259
+ );
260
+ } catch (error) {
261
+ recordAllocation(task, stage, task.allocation, resolution, parentTaskId);
262
+ throw error;
249
263
  }
250
- const text = await D.runApi(selectedRuntime.backend, resolution.model || selectedRuntime.model, system, prompt);
251
- return typeof text === "string" ? text : (text && text.text) || "";
264
+ const text = typeof observed === "string" ? observed : (observed && observed.text) || "";
265
+ const usage = observed && typeof observed === "object" ? observed.usage : null;
266
+ recordAllocation(task, stage, task.allocation, resolution, parentTaskId, usage);
267
+ return text;
252
268
  }
253
269
 
254
270
  const label = runtime.mode === "cli" ? runtime.kind : runtime.backend;
@@ -29,6 +29,8 @@ const SLASH_COMMANDS = [
29
29
  { command: "/mcp", args: "", ko: "MCP 서버 목록", en: "MCP servers" },
30
30
  { command: "/doctor", args: "", ko: "런타임·데이터 점검", en: "Health check" },
31
31
  { command: "/runtime", args: "<kind>", ko: "새 세션 런타임 지정", en: "Set runtime for new sessions" },
32
+ { command: "/model", args: "<id|default>", ko: "새 세션 모델 지정", en: "Set model for new sessions" },
33
+ { command: "/effort", args: "<level|none>", ko: "새 세션 추론 강도 지정", en: "Set effort for new sessions" },
32
34
  { command: "/permission", args: "<level>", ko: "새 세션 권한 지정", en: "Set permission for new sessions" },
33
35
  { command: "/login", args: "", ko: "Agentlas Cloud 로그인", en: "Sign in to Agentlas Cloud" },
34
36
  { command: "/whoami", args: "", ko: "로그인 상태", en: "Signed-in account" },
@@ -40,12 +42,49 @@ const SLASH_COMMANDS = [
40
42
  { command: "/storm", args: "<goal>", ko: "Goal+UltraCode 하니스", en: "Goal+UltraCode harness" },
41
43
  { command: "/swarm", args: "<goal>", ko: "에이전트 스웜", en: "Agent swarm" },
42
44
  { command: "/network", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
45
+ { command: "/workforce", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
46
+ { command: "/taskforce", args: "<request>", ko: "임시 태스크포스 편성", en: "Assemble a task force" },
47
+ { command: "/build", args: "\"<request>\"", ko: "에이전트·팀 제작/수리/패키징", en: "Build, repair or package an agent or team" },
48
+ { command: "/call", args: "\"a,b\" \"<ctx>\"", ko: "지정 에이전트 호출", en: "Call named agents" },
49
+ { command: "/route", args: "\"<req>\"", ko: "최적 에이전트 라우팅", en: "Route to the best agent" },
50
+ { command: "/browser", args: "[sub]", ko: "브라우저 하드포인트", en: "Browser hardpoint" },
51
+ { command: "/connect", args: "<target>", ko: "에이전트·팀 연결", en: "Connect an agent or team" },
52
+ { command: "/research", args: "<sub>", ko: "리서치", en: "Research" },
53
+ { command: "/upload", args: "<path>", ko: "Agent Cloud에 저장·발행", en: "Save to Agent Cloud or publish" },
54
+ { command: "/cloud", args: "<sub>", ko: "클라우드 자산 관리", en: "Cloud assets" },
55
+ { command: "/import", args: "<path>", ko: "로컬 폴더 에이전트 가져오기", en: "Import a local folder agent" },
56
+ { command: "/cd", args: "[path]", ko: "작업 폴더 이동", en: "Change working folder" },
57
+ { command: "/native", args: "prepare <agent>", ko: "네이티브 CLI 컨텍스트 생성", en: "Prepare native CLI context" },
58
+ { command: "/plugin", args: "<sub>", ko: "Hub 플러그인(MCP)", en: "Hub plugins (MCP servers)" },
59
+ { command: "/plugins", args: "", ko: "설치된 플러그인", en: "Installed plugins" },
60
+ { command: "/experience", args: "<sub>", ko: "이식 가능한 Experience", en: "Portable Experience" },
61
+ { command: "/variant", args: "resolve", ko: "로컬 변형 선택", en: "Local variant selection" },
62
+ { command: "/memory", args: "<sub>", ko: "메모리", en: "Memory" },
63
+ { command: "/evolve", args: "", ko: "프롬프트 진화 제안", en: "Prompt-evolution proposals" },
64
+ { command: "/ontology", args: "", ko: "프로젝트 지식", en: "Project knowledge" },
65
+ { command: "/career-graph", args: "", ko: "소스 라우팅 그래프", en: "Source routing graph" },
66
+ { command: "/journal", args: "<sub>", ko: "Stormbreaker 실행 일지", en: "Stormbreaker run journal" },
67
+ { command: "/project", args: "[status|init]", ko: ".agentlas 프로젝트 상태", en: "Private project state" },
68
+ { command: "/context", args: "<sub>", ko: "의존성 맵", en: "Dependency map" },
69
+ { command: "/creds", args: "<sub>", ko: "자격증명", en: "Credentials" },
70
+ { command: "/env", args: "", ko: "공유 환경 키", en: "Shared env keys" },
71
+ { command: "/multimodal", args: "", ko: "이미지·영상·음성 설정", en: "Image/video/audio providers" },
72
+ { command: "/telegram", args: "[sub]", ko: "텔레그램 연결", en: "Telegram bindings" },
73
+ { command: "/oberon", args: "[sub]", ko: "AI 필름", en: "AI film" },
74
+ { command: "/film", args: "<sub>", ko: "필름 렌더", en: "Film render" },
75
+ { command: "/hep", args: "<sub…>", ko: "Hephaestus 패스스루", en: "Hephaestus passthrough" },
76
+ { command: "/netadmin", args: "[sub]", ko: "로컬 네트워크 관리", en: "Local network admin" },
77
+ { command: "/update", args: "", ko: "npm 업데이트 확인", en: "npm update check" },
78
+ { command: "/version", args: "", ko: "버전", en: "Version" },
79
+ { command: "/logout", args: "", ko: "로그아웃", en: "Sign out" },
80
+ { command: "/uninstall", args: "<slug>", ko: "에이전트 제거", en: "Uninstall an agent" },
43
81
  { command: "/quit", args: "", ko: "종료", en: "Quit" },
44
82
  { command: "/exit", args: "", ko: "종료", en: "Quit" },
45
83
  ];
46
84
 
47
85
  const SLASH_NAMES = SLASH_COMMANDS.map((c) => c.command);
48
86
  const RUNTIME_KINDS = ["claude-code", "codex", "gemini"];
87
+ const EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
49
88
  const PERM_LEVELS = ["read", "write", "full"];
50
89
  // 세션 인자를 받는 명령 — 완성 후보를 살아있는 세션 키(s1, s2…)로 채운다.
51
90
  const SESSION_ARG_COMMANDS = new Set(["/s", "/switch", "/steer", "/kill", "/rm"]);
@@ -83,6 +122,7 @@ function makeCompleter(ctx = {}) {
83
122
 
84
123
  const cmd = tokens[0];
85
124
  if (cmd === "/runtime") return [uniqStartsWith(RUNTIME_KINDS, last), last];
125
+ if (cmd === "/effort") return [uniqStartsWith(EFFORT_LEVELS, last), last];
86
126
  if (cmd === "/permission") return [uniqStartsWith(PERM_LEVELS, last), last];
87
127
  if (SESSION_ARG_COMMANDS.has(cmd) && tokens.length === 2) return [uniqStartsWith(getSessions(), last), last];
88
128
  if (AGENT_ARG_COMMANDS.has(cmd) && tokens.length === 2) {