agentlas 0.9.7 → 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,19 @@
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
+
3
17
  ## 0.9.7 — 2026-07-25
4
18
 
5
19
  - Fix: the resident judge now reaches API/Ollama/BYOK runtimes, not only CLI
@@ -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,7 +669,6 @@ 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);
643
672
  ensureJudgmentRunnerInstalled(db);
644
673
  let judgment;
645
674
  try {
@@ -647,13 +676,15 @@ async function resolveAutoRoute(db, prompt, lang) {
647
676
  } catch {
648
677
  judgment = null;
649
678
  }
650
- if (!judgment || !judgment.hasJudgmentRunner()) return { ...prefilter, routeSource: "deterministic" };
679
+ // 연결 모델 없음 어휘로 전문 에이전트를 고르지 않는다. 정직하게 "판단 못 함" + 모델 연결 안내.
680
+ if (!judgment || !judgment.hasJudgmentRunner()) return noModelRouteChoice(resolvedLang, "no_runtime");
651
681
  const promptText = routeNormalize(routeStripPaths(prompt));
652
- // 잡담("hi")까지 모델에 물으면 턴이 느려진다 정확 매칭 가드는 닫힌형이라 결정적 유지.
653
- if (!promptText.trim() || isTrivialRoutePrompt(promptText)) return { ...prefilter, routeSource: "deterministic" };
682
+ // 잡담("hi") 닫힌형 결정적 가드로 직답모델은 연결돼 있으니 "모델 연결" 안내는 필요 없다.
683
+ if (!promptText.trim() || isTrivialRoutePrompt(promptText)) return { ...directRouteChoice(resolvedLang), routeSource: "deterministic" };
654
684
  const ranked = rankRouteAgents(db, prompt, resolvedLang);
655
685
  const meta = resolveMetaBuilder(db);
656
- if (!ranked.length && !meta) return { ...prefilter, routeSource: "deterministic" };
686
+ // 라우팅할 후보 자체가 없음(설치 에이전트 0) 직답. 모델은 연결돼 있으니 일반 직답 사유.
687
+ if (!ranked.length && !meta) return { ...directRouteChoice(resolvedLang), routeSource: "deterministic" };
657
688
  // App Builder는 합성 라벨로만 제시한다(동의 핸드셰이크가 걸린 특수 라우트임을 모델에 명시).
658
689
  const appBuilder = ranked.find((r) => r.agent.slug === APP_BUILDER_ROUTE_SLUG) || null;
659
690
  const candidates = ranked.filter((r) => r.agent.slug !== APP_BUILDER_ROUTE_SLUG).slice(0, ROUTE_JUDGE_CANDIDATE_CAP);
@@ -707,7 +738,9 @@ async function resolveAutoRoute(db, prompt, lang) {
707
738
  // model still aborts cleanly at this deadline rather than hanging forever.
708
739
  timeoutMs: 40000,
709
740
  });
710
- 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");
711
744
  const picked = verdict.labels[0];
712
745
  const reason =
713
746
  verdict.reason ||
@@ -717,15 +750,24 @@ async function resolveAutoRoute(db, prompt, lang) {
717
750
  return { agent: meta, score: 1000, strong: true, terms: [], reason, routeSource: "llm" };
718
751
  }
719
752
  const chosen = picked === ROUTE_JUDGE_APP_LABEL ? appBuilder : bySlug.get(picked);
720
- if (!chosen) return { ...prefilter, routeSource: "deterministic" };
753
+ // 모델이 라벨을 골랐지만 설치 에이전트로 해석되지 않음(경계 케이스) 어휘 대신 직답.
754
+ if (!chosen) return { ...directRouteChoice(resolvedLang), routeSource: "deterministic" };
721
755
  // 모델 확답은 strong 계약을 충족한다 — 어휘 근거(terms/score)는 참고로 보존.
722
756
  return { ...chosen, strong: true, reason, routeSource: "llm" };
723
757
  }
724
758
  // 라우트 영수증 라벨 — 누가 최종 판정했는지 반드시 찍는다(조용한 폴백 금지 하우스 룰).
759
+ // noModel 경로는 사유(reason)에 이미 "모델 연결" 안내가 담겨 있으니 짧은 기계 라벨만 덧붙인다.
760
+ // 그 외 deterministic(잡담·후보 없음: 모델은 연결됨)은 사유가 이미 설명하므로 라벨을 붙이지 않는다.
725
761
  function routeJudgeSourceNote(choice, lang) {
726
762
  if (!choice || !choice.routeSource) return "";
727
763
  if (choice.routeSource === "llm") return lang === "ko" ? " (판정: 연결 모델)" : " (judged by the connected model)";
728
- 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 "";
729
771
  }
730
772
  function autoRouteNote(choice, lang) {
731
773
  const resolvedLang = lang || prefsLang();
@@ -8623,8 +8665,9 @@ const RUNTIME_BIN = {
8623
8665
  * Wire the resident judgment service to whatever runtime the user actually has connected.
8624
8666
  * This is what lets classification be decided by MEANING instead of by a wordlist: the
8625
8667
  * judge asks the connected model, and the old keyword maps are passed only as hints. It is
8626
- * invisible to the user (no output, read-only, no tools) and every judged call falls back
8627
- * 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.
8628
8671
  */
8629
8672
  function installJudgmentRunner(db, rt) {
8630
8673
  let judgment;
@@ -8712,6 +8755,52 @@ function ensureJudgmentRunnerInstalled(db) {
8712
8755
  }
8713
8756
  }
8714
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
+ }
8802
+ }
8803
+
8715
8804
  function resolveRuntime(db, override) {
8716
8805
  const ar = activeRuntime(db);
8717
8806
  const activeCli = ar && RUNTIME_BIN[ar.kind]
@@ -11940,6 +12029,11 @@ async function main() {
11940
12029
  // in the REPL and in one-shot commands alike — instead of silently falling to
11941
12030
  // the deterministic label until executeOnce eventually installs it.
11942
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);
11943
12037
 
11944
12038
  // 인자 없이 `agentlas` → 에이전트 1개면 바로 대화형, 아니면 목록 + 사용법
11945
12039
  if (cmd === "") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "0.9.7",
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"