agentlas 1.0.10 → 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.
@@ -397,10 +397,32 @@ function resolveAllocationAcrossRuntimes(options = {}) {
397
397
  return { ...resolution, runtime, runtimeId, requestedRuntimeId: requestedId || null };
398
398
  }
399
399
 
400
- function createDecisionReceipt({ taskId, stage, decision, resolution }) {
400
+ function modelRoleForStage(stage) {
401
+ return ["plan", "planner", "leader", "verify", "verifier", "synthesize", "synthesis", "route", "clarify"]
402
+ .includes(cleanText(stage, 80).toLowerCase())
403
+ ? "orchestrator"
404
+ : "worker";
405
+ }
406
+
407
+ function normalizeObservedUsage(value) {
408
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
409
+ const inputTokens = value.inputTokens;
410
+ const outputTokens = value.outputTokens;
411
+ return Number.isInteger(inputTokens) && inputTokens >= 0
412
+ && Number.isInteger(outputTokens) && outputTokens >= 0
413
+ ? { inputTokens, outputTokens }
414
+ : null;
415
+ }
416
+
417
+ function createDecisionReceipt({ taskId, stage, decision, resolution, role, usage }) {
401
418
  const normalized = normalizeAllocation(decision);
402
419
  const validationIssues = [];
403
- if (!normalized) validationIssues.push("invalid-ai-allocation");
420
+ const decisionProvided = decision != null;
421
+ if (!decisionProvided) validationIssues.push("allocation_not_provided");
422
+ else if (!normalized) validationIssues.push("invalid_ai_allocation");
423
+ const resolvedRole = role === "worker" || role === "orchestrator"
424
+ ? role
425
+ : modelRoleForStage(stage);
404
426
  const resolutionCodes = cleanText(resolution && resolution.fallbackReason, 500)
405
427
  .split(",")
406
428
  .map((code) => cleanText(code, 120))
@@ -411,12 +433,13 @@ function createDecisionReceipt({ taskId, stage, decision, resolution }) {
411
433
  ])].slice(0, 32);
412
434
  const featurePayload = JSON.stringify(normalized ? {
413
435
  phase: normalized.phase,
436
+ role: resolvedRole,
414
437
  tier: normalized.tier,
415
438
  effort: normalized.effort,
416
439
  reasonCodes: normalized.reasonCodes,
417
440
  requiredCapabilities: normalized.requiredCapabilities,
418
441
  estimatedContextTokens: normalized.estimatedContextTokens,
419
- } : { phase: cleanText(stage, 80) || null, allocation: null });
442
+ } : { phase: cleanText(stage, 80) || null, role: resolvedRole, allocation: null });
420
443
  const featureHash = `sha256:${crypto.createHash("sha256").update(featurePayload, "utf8").digest("hex")}`;
421
444
  const source = resolution && resolution.source;
422
445
  const hasResolvedCurrent = Boolean(
@@ -439,6 +462,7 @@ function createDecisionReceipt({ taskId, stage, decision, resolution }) {
439
462
  ? normalized.decisionId
440
463
  : `terminal:model-allocation:${featureHash.slice("sha256:".length, "sha256:".length + 24)}`,
441
464
  packetId: cleanText(taskId, 255) || null,
465
+ role: resolvedRole,
442
466
  status,
443
467
  requested: {
444
468
  tier: normalized ? normalized.tier : null,
@@ -460,6 +484,7 @@ function createDecisionReceipt({ taskId, stage, decision, resolution }) {
460
484
  selectorVersion: normalized ? normalized.selectorVersion : "deterministic-host-fallback",
461
485
  independentVerificationRequired:
462
486
  riskCodes.has("high-risk") || riskCodes.has("critical-risk") || riskCodes.has("independent-verification"),
487
+ usage: normalizeObservedUsage(usage),
463
488
  validationIssues,
464
489
  privacy: { rawPromptIncluded: false, rawTranscriptIncluded: false },
465
490
  };
@@ -522,6 +547,7 @@ module.exports = {
522
547
  resolveAllocation,
523
548
  resolveAllocationAcrossRuntimes,
524
549
  createDecisionReceipt,
550
+ modelRoleForStage,
525
551
  appendDecisionReceipt,
526
552
  defaultReceiptPath,
527
553
  plannerSystemPrompt,
@@ -167,6 +167,14 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
167
167
  return { ok: false, skipped: true, reason: "lease" };
168
168
  }
169
169
 
170
+ // 데스크탑 스케줄러는 60초마다 리스를 갱신한다. 여기는 한 번 잡고 끝이라
171
+ // TTL(15분)을 넘긴 실행은 프로세스가 살아 있어도 회수 대상이 됐다 — 같은
172
+ // 자동화가 두 실행기에서 겹쳐 도는 경로. 같은 주기로 심장박동을 보낸다.
173
+ const leaseHeartbeat = setInterval(() => {
174
+ try { store.renewAutomationLease(db, row.id); } catch { /* best-effort */ }
175
+ }, 60_000);
176
+ if (typeof leaseHeartbeat.unref === "function") leaseHeartbeat.unref();
177
+
170
178
  try {
171
179
  // ── raw-row 실행 계약 게이트 (데스크탑 automation-scheduler.ts:538-549 동형) ──
172
180
  // 손상된 계약 값으로는 무인 실행하지 않는다 — 문구까지 데스크탑과 동일.
@@ -246,6 +254,7 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
246
254
  store.advanceAfterRun(db, row, { ok: false, advanceSchedule: !!opts.advanceSchedule });
247
255
  return { ok: false, error: msg };
248
256
  } finally {
257
+ clearInterval(leaseHeartbeat);
249
258
  store.releaseAutomation(db, row.id);
250
259
  }
251
260
  }
@@ -105,6 +105,27 @@ function claimAutomation(db, id, now = new Date(), owner = LEASE_OWNER) {
105
105
  return (result.changes ?? result.rowsAffected ?? 0) > 0;
106
106
  }
107
107
 
108
+ /**
109
+ * 이 프로세스가 아직 들고 있는 리스의 claimed_at 을 현재로 민다.
110
+ *
111
+ * claimAutomation 은 한 번만 부르고 갱신이 없었다. 데스크탑 스케줄러는 60초마다
112
+ * 갱신하므로, TTL(15분)을 넘긴 CLI 실행은 프로세스가 멀쩡히 살아 있는데도
113
+ * 회수 대상이 된다 — 에이전트 세션에서 15분은 평범하고, 그러면 같은 자동화가
114
+ * 두 실행기에서 겹쳐 돈다. 소유자가 나일 때만 갱신하므로, 이미 남에게 넘어간
115
+ * 리스를 되빼앗지는 않는다.
116
+ */
117
+ function renewAutomationLease(db, id, now = new Date(), owner = LEASE_OWNER) {
118
+ if (!leaseSupported(db)) return false;
119
+ try {
120
+ const result = db
121
+ .prepare("UPDATE automations SET claimed_at = ? WHERE id = ? AND lease_owner = ?")
122
+ .run(now.toISOString(), id, owner);
123
+ return (result.changes ?? result.rowsAffected ?? 0) > 0;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
108
129
  function releaseAutomation(db, id) {
109
130
  try {
110
131
  db.prepare("UPDATE automations SET claimed_at = NULL, lease_owner = NULL WHERE id = ?").run(id);
@@ -201,6 +222,7 @@ module.exports = {
201
222
  setEnabled,
202
223
  removeAutomation,
203
224
  claimAutomation,
225
+ renewAutomationLease,
204
226
  releaseAutomation,
205
227
  recordRun,
206
228
  listRuns,
@@ -5,6 +5,30 @@ CREATE TABLE active_runtime (
5
5
  id INTEGER PRIMARY KEY CHECK(id = 1),
6
6
  kind TEXT NOT NULL
7
7
  , backend TEXT, source TEXT, model TEXT, long_context INTEGER NOT NULL DEFAULT 0);
8
+ CREATE TABLE model_roles (
9
+ role TEXT PRIMARY KEY CHECK(role IN ('orchestrator','worker')),
10
+ kind TEXT NOT NULL,
11
+ backend TEXT,
12
+ source TEXT,
13
+ model TEXT,
14
+ effort TEXT,
15
+ long_context INTEGER NOT NULL DEFAULT 0 CHECK(long_context IN (0,1)),
16
+ inherit INTEGER NOT NULL DEFAULT 0 CHECK(inherit IN (0,1)),
17
+ updated_at TEXT NOT NULL,
18
+ CHECK(role = 'worker' OR inherit = 0)
19
+ );
20
+ CREATE TABLE model_role_members (
21
+ role TEXT NOT NULL CHECK(role IN ('orchestrator','worker')),
22
+ position INTEGER NOT NULL CHECK(position >= 1),
23
+ kind TEXT NOT NULL,
24
+ backend TEXT,
25
+ source TEXT,
26
+ model TEXT,
27
+ effort TEXT,
28
+ long_context INTEGER NOT NULL DEFAULT 0 CHECK(long_context IN (0,1)),
29
+ updated_at TEXT NOT NULL,
30
+ PRIMARY KEY(role, position)
31
+ );
8
32
  CREATE TABLE installed_agents (
9
33
  id TEXT PRIMARY KEY,
10
34
  slug TEXT UNIQUE NOT NULL,
@@ -9,6 +9,19 @@ const fs = require("node:fs");
9
9
  const path = require("node:path");
10
10
  const { dbPath, userDataDir } = require("../core/paths.cjs");
11
11
  const { listAvailableCliRuntimes, activeRuntimeRow } = require("../runtimes/detect.cjs");
12
+ const { resolvedModelRole } = require("../runtimes/roles.cjs");
13
+
14
+ function roleDetail(selection, role, en) {
15
+ if (!selection) return en ? "not set" : "미설정";
16
+ const provider = selection.kind === "byok" ? selection.backend || "byok" : selection.kind;
17
+ return [
18
+ `${role}=${provider}${selection.model ? `/${selection.model}` : ""}`,
19
+ selection.effort ? `effort=${selection.effort}` : null,
20
+ role === "worker" && selection.inherit
21
+ ? (en ? "inherits orchestrator" : "오케스트레이터 상속")
22
+ : null,
23
+ ].filter(Boolean).join(" · ");
24
+ }
12
25
 
13
26
  function run(ctx) {
14
27
  const en = ctx.lang === "en";
@@ -42,8 +55,17 @@ function run(ctx) {
42
55
  ctx.out(ctx.ui.dim(" npm i -g @anthropic-ai/claude-code · @openai/codex · @google/gemini-cli"));
43
56
  }
44
57
  try {
45
- const active = activeRuntimeRow(ctx.db());
58
+ const db = ctx.db();
59
+ const active = activeRuntimeRow(db);
46
60
  if (active) ok(en ? "active runtime" : "활성 런타임", `${active.kind}${active.model ? ` (${active.model})` : ""}`);
61
+ const orchestrator = resolvedModelRole(db, "orchestrator");
62
+ const worker = resolvedModelRole(db, "worker");
63
+ if (orchestrator && worker) {
64
+ ok(
65
+ en ? "model roles" : "모델 역할",
66
+ `${roleDetail(orchestrator, "orchestrator", en)} · ${roleDetail(worker, "worker", en)}`,
67
+ );
68
+ }
47
69
  } catch { /* db issue already reported */ }
48
70
 
49
71
  // 3) 로그인 상태 (세션 파일 관측만 — 네트워크 호출 없음)
@@ -50,11 +50,14 @@ async function run(ctx, args) {
50
50
  }
51
51
  const ceo = rowToAgent(ceoRow);
52
52
 
53
- // 간단 플래그 파싱(--runtime/--permission) — 명령끼리 참조 금지 규칙상 run.cjs 미차용.
53
+ // 간단 플래그 파싱 — 명령끼리 참조 금지 규칙상 run.cjs 미차용.
54
54
  const rest = args.slice(1);
55
- const flags = { runtime: null, permission: null, task: [] };
55
+ const flags = { runtime: null, model: null, effort: null, tier: null, permission: null, task: [] };
56
56
  for (let i = 0; i < rest.length; i++) {
57
57
  if (rest[i] === "--runtime") flags.runtime = rest[++i];
58
+ else if (rest[i] === "--model") flags.model = rest[++i];
59
+ else if (rest[i] === "--effort") flags.effort = rest[++i];
60
+ else if (rest[i] === "--tier") flags.tier = rest[++i];
58
61
  else if (rest[i] === "--permission") flags.permission = rest[++i];
59
62
  else flags.task.push(rest[i]);
60
63
  }
@@ -62,26 +65,64 @@ async function run(ctx, args) {
62
65
  if (task) {
63
66
  // 3-tier 위임 실행 — PLAN → DELEGATE → SYNTHESIZE (firms/orchestrate.cjs).
64
67
  const { runFirmTurn } = require("../firms/orchestrate.cjs");
65
- const { resolveRuntimeForAgent, unavailableOverrideNote } = require("../runtimes/overrides.cjs");
68
+ const {
69
+ resolveRuntimeForAgent,
70
+ unavailableOverrideNote,
71
+ unavailableRoleNote,
72
+ } = require("../runtimes/overrides.cjs");
73
+ const { EFFORTS, TIERS } = require("../agentlas-workload-routing.cjs");
66
74
  const { Orchestrator } = require("../sessions/orchestrator.cjs");
67
75
  const permissions = require("../agentlas-permissions.cjs");
76
+ if (flags.effort && !EFFORTS.includes(String(flags.effort))) {
77
+ ctx.err(`unknown --effort ${flags.effort} (use: ${EFFORTS.join(" | ")})`);
78
+ return 1;
79
+ }
80
+ if (flags.tier && !TIERS.includes(String(flags.tier))) {
81
+ ctx.err(`unknown --tier ${flags.tier} (use: ${TIERS.join(" | ")})`);
82
+ return 1;
83
+ }
84
+ if (flags.tier && !flags.model) {
85
+ ctx.err("--tier requires --model: Terminal never guesses a provider model id from a cost tier");
86
+ return 1;
87
+ }
68
88
  let runtime;
89
+ let workerRuntime;
69
90
  try {
70
- // 사다리: 명시 > (agent CEO > firm) 오버라이드 > prefs > active > detected.
91
+ // CEO는 orchestrator, 본부는 worker. 명시 핀은 기존 firm 호출과의 호환을 위해
92
+ // 두 역할 모두에 적용하고, 미지정 시 각각 model_roles 기본값을 사용한다.
71
93
  runtime = resolveRuntimeForAgent({
72
94
  db,
73
95
  prefs: ctx.prefs,
74
96
  explicit: flags.runtime,
97
+ model: flags.model,
98
+ effort: flags.effort,
99
+ role: "orchestrator",
75
100
  targets: [
76
101
  { scope: "agent", targetId: ceo.id },
77
102
  { scope: "firm", targetId: firm.id },
78
103
  ],
79
104
  });
105
+ workerRuntime = resolveRuntimeForAgent({
106
+ db,
107
+ prefs: ctx.prefs,
108
+ explicit: flags.runtime,
109
+ model: flags.model,
110
+ effort: flags.effort,
111
+ role: "worker",
112
+ targets: [{ scope: "firm", targetId: firm.id }],
113
+ });
114
+ if (flags.tier) {
115
+ runtime.modelTier = flags.tier;
116
+ workerRuntime.modelTier = flags.tier;
117
+ }
80
118
  } catch (e) {
81
119
  ctx.err(String((e && e.message) || e));
82
120
  return 1;
83
121
  }
84
122
  if (runtime.unavailableOverride) ctx.err(ctx.ui.dim(unavailableOverrideNote(runtime, ctx.lang)));
123
+ if (runtime.unavailableRoleSelection) ctx.err(ctx.ui.dim(unavailableRoleNote(runtime, ctx.lang)));
124
+ if (workerRuntime.unavailableOverride) ctx.err(ctx.ui.dim(unavailableOverrideNote(workerRuntime, ctx.lang)));
125
+ if (workerRuntime.unavailableRoleSelection) ctx.err(ctx.ui.dim(unavailableRoleNote(workerRuntime, ctx.lang)));
85
126
  const orch = new Orchestrator({ db, lang: ctx.lang });
86
127
  const dim = ctx.ui.dim;
87
128
  const result = await runFirmTurn({
@@ -91,6 +132,20 @@ async function run(ctx, args) {
91
132
  ceoAgent: ceo,
92
133
  task,
93
134
  runtime,
135
+ workerRuntime,
136
+ resolveWorkerRuntime: flags.runtime || flags.model || flags.effort
137
+ ? null
138
+ : (node) => resolveRuntimeForAgent({
139
+ db,
140
+ prefs: ctx.prefs,
141
+ explicit: null,
142
+ role: "worker",
143
+ targets: [
144
+ { scope: "agent", targetId: node.agent.id },
145
+ { scope: "division", targetId: `${firm.id}:${node.role}` },
146
+ { scope: "firm", targetId: firm.id },
147
+ ],
148
+ }),
94
149
  permission: permissions.normalize(flags.permission || (ctx.prefs && ctx.prefs.permission) || "write"),
95
150
  cwd: process.cwd(),
96
151
  onEvent: (ev) => {
@@ -9,8 +9,8 @@ const HELP = `agentlas — the operating system for agents, in your terminal
9
9
 
10
10
  TALK & RUN
11
11
  <agent> · chat <agent> jump into a chat with one agent
12
- run [agent] [prompt] one-shot (-p print · --runtime · --permission; stdin ok)
13
- firm <firm> [task] delegate to a company's CEO
12
+ run [agent] [prompt] one-shot (-p · --runtime · --model · --effort · --permission)
13
+ firm <firm> [task] delegate to a CEO (--runtime · --model · --effort)
14
14
  chats [n] · open <id> recent conversations · resume one
15
15
 
16
16
  AGENTS & HUB
@@ -20,7 +20,7 @@ AGENTS & HUB
20
20
  build "<request>" build/repair/package an agent or team
21
21
  upload <path> save owner-private in Agent Cloud (--visibility marketplace to publish)
22
22
  import <path> · cd · native prepare local folder agents
23
- list installed agents/companies + active runtime
23
+ list installed agents/companies + orchestrator/worker runtimes
24
24
  experience <sub> portable Experience: list|inspect|validate|save|publish|status|export|unpublish
25
25
  variant resolve local variant selection
26
26
 
@@ -51,10 +51,13 @@ ACCOUNT & OPS
51
51
 
52
52
  IN-REPL (agentlas → interactive, Orca multi-session)
53
53
  /spawn <agent> [task] · /sessions · /tree · /s <n> · /steer <n> <msg> ·
54
- /kill <n> · /rm <n> · /broadcast <msg> · /use · /runtime · /permission
54
+ /kill <n> · /rm <n> · /broadcast <msg> · /use · /runtime · /model · /effort · /permission
55
55
  typing during a running turn queues steering; ctrl-c interrupts the turn
56
56
 
57
- Options: -p|--print · --runtime claude-code|codex|gemini · --permission read|write|full
57
+ Options: -p|--print · --runtime claude-code|codex|gemini · --model <exact-id> ·
58
+ --effort none|minimal|low|medium|high|xhigh|max ·
59
+ --tier economy|balanced|frontier (requires --model) ·
60
+ --permission read|write|full
58
61
  `;
59
62
 
60
63
  function run(ctx) {
@@ -5,8 +5,25 @@
5
5
  * 데스크탑과 동일하게 목록에서 숨긴다.
6
6
  */
7
7
  const { activeRuntimeRow, listAvailableCliRuntimes } = require("../runtimes/detect.cjs");
8
+ const { resolvedModelRole } = require("../runtimes/roles.cjs");
8
9
  const { listAgents } = require("../agents/registry.cjs");
9
10
 
11
+ function roleRuntimeLabel(selection, role, en) {
12
+ if (!selection) return en ? "(not set)" : "(미설정)";
13
+ const provider = selection.kind === "byok"
14
+ ? selection.backend || "byok"
15
+ : selection.kind;
16
+ const bits = [
17
+ provider,
18
+ selection.model ? `(${selection.model})` : "",
19
+ selection.effort ? `· effort ${selection.effort}` : "",
20
+ ].filter(Boolean);
21
+ if (role === "worker" && selection.inherit) {
22
+ bits.push(en ? "· inherits orchestrator" : "· 오케스트레이터 상속");
23
+ }
24
+ return bits.join(" ");
25
+ }
26
+
10
27
  function run(ctx) {
11
28
  const db = ctx.db();
12
29
  // 프라이버시 정책(웹 전용/백그라운드 제외)은 registry가 소유한다 — 직접 SQL 금지.
@@ -36,7 +53,14 @@ function run(ctx) {
36
53
  const active = activeRuntimeRow(db);
37
54
  const clis = listAvailableCliRuntimes();
38
55
  ctx.out("");
39
- ctx.out(ctx.ui.bold(en ? "Runtime" : "런타임"));
56
+ ctx.out(ctx.ui.bold(en ? "Model roles" : "모델 역할"));
57
+ const orchestrator = resolvedModelRole(db, "orchestrator");
58
+ const worker = resolvedModelRole(db, "worker");
59
+ ctx.out(` orchestrator: ${roleRuntimeLabel(orchestrator, "orchestrator", en)}`);
60
+ ctx.out(` worker: ${roleRuntimeLabel(worker, "worker", en)}`);
61
+
62
+ ctx.out("");
63
+ ctx.out(ctx.ui.bold(en ? "Legacy runtime compatibility" : "레거시 런타임 호환"));
40
64
  if (active) {
41
65
  ctx.out(` active: ${active.kind}${active.model ? ` (${active.model})` : ""}${active.backend ? ` via ${active.backend}` : ""}`);
42
66
  } else {
@@ -3,6 +3,9 @@
3
3
  * run — 원샷 실행: agentlas run [agent] [prompt…]
4
4
  * -p | --print 최종 답만 stdout에 (스트리밍 UI 없음)
5
5
  * --runtime <kind> claude-code | codex | gemini
6
+ * --model <id> exact provider model id
7
+ * --effort <level> none | minimal | low | medium | high | xhigh | max
8
+ * --tier <tier> economy | balanced | frontier (requires --model)
6
9
  * --permission <level> read | write | full
7
10
  * 프롬프트가 없고 stdin이 TTY가 아니면 stdin을 읽는다.
8
11
  *
@@ -10,15 +13,19 @@
10
13
  * 판정(resolveAutoRoute)이 최적 에이전트를 고른다 — 어휘 점수는 후보 모집 전용이다.
11
14
  * 판정 런타임이 없으면 기본 에이전트로 정직 폴백하되 note를 stderr에 반드시 출력한다
12
15
  * (조용한 오라우팅/폴백 금지 — 오너 결정).
13
- * 런타임 사다리: 명시(--runtime) > 에이전트별 오버라이드(agent_runtime_overrides) >
14
- * prefs > active_runtime > detected (runtimes/overrides.cjs).
16
+ * 런타임 사다리: 명시 > 에이전트별 오버라이드(agent_runtime_overrides) >
17
+ * model_roles[orchestrator] > active_runtime > detected (runtimes/overrides.cjs).
15
18
  */
16
19
  const { findAgent, listAgents } = require("../agents/registry.cjs");
17
- const { resolveRuntime } = require("../runtimes/resolve.cjs");
18
- const { resolveRuntimeForAgent, unavailableOverrideNote } = require("../runtimes/overrides.cjs");
20
+ const {
21
+ resolveRuntimeForAgent,
22
+ unavailableOverrideNote,
23
+ unavailableRoleNote,
24
+ } = require("../runtimes/overrides.cjs");
19
25
  const { Orchestrator } = require("../sessions/orchestrator.cjs");
20
26
  const { Renderer } = require("../ui/renderer.cjs");
21
27
  const permissions = require("../agentlas-permissions.cjs");
28
+ const { EFFORTS, TIERS } = require("../agentlas-workload-routing.cjs");
22
29
 
23
30
  function readStdin() {
24
31
  return new Promise((resolve) => {
@@ -30,11 +37,22 @@ function readStdin() {
30
37
  }
31
38
 
32
39
  function parseArgs(args) {
33
- const out = { print: false, runtime: null, permission: null, rest: [] };
40
+ const out = {
41
+ print: false,
42
+ runtime: null,
43
+ model: null,
44
+ effort: null,
45
+ tier: null,
46
+ permission: null,
47
+ rest: [],
48
+ };
34
49
  for (let i = 0; i < args.length; i++) {
35
50
  const a = args[i];
36
51
  if (a === "-p" || a === "--print") out.print = true;
37
52
  else if (a === "--runtime") out.runtime = args[++i];
53
+ else if (a === "--model") out.model = args[++i];
54
+ else if (a === "--effort") out.effort = args[++i];
55
+ else if (a === "--tier") out.tier = args[++i];
38
56
  else if (a === "--permission") out.permission = args[++i];
39
57
  else out.rest.push(a);
40
58
  }
@@ -49,6 +67,18 @@ async function runOnce(ctx, args) {
49
67
  ctx.err(`unknown --permission ${parsed.permission} (use: read | write | full)`);
50
68
  return 1;
51
69
  }
70
+ if (parsed.effort && !EFFORTS.includes(String(parsed.effort))) {
71
+ ctx.err(`unknown --effort ${parsed.effort} (use: ${EFFORTS.join(" | ")})`);
72
+ return 1;
73
+ }
74
+ if (parsed.tier && !TIERS.includes(String(parsed.tier))) {
75
+ ctx.err(`unknown --tier ${parsed.tier} (use: ${TIERS.join(" | ")})`);
76
+ return 1;
77
+ }
78
+ if (parsed.tier && !parsed.model) {
79
+ ctx.err("--tier requires --model: Terminal never guesses a provider model id from a cost tier");
80
+ return 1;
81
+ }
52
82
  const db = ctx.db();
53
83
 
54
84
  let agent = null;
@@ -70,7 +100,15 @@ async function runOnce(ctx, args) {
70
100
  // 기본 런타임을 먼저 확정한다 — 판정 러너(자동 라우팅)도 이 런타임을 쓴다.
71
101
  let runtime;
72
102
  try {
73
- runtime = resolveRuntime({ db, prefs: ctx.prefs, explicit: parsed.runtime });
103
+ runtime = resolveRuntimeForAgent({
104
+ db,
105
+ prefs: ctx.prefs,
106
+ explicit: parsed.runtime,
107
+ model: parsed.model,
108
+ effort: parsed.effort,
109
+ role: "orchestrator",
110
+ });
111
+ if (parsed.tier) runtime.modelTier = parsed.tier;
74
112
  } catch (e) {
75
113
  ctx.err(String((e && e.message) || e));
76
114
  return 1;
@@ -103,13 +141,27 @@ async function runOnce(ctx, args) {
103
141
  if (choice && choice.note) ctx.err(ctx.uiInstance.c.dim(choice.note));
104
142
  }
105
143
 
106
- // 에이전트가 확정된 뒤 에이전트별 런타임 오버라이드를 얹는다(명시 --runtime이 항상 우선).
107
- if (!parsed.runtime && agent && agent.id) {
144
+ // 에이전트가 확정된 뒤 에이전트별 오버라이드를 포함한 전체 사다리를 다시 해석한다.
145
+ // 명시 runtime/model/effort 핀은 항상 최상단이라 에이전트 기본값보다 우선한다.
146
+ if (agent && agent.id) {
108
147
  try {
109
- const layered = resolveRuntimeForAgent({ db, prefs: ctx.prefs, explicit: null, agentId: agent.id });
148
+ const layered = resolveRuntimeForAgent({
149
+ db,
150
+ prefs: ctx.prefs,
151
+ explicit: parsed.runtime,
152
+ model: parsed.model,
153
+ effort: parsed.effort,
154
+ role: "orchestrator",
155
+ agentId: agent.id,
156
+ });
110
157
  if (layered.unavailableOverride) ctx.err(ctx.uiInstance.c.dim(unavailableOverrideNote(layered, ctx.lang)));
158
+ if (layered.unavailableRoleSelection) ctx.err(ctx.uiInstance.c.dim(unavailableRoleNote(layered, ctx.lang)));
159
+ if (parsed.tier) layered.modelTier = parsed.tier;
111
160
  runtime = layered;
112
- } catch { /* 오버라이드 층 실패 → 이미 확정된 기본 런타임 유지 */ }
161
+ } catch (e) {
162
+ ctx.err(String((e && e.message) || e));
163
+ return 1;
164
+ }
113
165
  }
114
166
 
115
167
  const orch = new Orchestrator({ db, lang: ctx.lang });
@@ -125,7 +177,9 @@ async function runOnce(ctx, args) {
125
177
  if (!parsed.print) {
126
178
  renderer = new Renderer(ctx.uiInstance);
127
179
  renderer.attach(session, { replay: false });
128
- ctx.err(ctx.uiInstance.c.dim(`${agent.slug} · ${runtime.kind}`));
180
+ ctx.err(ctx.uiInstance.c.dim(
181
+ `${agent.slug} · ${runtime.kind}${runtime.model ? ` · ${runtime.model}` : ""}${runtime.effort ? ` · ${runtime.effort}` : ""}`,
182
+ ));
129
183
  }
130
184
 
131
185
  const res = await session.send(prompt);
@@ -204,13 +204,15 @@ function turnText(res) {
204
204
  * 회사 1태스크 실행: PLAN → DELEGATE → SYNTHESIZE.
205
205
  * @param {object} p
206
206
  * db, orch(Orchestrator), firm(firms 행), ceoAgent(rowToAgent 결과),
207
- * task, runtime, permission, cwd, onEvent?({phase,...}),
207
+ * task, runtime(orchestrator), workerRuntime?, resolveWorkerRuntime?(division),
208
+ * permission, cwd, onEvent?({phase,...}),
208
209
  * spawnImplFor?({kind:'ceo'|'division', role?}) — 계약 테스트 전용 fake spawn 주입,
209
210
  * timeoutConfig? — 테스트 전용.
210
211
  * @returns {ok, text, chatId, plan:{text,delegations}, divisions:[{role,name,ok,text,chatId}]}
211
212
  */
212
213
  async function runFirmTurn(p) {
213
214
  const { db, orch, firm, ceoAgent, task, runtime, permission, cwd } = p;
215
+ const workerRuntime = p.workerRuntime || runtime;
214
216
  const onEvent = typeof p.onEvent === "function" ? p.onEvent : () => {};
215
217
  const parseDelegations = loadDelegateParser();
216
218
  const divisions = resolveDivisions(db, firm, ceoAgent);
@@ -269,15 +271,20 @@ async function runFirmTurn(p) {
269
271
  // kind='division' + parent_chat_id(CEO 챗)로 영속된다. 한 본부의 실패는 격리한다.
270
272
  onEvent({ phase: "delegate", targets: matched.map((m) => ({ role: m.node.role, name: m.node.name, brief: m.brief })) });
271
273
  const divisionResults = await parallelCap(matched, maxParallel(), async (m) => {
274
+ const divisionRuntime = typeof p.resolveWorkerRuntime === "function"
275
+ ? p.resolveWorkerRuntime(m.node)
276
+ : workerRuntime;
272
277
  const session = orch.spawn({
273
278
  agent: m.node.agent,
274
- runtime,
279
+ runtime: divisionRuntime,
275
280
  permission,
276
281
  cwd,
277
282
  title: `division: ${m.node.role}`,
278
283
  parentKey: ceoSession.key,
279
284
  activate: false,
280
- spawnImpl: p.spawnImplFor ? p.spawnImplFor({ kind: "division", role: m.node.role }) : undefined,
285
+ spawnImpl: p.spawnImplFor
286
+ ? p.spawnImplFor({ kind: "division", role: m.node.role, runtime: divisionRuntime })
287
+ : undefined,
281
288
  timeoutConfig: p.timeoutConfig,
282
289
  });
283
290
  // 본부 세션도 generic 자동 위임을 끈다 — 터미널 firm 은 아직 tier-3(전문가) 배선이