agentlas 0.9.6 → 0.9.8

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 CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.8 — 2026-07-25
4
+
5
+ - No more silent keyword fallback. When the connected model can't judge a route
6
+ (no runtime, or the model timed out / returned junk), Agentlas no longer picks
7
+ a specialist by keyword — it answers with the plain assistant and says why. The
8
+ note distinguishes "no model connected" (connect one) from "the model didn't
9
+ answer in time" (retry / check it), so a transient timeout isn't mistaken for a
10
+ missing model. Image-capability routing is likewise model-only: a keyword guess
11
+ never hijacks which runtime an agent runs on.
12
+ - The embedded Agentlas OS runtime's own judge (content-guard, pipeline,
13
+ research, privacy) now uses this host's connected model too, via a universal
14
+ callback — so provider/CLI users, not only local Ollama, get real judgment
15
+ there, with no model hardcoded. Pins Agentlas OS v1.1.62.
16
+
17
+ ## 0.9.7 — 2026-07-25
18
+
19
+ - Fix: the resident judge now reaches API/Ollama/BYOK runtimes, not only CLI
20
+ subprocess runtimes. Previously, when your connected runtime was Ollama or a
21
+ BYOK API model, every route, intent, and classification silently used the
22
+ deterministic wordlist fallback because the judge was wired to null — so a
23
+ non-English request the keyword lists could not read never got a model
24
+ verdict. The judge now runs on whatever runtime you actually have connected,
25
+ and its timeout signal aborts the underlying request cleanly.
26
+ - Fix: the judge is installed at startup, before the first routing decision.
27
+ It was previously wired only inside the run turn, which happens after routing,
28
+ so the very first auto-route in a one-shot always fell back.
29
+ - Routing gives the model a more generous deadline (a one-shot pre-run gate), so
30
+ a slower local model is judged rather than frequently falling back. When it
31
+ still cannot answer in time, the route receipt says so explicitly.
32
+
3
33
  ## 0.9.6 — 2026-07-25
4
34
 
5
35
  - Agent and App Builder routing is now decided by the connected model: lexical
@@ -63,9 +63,10 @@ const IMAGE_TOOL_MARKERS = [/nano-?banana/i, /\bimagen\b/i, /gpt-image/i, /grok\
63
63
  // 겹치는 정규식 여러 개를 동시에 때려도 1클러스터다.
64
64
  const MIN_BODY_IMAGE_SENTENCES = 3;
65
65
  const BODY_SCAN_CAP = 16000; // 로컬 임포트 상한과 동일 — 클라우드 무제한 프롬프트의 전문 스캔 방지
66
- // 어휘 판정연결 모델이 없을 때의 결정적 폴백이자, 모델 판정 전의 참고 프리필터.
67
- // 하우스 룰: 단어목록은 최종 결정을 내리지 않는다. 최종 판정은 resolveNeedsImage가
68
- // 상주 판정 서비스(judgeLabels) 묻고, 이 함수는 그 폴백으로만 살아남는다.
66
+ // 어휘 스코어IMAGE_HINTS 단어목록에 대한 참고용 스코어러. 하우스 룰(2026-07-25):
67
+ // 단어목록은 최종 결정을 내리지 않는다. resolveNeedsImage가 IMAGE_HINTS를 힌트로만 모델에
68
+ // 넘기고, 연결 모델이 없으면 이미지 여부를 판정하지 않는다(어휘 폴백 제거). 이 함수는 힌트
69
+ // 품질을 고정하는 참조 스코어러로만 남는다(회귀 테스트가 정밀도를 검증).
69
70
  function needsImageLexical(agent) {
70
71
  if (!agent) return false;
71
72
  if (NON_IMAGE_ROLES.has(String(agent.role || "").toLowerCase())) return false;
@@ -93,7 +94,7 @@ function needsImageLexical(agent) {
93
94
  // ── 상주 판정 서비스 배선 — 모델이 의미로 최종 판정, IMAGE_HINTS는 참고 힌트 ──────
94
95
  // needsImage 호출자(REPL 배지·autoRuntimeFor·routingNote)는 동기라서 warm-cache 패턴:
95
96
  // 비동기 경로(resolveNeedsImage)가 먼저 판정해 캐시를 데우고, 동기 needsImage는 캐시만
96
- // 읽는다. 캐시 미스 = 어휘 폴백 그대로imageJudgmentSource가 어느 쪽이었는지 라벨한다.
97
+ // 읽는다. 캐시 미스 = "이미지 아님"으로 처리(어휘 폴백 없음)모델 판정만 이미지로 인정한다.
97
98
  const IMAGE_VERDICT_CACHE_MAX = 200;
98
99
  const imageVerdicts = new Map();
99
100
  function imageJudgeInput(agent) {
@@ -109,17 +110,20 @@ function imageJudgeVetoed(agent) {
109
110
  return false;
110
111
  }
111
112
  // 이 에이전트의 직무가 이미지 생산인지를 연결 모델이 의미로 판정한다.
112
- // 러너 없음/타임아웃/정크 어휘 판정을 "fallback"으로 라벨해 반환 (조용한 회귀 금지).
113
+ // 하우스 룰(2026-07-25): 연결 모델이 없으면 단어목록으로 이미지 여부를 결정하지 않는다.
114
+ // 러너 없음/타임아웃/정크 → source:"unavailable"(판정 불가)로 반환하고, 호출자는 안전
115
+ // 기본값(세션 런타임 유지, gemini/codex 하이재킹 금지)을 지킨다. 이미지 판정은 저위험
116
+ // 능력 추론이라 실패-닫힘이 아니라 "판정 안 함"으로 정직하게 둔다.
113
117
  async function resolveNeedsImage(agent) {
114
- const lexical = needsImageLexical(agent);
115
- if (imageJudgeVetoed(agent)) return { image: lexical, source: "deterministic" };
118
+ // 팀/역할 하드 가드는 구조적 판단(키워드 아님) — 판정 대상에서 제외하고 결정적으로 not-image.
119
+ if (imageJudgeVetoed(agent)) return { image: false, source: "deterministic" };
116
120
  let judgment;
117
121
  try {
118
122
  judgment = require("./agentlas-judgment.cjs");
119
123
  } catch {
120
124
  judgment = null;
121
125
  }
122
- if (!judgment || !judgment.hasJudgmentRunner()) return { image: lexical, source: "fallback" };
126
+ if (!judgment || !judgment.hasJudgmentRunner()) return { image: false, source: "unavailable", decided: false };
123
127
  const input = imageJudgeInput(agent);
124
128
  const cached = imageVerdicts.get(input);
125
129
  if (cached) return cached;
@@ -136,9 +140,10 @@ async function resolveNeedsImage(agent) {
136
140
  "producing them: builders, orchestrators, PMs, curators, and coordination brains that commission or " +
137
141
  "delegate image work are 'not-image'. Refusals or prohibitions ('never generate images') declare the " +
138
142
  "opposite of a capability.",
139
- fallback: [lexical ? "image" : "not-image"],
143
+ fallback: [],
140
144
  });
141
- if (verdict.source !== "llm" || !verdict.labels.length) return { image: lexical, source: "fallback" };
145
+ // 모델이 판정을 못 냈다 → 어휘 폴백 없이 "판정 불가". 캐시하지 않아 이후 모델 연결 재판정한다.
146
+ if (verdict.source !== "llm" || !verdict.labels.length) return { image: false, source: "unavailable", decided: false };
142
147
  const out = { image: verdict.labels[0] === "image", source: "llm", reason: verdict.reason || "" };
143
148
  imageVerdicts.set(input, out);
144
149
  if (imageVerdicts.size > IMAGE_VERDICT_CACHE_MAX) {
@@ -148,14 +153,15 @@ async function resolveNeedsImage(agent) {
148
153
  return out;
149
154
  }
150
155
  // Does this agent's job involve generating/handling images? Sync surface for badges and
151
- // autoRuntimeFor: model verdict from the warm cache wins; miss = deterministic lexical fallback.
156
+ // autoRuntimeFor. House rule (2026-07-25): only a warm MODEL verdict counts as "image".
157
+ // A cache miss (no model judgment / not warmed) is treated as "not specifically an image
158
+ // agent" so a keyword guess can never hijack the runtime to gemini/codex. The lexical
159
+ // scorer (needsImageLexical) survives only as the hint source for the judge, not a decision.
152
160
  function needsImage(agent) {
153
161
  if (!agent) return false;
154
- if (!imageJudgeVetoed(agent)) {
155
- const cached = imageVerdicts.get(imageJudgeInput(agent));
156
- if (cached) return cached.image;
157
- }
158
- return needsImageLexical(agent);
162
+ if (imageJudgeVetoed(agent)) return false;
163
+ const cached = imageVerdicts.get(imageJudgeInput(agent));
164
+ return cached ? cached.image : false;
159
165
  }
160
166
  // 라벨용 — 이 에이전트의 현재 이미지 판정이 모델("llm")인지 결정적 경로("deterministic")인지.
161
167
  function imageJudgmentSource(agent) {
Binary file
@@ -573,6 +573,34 @@ function directRouteChoice(lang) {
573
573
  : "this is a general request that needs no specialist agent",
574
574
  };
575
575
  }
576
+ // 연결 모델 없음 직답 — 하우스 룰: 모델이 없으면 어휘(단어목록)로 전문 에이전트를 고르지
577
+ // 않는다. 어떤 전문 에이전트가 맞는지 "판단하지 못한다"고 정직하게 밝히고 기본 어시스턴트로
578
+ // 답한다. routeSource는 "deterministic"(=판정 없음)으로 유지하되 noModel 플래그로 구분해
579
+ // 사용자에게 "모델 연결" 안내를 띄운다. 이 경로가 조용히 키워드로 결정하던 것이 사고의 근본.
580
+ // reason 구분: "no_runtime"=연결된 런타임이 아예 없음(드묾), "model_unavailable"=런타임은
581
+ // 연결됐지만 그 한 번의 판정이 타임아웃/불량 응답으로 실패(현실적 케이스). 모델이 거의 항상
582
+ // 연결돼 있으므로 후자를 "연결 안 됨"으로 오인시키지 않도록 문구를 다르게 준다.
583
+ function noModelRouteChoice(lang, reason = "no_runtime") {
584
+ const resolvedLang = lang || prefsLang();
585
+ const message = reason === "model_unavailable"
586
+ ? (resolvedLang === "ko"
587
+ ? "연결된 모델이 제때 응답하지 않아 어떤 전문 에이전트가 맞는지 판단하지 못했습니다. 잠시 후 다시 시도하거나 모델 상태를 확인해 주세요. 지금은 기본 어시스턴트로 답합니다"
588
+ : "the connected model didn't answer in time, so I couldn't judge which specialist agent fits; retry in a moment or check the model, and I'll answer with the default assistant for now")
589
+ : (resolvedLang === "ko"
590
+ ? "연결된 모델이 없어 어떤 전문 에이전트가 맞는지 판단하지 못했습니다. 모델을 연결하면 자동 라우팅이 됩니다. 지금은 기본 어시스턴트로 답합니다"
591
+ : "no model is connected, so I couldn't judge which specialist agent fits; connect a model to enable auto-routing, and I'll answer with the default assistant for now");
592
+ return {
593
+ direct: true,
594
+ agent: null,
595
+ score: 0,
596
+ terms: [],
597
+ strong: false,
598
+ noModel: true,
599
+ noModelReason: reason,
600
+ routeSource: "deterministic",
601
+ reason: message,
602
+ };
603
+ }
576
604
  // 직답 모드 시스템 프롬프트 — 페르소나·라우팅 오염 없이 현재 런타임 그대로 답한다.
577
605
  function directSystemPrompt(lang) {
578
606
  const resolvedLang = lang || prefsLang();
@@ -630,8 +658,10 @@ function autoRouteAgent(db, prompt, lang) {
630
658
  // direct) 중에서 의미로 하나를 고른다 — 어휘 점수 0점인 에이전트(아랍어 등 어떤 언어의
631
659
  // 요청이든)도 모델은 뽑을 수 있다. 닫힌형 가드는 결정적으로 유지: 잡담 short-circuit
632
660
  // (isTrivialRoutePrompt)과 경로 스트리핑(ROUTE_PATH_RE)은 모델 호출 전에 그대로 적용.
633
- // 러너 없음/타임아웃/정크 → 기존 어휘 라우팅(autoRouteAgent)을 routeSource:"deterministic"
634
- // 으로 라벨해 반환한다 조용한 폴백 금지(라우트 노트에 판정 주체가 찍힌다).
661
+ // 러너 없음/타임아웃/정크 → 어휘로 전문 에이전트를 고르지 않는다. 연결 모델이 없으면
662
+ // "판단 함"을 정직하게 밝히고 기본 어시스턴트로 직답한다(noModelRouteChoice)
663
+ // 어휘 스코어러(autoRouteAgent/rankRouteAgents)는 후보 모집 전용이지 반환 결정이 아니다.
664
+ // routeSource:"deterministic"은 "판정 없음"을 뜻하는 기계 플래그로만 남는다(키워드 픽 아님).
635
665
  const ROUTE_JUDGE_CANDIDATE_CAP = 30;
636
666
  const ROUTE_JUDGE_DIRECT_LABEL = "direct";
637
667
  const ROUTE_JUDGE_META_LABEL = "meta-builder";
@@ -639,20 +669,22 @@ const ROUTE_JUDGE_APP_LABEL = "app-builder";
639
669
  const APP_BUILDER_ROUTE_SLUG = "agentlas-app-builder";
640
670
  async function resolveAutoRoute(db, prompt, lang) {
641
671
  const resolvedLang = lang || prefsLang();
642
- const prefilter = autoRouteAgent(db, prompt, resolvedLang);
672
+ ensureJudgmentRunnerInstalled(db);
643
673
  let judgment;
644
674
  try {
645
675
  judgment = require("./agentlas-judgment.cjs");
646
676
  } catch {
647
677
  judgment = null;
648
678
  }
649
- if (!judgment || !judgment.hasJudgmentRunner()) return { ...prefilter, routeSource: "deterministic" };
679
+ // 연결 모델 없음 어휘로 전문 에이전트를 고르지 않는다. 정직하게 "판단 못 함" + 모델 연결 안내.
680
+ if (!judgment || !judgment.hasJudgmentRunner()) return noModelRouteChoice(resolvedLang, "no_runtime");
650
681
  const promptText = routeNormalize(routeStripPaths(prompt));
651
- // 잡담("hi")까지 모델에 물으면 턴이 느려진다 정확 매칭 가드는 닫힌형이라 결정적 유지.
652
- if (!promptText.trim() || isTrivialRoutePrompt(promptText)) return { ...prefilter, routeSource: "deterministic" };
682
+ // 잡담("hi") 닫힌형 결정적 가드로 직답모델은 연결돼 있으니 "모델 연결" 안내는 필요 없다.
683
+ if (!promptText.trim() || isTrivialRoutePrompt(promptText)) return { ...directRouteChoice(resolvedLang), routeSource: "deterministic" };
653
684
  const ranked = rankRouteAgents(db, prompt, resolvedLang);
654
685
  const meta = resolveMetaBuilder(db);
655
- if (!ranked.length && !meta) return { ...prefilter, routeSource: "deterministic" };
686
+ // 라우팅할 후보 자체가 없음(설치 에이전트 0) 직답. 모델은 연결돼 있으니 일반 직답 사유.
687
+ if (!ranked.length && !meta) return { ...directRouteChoice(resolvedLang), routeSource: "deterministic" };
656
688
  // App Builder는 합성 라벨로만 제시한다(동의 핸드셰이크가 걸린 특수 라우트임을 모델에 명시).
657
689
  const appBuilder = ranked.find((r) => r.agent.slug === APP_BUILDER_ROUTE_SLUG) || null;
658
690
  const candidates = ranked.filter((r) => r.agent.slug !== APP_BUILDER_ROUTE_SLUG).slice(0, ROUTE_JUDGE_CANDIDATE_CAP);
@@ -700,8 +732,15 @@ async function resolveAutoRoute(db, prompt, lang) {
700
732
  ].join("\n"),
701
733
  multi: false,
702
734
  fallback: [],
735
+ // Routing is a one-shot pre-run gate: correctness matters more than latency,
736
+ // and a local 30B model with a full roster can need well over the 20s default.
737
+ // The judge's abort signal is threaded into the request, so a genuinely hung
738
+ // model still aborts cleanly at this deadline rather than hanging forever.
739
+ timeoutMs: 40000,
703
740
  });
704
- if (verdict.source !== "llm" || !verdict.labels.length) return { ...prefilter, routeSource: "deterministic" };
741
+ // 모델이 판정을 냈다(러너 실패/타임아웃/정크 응답) 어휘 픽으로 떨어지지 않는다.
742
+ // 연결 모델이 사실상 판단하지 못한 것이므로 "판단 못 함" 직답으로 정직하게 종결한다.
743
+ if (verdict.source !== "llm" || !verdict.labels.length) return noModelRouteChoice(resolvedLang, "model_unavailable");
705
744
  const picked = verdict.labels[0];
706
745
  const reason =
707
746
  verdict.reason ||
@@ -711,15 +750,24 @@ async function resolveAutoRoute(db, prompt, lang) {
711
750
  return { agent: meta, score: 1000, strong: true, terms: [], reason, routeSource: "llm" };
712
751
  }
713
752
  const chosen = picked === ROUTE_JUDGE_APP_LABEL ? appBuilder : bySlug.get(picked);
714
- if (!chosen) return { ...prefilter, routeSource: "deterministic" };
753
+ // 모델이 라벨을 골랐지만 설치 에이전트로 해석되지 않음(경계 케이스) 어휘 대신 직답.
754
+ if (!chosen) return { ...directRouteChoice(resolvedLang), routeSource: "deterministic" };
715
755
  // 모델 확답은 strong 계약을 충족한다 — 어휘 근거(terms/score)는 참고로 보존.
716
756
  return { ...chosen, strong: true, reason, routeSource: "llm" };
717
757
  }
718
758
  // 라우트 영수증 라벨 — 누가 최종 판정했는지 반드시 찍는다(조용한 폴백 금지 하우스 룰).
759
+ // noModel 경로는 사유(reason)에 이미 "모델 연결" 안내가 담겨 있으니 짧은 기계 라벨만 덧붙인다.
760
+ // 그 외 deterministic(잡담·후보 없음: 모델은 연결됨)은 사유가 이미 설명하므로 라벨을 붙이지 않는다.
719
761
  function routeJudgeSourceNote(choice, lang) {
720
762
  if (!choice || !choice.routeSource) return "";
721
763
  if (choice.routeSource === "llm") return lang === "ko" ? " (판정: 연결 모델)" : " (judged by the connected model)";
722
- return lang === "ko" ? " (판정: 결정적 폴백 — 연결 모델 없음)" : " (deterministic fallback — no connected-model verdict)";
764
+ if (choice.noModel) {
765
+ if (choice.noModelReason === "model_unavailable") {
766
+ return lang === "ko" ? " (판정 없음 — 모델 응답 없음)" : " (no judgment — model did not answer)";
767
+ }
768
+ return lang === "ko" ? " (판정 없음 — 연결 모델 없음)" : " (no judgment — no model connected)";
769
+ }
770
+ return "";
723
771
  }
724
772
  function autoRouteNote(choice, lang) {
725
773
  const resolvedLang = lang || prefsLang();
@@ -8617,8 +8665,9 @@ const RUNTIME_BIN = {
8617
8665
  * Wire the resident judgment service to whatever runtime the user actually has connected.
8618
8666
  * This is what lets classification be decided by MEANING instead of by a wordlist: the
8619
8667
  * judge asks the connected model, and the old keyword maps are passed only as hints. It is
8620
- * invisible to the user (no output, read-only, no tools) and every judged call falls back
8621
- * to the deterministic prefilter when no runtime answers.
8668
+ * invisible to the user (no output, read-only, no tools). When no runtime answers, judged
8669
+ * callers do NOT keyword-decide: routing/intent go to the plain assistant with a "connect a
8670
+ * model" note, capability inference returns undecided, and gates fail closed.
8622
8671
  */
8623
8672
  function installJudgmentRunner(db, rt) {
8624
8673
  let judgment;
@@ -8627,32 +8676,129 @@ function installJudgmentRunner(db, rt) {
8627
8676
  } catch {
8628
8677
  return;
8629
8678
  }
8630
- if (!rt || rt.mode !== "cli" || !RUNTIME_BIN[rt.kind]) {
8631
- judgment.setJudgmentRunner(null);
8679
+ // CLI runtime (claude-code/codex/gemini): the judge runs a silent native turn.
8680
+ if (rt && rt.mode === "cli" && RUNTIME_BIN[rt.kind]) {
8681
+ judgment.setJudgmentRunner(async ({ system, prompt, signal }) => {
8682
+ const { runNativeTurn } = require("./agentlas-native-host.cjs");
8683
+ const { Ui } = require("./agentlas-ui.cjs");
8684
+ const silent = new Ui({ lang: prefsLang(), quiet: true });
8685
+ for (const method of ["write", "warn", "info", "tool", "result", "status", "line"]) {
8686
+ if (typeof silent[method] === "function") silent[method] = () => {};
8687
+ }
8688
+ const res = await runNativeTurn({
8689
+ kind: rt.kind,
8690
+ bin: RUNTIME_BIN[rt.kind],
8691
+ prompt,
8692
+ systemPrompt: system,
8693
+ cwd: projectCwd(),
8694
+ permission: "read",
8695
+ session: {},
8696
+ ui: silent,
8697
+ env: process.env,
8698
+ signal,
8699
+ });
8700
+ if (res && res.error) return "";
8701
+ return (res && res.text) || "";
8702
+ });
8632
8703
  return;
8633
8704
  }
8634
- judgment.setJudgmentRunner(async ({ system, prompt, signal }) => {
8635
- const { runNativeTurn } = require("./agentlas-native-host.cjs");
8636
- const { Ui } = require("./agentlas-ui.cjs");
8637
- const silent = new Ui({ lang: prefsLang(), quiet: true });
8638
- for (const method of ["write", "warn", "info", "tool", "result", "status", "line"]) {
8639
- if (typeof silent[method] === "function") silent[method] = () => {};
8640
- }
8641
- const res = await runNativeTurn({
8642
- kind: rt.kind,
8643
- bin: RUNTIME_BIN[rt.kind],
8644
- prompt,
8645
- systemPrompt: system,
8646
- cwd: projectCwd(),
8647
- permission: "read",
8648
- session: {},
8649
- ui: silent,
8650
- env: process.env,
8651
- signal,
8705
+
8706
+ // API runtime (BYOK / Ollama): the judge uses the same one-shot API path the
8707
+ // rest of the terminal uses, so "the connected model decides" holds for the
8708
+ // user's actual runtime not only CLI subprocess runtimes. Without this, an
8709
+ // Ollama/BYOK user silently gets the deterministic wordlist fallback for every
8710
+ // route, intent, and classification decision.
8711
+ if (rt && rt.mode === "api" && rt.backend) {
8712
+ judgment.setJudgmentRunner(async ({ system, prompt, signal }) => {
8713
+ // Thread the judge's timeout signal into the actual HTTP request so a slow
8714
+ // model aborts the fetch instead of hanging past the deadline.
8715
+ const baseFetch = globalThis.fetch;
8716
+ const fetchImpl = typeof baseFetch === "function" && signal
8717
+ ? (url, init) => baseFetch(url, { ...(init || {}), signal })
8718
+ : baseFetch;
8719
+ try {
8720
+ return await runApi(rt.backend, rt.model, system, prompt, { fetch: fetchImpl });
8721
+ } catch {
8722
+ return "";
8723
+ }
8652
8724
  });
8653
- if (res && res.error) return "";
8654
- return (res && res.text) || "";
8655
- });
8725
+ return;
8726
+ }
8727
+
8728
+ judgment.setJudgmentRunner(null);
8729
+ }
8730
+
8731
+ // Make sure the resident judge can reach the connected model BEFORE the first
8732
+ // judged decision (routing, image capability, task class). The only other
8733
+ // install site is inside executeOnce, which runs AFTER routing — so a one-shot
8734
+ // `run`/auto-route would always fall to the deterministic label without this.
8735
+ // Uses the active runtime (CLI or API/Ollama/BYOK); a pinned override still
8736
+ // reinstalls in executeOnce for the actual task turn.
8737
+ function ensureJudgmentRunnerInstalled(db) {
8738
+ let judgment;
8739
+ try {
8740
+ judgment = require("./agentlas-judgment.cjs");
8741
+ } catch {
8742
+ return;
8743
+ }
8744
+ if (judgment.hasJudgmentRunner()) return;
8745
+ try {
8746
+ const ar = activeRuntime(db);
8747
+ let rt = null;
8748
+ if (ar && RUNTIME_BIN[ar.kind]) rt = { mode: "cli", kind: ar.kind, model: ar.model || null };
8749
+ else if (ar && ar.kind === "byok" && ar.backend) rt = { mode: "api", backend: ar.backend, model: ar.model };
8750
+ else if (ar && ar.kind === "ollama") rt = { mode: "api", backend: "ollama", model: ar.model };
8751
+ if (rt) installJudgmentRunner(db, rt);
8752
+ } catch {
8753
+ // No resolvable runtime → leave the runner unset; judged sites use their
8754
+ // labeled deterministic fallback rather than crashing.
8755
+ }
8756
+ }
8757
+
8758
+ // Hand the embedded Agentlas OS runtime a UNIVERSAL judge callback so ITS
8759
+ // resident judge (content-guard, pipeline, research, privacy) decides by meaning
8760
+ // for EVERY connected runtime — Claude/Codex/Gemini CLIs, all BYOK providers, and
8761
+ // local models alike — with no model hardcoded and no provider code duplicated in
8762
+ // Python. The OS engine stays BYOC: it never calls a model on its own; it only
8763
+ // invokes this host command, which runs the user's own connected runtime. If no
8764
+ // runtime resolves, the env is left unset and OS judged sites honestly report
8765
+ // "connect a model" instead of keyword-deciding.
8766
+ function exportOsJudgeRuntimeEnv(db) {
8767
+ try {
8768
+ const ar = activeRuntime(db);
8769
+ if (!ar) return;
8770
+ const config = {
8771
+ kind: "host-cmd",
8772
+ cmd: [process.execPath, path.resolve(__dirname, "..", "bin", "agentlas.cjs"), "__judge"],
8773
+ };
8774
+ process.env.AGENTLAS_JUDGE_RUNTIME = JSON.stringify(config);
8775
+ } catch {
8776
+ // Leave the env unset — OS judged sites then report "connect a model" rather
8777
+ // than keyword-deciding.
8778
+ }
8779
+ }
8780
+
8781
+ // Hidden helper the embedded OS runtime calls for one-shot classification. Reads
8782
+ // {"system","prompt"} JSON on stdin, runs the connected runtime (any provider/CLI)
8783
+ // as a silent read-only turn, prints the reply text on stdout. Judgment is a
8784
+ // lightweight task, so this uses whatever model the user already has connected —
8785
+ // no hardcoded model, no separate credentials.
8786
+ async function cmdInternalJudge(db) {
8787
+ let raw = "";
8788
+ try {
8789
+ raw = await readStdin();
8790
+ const { system, prompt } = JSON.parse(raw || "{}");
8791
+ if (!prompt || !prompt.trim()) { process.stdout.write(""); return; }
8792
+ let judgment;
8793
+ try { judgment = require("./agentlas-judgment.cjs"); } catch { judgment = null; }
8794
+ ensureJudgmentRunnerInstalled(db);
8795
+ if (!judgment || !judgment.hasJudgmentRunner()) { process.stdout.write(""); return; }
8796
+ // Reuse the installed runner directly for an opaque system+prompt turn.
8797
+ const text = await judgment.runRaw(String(system || ""), String(prompt), 40000);
8798
+ process.stdout.write(text || "");
8799
+ } catch {
8800
+ process.stdout.write("");
8801
+ }
8656
8802
  }
8657
8803
 
8658
8804
  function resolveRuntime(db, override) {
@@ -11878,6 +12024,17 @@ async function main() {
11878
12024
  // Agentlas 아키텍처 빌트인 에이전트를 보장(앱과 동일, 멱등·버전 게이팅). 스키마가 준비됐을 때만.
11879
12025
  try { seedBuiltins(db); } catch { /* best-effort */ }
11880
12026
 
12027
+ // Wire the resident judge to the connected runtime up front, so the FIRST
12028
+ // routing / image-capability / task-class decision is judged by the model —
12029
+ // in the REPL and in one-shot commands alike — instead of silently falling to
12030
+ // the deterministic label until executeOnce eventually installs it.
12031
+ ensureJudgmentRunnerInstalled(db);
12032
+ // Hidden judge-callback for the embedded OS runtime — handle before exporting
12033
+ // the env or dispatching, and never re-enter the OS from here.
12034
+ if (cmd === "__judge") return cmdInternalJudge(db);
12035
+ // Hand the same connected model to the embedded Agentlas OS runtime's judge.
12036
+ exportOsJudgeRuntimeEnv(db);
12037
+
11881
12038
  // 인자 없이 `agentlas` → 에이전트 1개면 바로 대화형, 아니면 목록 + 사용법
11882
12039
  if (cmd === "") {
11883
12040
  const agents = listAgents(db);
@@ -12140,6 +12297,9 @@ module.exports = {
12140
12297
  autoRouteNote,
12141
12298
  autoRoutePreamble,
12142
12299
  directSystemPrompt,
12300
+ // Test bridge: prove the resident judge is wired to API/Ollama/BYOK runtimes,
12301
+ // not only CLI subprocess runtimes.
12302
+ installJudgmentRunner,
12143
12303
  // Hub 플러그인 설치 회귀 테스트 표면 — 레포 URL을 MCP 서버로 등록하지 않는 규칙 검증용.
12144
12304
  pluginMcpRowCli,
12145
12305
  planPluginMcpInstallCli,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "description": "Agentlas agent terminal — chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
5
5
  "bin": {
6
6
  "agentlas": "bin/agentlas.cjs"