agentlas 0.9.10 → 1.0.2

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.
Files changed (145) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +319 -341
  3. package/bin/agentlas.cjs +32 -9
  4. package/engine/agentlas-banner.cjs +24 -3
  5. package/engine/agentlas-composer.cjs +239 -31
  6. package/engine/agentlas-config.cjs +103 -7
  7. package/engine/agentlas-core-harness.cjs +48 -4
  8. package/engine/agentlas-evolution.cjs +3 -4
  9. package/engine/agentlas-i18n.cjs +88 -32
  10. package/engine/agentlas-input.cjs +234 -18
  11. package/engine/agentlas-memory-governance.cjs +3 -5
  12. package/engine/agentlas-memory-import.cjs +3 -3
  13. package/engine/agentlas-native-host.cjs +200 -9
  14. package/engine/agentlas-onboard.cjs +112 -20
  15. package/engine/agentlas-permissions.cjs +14 -7
  16. package/engine/agentlas-sqlite-policy.cjs +34 -0
  17. package/engine/agentlas-ui.cjs +93 -42
  18. package/engine/agentlas-workforce.cjs +472 -58
  19. package/engine/agentlas-workload-routing.cjs +8 -3
  20. package/engine/agentlas.cjs +140 -12367
  21. package/engine/agents/files.cjs +61 -0
  22. package/engine/agents/import-local.cjs +237 -0
  23. package/engine/agents/registry.cjs +158 -0
  24. package/engine/agents/router.cjs +565 -0
  25. package/engine/agents/routes.cjs +43 -0
  26. package/engine/architecture.data.json +2 -2
  27. package/engine/automation/daemon.cjs +335 -0
  28. package/engine/automation/schedule.cjs +181 -0
  29. package/engine/automation/store.cjs +209 -0
  30. package/engine/cloud/auth.cjs +279 -0
  31. package/engine/cloud/hub-client.cjs +239 -0
  32. package/engine/cloud-assets/cargo.cjs +49 -0
  33. package/engine/cloud-assets/cas.cjs +235 -0
  34. package/engine/cloud-assets/commands.cjs +273 -0
  35. package/engine/cloud-assets/package.cjs +936 -0
  36. package/engine/cloud-assets/restore.cjs +172 -0
  37. package/engine/cloud-assets/state.cjs +268 -0
  38. package/engine/commands/automation.cjs +195 -0
  39. package/engine/commands/billing.cjs +96 -0
  40. package/engine/commands/browser.cjs +20 -0
  41. package/engine/commands/build.cjs +33 -0
  42. package/engine/commands/call.cjs +24 -0
  43. package/engine/commands/career-graph.cjs +51 -0
  44. package/engine/commands/cd.cjs +22 -0
  45. package/engine/commands/chat.cjs +12 -0
  46. package/engine/commands/chats.cjs +28 -0
  47. package/engine/commands/cloud.cjs +17 -0
  48. package/engine/commands/connect.cjs +20 -0
  49. package/engine/commands/context.cjs +66 -0
  50. package/engine/commands/creds.cjs +203 -0
  51. package/engine/commands/doctor.cjs +68 -0
  52. package/engine/commands/env.cjs +33 -0
  53. package/engine/commands/evolve.cjs +23 -0
  54. package/engine/commands/experience.cjs +34 -0
  55. package/engine/commands/film.cjs +8 -0
  56. package/engine/commands/firm.cjs +115 -0
  57. package/engine/commands/help.cjs +65 -0
  58. package/engine/commands/hep.cjs +23 -0
  59. package/engine/commands/import.cjs +35 -0
  60. package/engine/commands/index.cjs +136 -0
  61. package/engine/commands/install.cjs +27 -0
  62. package/engine/commands/journal.cjs +31 -0
  63. package/engine/commands/legacy-network.cjs +29 -0
  64. package/engine/commands/list.cjs +49 -0
  65. package/engine/commands/login.cjs +68 -0
  66. package/engine/commands/logout.cjs +28 -0
  67. package/engine/commands/mcp.cjs +91 -0
  68. package/engine/commands/memory.cjs +21 -0
  69. package/engine/commands/multimodal.cjs +91 -0
  70. package/engine/commands/native.cjs +34 -0
  71. package/engine/commands/netadmin.cjs +31 -0
  72. package/engine/commands/oberon.cjs +70 -0
  73. package/engine/commands/ontology.cjs +24 -0
  74. package/engine/commands/open.cjs +48 -0
  75. package/engine/commands/plugin.cjs +101 -0
  76. package/engine/commands/project.cjs +47 -0
  77. package/engine/commands/research.cjs +36 -0
  78. package/engine/commands/route.cjs +37 -0
  79. package/engine/commands/run.cjs +149 -0
  80. package/engine/commands/search.cjs +55 -0
  81. package/engine/commands/setup.cjs +45 -0
  82. package/engine/commands/storm.cjs +75 -0
  83. package/engine/commands/swarm.cjs +75 -0
  84. package/engine/commands/telegram.cjs +32 -0
  85. package/engine/commands/uninstall.cjs +68 -0
  86. package/engine/commands/update.cjs +54 -0
  87. package/engine/commands/upload.cjs +18 -0
  88. package/engine/commands/usage.cjs +35 -0
  89. package/engine/commands/variant.cjs +25 -0
  90. package/engine/commands/version.cjs +9 -0
  91. package/engine/commands/whoami.cjs +38 -0
  92. package/engine/commands/workforce.cjs +103 -0
  93. package/engine/core/db.cjs +160 -0
  94. package/engine/core/paths.cjs +35 -0
  95. package/engine/experience/build.cjs +181 -0
  96. package/engine/experience/intents.cjs +492 -0
  97. package/engine/experience/runtime.cjs +242 -0
  98. package/engine/experience/variant.cjs +196 -0
  99. package/engine/firms/orchestrate.cjs +333 -0
  100. package/engine/hephaestus/runtime.cjs +697 -0
  101. package/engine/hub/install.cjs +872 -0
  102. package/engine/hub/plugins.cjs +213 -0
  103. package/engine/mcp/consent.cjs +289 -0
  104. package/engine/mcp/contract.cjs +202 -0
  105. package/engine/mcp/index.cjs +43 -0
  106. package/engine/mcp/inventory.cjs +322 -0
  107. package/engine/mcp/plan.cjs +286 -0
  108. package/engine/mcp/probe.cjs +151 -0
  109. package/engine/memory-cli/curate.cjs +163 -0
  110. package/engine/oberon/common.cjs +69 -0
  111. package/engine/oberon/manifest.cjs +164 -0
  112. package/engine/oberon/outputs.cjs +70 -0
  113. package/engine/oberon/render.cjs +164 -0
  114. package/engine/project/career-graph.cjs +249 -0
  115. package/engine/project/credentials.cjs +262 -0
  116. package/engine/project/env-file.cjs +46 -0
  117. package/engine/project/index.cjs +27 -0
  118. package/engine/project/memory-context.cjs +453 -0
  119. package/engine/project/ontology.cjs +467 -0
  120. package/engine/project/paths.cjs +39 -0
  121. package/engine/project/seed.cjs +200 -0
  122. package/engine/project/state.cjs +403 -0
  123. package/engine/project/super-ontology-seed.json +3288 -0
  124. package/engine/runtimes/detect.cjs +54 -0
  125. package/engine/runtimes/overrides.cjs +139 -0
  126. package/engine/runtimes/resolve.cjs +64 -0
  127. package/engine/sessions/apply-fences.cjs +188 -0
  128. package/engine/sessions/fences.cjs +362 -0
  129. package/engine/sessions/orchestrator.cjs +170 -0
  130. package/engine/sessions/prompt.cjs +212 -0
  131. package/engine/sessions/session.cjs +245 -0
  132. package/engine/sessions/sink.cjs +54 -0
  133. package/engine/sessions/store.cjs +79 -0
  134. package/engine/storm/deps.cjs +88 -0
  135. package/engine/storm/storm.cjs +218 -0
  136. package/engine/storm/swarm.cjs +422 -0
  137. package/engine/ui/palette.cjs +105 -0
  138. package/engine/ui/renderer.cjs +85 -0
  139. package/engine/ui/repl.cjs +444 -0
  140. package/engine/workforce/capture.cjs +701 -0
  141. package/engine/workforce/deps.cjs +472 -0
  142. package/package.json +2 -6
  143. package/engine/agentlas-experience-mcp.cjs +0 -1709
  144. package/engine/agentlas-parity.cjs +0 -1499
  145. package/engine/agentlas-repl.cjs +0 -1780
@@ -0,0 +1,565 @@
1
+ "use strict";
2
+ /*
3
+ * agents/router — 자동 라우팅: `agentlas "<task>"` 의 최적 에이전트 판정.
4
+ *
5
+ * v1 모놀리스(legacy-v1-engine-snapshot engine/agentlas.cjs ~209-890)의 라우팅
6
+ * 로직을 v2 모듈 경계로 포팅했다. 하우스 룰(오너 결정, 위반 = 하드 실패):
7
+ *
8
+ * 1. 최종 픽 = 호스트 LLM 판정(engine/agentlas-judgment.cjs). 어휘/키워드
9
+ * 스코어러(rankRouteAgents)는 후보 "모집(recall widening)" 전용이다 —
10
+ * 절대 최종 라우트를 결정하지 않는다.
11
+ * 2. 판정 런타임이 없으면 정직한 폴백: 오케스트레이터/기본 에이전트로 가되
12
+ * 반드시 note(왜 판정을 못 했는지)를 실어 보낸다. 조용한 폴백 금지.
13
+ * 3. 웹 전용(private) 에이전트는 후보에 절대 들어가지 않는다 —
14
+ * registry.listRoutableAgents 가 유일한 후보 소스다.
15
+ *
16
+ * 닫힌형 결정적 가드(모델 호출 전, v1 동형):
17
+ * - 잡담 short-circuit (isTrivialRoutePrompt)
18
+ * - 경로 스트리핑 (ROUTE_PATH_RE — 경로 토큰이 strong 채널을 때리던 사고 수리)
19
+ * - 명시적 "에이전트/팀/회사 만들기" 의도 → 메타빌더 직행 (AGENT_BUILD_TERMS 게이트;
20
+ * 큐레이션된 닫힌형 의도 목록으로, 약한 키워드 점수 경쟁이 아니다)
21
+ */
22
+ const crypto = require("node:crypto");
23
+ const { listRoutableAgents, listAgents, rowToAgent } = require("./registry.cjs");
24
+
25
+ const GLOBAL_ORCHESTRATOR_SLUG = "agentlas-orchestrator";
26
+ // 메타-빌더(에이전트/팀/회사 생성) — 약한 키워드 점수 경쟁에서 빼고, 명시적 build 의도일 때만 직행.
27
+ const META_BUILDER_SLUGS = ["agentlas-core-engine-meta-agent-builtin", "agentlas-meta-agent"];
28
+ const NON_GENERIC_ROUTE_SLUGS = new Set([GLOBAL_ORCHESTRATOR_SLUG, ...META_BUILDER_SLUGS]);
29
+ const AGENT_BUILD_TERMS = [
30
+ "build an agent", "build a team", "build me an agent", "build me a team", "make an agent",
31
+ "make a team", "create an agent", "create a team", "agent team", "agent for me",
32
+ "scaffold a team", "scaffold an agent", "build a company", "create a company",
33
+ "new agent team", "set up an agent", "set up a team", "spin up an agent",
34
+ "에이전트팀", "에이전트 팀", "에이전트 만들", "에이전트를 만들", "에이전트 좀 만들",
35
+ "에이전트 생성", "에이전트 구축", "팀 만들", "팀을 만들", "팀 생성", "회사 만들",
36
+ "회사를 만들", "에이전트 하나 만들",
37
+ ];
38
+ // 한국어 조사("하나만","좀","를")가 끼면 고정 구문 매칭이 깨지므로, 엔티티+동사 근접 규칙을 보강한다.
39
+ const BUILD_ENTITY_RE = /(에이전트|agent|팀|team|회사|company)/i;
40
+ const BUILD_VERB_RE = /(만들|만든|생성|구축|구성해|꾸려|세팅|패키징|scaffold|build|create|\bmake\b|set\s?up|spin\s?up)/i;
41
+
42
+ // "ai"/"llm" 같은 초범용 토큰은 판별력이 0 — 이런 단어 하나로 전문 에이전트가 선택되던
43
+ // 오라우팅을 막는다. "local"/"imported"/"team"은 임포터 보일러플레이트/slug에 편재.
44
+ const ROUTE_STOP_WORDS = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "make", "build", "create", "agent", "agents", "team", "please", "ai", "llm", "local", "imported", "인공지능", "에이아이", "좀", "해주세요", "해줘", "만들어", "붙여", "연결", "작업", "요청"]);
45
+
46
+ const ROUTE_HINTS = [
47
+ {
48
+ slug: "agentlas-app-builder",
49
+ terms: [
50
+ "apps generate", "app builder", "make an app", "build an app", "create an app",
51
+ "generated app", "generate app", "internal app", "dedicated app", "workflow app",
52
+ "dashboard app", "studio app", "service-app", "creative-studio", "scaffold-app",
53
+ "operate-app", "앱빌더", "앱 빌더", "앱 만들어", "앱 만들", "전용 앱", "내장 앱",
54
+ "내부 앱", "생성 앱", "워크플로우 앱", "대시보드 앱", "스튜디오 앱",
55
+ ],
56
+ reasonKo: "Agentlas 안에서 열리는 내부 App 생성/설계 요청입니다",
57
+ reasonEn: "the request is to create or design an internal Agentlas App",
58
+ },
59
+ {
60
+ slug: "agentlas-memory-curator",
61
+ terms: ["memory", "remember", "recall", "request_context", "context_json", "메모리", "기억", "회상", "저장"],
62
+ reasonKo: "기억 저장/검색/스코프 품질을 다루는 요청입니다",
63
+ reasonEn: "the request concerns memory storage, recall, or scope quality",
64
+ },
65
+ {
66
+ slug: "agentlas-task-bias",
67
+ terms: ["bias", "sitemap", "evidence", "completion", "coverage", "편향", "사이트맵", "증거", "검증"],
68
+ reasonKo: "작업 편향, 사이트맵, 검증 증거를 다루는 요청입니다",
69
+ reasonEn: "the request concerns task bias, sitemap, or validation evidence",
70
+ },
71
+ {
72
+ slug: "agentlas-pm-soul",
73
+ terms: ["project", "plan", "decision", "handoff", "continuity", "프로젝트", "계획", "결정", "연속성", "핸드오프"],
74
+ reasonKo: "프로젝트 연속성/결정/조율이 중심인 요청입니다",
75
+ reasonEn: "the request is centered on project continuity, decisions, or coordination",
76
+ },
77
+ ];
78
+
79
+ function routeNormalize(value) {
80
+ return String(value || "").toLowerCase().replace(/[_/]+/g, "-");
81
+ }
82
+ // 경로 디렉터리 성분은 라우팅 의도가 아니다 — 마지막 세그먼트(파일/폴더명)만 남긴다.
83
+ // (v1 사고 2026-07-12: 프롬프트 속 절대경로 토큰이 임포트 에이전트 system_prompt 속
84
+ // 절대경로와 우연 일치해 점수를 쌓고 strong 게이트까지 뚫었다. 대칭 적용 필수.)
85
+ const ROUTE_PATH_RE = /(^|[\s"'`(<\[{])((?:~|[A-Za-z]:)?[\\/]{1,2}(?:[^\s\\/]+(?: [A-Z][^\s\\/]*)?[\\/]+){2,}[^\s\\/]*|(?:[^\s\\/]+[\\/]+){2,}[^\s\\/]+\.[A-Za-z0-9]{1,6})/g;
86
+ function routeStripPaths(value) {
87
+ return String(value || "").replace(ROUTE_PATH_RE, (whole, pre, p) => {
88
+ const segs = p.split(/[\\/]+/).filter(Boolean);
89
+ return pre + (segs.length ? segs[segs.length - 1] : "");
90
+ });
91
+ }
92
+ function routeTokenize(value) {
93
+ // 매치가 영숫자로 끝나도록 강제해 "users-mason-documents-" 같은 후행 하이픈 토큰을 원천 차단.
94
+ const matches = routeNormalize(routeStripPaths(value)).match(/[a-z0-9][a-z0-9-]*[a-z0-9]|[가-힣]{2,}/g) || [];
95
+ const expanded = matches.flatMap((term) => term.split("-").filter(Boolean).concat(term));
96
+ return [...new Set(expanded.filter((term) => term.length >= 2 && !ROUTE_STOP_WORDS.has(term)))];
97
+ }
98
+
99
+ // 정체성 존(slug/이름/태그라인) 적중은 강한 신호. systemPrompt 본문 적중은 약한 신호.
100
+ function routeIdentityHaystack(agent) {
101
+ return routeNormalize(routeStripPaths([agent.slug, agent.name, agent.nameEn, agent.tagline, agent.taglineEn].join("\n")));
102
+ }
103
+ function routeHaystack(agent) {
104
+ return routeNormalize(routeStripPaths([
105
+ agent.slug, agent.name, agent.nameEn, agent.tagline, agent.taglineEn,
106
+ String(agent.systemPrompt || "").slice(0, 3500),
107
+ ].join("\n")));
108
+ }
109
+
110
+ const APP_BUILDER_EXPLICIT_TERMS = ROUTE_HINTS[0].terms;
111
+ const APP_BUILDER_REPEAT_TERMS = [
112
+ "automation", "automate", "automatic", "recurring", "repeat", "scheduled",
113
+ "scheduler", "every day", "every week", "workflow", "pipeline", "cron",
114
+ "자동화", "자동", "반복", "정기", "매일", "매주", "스케줄", "예약",
115
+ "워크플로우", "파이프라인",
116
+ ];
117
+ const APP_BUILDER_SURFACE_TERMS = [
118
+ "dashboard", "studio", "editor", "settings", "state", "save", "saved",
119
+ "export", "import", "approve", "approval", "review", "queue", "table",
120
+ "filter", "template", "memory", "profile", "대시보드", "스튜디오", "편집",
121
+ "수정", "설정", "상태", "저장", "내보내기", "불러오기", "승인", "검토",
122
+ "큐", "목록", "테이블", "필터", "템플릿", "학습", "메모리", "프로필",
123
+ ];
124
+ const APP_BUILDER_ACTION_TERMS = [
125
+ "build", "create", "generate", "compose", "manage", "track", "research",
126
+ "analyze", "monitor", "render", "convert", "만들", "생성", "작성", "관리",
127
+ "추적", "리서치", "조사", "분석", "모니터", "렌더", "변환",
128
+ ];
129
+ const TRIVIAL_ROUTE_PROMPTS = new Set(["hi", "hello", "hey", "thanks", "thankyou", "안녕", "안녕하세요", "고마워", "감사", "뭐해"]);
130
+
131
+ function routeIncludesTerm(haystack, term) {
132
+ return haystack.includes(routeNormalize(term));
133
+ }
134
+ function routeMatchedTerms(promptText, terms) {
135
+ return [...new Set(terms.filter((term) => routeIncludesTerm(promptText, term)))];
136
+ }
137
+ function isTrivialRoutePrompt(promptText) {
138
+ const compact = String(promptText || "").replace(/\s+/g, " ").trim();
139
+ const stripped = compact.replace(/[.!?~。!?,,ㅋㅎ\s]/g, "");
140
+ if (!stripped) return true;
141
+ if (stripped.length <= 18 && TRIVIAL_ROUTE_PROMPTS.has(stripped)) return true;
142
+ const words = compact.split(/\s+/).filter(Boolean);
143
+ return words.length <= 3 && TRIVIAL_ROUTE_PROMPTS.has(stripped);
144
+ }
145
+
146
+ function isAgentBuildIntent(prompt) {
147
+ // 경로/파일 참조는 빌드 의도의 증거가 아니다 — "agent-notes.md" 같은 파일명이
148
+ // BUILD_ENTITY_RE를 때려 메타빌더(score 1000)로 직행하던 우회로 차단(v1 수리 유지).
149
+ // ⚠️ 남은 슬래시는 통째로 지우지 않고 공백으로만 벌린다 — "에이전트/팀 만들어줘"의
150
+ // 슬래시-엔티티를 삭제하면 빌드 의도를 놓친다.
151
+ const p = routeNormalize(
152
+ routeStripPaths(prompt)
153
+ .replace(/\S+\.[A-Za-z0-9]{1,6}(?=\s|$)/g, " ")
154
+ .replace(/[\\/]+/g, " "),
155
+ );
156
+ if (!p.trim() || isTrivialRoutePrompt(p)) return false;
157
+ if (AGENT_BUILD_TERMS.some((term) => p.includes(routeNormalize(term)))) return true;
158
+ return BUILD_ENTITY_RE.test(p) && BUILD_VERB_RE.test(p);
159
+ }
160
+
161
+ /** 메타빌더 해석 — private 필터를 우회한 직접 조회(v1 동형): 메타빌더는 목록/일반
162
+ * 스코어링에서는 숨겨지지만, 명시적 build 의도로는 직행 라우팅되어야 한다. */
163
+ function resolveMetaBuilder(db) {
164
+ try {
165
+ const rows = db
166
+ .prepare("SELECT * FROM installed_agents WHERE slug IN ('agentlas-core-engine-meta-agent-builtin','agentlas-meta-agent')")
167
+ .all();
168
+ for (const slug of META_BUILDER_SLUGS) {
169
+ const row = rows.find((r) => r.slug === slug);
170
+ if (row) return rowToAgent(row);
171
+ }
172
+ } catch { /* ignore */ }
173
+ return null;
174
+ }
175
+
176
+ /** 판정 폴백용 기본 에이전트: 오케스트레이터 → 첫 visible 에이전트. */
177
+ function resolveDefaultRouteAgent(db) {
178
+ try {
179
+ const routable = listRoutableAgents(db);
180
+ const orch = routable.find((a) => a.slug === GLOBAL_ORCHESTRATOR_SLUG);
181
+ if (orch) return orch;
182
+ const visible = listAgents(db);
183
+ return visible[0] || routable[0] || null;
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+
189
+ function isAppBuilderWorthyRoutePrompt(prompt) {
190
+ const promptText = routeNormalize(routeStripPaths(prompt));
191
+ if (!promptText.trim() || isTrivialRoutePrompt(promptText)) return false;
192
+ const explicit = routeMatchedTerms(promptText, APP_BUILDER_EXPLICIT_TERMS);
193
+ if (explicit.length) return true;
194
+ const repeat = routeMatchedTerms(promptText, APP_BUILDER_REPEAT_TERMS);
195
+ const surface = routeMatchedTerms(promptText, APP_BUILDER_SURFACE_TERMS);
196
+ const action = routeMatchedTerms(promptText, APP_BUILDER_ACTION_TERMS);
197
+ const signalCount = new Set([...repeat, ...surface, ...action]).size;
198
+ if (repeat.length && (surface.length || action.length)) return true;
199
+ if (surface.length >= 2 && action.length) return true;
200
+ return signalCount >= 4;
201
+ }
202
+
203
+ function routeHint(promptText, agent, lang) {
204
+ const hint = ROUTE_HINTS.find((item) => item.slug === agent.slug);
205
+ if (!hint) return { score: 0, terms: [], reason: "" };
206
+ if (hint.slug === "agentlas-app-builder" && !isAppBuilderWorthyRoutePrompt(promptText)) {
207
+ return { score: 0, terms: [], reason: "" };
208
+ }
209
+ const terms = hint.terms.filter((term) => promptText.includes(routeNormalize(term)));
210
+ if (!terms.length) return { score: 0, terms: [], reason: "" };
211
+ return { score: 12 + terms.length * 3, terms, reason: lang === "ko" ? hint.reasonKo : hint.reasonEn };
212
+ }
213
+
214
+ function scoreRouteAgent(prompt, promptTerms, agent, lang, pre) {
215
+ // 대칭 스트리핑 필수: promptText는 이름(+20)·힌트(+12↑) strong 채널의 입력이다.
216
+ const promptText = routeNormalize(routeStripPaths(prompt));
217
+ if (agent.slug === "agentlas-app-builder" && !isAppBuilderWorthyRoutePrompt(promptText)) {
218
+ return {
219
+ agent,
220
+ score: 0,
221
+ reason: lang === "ko"
222
+ ? "전용 App을 만들 만큼 반복·상태·편집·자동화가 뚜렷하지 않아 App Builder 라우트를 보류했습니다"
223
+ : "the request does not clearly need a dedicated App with durable workflow, state, editing, or automation",
224
+ terms: [],
225
+ strong: false,
226
+ };
227
+ }
228
+ const identityHay = (pre && pre.identityHay) || routeIdentityHaystack(agent);
229
+ const haystack = (pre && pre.haystack) || routeHaystack(agent);
230
+ let score = 0;
231
+ let strong = false; // 이름 언급/정체성 적중/큐레이션 힌트 — strong 증거가 있어야 후보로서 의미가 있다
232
+ const terms = [];
233
+ const seenNames = new Set();
234
+ for (const name of [agent.slug, agent.name, agent.nameEn].filter(Boolean)) {
235
+ const n = routeNormalize(name);
236
+ // 4자 미만 일반 단어가 +20을 독식하지 않도록 가드; name===nameEn 중복 +20 방지.
237
+ if (!n || n.length < 4 || seenNames.has(n)) continue;
238
+ seenNames.add(n);
239
+ if (promptText.includes(n)) {
240
+ score += 20;
241
+ terms.push(name);
242
+ strong = true;
243
+ }
244
+ }
245
+ for (const term of promptTerms) {
246
+ if (identityHay.includes(term)) {
247
+ score += 6;
248
+ terms.push(term);
249
+ strong = true;
250
+ } else if (haystack.includes(term)) {
251
+ score += term.length >= 5 ? 3 : 2;
252
+ terms.push(term);
253
+ }
254
+ }
255
+ const hint = routeHint(promptText, agent, lang);
256
+ score += hint.score;
257
+ if (hint.score) strong = true;
258
+ terms.push(...hint.terms);
259
+ const unique = [...new Set(terms)].slice(0, 6);
260
+ const reason = hint.reason || (lang === "ko"
261
+ ? unique.length
262
+ ? `요청어 ${unique.map((term) => `"${term}"`).join(", ")}가 이 에이전트의 역할/트리거와 가장 가깝습니다`
263
+ : "명확한 전문 라우트가 없어 기본 프로젝트 조율 에이전트가 가장 안전합니다"
264
+ : unique.length
265
+ ? `request terms ${unique.map((term) => `"${term}"`).join(", ")} best match this agent's role/triggers`
266
+ : "no specialist matched clearly, so the default project coordinator is safest");
267
+ return { agent, score, reason, terms: unique, strong };
268
+ }
269
+
270
+ /**
271
+ * 어휘 스코어링 — 후보 "모집" 전용(recall widening). 하우스 룰: 이 점수는 절대 최종
272
+ * 라우트를 결정하지 않는다. 후보는 listRoutableAgents(웹 전용 private 제외)만 쓴다.
273
+ */
274
+ function rankRouteAgents(db, prompt, lang) {
275
+ const resolvedLang = lang || "en";
276
+ const agents = listRoutableAgents(db).filter((agent) => !NON_GENERIC_ROUTE_SLUGS.has(agent.slug));
277
+ if (!agents.length) return [];
278
+ let terms = routeTokenize(prompt);
279
+ const hays = agents.map((agent) => ({ identityHay: routeIdentityHaystack(agent), haystack: routeHaystack(agent) }));
280
+ // IDF 근사 — 설치 에이전트 절반 이상의 haystack에 나오는 단어는 판별력이 없어 제외.
281
+ if (agents.length >= 3) {
282
+ terms = terms.filter((term) => hays.filter((h) => h.haystack.includes(term)).length * 2 <= agents.length);
283
+ }
284
+ return agents
285
+ .map((agent, i) => scoreRouteAgent(prompt, terms, agent, resolvedLang, hays[i]))
286
+ .sort((a, b) => b.score - a.score);
287
+ }
288
+
289
+ function directRouteChoice(lang) {
290
+ return {
291
+ direct: true,
292
+ agent: null,
293
+ score: 0,
294
+ terms: [],
295
+ strong: false,
296
+ reason: lang === "ko"
297
+ ? "특정 전문 에이전트가 필요 없는 일반 요청입니다"
298
+ : "this is a general request that needs no specialist agent",
299
+ };
300
+ }
301
+
302
+ /**
303
+ * 판정 불가 정직 폴백(하우스 룰): 어휘로 전문 에이전트를 고르지 않는다. 기본
304
+ * 에이전트(오케스트레이터)로 가되 사유를 note로 반드시 노출한다 — 조용한 폴백 금지.
305
+ * reason 구분: "no_runtime"=연결 모델 없음, "model_unavailable"=모델은 연결됐지만
306
+ * 이번 판정이 타임아웃/불량 응답으로 실패.
307
+ */
308
+ function noModelRouteChoice(db, lang, reason = "no_runtime") {
309
+ const fallbackAgent = resolveDefaultRouteAgent(db);
310
+ const message = reason === "model_unavailable"
311
+ ? (lang === "ko"
312
+ ? "연결된 모델이 제때 응답하지 않아 어떤 전문 에이전트가 맞는지 판단하지 못했습니다. 잠시 후 다시 시도하거나 모델 상태를 확인해 주세요. 지금은 기본 에이전트로 실행합니다"
313
+ : "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 — running with the default agent for now")
314
+ : (lang === "ko"
315
+ ? "연결된 모델이 없어 어떤 전문 에이전트가 맞는지 판단하지 못했습니다. 모델을 연결하면 자동 라우팅이 됩니다. 지금은 기본 에이전트로 실행합니다"
316
+ : "no model is connected, so I couldn't judge which specialist agent fits; connect a model to enable auto-routing — running with the default agent for now");
317
+ return {
318
+ direct: !fallbackAgent,
319
+ agent: fallbackAgent,
320
+ score: 0,
321
+ terms: [],
322
+ strong: false,
323
+ noModel: true,
324
+ noModelReason: reason,
325
+ routeSource: "deterministic",
326
+ reason: message,
327
+ };
328
+ }
329
+
330
+ /** 직답 모드 시스템 프롬프트 — 페르소나·라우팅 오염 없이 현재 런타임 그대로 답한다. */
331
+ function directSystemPrompt(lang) {
332
+ return lang === "ko"
333
+ ? "당신은 Agentlas 터미널의 기본 어시스턴트입니다. 특별한 페르소나 없이 사용자의 요청에 정확하고 간결하게 바로 답하세요. 에이전트 라우팅이나 이미지 생성 능력을 스스로 언급하지 마세요."
334
+ : "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.";
335
+ }
336
+
337
+ // ── 판정 러너 배선 ─────────────────────────────────────────
338
+ // 하우스 룰: 모델 접근은 주입식(setJudgmentRunner). 이미 러너가 설치돼 있으면 절대
339
+ // 덮어쓰지 않는다(테스트의 fake 러너 / 상위 호스트의 배선 존중). 없을 때만, 해석된
340
+ // 런타임으로 헤드리스 캡처(workforce/capture.cjs captureRuntime) 러너를 깐다.
341
+ // BYOK/Ollama는 같은 모듈의 runApi 원샷 경로 — "연결 모델이 판정한다"는 계약이
342
+ // CLI 서브프로세스 런타임에만 성립하는 반쪽이 되지 않게 한다(v1 수리 유지).
343
+ function ensureJudgeRunner(db, runtime) {
344
+ let judgment;
345
+ try {
346
+ judgment = require("../agentlas-judgment.cjs");
347
+ } catch {
348
+ return null;
349
+ }
350
+ if (judgment.hasJudgmentRunner()) return judgment;
351
+ const capture = require("../workforce/capture.cjs");
352
+ let rt = runtime || null;
353
+ if (!rt && db) {
354
+ // 명시 런타임이 없으면 공유 DB의 active_runtime(데스크탑이 확정한 런타임)로 시도.
355
+ try {
356
+ const ar = require("../runtimes/detect.cjs").activeRuntimeRow(db);
357
+ if (ar && capture.RUNTIME_BIN[ar.kind]) rt = { kind: ar.kind, model: ar.model || null };
358
+ else if (ar && ar.kind === "byok" && ar.backend) rt = { kind: "byok", backend: ar.backend, model: ar.model || null };
359
+ else if (ar && ar.kind === "ollama") rt = { kind: "ollama", model: ar.model || null };
360
+ } catch { /* 러너 미설치로 남는다 — 정직 폴백 경로 */ }
361
+ }
362
+ if (rt && capture.RUNTIME_BIN[rt.kind]) {
363
+ judgment.setJudgmentRunner(async ({ system, prompt, signal }) => {
364
+ try {
365
+ return await capture.captureRuntime(rt.kind, system, prompt, {
366
+ cwd: capture.projectCwd(),
367
+ permission: "read",
368
+ model: rt.model || undefined,
369
+ signal,
370
+ });
371
+ } catch {
372
+ return ""; // 러너 실패 → judgeLabels가 fallback verdict로 정직하게 처리
373
+ }
374
+ });
375
+ return judgment;
376
+ }
377
+ if (rt && (rt.kind === "byok" || rt.kind === "ollama")) {
378
+ const backend = rt.kind === "ollama" ? "ollama" : rt.backend;
379
+ if (backend) {
380
+ judgment.setJudgmentRunner(async ({ system, prompt, signal }) => {
381
+ const baseFetch = globalThis.fetch;
382
+ const fetchImpl = typeof baseFetch === "function" && signal
383
+ ? (url, init) => baseFetch(url, { ...(init || {}), signal })
384
+ : baseFetch;
385
+ try {
386
+ return await capture.runApi(backend, rt.model || null, system, prompt, { fetch: fetchImpl });
387
+ } catch {
388
+ return "";
389
+ }
390
+ });
391
+ return judgment;
392
+ }
393
+ }
394
+ return judgment;
395
+ }
396
+
397
+ // ── 모델 최종 라우팅 판정 ──────────────────────────────────
398
+ const ROUTE_JUDGE_CANDIDATE_CAP = 30;
399
+ const ROUTE_JUDGE_DIRECT_LABEL = "direct";
400
+ const ROUTE_JUDGE_META_LABEL = "meta-builder";
401
+ const ROUTE_JUDGE_APP_LABEL = "app-builder";
402
+ const APP_BUILDER_ROUTE_SLUG = "agentlas-app-builder";
403
+
404
+ /**
405
+ * 자동 라우팅 최종 판정.
406
+ * @param db 공유 SQLite
407
+ * @param task 사용자 요청 원문
408
+ * @param opts { lang?, signal?, runtime?, timeoutMs? }
409
+ * runtime: runtimes/resolve.cjs 가 확정한 런타임 — 판정 러너 배선에 쓴다.
410
+ * @returns choice = { agent|null, direct?, score, terms, strong, reason,
411
+ * routeSource: "llm"|"deterministic", noModel?, noModelReason?, note }
412
+ * note는 사용자에게 반드시 출력해야 하는 한 줄(조용한 라우팅 금지).
413
+ */
414
+ async function resolveAutoRoute(db, task, opts = {}) {
415
+ const lang = opts.lang === "ko" ? "ko" : "en";
416
+ const finish = (choice) => ({ ...choice, note: autoRouteNote(choice, lang) });
417
+ const judgment = ensureJudgeRunner(db, opts.runtime || null);
418
+
419
+ const promptText = routeNormalize(routeStripPaths(task));
420
+ // 잡담은 닫힌형 결정적 가드로 직답 — 모델 호출 없이 종결(v1 동형).
421
+ if (!promptText.trim() || isTrivialRoutePrompt(promptText)) {
422
+ return finish({ ...directRouteChoice(lang), routeSource: "deterministic" });
423
+ }
424
+ // 명확한 "에이전트/팀/회사 만들기" 의도 → 메타빌더 직행. AGENT_BUILD_TERMS는
425
+ // 큐레이션된 닫힌형 의도 게이트(잡담 가드와 같은 계층)이지 점수 경쟁이 아니다 —
426
+ // 명시적 build 구문일 때만 발화하고, 파일명/경로로는 발화하지 않는다.
427
+ if (isAgentBuildIntent(task)) {
428
+ const meta = resolveMetaBuilder(db);
429
+ if (meta) {
430
+ return finish({
431
+ agent: meta,
432
+ score: 1000,
433
+ strong: true,
434
+ terms: [],
435
+ routeSource: "deterministic",
436
+ reason: lang === "ko"
437
+ ? "새 에이전트/팀/회사를 만드는 요청이라 메타에이전트(빌더)로 라우팅했습니다"
438
+ : "the request is to build a new agent/team/company, so it routes to the meta-agent (builder)",
439
+ });
440
+ }
441
+ }
442
+ // 연결 모델 없음 → 어휘로 전문 에이전트를 고르지 않는다. 정직한 기본 에이전트 폴백 + note.
443
+ if (!judgment || !judgment.hasJudgmentRunner()) {
444
+ return finish(noModelRouteChoice(db, lang, "no_runtime"));
445
+ }
446
+
447
+ const ranked = rankRouteAgents(db, task, lang);
448
+ const meta = resolveMetaBuilder(db);
449
+ if (!ranked.length && !meta) {
450
+ return finish({ ...directRouteChoice(lang), routeSource: "deterministic" });
451
+ }
452
+ // App Builder는 합성 라벨로만 제시(동의 핸드셰이크가 걸린 특수 라우트임을 모델에 명시).
453
+ const appBuilder = ranked.find((r) => r.agent.slug === APP_BUILDER_ROUTE_SLUG) || null;
454
+ const candidates = ranked.filter((r) => r.agent.slug !== APP_BUILDER_ROUTE_SLUG).slice(0, ROUTE_JUDGE_CANDIDATE_CAP);
455
+ const bySlug = new Map(candidates.map((r) => [r.agent.slug, r]));
456
+ const labels = [...bySlug.keys()];
457
+ const hints = {};
458
+ const roster = [];
459
+ for (const r of candidates) {
460
+ const a = r.agent;
461
+ const name = [...new Set([a.name, a.nameEn].filter(Boolean))].join(" / ");
462
+ const tagline = [...new Set([a.tagline, a.taglineEn].filter(Boolean))].join(" / ");
463
+ roster.push(`- ${a.slug}: ${String(name).slice(0, 80)}${tagline ? ` — ${String(tagline).slice(0, 120)}` : ""}`);
464
+ // 옛 단어목록은 힌트로 강등: 큐레이션 힌트 용어 + 이 프롬프트에서 어휘 스코어러가 맞춘 용어.
465
+ const curated = ROUTE_HINTS.find((h) => h.slug === a.slug);
466
+ const hintTerms = [...new Set([...(curated ? curated.terms : []), ...(r.terms || [])])];
467
+ if (hintTerms.length) hints[a.slug] = hintTerms;
468
+ }
469
+ if (appBuilder) {
470
+ labels.push(ROUTE_JUDGE_APP_LABEL);
471
+ roster.push(`- ${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`);
472
+ hints[ROUTE_JUDGE_APP_LABEL] = APP_BUILDER_EXPLICIT_TERMS;
473
+ }
474
+ if (meta) {
475
+ labels.push(ROUTE_JUDGE_META_LABEL);
476
+ roster.push(`- ${ROUTE_JUDGE_META_LABEL}: build a NEW agent/team/company itself (the meta-builder)`);
477
+ hints[ROUTE_JUDGE_META_LABEL] = AGENT_BUILD_TERMS;
478
+ }
479
+ labels.push(ROUTE_JUDGE_DIRECT_LABEL);
480
+ roster.push(`- ${ROUTE_JUDGE_DIRECT_LABEL}: no installed agent clearly fits; answer as the plain assistant`);
481
+
482
+ const verdict = await judgment.judgeLabels({
483
+ // 라벨 집합(설치 상태)이 바뀌면 캐시 키도 바뀌어야 한다 — kind에 라벨 지문을 넣는다.
484
+ kind: `terminal-auto-route:${crypto.createHash("sha256").update(labels.join("\n")).digest("hex").slice(0, 12)}`,
485
+ question: "Which installed agent should own this user request, if any?",
486
+ labels,
487
+ input: routeStripPaths(String(task || "")),
488
+ hints,
489
+ guidance: [
490
+ "Installed agents:",
491
+ ...roster,
492
+ "Mentioning a word is not intent — judge what the user actually asks to be done, in any language.",
493
+ `Pick "${ROUTE_JUDGE_META_LABEL}" only when the user asks to create a new agent/team/company itself.`,
494
+ `Pick "${ROUTE_JUDGE_DIRECT_LABEL}" when no installed agent clearly fits the request.`,
495
+ ].join("\n"),
496
+ multi: false,
497
+ fallback: [],
498
+ signal: opts.signal,
499
+ // 라우팅은 1회성 사전 게이트 — 정확성이 지연보다 중요. 로컬 30B 모델 + 전체 로스터는
500
+ // 기본 20s를 넘길 수 있다. judge의 abort signal이 요청까지 전파되므로 진짜 행은 여기서 끊긴다.
501
+ timeoutMs: opts.timeoutMs || 40000,
502
+ });
503
+
504
+ // 모델이 판정을 못 냄(러너 실패/타임아웃/정크) → 어휘 픽으로 떨어지지 않는다.
505
+ if (verdict.source !== "llm" || !verdict.labels.length) {
506
+ return finish(noModelRouteChoice(db, lang, "model_unavailable"));
507
+ }
508
+ const picked = verdict.labels[0];
509
+ const reason = verdict.reason ||
510
+ (lang === "ko" ? "연결 모델이 요청의 의미로 판정했습니다" : "the connected model judged the request by meaning");
511
+ if (picked === ROUTE_JUDGE_DIRECT_LABEL) {
512
+ return finish({ ...directRouteChoice(lang), reason, routeSource: "llm" });
513
+ }
514
+ if (picked === ROUTE_JUDGE_META_LABEL && meta) {
515
+ return finish({ agent: meta, score: 1000, strong: true, terms: [], reason, routeSource: "llm" });
516
+ }
517
+ const chosen = picked === ROUTE_JUDGE_APP_LABEL ? appBuilder : bySlug.get(picked);
518
+ // 모델이 라벨을 골랐지만 설치 에이전트로 해석되지 않음(경계 케이스) → 어휘 픽 대신 직답.
519
+ if (!chosen) return finish({ ...directRouteChoice(lang), routeSource: "deterministic" });
520
+ // 모델 확답은 strong 계약을 충족 — 어휘 근거(terms/score)는 참고로 보존.
521
+ return finish({ ...chosen, strong: true, reason, routeSource: "llm" });
522
+ }
523
+
524
+ // 라우트 영수증 라벨 — 누가 최종 판정했는지 반드시 찍는다(조용한 폴백 금지 하우스 룰).
525
+ function routeJudgeSourceNote(choice, lang) {
526
+ if (!choice || !choice.routeSource) return "";
527
+ if (choice.routeSource === "llm") return lang === "ko" ? " (판정: 연결 모델)" : " (judged by the connected model)";
528
+ if (choice.noModel) {
529
+ if (choice.noModelReason === "model_unavailable") {
530
+ return lang === "ko" ? " (판정 없음 — 모델 응답 없음)" : " (no judgment — model did not answer)";
531
+ }
532
+ return lang === "ko" ? " (판정 없음 — 연결 모델 없음)" : " (no judgment — no model connected)";
533
+ }
534
+ return "";
535
+ }
536
+
537
+ function autoRouteNote(choice, lang) {
538
+ const sourceNote = routeJudgeSourceNote(choice, lang);
539
+ if (choice.direct || !choice.agent) {
540
+ return lang === "ko"
541
+ ? `사용 에이전트: 없음 — 바로 답합니다. 이유: ${choice.reason}.${sourceNote}`
542
+ : `Selected agent: none — answering directly. Reason: ${choice.reason}.${sourceNote}`;
543
+ }
544
+ const name = lang === "ko" ? choice.agent.name : choice.agent.nameEn || choice.agent.name;
545
+ return lang === "ko"
546
+ ? `사용 에이전트: ${name}. 이유: ${choice.reason}.${sourceNote}`
547
+ : `Selected agent: ${name}. Reason: ${choice.reason}.${sourceNote}`;
548
+ }
549
+
550
+ module.exports = {
551
+ GLOBAL_ORCHESTRATOR_SLUG,
552
+ META_BUILDER_SLUGS,
553
+ AGENT_BUILD_TERMS,
554
+ resolveAutoRoute,
555
+ rankRouteAgents,
556
+ isAgentBuildIntent,
557
+ isTrivialRoutePrompt,
558
+ resolveMetaBuilder,
559
+ resolveDefaultRouteAgent,
560
+ ensureJudgeRunner,
561
+ autoRouteNote,
562
+ directSystemPrompt,
563
+ routeStripPaths,
564
+ routeTokenize,
565
+ };
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /*
3
+ * agents/routes — 에이전트 → 로컬 폴더/런타임 매핑 (userData/agent-routes.json).
4
+ * 데스크탑 electron/agents/routes.ts 와 같은 파일을 공유한다.
5
+ */
6
+ const fs = require("node:fs");
7
+ const path = require("node:path");
8
+ const { userDataDir } = require("../core/paths.cjs");
9
+
10
+ function routesPath() {
11
+ return path.join(userDataDir(), "agent-routes.json");
12
+ }
13
+
14
+ function routesMap() {
15
+ try {
16
+ const parsed = JSON.parse(fs.readFileSync(routesPath(), "utf8"));
17
+ return parsed && typeof parsed === "object" ? parsed : {};
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+
23
+ /** 원자적 사설 쓰기 (0600) — 데스크탑과 파일을 공유하므로 부분 쓰기가 보이면 안 된다. */
24
+ function saveRoutes(routes) {
25
+ const file = routesPath();
26
+ fs.mkdirSync(path.dirname(file), { recursive: true });
27
+ const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
28
+ const fd = fs.openSync(tmp, "wx", 0o600);
29
+ try {
30
+ fs.writeFileSync(fd, JSON.stringify(routes, null, 2), "utf8");
31
+ fs.fsyncSync(fd);
32
+ } finally {
33
+ fs.closeSync(fd);
34
+ }
35
+ fs.renameSync(tmp, file);
36
+ try { fs.chmodSync(file, 0o600); } catch { /* win32 */ }
37
+ }
38
+
39
+ function routeForAgent(agentId) {
40
+ return routesMap()[agentId] || null;
41
+ }
42
+
43
+ module.exports = { routesPath, routesMap, saveRoutes, routeForAgent };