agentlas 0.9.9 → 1.0.1

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 +92 -0
  2. package/README.md +319 -342
  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 +318 -8
  19. package/engine/agentlas-workload-routing.cjs +8 -3
  20. package/engine/agentlas.cjs +140 -12336
  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 +3 -7
  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,212 @@
1
+ "use strict";
2
+ /*
3
+ * sessions/prompt — 세션 턴의 시스템 프롬프트 조립 (데스크탑 러너 동형).
4
+ *
5
+ * 조립 순서(v1 augmentSystem과 동일):
6
+ * 언어/말투 지시 → 에이전트 시스템 프롬프트 → 전역 연결 스킬 →
7
+ * 거버넌스 메모리 컨텍스트(프로젝트 soul + Context Map 슬라이스 + scoped timeline) →
8
+ * 메모리 이미터(항상 150토큰 이하 코어; 메모리 의도 감지 시에만 전체 스키마) →
9
+ * 자격증명 인덱스 리마인더(배포/결제/인증 의도 시에만).
10
+ *
11
+ * 계약(memory-prompt-budget 테스트가 고정):
12
+ * - TERMINAL_MEMORY_CORE는 150토큰(≈byte/3) 이하. 평상시 턴은 코어만 받는다.
13
+ * - 전체 스키마에도 request_context / Local Credential Index를 넣지 않는다.
14
+ */
15
+ const fs = require("node:fs");
16
+ const path = require("node:path");
17
+ const { loadArch, tableExists, columnExists } = require("../core/db.cjs");
18
+ const { userDataDir } = require("../core/paths.cjs");
19
+ const { responseDirective } = require("../agentlas-style.cjs");
20
+ const memoryGovernance = require("../agentlas-memory-governance.cjs");
21
+ const { resolveCoreRuntimeRoot, captureCoreJsonSync } = require("../agentlas-core-harness.cjs");
22
+
23
+ const TERMINAL_MEMORY_CORE_MAX_TOKENS = 150;
24
+ const TERMINAL_MEMORY_CORE = [
25
+ "## Memory governance",
26
+ "End every completed reply with hidden `## Memory Events` plus fenced JSON:",
27
+ '{"turn_id":"<stable-id>","observation":{"outcome":"completed","summary":"safe short outcome"},"candidates":[]}',
28
+ "Candidates 0..N: memory_kind,content,suggested_scope,confidence.",
29
+ "Scopes: user_global|team|agent|project|session|discard.",
30
+ "No raw prompts/transcripts, secrets, logs, or absolute paths. Curator suggests; deterministic gates decide writes.",
31
+ ].join("\n");
32
+ const MEMORY_DETAIL_RE = /\b(?:remember|memory|save this|record this|memory event)\b|기억|메모리|저장해|기록해|남겨/i;
33
+ const CREDENTIAL_INDEX_RE = /\b(?:deploy|release|billing|auth|oauth|credential|api key|secret key|cloud)\b|배포|릴리스|출시|결제|인증|자격 증명|API\s*키|시크릿|클라우드/i;
34
+
35
+ function approximatePromptTokens(text) {
36
+ return Math.ceil(Buffer.byteLength(String(text || ""), "utf8") / 3);
37
+ }
38
+ if (approximatePromptTokens(TERMINAL_MEMORY_CORE) > TERMINAL_MEMORY_CORE_MAX_TOKENS) {
39
+ throw new Error("Terminal always-on memory core exceeds 150 tokens");
40
+ }
41
+
42
+ function memoryEmitterPromptFor(request, arch = loadArch(), turnId = null, permission = "write") {
43
+ const stableId = String(turnId || "").replace(/[^A-Za-z0-9:._-]/g, "").slice(0, 160);
44
+ let prompt = TERMINAL_MEMORY_CORE;
45
+ if (stableId) prompt += `\nUse turn_id=${stableId}. permission=${permission === "read" ? "receipt-only" : "curated-write"}.`;
46
+ if (!MEMORY_DETAIL_RE.test(String(request || ""))) return prompt;
47
+ const kinds = Array.isArray(arch?.kinds) && arch.kinds.length ? arch.kinds.join("|") : "fact|decision|preference|risk|procedure";
48
+ prompt += [
49
+ "",
50
+ `Allowed memory_kind: ${kinds}.`,
51
+ "Global requires explicit owner authorization; suggest only, never promote.",
52
+ "Do not emit request_context; put only a safe, short outcome in observation.",
53
+ ].join("\n");
54
+ return prompt;
55
+ }
56
+
57
+ function credentialIndexReminderFor(request) {
58
+ if (!CREDENTIAL_INDEX_RE.test(String(request || ""))) return "";
59
+ return [
60
+ "## Local credential lookup (triggered)",
61
+ "Before saying a deploy, release, billing, auth, API, or cloud credential is missing, read `.agentlas/local-credentials.map.json` and the Local Credential Index in `.agentlas/project-soul-memory.md`.",
62
+ "Use only env names and local relative references; never copy credential values into memory or output.",
63
+ ].join("\n");
64
+ }
65
+
66
+ function loadGlobalConnectionSkill() {
67
+ // 앱 빌드 산출물이 없는 standalone에선 v1과 동일한 인라인 스킬을 쓴다.
68
+ return [
69
+ "## Connecting accounts for the user (always-on background skill)",
70
+ "When the task needs Slack, Gmail, Google Cloud, Firebase, a database, store release access, or another third-party service, open the provider website with the browser tool when available, guide the user one tiny step at a time, and save the issued credential with `agentlas creds save --provider <name> --key <ENV_NAME> --value \"<value>\" --project \"<cwd>\"` or `agentlas creds file --source <path> --provider <name> --env <ENV_NAME> --project \"<cwd>\"`.",
71
+ "Do not lecture the user about security in this local single-user setup; make the connection effortless.",
72
+ ].join("\n\n");
73
+ }
74
+
75
+ function contextLine(json) {
76
+ try {
77
+ const ctx = JSON.parse(json || "{}");
78
+ const parts = [
79
+ ctx.user_intent || ctx.userIntent,
80
+ (ctx.target_project || ctx.targetProject) ? `target:${ctx.target_project || ctx.targetProject}` : null,
81
+ Array.isArray(ctx.trigger_terms || ctx.triggerTerms) && (ctx.trigger_terms || ctx.triggerTerms).length
82
+ ? `terms:${(ctx.trigger_terms || ctx.triggerTerms).join(",")}`
83
+ : null,
84
+ ].filter(Boolean);
85
+ return parts.length ? ` (context: ${parts.join("; ").slice(0, 180)})` : "";
86
+ } catch {
87
+ return "";
88
+ }
89
+ }
90
+
91
+ function ensureMemoryContextColumn(db) {
92
+ try {
93
+ if (tableExists(db, "memory_entries") && !columnExists(db, "memory_entries", "context_json")) {
94
+ db.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
95
+ }
96
+ } catch { /* 앱 마이그레이션 중이면 다음 턴에 */ }
97
+ }
98
+
99
+ /** Context Map 슬라이스 — Core 부재 시 정직하게 빈 문자열 (조작된 지도 금지). */
100
+ function cliProjectContextSlice(projectPath, task) {
101
+ if (!projectPath || !String(task || "").trim()) return "";
102
+ try {
103
+ const coreRoot = resolveCoreRuntimeRoot();
104
+ if (!coreRoot) return "";
105
+ const result = captureCoreJsonSync(
106
+ "agentlas_cloud",
107
+ ["context", "slice", "--project", projectPath, "--task-stdin", "--no-refresh", "--render"],
108
+ { cwd: projectPath, input: String(task || "").slice(0, 12_000), timeout: 4_000 },
109
+ coreRoot,
110
+ );
111
+ return result
112
+ && result.schemaVersion === "agentlas.context-slice.v1"
113
+ && typeof result.rendered === "string"
114
+ ? result.rendered.trim()
115
+ : "";
116
+ } catch {
117
+ return "";
118
+ }
119
+ }
120
+
121
+ /**
122
+ * 거버넌스 스코프 메모리 컨텍스트 (v1 cliMemoryContext 충실 이식).
123
+ * 프로젝트 B가 프로젝트 A의 로컬 메모리를 소환하지 못하는 스코프 규칙이 핵심 —
124
+ * 레거시 행도 user-global/현재 프로젝트/현재 소유자만 통과한다(옛 전역 팀메모리
125
+ * 누수 쿼리를 되살리지 않는다).
126
+ */
127
+ function cliMemoryContext(db, projectPath, agentId = null, task = "") {
128
+ const sections = [];
129
+ const arch = loadArch();
130
+ ensureMemoryContextColumn(db);
131
+ if (projectPath) {
132
+ try {
133
+ const soulPath = path.join(projectPath, arch.memoryDir || ".agentlas", arch.soulFile || "project-soul-memory.md");
134
+ if (fs.existsSync(soulPath)) {
135
+ let s = fs.readFileSync(soulPath, "utf8");
136
+ if (s.length > 1800) s = s.slice(0, 1800) + "\n…(truncated)";
137
+ if (s.trim()) sections.push(`### Project memory (${projectPath})\n${s.trim()}`);
138
+ }
139
+ } catch { /* ignore */ }
140
+ const contextSlice = cliProjectContextSlice(projectPath, task);
141
+ if (contextSlice) sections.push(contextSlice);
142
+ }
143
+ if (tableExists(db, "memory_entries")) {
144
+ try {
145
+ const governed = memoryGovernance.listScopedTimeline(db, { projectPath, agentId, limit: 16 });
146
+ const seen = new Set(governed.map((row) => row.id));
147
+ const legacy = projectPath
148
+ ? db.prepare(`
149
+ SELECT id,kind,content,context_json,created_at
150
+ FROM memory_entries
151
+ WHERE superseded_at IS NULL AND (
152
+ (scope='user_identity' AND project_path IS NULL)
153
+ OR (scope='project' AND project_path=?)
154
+ OR (scope IN ('team_memory','agent_team','agent_repo') AND agent_id=? AND (project_path IS NULL OR project_path=?))
155
+ )
156
+ ORDER BY created_at DESC LIMIT 16
157
+ `).all(projectPath, agentId, projectPath)
158
+ : db.prepare(`
159
+ SELECT id,kind,content,context_json,created_at
160
+ FROM memory_entries
161
+ WHERE superseded_at IS NULL AND (
162
+ (scope='user_identity' AND project_path IS NULL)
163
+ OR (scope IN ('team_memory','agent_team','agent_repo') AND agent_id=? AND project_path IS NULL)
164
+ )
165
+ ORDER BY created_at DESC LIMIT 16
166
+ `).all(agentId);
167
+ const rows = [...governed, ...legacy.filter((row) => !seen.has(row.id))].slice(0, 16);
168
+ if (rows.length) {
169
+ sections.push(
170
+ (projectPath ? "### Scoped global + current-project memory timeline\n" : "### Curated user-global memory\n") +
171
+ rows.map((r) => `- [${r.kind}] ${r.content}${contextLine(r.context_json)}`).join("\n"),
172
+ );
173
+ }
174
+ } catch { /* ignore */ }
175
+ }
176
+ if (!sections.length) return "";
177
+ return "## Agentlas memory (read before answering; governed scope recall)\n\n" + sections.join("\n\n");
178
+ }
179
+
180
+ /**
181
+ * 최종 시스템 프롬프트 조립. ctx = { lang, projectPath, agentId, turnId, permission }.
182
+ * withEmitter=false 는 이미터/리마인더 없이(캡처·판정 등 내부 턴용).
183
+ */
184
+ function augmentSystem(db, baseSystem, ctx, withEmitter, request = "") {
185
+ const arch = loadArch();
186
+ let sys = baseSystem || "";
187
+ // 언어/말투 지시를 맨 앞에 둔다. imported/cloud/company agents도 같은 전역 계약을 따른다.
188
+ const lang = (ctx && ctx.lang) || "en";
189
+ sys = responseDirective(lang) + (sys ? "\n\n" + sys : "");
190
+ const connectionSkill = loadGlobalConnectionSkill();
191
+ if (connectionSkill) sys += "\n\n" + connectionSkill;
192
+ const mem = cliMemoryContext(db, ctx && ctx.projectPath, ctx && ctx.agentId, request);
193
+ if (mem) sys += "\n\n" + mem;
194
+ if (withEmitter) {
195
+ sys += "\n\n" + memoryEmitterPromptFor(request, arch, ctx && ctx.turnId, ctx && ctx.permission);
196
+ const credentialReminder = credentialIndexReminderFor(request);
197
+ if (credentialReminder) sys += "\n\n" + credentialReminder;
198
+ }
199
+ return sys;
200
+ }
201
+
202
+ module.exports = {
203
+ TERMINAL_MEMORY_CORE,
204
+ TERMINAL_MEMORY_CORE_MAX_TOKENS,
205
+ approximatePromptTokens,
206
+ memoryEmitterPromptFor,
207
+ credentialIndexReminderFor,
208
+ cliMemoryContext,
209
+ cliProjectContextSlice,
210
+ augmentSystem,
211
+ loadGlobalConnectionSkill,
212
+ };
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ /*
3
+ * sessions/session — 살아있는 대화 하나.
4
+ *
5
+ * 세션 = 에이전트 + 챗(영속) + 런타임 resume 상태 + 이벤트 링버퍼.
6
+ * 포그라운드/서브에이전트 구분 없이 실행 경로는 이것 하나다(제2 경로 금지).
7
+ *
8
+ * 스티어링: 네이티브 러너는 stdin이 닫힌 헤드리스 실행이므로 "실행 중 주입"이
9
+ * 아니라 "다음 턴 큐잉"이다 — steer()로 넣은 메시지는 현재 턴이 끝나는 즉시
10
+ * resume 세션으로 이어 실행된다. (조용히 버리지 않고 큐에 쌓였음을 이벤트로 알린다.)
11
+ */
12
+ const crypto = require("node:crypto");
13
+ const { EventEmitter } = require("node:events");
14
+ const nativeHost = require("../agentlas-native-host.cjs");
15
+ const permissions = require("../agentlas-permissions.cjs");
16
+ const { EventSink } = require("./sink.cjs");
17
+ const store = require("./store.cjs");
18
+
19
+ const RING_LIMIT = 2000;
20
+
21
+ class Session extends EventEmitter {
22
+ /**
23
+ * @param {object} opts
24
+ * db, agent {id,slug,name,systemPrompt}, runtime {kind,bin,model?},
25
+ * permission, cwd, lang, parent (Session|null), title, chatId?(재개)
26
+ */
27
+ constructor(opts) {
28
+ super();
29
+ this.db = opts.db;
30
+ this.agent = opts.agent;
31
+ this.runtime = opts.runtime;
32
+ this.permission = permissions.normalize(opts.permission);
33
+ this.cwd = opts.cwd || process.cwd();
34
+ this.lang = opts.lang || "en";
35
+ this.parent = opts.parent || null;
36
+ this.children = [];
37
+ this.status = "idle"; // idle | running | done | failed | killed
38
+ this.lastLine = "";
39
+ this.lastError = null;
40
+ this.startedAt = null;
41
+ this.endedAt = null;
42
+ this.usage = null;
43
+ this.queue = [];
44
+ this._child = null;
45
+ this._events = [];
46
+ this._turnPromise = null;
47
+ // 계약 테스트용 spawn 주입(runNativeTurn의 req.spawn). 프로덕션 경로에선 null.
48
+ this._spawnImpl = opts.spawnImpl || null;
49
+ this._timeoutConfig = opts.timeoutConfig || null;
50
+
51
+ this.chatId = opts.chatId || store.createChat(this.db, {
52
+ agentId: this.agent.id,
53
+ title: opts.title || (opts.parent ? `sub: ${this.agent.slug}` : "New chat"),
54
+ kind: opts.parent ? "division" : "user",
55
+ parentChatId: opts.parent ? opts.parent.chatId : null,
56
+ workingFolder: this.cwd,
57
+ });
58
+ // 이 세션이 붙은 챗의 kind — apply-fences의 division 재귀 가드가 parent 없는
59
+ // 기존 division 챗(자동화 marker 세션 등)에도 걸리게 한다.
60
+ // 데스크탑은 chat.kind !== 'division' 조건으로 같은 가드를 건다(client.ts:3493).
61
+ this.chatKind = opts.parent ? "division" : "user";
62
+ if (opts.chatId) {
63
+ try {
64
+ const chatRow = this.db.prepare("SELECT kind FROM chats WHERE id=?").get(opts.chatId);
65
+ if (chatRow && chatRow.kind === "division") this.chatKind = "division";
66
+ } catch { /* kind 열이 없는 구형 DB — user 취급(레거시 NULL=user 계약) */ }
67
+ }
68
+
69
+ this.fingerprint = crypto.createHash("sha256")
70
+ .update(`${this.runtime.kind}\n${this.agent.id}\n${this.agent.systemPrompt || ""}`)
71
+ .digest("hex");
72
+ this.runtimeSession = store.loadRuntimeSession(this.db, this.chatId, this.runtime.kind, this.fingerprint);
73
+
74
+ this._sink = new EventSink({
75
+ lang: this.lang,
76
+ onEvent: (ev) => this._record(ev),
77
+ });
78
+ }
79
+
80
+ _record(ev) {
81
+ this._events.push(ev);
82
+ if (this._events.length > RING_LIMIT) this._events.splice(0, this._events.length - RING_LIMIT);
83
+ if (ev.type === "stream-delta") {
84
+ const tail = (this.lastLine + ev.text).split(/\r?\n/).filter((l) => l.trim());
85
+ this.lastLine = tail.length ? tail[tail.length - 1].slice(0, 200) : this.lastLine;
86
+ } else if (ev.type === "status" || ev.type === "tool") {
87
+ this.lastLine = ev.type === "tool" ? `${ev.name}(${ev.summary || ""})`.slice(0, 200) : ev.text.slice(0, 200);
88
+ } else if (ev.type === "error") {
89
+ this.lastError = ev.text;
90
+ }
91
+ this.emit("event", ev);
92
+ }
93
+
94
+ eventsTail(n = 200) {
95
+ return this._events.slice(-n);
96
+ }
97
+
98
+ isBusy() {
99
+ return this.status === "running";
100
+ }
101
+
102
+ /** 실행 중이면 큐잉, 아니면 즉시 실행. 반환: 최종 상태로 settle되는 Promise. */
103
+ send(prompt) {
104
+ const text = String(prompt || "").trim();
105
+ if (!text) return Promise.resolve(null);
106
+ if (this.isBusy()) {
107
+ this.queue.push(text);
108
+ this._record({ type: "queued", at: Date.now(), text });
109
+ return this._turnPromise;
110
+ }
111
+ this._turnPromise = this._runLoop(text);
112
+ return this._turnPromise;
113
+ }
114
+
115
+ async _runLoop(firstPrompt) {
116
+ let prompt = firstPrompt;
117
+ let result = null;
118
+ while (prompt != null) {
119
+ result = await this._runTurn(prompt);
120
+ prompt = this.queue.length ? this.queue.shift() : null;
121
+ }
122
+ return result;
123
+ }
124
+
125
+ async _runTurn(prompt) {
126
+ this.status = "running";
127
+ this.startedAt = Date.now();
128
+ this.lastError = null;
129
+ this._record({ type: "turn-start", at: Date.now(), prompt });
130
+ store.appendMessage(this.db, this.chatId, "user", prompt);
131
+ // 데스크탑처럼 첫 프롬프트로 자동 제목 — "New chat"으로 남는 목록 방지(실사용 테스트 발견).
132
+ try {
133
+ const row = this.db.prepare("SELECT title FROM chats WHERE id=?").get(this.chatId);
134
+ if (row && (row.title === "New chat" || !row.title)) {
135
+ store.retitleChat(this.db, this.chatId, prompt.slice(0, 60));
136
+ }
137
+ } catch { /* 제목은 장식 — 실패해도 턴 진행 */ }
138
+
139
+ // 데스크탑 러너 동형 프롬프트 조립: 언어지시 + 에이전트 프롬프트 + 연결 스킬 +
140
+ // 거버넌스 메모리 컨텍스트 + 메모리 이미터 코어(+의도 시 전체 스키마/자격증명 리마인더).
141
+ // projectPath는 명시 초기화(.agentlas 존재) 프로젝트만 — project init 경계 유지.
142
+ let systemPrompt = this.agent.systemPrompt || "";
143
+ try {
144
+ const { augmentSystem } = require("./prompt.cjs");
145
+ const fs = require("node:fs");
146
+ const path = require("node:path");
147
+ const projectPath = fs.existsSync(path.join(this.cwd, ".agentlas")) ? this.cwd : null;
148
+ systemPrompt = augmentSystem(this.db, systemPrompt, {
149
+ lang: this.lang,
150
+ projectPath,
151
+ agentId: this.agent.id,
152
+ turnId: `${this.chatId}:${Date.now()}`,
153
+ permission: this.permission,
154
+ }, true, prompt);
155
+ } catch { /* 프롬프트 증강 실패는 턴을 막지 않는다 — 원 프롬프트로 진행 */ }
156
+
157
+ const req = {
158
+ kind: this.runtime.kind,
159
+ bin: this.runtime.bin,
160
+ ui: this._sink,
161
+ cwd: this.cwd,
162
+ prompt,
163
+ systemPrompt,
164
+ permission: this.permission,
165
+ session: { ...this.runtimeSession },
166
+ model: this.runtime.model,
167
+ onSpawn: (child) => { this._child = child; },
168
+ };
169
+ if (this._spawnImpl) req.spawn = this._spawnImpl;
170
+ if (this._timeoutConfig) req.timeoutConfig = this._timeoutConfig;
171
+
172
+ let res;
173
+ try {
174
+ res = await nativeHost.runNativeTurn(req);
175
+ } catch (e) {
176
+ res = { text: "", session: req.session, error: (e && e.message) || String(e) };
177
+ }
178
+ this._child = null;
179
+
180
+ const finalText = (res && (res.finalText || res.text)) || "";
181
+ /*
182
+ * 펜스 프로토콜(Desktop runner 패리티): 성공 턴의 최종 텍스트에서 숨은 제어
183
+ * 블록(## Memory Events / ## Delegate / ## Automation / <<agentlas-ask>>)을
184
+ * 파싱하고, 영속·표시에는 cleanText 만 남긴다. 실패/킬 턴은 파싱하지 않는다 —
185
+ * 반쯤 죽은 응답의 위임/자동화를 실행하지 않기 위해. 파서 예외 시 원문 보존
186
+ * (데이터 유실이 오폭보다 낫다). require 는 lazy — orchestrator 와의 로드
187
+ * 사이클 방지.
188
+ */
189
+ let persistText = finalText;
190
+ let parsedFences = null;
191
+ if (finalText && !(res && res.error) && this.status !== "killed") {
192
+ try {
193
+ parsedFences = require("./fences.cjs").parseReplyFences(finalText);
194
+ persistText = parsedFences.cleanText;
195
+ } catch {
196
+ parsedFences = null;
197
+ persistText = finalText;
198
+ }
199
+ }
200
+ if (persistText) store.appendMessage(this.db, this.chatId, "assistant", persistText);
201
+ if (res && res.session && res.session.id) {
202
+ this.runtimeSession = { id: res.session.id };
203
+ store.saveRuntimeSession(this.db, this.chatId, this.runtime.kind, this.runtimeSession, this.fingerprint);
204
+ }
205
+ if (res && res.usage) this.usage = res.usage;
206
+
207
+ this.endedAt = Date.now();
208
+ if (this.status === "killed") {
209
+ this._record({ type: "turn-end", at: Date.now(), ok: false, killed: true });
210
+ return res;
211
+ }
212
+ if (res && res.error) {
213
+ this.status = "failed";
214
+ this.lastError = res.error;
215
+ this._record({ type: "turn-end", at: Date.now(), ok: false, error: res.error });
216
+ } else {
217
+ this.status = "done";
218
+ // 링버퍼 표시 이벤트에도 raw 대신 cleanText — 제어 블록이 패널/lastLine 에 새지 않게.
219
+ this._record({ type: "turn-end", at: Date.now(), ok: true, text: persistText });
220
+ }
221
+ // 적용은 상태 확정 후 — 이 세션이 더 이상 running 으로 집계되지 않아 위임 스폰이
222
+ // 동시 상한을 자기 자신으로 소모하지 않는다. 적용 실패는 이벤트로 표면화.
223
+ if (parsedFences) {
224
+ try {
225
+ require("./apply-fences.cjs").applyReplyFences(this, parsedFences, { orch: this.orchestrator });
226
+ } catch (e) {
227
+ this._record({ type: "error", at: Date.now(), text: `fence apply failed: ${(e && e.message) || String(e)}` });
228
+ }
229
+ }
230
+ return res;
231
+ }
232
+
233
+ /** 실행 중 턴을 중단한다. 큐는 비운다. */
234
+ kill() {
235
+ this.queue.length = 0;
236
+ if (this._child) {
237
+ this.status = "killed";
238
+ try { nativeHost.terminateNativeChild(this._child); } catch { /* already dead */ }
239
+ } else if (this.status === "running") {
240
+ this.status = "killed";
241
+ }
242
+ }
243
+ }
244
+
245
+ module.exports = { Session };
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /*
3
+ * sessions/sink — native-host의 ui 인터페이스를 "이벤트"로 변환하는 싱크.
4
+ *
5
+ * 세션은 터미널에 직접 그리지 않는다. 모든 런타임 출력은 타입드 이벤트로
6
+ * 세션 링버퍼에 쌓이고, 렌더러(REPL)가 "활성 세션"의 이벤트만 실시간으로
7
+ * 그린다 — 이것이 오르카식 멀티세션의 기반이다(백그라운드 세션 전환 시
8
+ * 버퍼 테일 재생 + 라이브 구독).
9
+ *
10
+ * 구현 인터페이스(native-host가 호출): status, warn, error, line, tool,
11
+ * toolResult, streamStart, streamDelta, streamEnd, applyTaskTool,
12
+ * applyTaskResult, c(팔레트 — 렌더러가 색을 정하므로 여기선 무색 passthrough), t(i18n).
13
+ */
14
+ const i18n = require("../agentlas-i18n.cjs");
15
+
16
+ const NOOP_PALETTE = new Proxy({}, { get: () => (s) => String(s) });
17
+
18
+ class EventSink {
19
+ constructor({ lang = "en", onEvent } = {}) {
20
+ this.lang = lang;
21
+ this.t = (key, ...args) => i18n.t(this.lang, key, ...args);
22
+ this.c = NOOP_PALETTE;
23
+ this._onEvent = onEvent || (() => {});
24
+ this._streamOpen = false;
25
+ }
26
+
27
+ _emit(type, data) {
28
+ this._onEvent({ type, at: Date.now(), ...data });
29
+ }
30
+
31
+ status(text) { this._emit("status", { text: String(text || "") }); }
32
+ warn(text) { this._emit("warn", { text: String(text || "") }); }
33
+ error(text) { this._emit("error", { text: String(text || "") }); }
34
+ line(text) { this._emit("line", { text: String(text || "") }); }
35
+ tool(name, summary) { this._emit("tool", { name: String(name || "tool"), summary: String(summary || "") }); }
36
+ toolResult(text, ok) { this._emit("tool-result", { text: String(text || ""), ok: ok !== false }); }
37
+ streamStart() {
38
+ if (!this._streamOpen) { this._streamOpen = true; this._emit("stream-start", {}); }
39
+ }
40
+ streamDelta(text) {
41
+ if (!this._streamOpen) this.streamStart();
42
+ this._emit("stream-delta", { text: String(text || "") });
43
+ }
44
+ streamEnd() {
45
+ if (this._streamOpen) { this._streamOpen = false; this._emit("stream-end", {}); }
46
+ }
47
+ applyTaskTool(name, input, id) { this._emit("task-tool", { name, input, id }); }
48
+ applyTaskResult(name, result, id) { this._emit("task-result", { name, result, id }); }
49
+ info(text) { this._emit("line", { text: String(text || "") }); }
50
+ cost(usage) { this._emit("cost", { usage }); }
51
+ stopSpinner() { /* 스피너는 렌더러 소관 — 이벤트 불필요 */ }
52
+ }
53
+
54
+ module.exports = { EventSink };
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /*
3
+ * sessions/store — chats/chat_messages 영속화 (데스크탑과 동일 테이블·어휘).
4
+ * 메인 세션: kind='user'. 서브에이전트 세션: kind='division' + parent_chat_id
5
+ * (데스크탑 division 서브챗과 같은 패턴 — 데스크탑 UI에서도 동일하게 보인다).
6
+ */
7
+ const crypto = require("node:crypto");
8
+ const { runWriteTransaction } = require("../core/db.cjs");
9
+
10
+ function newId() {
11
+ return crypto.randomUUID();
12
+ }
13
+
14
+ function nowIso() {
15
+ return new Date().toISOString();
16
+ }
17
+
18
+ function createChat(db, { agentId, title, kind = "user", parentChatId = null, workingFolder = null }) {
19
+ const id = newId();
20
+ const now = nowIso();
21
+ runWriteTransaction(db, () => {
22
+ db.prepare(
23
+ "INSERT INTO chats (id, agent_id, title, created_at, updated_at, kind, parent_chat_id, working_folder) VALUES (?,?,?,?,?,?,?,?)",
24
+ ).run(id, agentId, title || "New chat", now, now, kind, parentChatId, workingFolder);
25
+ });
26
+ return id;
27
+ }
28
+
29
+ function appendMessage(db, chatId, role, text) {
30
+ const id = newId();
31
+ const now = nowIso();
32
+ runWriteTransaction(db, () => {
33
+ db.prepare(
34
+ "INSERT INTO chat_messages (id, chat_id, role, text, created_at) VALUES (?,?,?,?,?)",
35
+ ).run(id, chatId, role, String(text || ""), now);
36
+ db.prepare("UPDATE chats SET updated_at=?, used_at=? WHERE id=?").run(now, now, chatId);
37
+ });
38
+ return id;
39
+ }
40
+
41
+ function retitleChat(db, chatId, title) {
42
+ runWriteTransaction(db, () => {
43
+ db.prepare("UPDATE chats SET title=?, updated_at=? WHERE id=?").run(String(title || "New chat").slice(0, 120), nowIso(), chatId);
44
+ });
45
+ }
46
+
47
+ function chatHistory(db, chatId, limit = 40) {
48
+ return db.prepare(
49
+ "SELECT role, text, created_at FROM chat_messages WHERE chat_id=? ORDER BY created_at DESC, rowid DESC LIMIT ?",
50
+ ).all(chatId, limit).reverse();
51
+ }
52
+
53
+ /*
54
+ * CLI resume 세션 ID 영속화 — 데스크탑 chat_runtime_sessions와 동일 스키마
55
+ * (chat_id, kind, session_id, fingerprint). fingerprint가 다르면 resume하지
56
+ * 않는다 — 시스템 프롬프트가 바뀐 세션을 이어붙이면 지시가 오염된다.
57
+ */
58
+ function loadRuntimeSession(db, chatId, kind, fingerprint) {
59
+ try {
60
+ const row = db.prepare("SELECT session_id, fingerprint FROM chat_runtime_sessions WHERE chat_id=? AND kind=?").get(chatId, kind);
61
+ if (row && row.session_id && row.fingerprint === fingerprint) return { id: row.session_id };
62
+ } catch { /* 테이블 부재 — resume 없이 진행 */ }
63
+ return {};
64
+ }
65
+
66
+ function saveRuntimeSession(db, chatId, kind, session, fingerprint) {
67
+ const sessionId = session && session.id ? String(session.id) : "";
68
+ if (!sessionId) return;
69
+ try {
70
+ runWriteTransaction(db, () => {
71
+ db.prepare(
72
+ "INSERT INTO chat_runtime_sessions (chat_id, kind, session_id, fingerprint, updated_at) VALUES (?,?,?,?,?) " +
73
+ "ON CONFLICT(chat_id, kind) DO UPDATE SET session_id=excluded.session_id, fingerprint=excluded.fingerprint, updated_at=excluded.updated_at",
74
+ ).run(chatId, kind, sessionId, fingerprint, nowIso());
75
+ });
76
+ } catch { /* 스키마가 다르면 resume만 포기 — 턴 자체는 유효 */ }
77
+ }
78
+
79
+ module.exports = { createChat, appendMessage, retitleChat, chatHistory, loadRuntimeSession, saveRuntimeSession, newId };
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ /*
3
+ * storm/deps — storm/swarm 하네스의 의존성 주머니(D bag) 조립 + 로컬 런타임 인벤토리.
4
+ *
5
+ * v1에서는 모놀리스(engine/agentlas.cjs)의 lazy 팩토리 parity()가 이 D를 만들었다
6
+ * (legacy-v1-engine-snapshot, 10727–10758행). v2에서는 각 멤버를 모듈 경계에 맞게
7
+ * 다시 배선한다:
8
+ *
9
+ * captureRuntime / runApi / buildChildEnvCli / projectCwd / runCwd
10
+ * → engine/workforce/capture.cjs (검증된 헤드리스 캡처 단일 경로 —
11
+ * storm/swarm 워커용 제2의 스폰 경로를 만들지 않는다)
12
+ * resolveRuntime → engine/workforce/deps.cjs resolveWorkforceRuntime
13
+ * (v1 사다리 그대로: 명시 override > prefs 저장 CLI >
14
+ * 공유 DB active_runtime(byok/ollama 포함) > PATH 탐지.
15
+ * 아무것도 없으면 code="no_runtime" 정직 정지 — 폴백 금지)
16
+ * listAvailableRuntimes → 여기서 v1 모놀리스 9245–9275행을 포팅 (아래)
17
+ * modelRoutingReceiptPath → v1과 동일한 userData 하위 JSONL
18
+ *
19
+ * 불변식(오너 결정): 인벤토리는 이 호스트에 실제 설치·연결된 런타임만 광고한다.
20
+ * Terminal/plugin이 스케줄할 수 없는 런타임을 시늉하지 않는다.
21
+ */
22
+ const path = require("node:path");
23
+
24
+ const { userDataDir } = require("../core/paths.cjs");
25
+ const routing = require("../agentlas-workload-routing.cjs");
26
+ const capture = require("../workforce/capture.cjs");
27
+ const { resolveWorkforceRuntime } = require("../workforce/deps.cjs");
28
+
29
+ // v1 모놀리스 listAvailableRuntimes 포팅 (legacy-v1-engine-snapshot 9245–9275행).
30
+ // 상위(부모 AI) 워크로드 할당자를 위한 "실행 가능한 런타임 인벤토리"를 만든다.
31
+ function listAvailableRuntimes(db, fallbackRuntime = null) {
32
+ const active = fallbackRuntime || resolveWorkforceRuntime(db);
33
+ const candidates = [];
34
+ const add = (runtime) => {
35
+ if (!runtime) return;
36
+ const key = runtime.mode === "cli" ? `cli:${runtime.kind}` : `api:${runtime.backend}:${runtime.model || ""}`;
37
+ if (candidates.some((item) => item.key === key)) return;
38
+ const discovered = routing.defaultAvailableModels(runtime);
39
+ const availableModels = [...discovered];
40
+ if (runtime.model && !availableModels.some((model) => model.id === runtime.model)) {
41
+ availableModels.push({
42
+ id: runtime.model,
43
+ tier: runtime.modelTier || runtime.tier || null,
44
+ capabilities: runtime.capabilities || [],
45
+ contextWindow: runtime.contextWindow || null,
46
+ efforts: runtime.efforts || [],
47
+ description: runtime.modelDescription || "host-selected current model",
48
+ });
49
+ }
50
+ candidates.push({ ...runtime, key, availableModels });
51
+ };
52
+ add(active);
53
+ for (const kind of Object.keys(capture.RUNTIME_BIN)) {
54
+ if (!capture.which(capture.RUNTIME_BIN[kind])) continue;
55
+ add({ mode: "cli", kind });
56
+ }
57
+ return candidates
58
+ .filter((runtime) => runtime.availableModels.length)
59
+ .map(({ key, ...runtime }, index) => ({ ...runtime, runtimeId: `runtime-${index + 1}` }));
60
+ }
61
+
62
+ // v1 D.modelRoutingReceiptPath와 동일한 경로. workload-routing의 defaultReceiptPath
63
+ // (~/.agentlas/…)가 아니라 userData 하위를 쓰는 것이 Terminal의 v1 계약이다.
64
+ function modelRoutingReceiptPath() {
65
+ return path.join(userDataDir(), "model-routing-receipts.jsonl");
66
+ }
67
+
68
+ /**
69
+ * D bag 조립. ctx = { lang?, out? } (엔진 ctx의 부분집합).
70
+ * 모든 멤버는 v1 parity() 팩토리와 같은 계약 형태를 유지한다 —
71
+ * storm/swarm 모듈은 이 D만 사용하고 자체 스폰/자체 상태를 갖지 않는다.
72
+ */
73
+ function buildStormDeps(ctx = {}) {
74
+ return {
75
+ prefsLang: () => ctx.lang || "en",
76
+ out: typeof ctx.out === "function" ? ctx.out : (s) => process.stdout.write(`${s}\n`),
77
+ resolveRuntime: resolveWorkforceRuntime,
78
+ listAvailableRuntimes,
79
+ captureRuntime: capture.captureRuntime,
80
+ runApi: capture.runApi,
81
+ buildChildEnvCli: capture.buildChildEnv,
82
+ projectCwd: capture.projectCwd,
83
+ runCwd: capture.runCwd,
84
+ modelRoutingReceiptPath,
85
+ };
86
+ }
87
+
88
+ module.exports = { listAvailableRuntimes, modelRoutingReceiptPath, buildStormDeps };