agentlas 0.9.4 → 0.9.6

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.
@@ -580,6 +580,24 @@ function directSystemPrompt(lang) {
580
580
  ? "당신은 Agentlas 터미널의 기본 어시스턴트입니다. 특별한 페르소나 없이 사용자의 요청에 정확하고 간결하게 바로 답하세요. 에이전트 라우팅이나 이미지 생성 능력을 스스로 언급하지 마세요."
581
581
  : "You are the Agentlas terminal's default assistant. Answer the user's request directly and concisely, with no special persona. Do not bring up agent routing or image-generation capabilities on your own.";
582
582
  }
583
+ // 어휘 스코어링 — 설치 에이전트 전원을 점수순으로 나열한다. 하우스 룰에 따라 이 점수는
584
+ // (1) resolveAutoRoute의 후보 모집과 (2) 라벨 붙은 오프라인 폴백(autoRouteAgent)으로만
585
+ // 쓰인다 — 연결 모델이 있으면 최종 라우트 결정은 상주 판정 서비스가 의미로 내린다.
586
+ function rankRouteAgents(db, prompt, lang) {
587
+ const resolvedLang = lang || prefsLang();
588
+ const agents = listRoutableAgents(db).filter((agent) => !NON_GENERIC_ROUTE_SLUGS.has(agent.slug));
589
+ if (!agents.length) return [];
590
+ let terms = routeTokenize(prompt);
591
+ // 헤이스택은 한 번만 계산해 IDF와 스코어링 양쪽에서 재사용한다.
592
+ const hays = agents.map((agent) => ({ identityHay: routeIdentityHaystack(agent), haystack: routeHaystack(agent) }));
593
+ // IDF 근사 — 설치 에이전트 절반 이상의 haystack에 나오는 단어("ai","도구" 등)는 판별력이 없어 제외.
594
+ if (agents.length >= 3) {
595
+ terms = terms.filter((term) => hays.filter((h) => h.haystack.includes(term)).length * 2 <= agents.length);
596
+ }
597
+ return agents
598
+ .map((agent, i) => scoreRouteAgent(prompt, terms, agent, resolvedLang, hays[i]))
599
+ .sort((a, b) => b.score - a.score);
600
+ }
583
601
  function autoRouteAgent(db, prompt, lang) {
584
602
  const resolvedLang = lang || prefsLang();
585
603
  // 명확한 "에이전트/팀/회사 만들기" 의도 → 메타-빌더로 직행 (약한 키워드 점수에 밀리지 않게).
@@ -598,35 +616,123 @@ function autoRouteAgent(db, prompt, lang) {
598
616
  };
599
617
  }
600
618
  }
601
- const agents = listRoutableAgents(db).filter((agent) => !NON_GENERIC_ROUTE_SLUGS.has(agent.slug));
602
- if (!agents.length) return directRouteChoice(resolvedLang);
603
- let terms = routeTokenize(prompt);
604
- // 헤이스택은 한 번만 계산해 IDF와 스코어링 양쪽에서 재사용한다.
605
- const hays = agents.map((agent) => ({ identityHay: routeIdentityHaystack(agent), haystack: routeHaystack(agent) }));
606
- // IDF 근사 — 설치 에이전트 절반 이상의 haystack에 나오는 단어("ai","도구" 등)는 판별력이 없어 제외.
607
- if (agents.length >= 3) {
608
- terms = terms.filter((term) => hays.filter((h) => h.haystack.includes(term)).length * 2 <= agents.length);
609
- }
610
- const ranked = agents
611
- .map((agent, i) => scoreRouteAgent(prompt, terms, agent, resolvedLang, hays[i]))
612
- .sort((a, b) => b.score - a.score);
619
+ const ranked = rankRouteAgents(db, prompt, resolvedLang);
620
+ if (!ranked.length) return directRouteChoice(resolvedLang);
613
621
  // 1위가 아니라 "임계값+strong을 모두 만족하는 최고 순위"를 뽑는다 — 장황한 프롬프트의
614
622
  // 약한 단어 적중이 점수 1위를 먹어도, 자격 있는 전문 에이전트가 직답으로 밀려나지 않는다.
615
623
  const pick = ranked.find((r) => r.score >= MIN_ROUTE_SCORE && r.strong);
616
624
  if (pick) return pick;
617
625
  return directRouteChoice(resolvedLang);
618
626
  }
627
+ // ── 모델 최종 라우팅 판정 ──────────────────────────────────
628
+ // 하우스 룰: 단어목록/정규식은 최종 라우트 결정을 내리지 않는다. 연결 모델이 있으면
629
+ // judgeLabels가 후보(어휘 모집 + 설치 전원, 상한) + 합성 라벨(meta-builder/app-builder/
630
+ // direct) 중에서 의미로 하나를 고른다 — 어휘 점수 0점인 에이전트(아랍어 등 어떤 언어의
631
+ // 요청이든)도 모델은 뽑을 수 있다. 닫힌형 가드는 결정적으로 유지: 잡담 short-circuit
632
+ // (isTrivialRoutePrompt)과 경로 스트리핑(ROUTE_PATH_RE)은 모델 호출 전에 그대로 적용.
633
+ // 러너 없음/타임아웃/정크 → 기존 어휘 라우팅(autoRouteAgent)을 routeSource:"deterministic"
634
+ // 으로 라벨해 반환한다 — 조용한 폴백 금지(라우트 노트에 판정 주체가 찍힌다).
635
+ const ROUTE_JUDGE_CANDIDATE_CAP = 30;
636
+ const ROUTE_JUDGE_DIRECT_LABEL = "direct";
637
+ const ROUTE_JUDGE_META_LABEL = "meta-builder";
638
+ const ROUTE_JUDGE_APP_LABEL = "app-builder";
639
+ const APP_BUILDER_ROUTE_SLUG = "agentlas-app-builder";
640
+ async function resolveAutoRoute(db, prompt, lang) {
641
+ const resolvedLang = lang || prefsLang();
642
+ const prefilter = autoRouteAgent(db, prompt, resolvedLang);
643
+ let judgment;
644
+ try {
645
+ judgment = require("./agentlas-judgment.cjs");
646
+ } catch {
647
+ judgment = null;
648
+ }
649
+ if (!judgment || !judgment.hasJudgmentRunner()) return { ...prefilter, routeSource: "deterministic" };
650
+ const promptText = routeNormalize(routeStripPaths(prompt));
651
+ // 잡담("hi")까지 모델에 물으면 매 턴이 느려진다 — 정확 매칭 가드는 닫힌형이라 결정적 유지.
652
+ if (!promptText.trim() || isTrivialRoutePrompt(promptText)) return { ...prefilter, routeSource: "deterministic" };
653
+ const ranked = rankRouteAgents(db, prompt, resolvedLang);
654
+ const meta = resolveMetaBuilder(db);
655
+ if (!ranked.length && !meta) return { ...prefilter, routeSource: "deterministic" };
656
+ // App Builder는 합성 라벨로만 제시한다(동의 핸드셰이크가 걸린 특수 라우트임을 모델에 명시).
657
+ const appBuilder = ranked.find((r) => r.agent.slug === APP_BUILDER_ROUTE_SLUG) || null;
658
+ const candidates = ranked.filter((r) => r.agent.slug !== APP_BUILDER_ROUTE_SLUG).slice(0, ROUTE_JUDGE_CANDIDATE_CAP);
659
+ const bySlug = new Map(candidates.map((r) => [r.agent.slug, r]));
660
+ const labels = [...bySlug.keys()];
661
+ const hints = {};
662
+ const roster = [];
663
+ for (const r of candidates) {
664
+ const a = r.agent;
665
+ const name = [...new Set([a.name, a.name_en].filter(Boolean))].join(" / ");
666
+ const tagline = [...new Set([a.tagline, a.tagline_en].filter(Boolean))].join(" / ");
667
+ roster.push(`- ${a.slug}: ${String(name).slice(0, 80)}${tagline ? ` — ${String(tagline).slice(0, 120)}` : ""}`);
668
+ // 옛 단어목록은 힌트로 강등: 큐레이션 힌트 용어 + 이 프롬프트에서 어휘 스코어러가 맞춘 용어.
669
+ const curated = ROUTE_HINTS.find((h) => h.slug === a.slug);
670
+ const hintTerms = [...new Set([...(curated ? curated.terms : []), ...(r.terms || [])])];
671
+ if (hintTerms.length) hints[a.slug] = hintTerms;
672
+ }
673
+ if (appBuilder) {
674
+ labels.push(ROUTE_JUDGE_APP_LABEL);
675
+ roster.push(
676
+ `- ${ROUTE_JUDGE_APP_LABEL}: build a dedicated internal Agentlas App (recurring workflow, durable state, editing surfaces); explicit user consent is asked separately before anything is created`,
677
+ );
678
+ hints[ROUTE_JUDGE_APP_LABEL] = APP_BUILDER_EXPLICIT_TERMS;
679
+ }
680
+ if (meta) {
681
+ labels.push(ROUTE_JUDGE_META_LABEL);
682
+ roster.push(`- ${ROUTE_JUDGE_META_LABEL}: build a NEW agent/team/company itself (the meta-builder)`);
683
+ hints[ROUTE_JUDGE_META_LABEL] = AGENT_BUILD_TERMS;
684
+ }
685
+ labels.push(ROUTE_JUDGE_DIRECT_LABEL);
686
+ roster.push(`- ${ROUTE_JUDGE_DIRECT_LABEL}: no installed agent clearly fits; answer as the plain assistant`);
687
+ const verdict = await judgment.judgeLabels({
688
+ // 라벨 집합(설치 상태)이 바뀌면 캐시 키도 바뀌어야 한다 — kind에 라벨 지문을 넣는다.
689
+ kind: `terminal-auto-route:${crypto.createHash("sha256").update(labels.join("\n")).digest("hex").slice(0, 12)}`,
690
+ question: "Which installed agent should own this user request, if any?",
691
+ labels,
692
+ input: routeStripPaths(String(prompt || "")),
693
+ hints,
694
+ guidance: [
695
+ "Installed agents:",
696
+ ...roster,
697
+ "Mentioning a word is not intent — judge what the user actually asks to be done, in any language.",
698
+ `Pick "${ROUTE_JUDGE_META_LABEL}" only when the user asks to create a new agent/team/company itself.`,
699
+ `Pick "${ROUTE_JUDGE_DIRECT_LABEL}" when no installed agent clearly fits the request.`,
700
+ ].join("\n"),
701
+ multi: false,
702
+ fallback: [],
703
+ });
704
+ if (verdict.source !== "llm" || !verdict.labels.length) return { ...prefilter, routeSource: "deterministic" };
705
+ const picked = verdict.labels[0];
706
+ const reason =
707
+ verdict.reason ||
708
+ (resolvedLang === "ko" ? "연결 모델이 요청의 의미로 판정했습니다" : "the connected model judged the request by meaning");
709
+ if (picked === ROUTE_JUDGE_DIRECT_LABEL) return { ...directRouteChoice(resolvedLang), reason, routeSource: "llm" };
710
+ if (picked === ROUTE_JUDGE_META_LABEL && meta) {
711
+ return { agent: meta, score: 1000, strong: true, terms: [], reason, routeSource: "llm" };
712
+ }
713
+ const chosen = picked === ROUTE_JUDGE_APP_LABEL ? appBuilder : bySlug.get(picked);
714
+ if (!chosen) return { ...prefilter, routeSource: "deterministic" };
715
+ // 모델 확답은 strong 계약을 충족한다 — 어휘 근거(terms/score)는 참고로 보존.
716
+ return { ...chosen, strong: true, reason, routeSource: "llm" };
717
+ }
718
+ // 라우트 영수증 라벨 — 누가 최종 판정했는지 반드시 찍는다(조용한 폴백 금지 하우스 룰).
719
+ function routeJudgeSourceNote(choice, lang) {
720
+ if (!choice || !choice.routeSource) return "";
721
+ if (choice.routeSource === "llm") return lang === "ko" ? " (판정: 연결 모델)" : " (judged by the connected model)";
722
+ return lang === "ko" ? " (판정: 결정적 폴백 — 연결 모델 없음)" : " (deterministic fallback — no connected-model verdict)";
723
+ }
619
724
  function autoRouteNote(choice, lang) {
620
725
  const resolvedLang = lang || prefsLang();
726
+ const sourceNote = routeJudgeSourceNote(choice, resolvedLang);
621
727
  if (choice.direct) {
622
728
  return resolvedLang === "ko"
623
- ? `사용 에이전트: 없음 — 바로 답합니다. 이유: ${choice.reason}.`
624
- : `Selected agent: none — answering directly. Reason: ${choice.reason}.`;
729
+ ? `사용 에이전트: 없음 — 바로 답합니다. 이유: ${choice.reason}.${sourceNote}`
730
+ : `Selected agent: none — answering directly. Reason: ${choice.reason}.${sourceNote}`;
625
731
  }
626
732
  const name = resolvedLang === "ko" ? choice.agent.name : choice.agent.name_en || choice.agent.name;
627
733
  return resolvedLang === "ko"
628
- ? `사용 에이전트: ${name}. 이유: ${choice.reason}.`
629
- : `Selected agent: ${name}. Reason: ${choice.reason}.`;
734
+ ? `사용 에이전트: ${name}. 이유: ${choice.reason}.${sourceNote}`
735
+ : `Selected agent: ${name}. Reason: ${choice.reason}.${sourceNote}`;
630
736
  }
631
737
  function autoRoutePreamble(choice, lang) {
632
738
  const resolvedLang = lang || prefsLang();
@@ -8507,6 +8613,48 @@ const RUNTIME_BIN = {
8507
8613
  };
8508
8614
 
8509
8615
  // 활성 런타임 → 실행 방식 결정. CLI(claude/codex/gemini) 또는 API(BYOK/Ollama).
8616
+ /**
8617
+ * Wire the resident judgment service to whatever runtime the user actually has connected.
8618
+ * This is what lets classification be decided by MEANING instead of by a wordlist: the
8619
+ * 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.
8622
+ */
8623
+ function installJudgmentRunner(db, rt) {
8624
+ let judgment;
8625
+ try {
8626
+ judgment = require("./agentlas-judgment.cjs");
8627
+ } catch {
8628
+ return;
8629
+ }
8630
+ if (!rt || rt.mode !== "cli" || !RUNTIME_BIN[rt.kind]) {
8631
+ judgment.setJudgmentRunner(null);
8632
+ return;
8633
+ }
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,
8652
+ });
8653
+ if (res && res.error) return "";
8654
+ return (res && res.text) || "";
8655
+ });
8656
+ }
8657
+
8510
8658
  function resolveRuntime(db, override) {
8511
8659
  const ar = activeRuntime(db);
8512
8660
  const activeCli = ar && RUNTIME_BIN[ar.kind]
@@ -8850,6 +8998,9 @@ async function executeOnce(db, system, prompt, override, ctx) {
8850
8998
  }
8851
8999
  const rt = resolveRuntime(db, override);
8852
9000
  memoryRuntime = rt;
9001
+ // The resident judge uses the same connected runtime, so wordlist-based classification is
9002
+ // replaced by meaning-based judgment for this turn.
9003
+ installJudgmentRunner(db, rt);
8853
9004
  if (rt.mode === "cli") {
8854
9005
  // 네이티브 CLI에도 같은 Memory emitter를 주입하되 guard가 화면의 JSON 블록을 숨긴다.
8855
9006
  // 큐레이터가 만든 구조화 Memory만 성공 RunReceipt 이후 Experience intake로 전달된다.
@@ -9502,6 +9653,7 @@ function buildHelpers(db) {
9502
9653
  listFirms,
9503
9654
  firmSystemPrompt,
9504
9655
  autoRouteAgent: (db_, prompt, lang) => autoRouteAgent(db_, prompt, lang),
9656
+ resolveAutoRoute: (db_, prompt, lang) => resolveAutoRoute(db_, prompt, lang),
9505
9657
  autoRouteNote: (choice, lang) => autoRouteNote(choice, lang),
9506
9658
  autoRoutePreamble: (choice, lang) => autoRoutePreamble(choice, lang),
9507
9659
  directSystemPrompt: (lang) => directSystemPrompt(lang),
@@ -10022,6 +10174,19 @@ function cmdList(db) {
10022
10174
  : "\n(Built-in orchestration agents run in the background. Find agents with `agentlas cloud search \"what you need\"`, or just open `agentlas` and type a task.)",
10023
10175
  );
10024
10176
  }
10177
+ // Phase 2+: 검토 대기 중인 에이전트 성장 제안이 있으면 홈에 한 줄로 노출.
10178
+ try {
10179
+ const pendingGrowth = require("./agentlas-evolution.cjs").countPendingGrowthProposals(db);
10180
+ if (pendingGrowth > 0) {
10181
+ out(
10182
+ lang === "ko"
10183
+ ? `\n🧬 에이전트 성장 제안 ${pendingGrowth}건 · \`agentlas evolve\`로 검토`
10184
+ : `\n🧬 ${pendingGrowth} agent growth proposal(s) · review with \`agentlas evolve\``,
10185
+ );
10186
+ }
10187
+ } catch {
10188
+ /* 홈 배너 실패는 무해 */
10189
+ }
10025
10190
  out("\nRun: agentlas <agent> · agentlas firm <firm> · agentlas run <agent> \"...\"");
10026
10191
  }
10027
10192
 
@@ -10172,7 +10337,8 @@ async function cmdRun(db, query, prompt, runtimeOverride, runtimeExperience = nu
10172
10337
 
10173
10338
  async function cmdAutoRun(db, prompt, runtimeOverride, runtimeExperience = null) {
10174
10339
  const lang = prefsLang();
10175
- const choice = autoRouteAgent(db, prompt, lang);
10340
+ // 연결 모델이 라우트를 최종 판정한다 — 어휘 스코어는 후보 모집/라벨 붙은 폴백 전용.
10341
+ const choice = await resolveAutoRoute(db, prompt, lang);
10176
10342
  if (!choice) fail("No agent is available for automatic routing. Check installation with agentlas list.");
10177
10343
  if (choice.direct) {
10178
10344
  // 전문 에이전트 확신 없음 → 페르소나/능력 라우팅 없이 현재 런타임으로 직답.
@@ -11737,6 +11903,12 @@ async function main() {
11737
11903
  return cmdFirm(db, rest[1], rest.slice(2).join(" "), runtimeOverride);
11738
11904
  case "env":
11739
11905
  return cmdEnv(db);
11906
+ case "memory":
11907
+ // Phase 1b: 기존 마크다운 메모리 → 공유 agentlas.sqlite 이관(dry-run 기본, --apply).
11908
+ return require("./agentlas-memory-import.cjs").cmdMemory({ db, args: rest.slice(1), out, fail });
11909
+ case "evolve":
11910
+ // Phase 2/2+: 데스크탑 트리거가 만든 성장 제안 검토·적용·되돌리기(공유 DB).
11911
+ return require("./agentlas-evolution.cjs").cmdEvolve({ db, args: rest.slice(1), out, fail, agentFolder });
11740
11912
  case "multimodal":
11741
11913
  return cmdMultimodal(db, rest.slice(1));
11742
11914
  case "oberon":
@@ -11963,6 +12135,8 @@ module.exports = {
11963
12135
  ANTHROPIC_COMPAT_API,
11964
12136
  // 자동 라우팅 회귀 테스트 표면 — 약한 매치 직답/오라우팅 방지 규칙 검증용.
11965
12137
  autoRouteAgent,
12138
+ // 모델 최종 라우팅 — 어휘 점수는 후보 모집 전용, 연결 모델이 의미로 판정.
12139
+ resolveAutoRoute,
11966
12140
  autoRouteNote,
11967
12141
  autoRoutePreamble,
11968
12142
  directSystemPrompt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
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"